1# ##### BEGIN GPL LICENSE BLOCK #####
2#
3#  This program is free software; you can redistribute it and/or
4#  modify it under the terms of the GNU General Public License
5#  as published by the Free Software Foundation; either version 2
6#  of the License, or (at your option) any later version.
7#
8#  This program is distributed in the hope that it will be useful,
9#  but WITHOUT ANY WARRANTY; without even the implied warranty of
10#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11#  GNU General Public License for more details.
12#
13#  You should have received a copy of the GNU General Public License
14#  along with this program; if not, write to the Free Software Foundation,
15#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16#
17# ##### END GPL LICENSE BLOCK #####
18
19# <pep8 compliant>
20
21import bpy
22from bpy.types import Operator
23import mathutils
24
25
26class prettyface:
27    __slots__ = (
28        "uv",
29        "width",
30        "height",
31        "children",
32        "xoff",
33        "yoff",
34        "has_parent",
35        "rot",
36    )
37
38    def __init__(self, data):
39        self.has_parent = False
40        self.rot = False  # only used for triangles
41        self.xoff = 0
42        self.yoff = 0
43
44        if type(data) == list:  # list of data
45            self.uv = None
46
47            # join the data
48            if len(data) == 2:
49                # 2 vertical blocks
50                data[1].xoff = data[0].width
51                self.width = data[0].width * 2
52                self.height = data[0].height
53
54            elif len(data) == 4:
55                # 4 blocks all the same size
56                d = data[0].width  # dimension x/y are the same
57
58                data[1].xoff += d
59                data[2].yoff += d
60
61                data[3].xoff += d
62                data[3].yoff += d
63
64                self.width = self.height = d * 2
65
66            # else:
67            #     print(len(data), data)
68            #     raise "Error"
69
70            for pf in data:
71                pf.has_parent = True
72
73            self.children = data
74
75        elif type(data) == tuple:
76            # 2 blender faces
77            # f, (len_min, len_mid, len_max)
78            self.uv = data
79
80            _f1, lens1, lens1ord = data[0]
81            if data[1]:
82                _f2, lens2, lens2ord = data[1]
83                self.width = (lens1[lens1ord[0]] + lens2[lens2ord[0]]) / 2.0
84                self.height = (lens1[lens1ord[1]] + lens2[lens2ord[1]]) / 2.0
85            else:  # 1 tri :/
86                self.width = lens1[0]
87                self.height = lens1[1]
88
89            self.children = []
90
91        else:  # blender face
92            uv_layer = data.id_data.uv_layers.active.data
93            self.uv = [uv_layer[i].uv for i in data.loop_indices]
94
95            # cos = [v.co for v in data]
96            cos = [data.id_data.vertices[v].co for v in data.vertices]  # XXX25
97
98            if len(self.uv) == 4:
99                self.width = ((cos[0] - cos[1]).length + (cos[2] - cos[3]).length) / 2.0
100                self.height = ((cos[1] - cos[2]).length + (cos[0] - cos[3]).length) / 2.0
101            else:
102                # ngon, note:
103                # for ngons to calculate the width/height we need to do the
104                # whole projection, unlike other faces
105                # we store normalized UV's in the faces coords to avoid
106                # calculating the projection and rotating it twice.
107
108                no = data.normal
109                r = no.rotation_difference(mathutils.Vector((0.0, 0.0, 1.0)))
110                cos_2d = [(r @ co).xy for co in cos]
111                # print(cos_2d)
112                angle = mathutils.geometry.box_fit_2d(cos_2d)
113
114                mat = mathutils.Matrix.Rotation(angle, 2)
115                cos_2d = [(mat @ co) for co in cos_2d]
116                xs = [co.x for co in cos_2d]
117                ys = [co.y for co in cos_2d]
118
119                xmin = min(xs)
120                ymin = min(ys)
121                xmax = max(xs)
122                ymax = max(ys)
123
124                xspan = xmax - xmin
125                yspan = ymax - ymin
126
127                self.width = xspan
128                self.height = yspan
129
130                # ngons work different, we store projected result
131                # in UV's to avoid having to re-project later.
132                for i, co in enumerate(cos_2d):
133                    self.uv[i][:] = ((co.x - xmin) / xspan,
134                                     (co.y - ymin) / yspan)
135
136            self.children = []
137
138    def spin(self):
139        if self.uv and len(self.uv) == 4:
140            self.uv = self.uv[1], self.uv[2], self.uv[3], self.uv[0]
141
142        self.width, self.height = self.height, self.width
143        self.xoff, self.yoff = self.yoff, self.xoff  # not needed?
144        self.rot = not self.rot  # only for tri pairs and ngons.
145        # print("spinning")
146        for pf in self.children:
147            pf.spin()
148
149    def place(self, xoff, yoff, xfac, yfac, margin_w, margin_h):
150        from math import pi
151
152        xoff += self.xoff
153        yoff += self.yoff
154
155        for pf in self.children:
156            pf.place(xoff, yoff, xfac, yfac, margin_w, margin_h)
157
158        uv = self.uv
159        if not uv:
160            return
161
162        x1 = xoff
163        y1 = yoff
164        x2 = xoff + self.width
165        y2 = yoff + self.height
166
167        # Scale the values
168        x1 = x1 / xfac + margin_w
169        x2 = x2 / xfac - margin_w
170        y1 = y1 / yfac + margin_h
171        y2 = y2 / yfac - margin_h
172
173        # 2 Tri pairs
174        if len(uv) == 2:
175            # match the order of angle sizes of the 3d verts with the UV angles and rotate.
176            def get_tri_angles(v1, v2, v3):
177                a1 = (v2 - v1).angle(v3 - v1, pi)
178                a2 = (v1 - v2).angle(v3 - v2, pi)
179                a3 = pi - (a1 + a2)  # a3= (v2 - v3).angle(v1 - v3)
180
181                return [(a1, 0), (a2, 1), (a3, 2)]
182
183            def set_uv(f, p1, p2, p3):
184
185                # cos =
186                #v1 = cos[0]-cos[1]
187                #v2 = cos[1]-cos[2]
188                #v3 = cos[2]-cos[0]
189
190                # angles_co = get_tri_angles(*[v.co for v in f])
191                angles_co = get_tri_angles(*[f.id_data.vertices[v].co for v in f.vertices])  # XXX25
192
193                angles_co.sort()
194                I = [i for a, i in angles_co]
195
196                uv_layer = f.id_data.uv_layers.active.data
197                fuv = [uv_layer[i].uv for i in f.loop_indices]
198
199                if self.rot:
200                    fuv[I[2]][:] = p1
201                    fuv[I[1]][:] = p2
202                    fuv[I[0]][:] = p3
203                else:
204                    fuv[I[2]][:] = p1
205                    fuv[I[0]][:] = p2
206                    fuv[I[1]][:] = p3
207
208            f = uv[0][0]
209
210            set_uv(f, (x1, y1), (x1, y2 - margin_h), (x2 - margin_w, y1))
211
212            if uv[1]:
213                f = uv[1][0]
214                set_uv(f, (x2, y2), (x2, y1 + margin_h), (x1 + margin_w, y2))
215
216        else:  # 1 QUAD
217            if len(uv) == 4:
218                uv[1][:] = x1, y1
219                uv[2][:] = x1, y2
220                uv[3][:] = x2, y2
221                uv[0][:] = x2, y1
222            else:
223                # NGon
224                xspan = x2 - x1
225                yspan = y2 - y1
226                for uvco in uv:
227                    x, y = uvco
228                    uvco[:] = ((x1 + (x * xspan)),
229                               (y1 + (y * yspan)))
230
231    def __hash__(self):
232        # None unique hash
233        return self.width, self.height
234
235
236def lightmap_uvpack(
237        meshes,
238        PREF_SEL_ONLY=True,
239        PREF_NEW_UVLAYER=False,
240        PREF_PACK_IN_ONE=False,
241        PREF_APPLY_IMAGE=False,
242        PREF_IMG_PX_SIZE=512,
243        PREF_BOX_DIV=8,
244        PREF_MARGIN_DIV=512,
245):
246    """
247    BOX_DIV if the maximum division of the UV map that
248    a box may be consolidated into.
249    Basically, a lower value will be slower but waist less space
250    and a higher value will have more clumpy boxes but more wasted space
251    """
252    import time
253    from math import sqrt
254
255    if not meshes:
256        return
257
258    t = time.time()
259
260    if PREF_PACK_IN_ONE:
261        if PREF_APPLY_IMAGE:
262            image = bpy.data.images.new(name="lightmap", width=PREF_IMG_PX_SIZE, height=PREF_IMG_PX_SIZE, alpha=False)
263        face_groups = [[]]
264    else:
265        face_groups = []
266
267    for me in meshes:
268        if PREF_SEL_ONLY:
269            faces = [f for f in me.polygons if f.select]
270        else:
271            faces = me.polygons[:]
272
273        if PREF_PACK_IN_ONE:
274            face_groups[0].extend(faces)
275        else:
276            face_groups.append(faces)
277
278        if PREF_NEW_UVLAYER:
279            me.uv_layers.new()
280
281        # Add face UV if it does not exist.
282        # All new faces are selected.
283        if not me.uv_layers:
284            me.uv_layers.new()
285
286    for face_sel in face_groups:
287        print("\nStarting unwrap")
288
289        if not face_sel:
290            continue
291
292        pretty_faces = [prettyface(f) for f in face_sel if f.loop_total >= 4]
293
294        # Do we have any triangles?
295        if len(pretty_faces) != len(face_sel):
296
297            # Now add triangles, not so simple because we need to pair them up.
298            def trylens(f):
299                # f must be a tri
300
301                # cos = [v.co for v in f]
302                cos = [f.id_data.vertices[v].co for v in f.vertices]  # XXX25
303
304                lens = [(cos[0] - cos[1]).length, (cos[1] - cos[2]).length, (cos[2] - cos[0]).length]
305
306                lens_min = lens.index(min(lens))
307                lens_max = lens.index(max(lens))
308                for i in range(3):
309                    if i != lens_min and i != lens_max:
310                        lens_mid = i
311                        break
312                lens_order = lens_min, lens_mid, lens_max
313
314                return f, lens, lens_order
315
316            tri_lengths = [trylens(f) for f in face_sel if f.loop_total == 3]
317            del trylens
318
319            def trilensdiff(t1, t2):
320                return (abs(t1[1][t1[2][0]] - t2[1][t2[2][0]]) +
321                        abs(t1[1][t1[2][1]] - t2[1][t2[2][1]]) +
322                        abs(t1[1][t1[2][2]] - t2[1][t2[2][2]]))
323
324            while tri_lengths:
325                tri1 = tri_lengths.pop()
326
327                if not tri_lengths:
328                    pretty_faces.append(prettyface((tri1, None)))
329                    break
330
331                best_tri_index = -1
332                best_tri_diff = 100000000.0
333
334                for i, tri2 in enumerate(tri_lengths):
335                    diff = trilensdiff(tri1, tri2)
336                    if diff < best_tri_diff:
337                        best_tri_index = i
338                        best_tri_diff = diff
339
340                pretty_faces.append(prettyface((tri1, tri_lengths.pop(best_tri_index))))
341
342        # Get the min, max and total areas
343        max_area = 0.0
344        min_area = 100000000.0
345        tot_area = 0
346        for f in face_sel:
347            area = f.area
348            if area > max_area:
349                max_area = area
350            if area < min_area:
351                min_area = area
352            tot_area += area
353
354        max_len = sqrt(max_area)
355        min_len = sqrt(min_area)
356        side_len = sqrt(tot_area)
357
358        # Build widths
359
360        curr_len = max_len
361
362        print("\tGenerating lengths...", end="")
363
364        lengths = []
365        while curr_len > min_len:
366            lengths.append(curr_len)
367            curr_len = curr_len / 2.0
368
369            # Don't allow boxes smaller then the margin
370            # since we contract on the margin, boxes that are smaller will create errors
371            # print(curr_len, side_len/MARGIN_DIV)
372            if curr_len / 4.0 < side_len / PREF_MARGIN_DIV:
373                break
374
375        if not lengths:
376            lengths.append(curr_len)
377
378        # convert into ints
379        lengths_to_ints = {}
380
381        l_int = 1
382        for l in reversed(lengths):
383            lengths_to_ints[l] = l_int
384            l_int *= 2
385
386        lengths_to_ints = list(lengths_to_ints.items())
387        lengths_to_ints.sort()
388        print("done")
389
390        # apply quantized values.
391
392        for pf in pretty_faces:
393            w = pf.width
394            h = pf.height
395            bestw_diff = 1000000000.0
396            besth_diff = 1000000000.0
397            new_w = 0.0
398            new_h = 0.0
399            for l, i in lengths_to_ints:
400                d = abs(l - w)
401                if d < bestw_diff:
402                    bestw_diff = d
403                    new_w = i  # assign the int version
404
405                d = abs(l - h)
406                if d < besth_diff:
407                    besth_diff = d
408                    new_h = i  # ditto
409
410            pf.width = new_w
411            pf.height = new_h
412
413            if new_w > new_h:
414                pf.spin()
415
416        print("...done")
417
418        # Since the boxes are sized in powers of 2, we can neatly group them into bigger squares
419        # this is done hierarchically, so that we may avoid running the pack function
420        # on many thousands of boxes, (under 1k is best) because it would get slow.
421        # Using an off and even dict us useful because they are packed differently
422        # where w/h are the same, their packed in groups of 4
423        # where they are different they are packed in pairs
424        #
425        # After this is done an external pack func is done that packs the whole group.
426
427        print("\tConsolidating Boxes...", end="")
428        even_dict = {}  # w/h are the same, the key is an int (w)
429        odd_dict = {}  # w/h are different, the key is the (w,h)
430
431        for pf in pretty_faces:
432            w, h = pf.width, pf.height
433            if w == h:
434                even_dict.setdefault(w, []).append(pf)
435            else:
436                odd_dict.setdefault((w, h), []).append(pf)
437
438        # Count the number of boxes consolidated, only used for stats.
439        c = 0
440
441        # This is tricky. the total area of all packed boxes, then sqrt() that to get an estimated size
442        # this is used then converted into out INT space so we can compare it with
443        # the ints assigned to the boxes size
444        # and divided by BOX_DIV, basically if BOX_DIV is 8
445        # ...then the maximum box consolidation (recursive grouping) will have a max width & height
446        # ...1/8th of the UV size.
447        # ...limiting this is needed or you end up with bug unused texture spaces
448        # ...however if its too high, box-packing is way too slow for high poly meshes.
449        float_to_int_factor = lengths_to_ints[0][0]
450        if float_to_int_factor > 0:
451            max_int_dimension = int(((side_len / float_to_int_factor)) / PREF_BOX_DIV)
452            ok = True
453        else:
454            max_int_dimension = 0.0  # won't be used
455            ok = False
456
457        # RECURSIVE pretty face grouping
458        while ok:
459            ok = False
460
461            # Tall boxes in groups of 2
462            for d, boxes in list(odd_dict.items()):
463                if d[1] < max_int_dimension:
464                    # boxes.sort(key=lambda a: len(a.children))
465                    while len(boxes) >= 2:
466                        # print("foo", len(boxes))
467                        ok = True
468                        c += 1
469                        pf_parent = prettyface([boxes.pop(), boxes.pop()])
470                        pretty_faces.append(pf_parent)
471
472                        w, h = pf_parent.width, pf_parent.height
473                        assert(w <= h)
474
475                        if w == h:
476                            even_dict.setdefault(w, []).append(pf_parent)
477                        else:
478                            odd_dict.setdefault((w, h), []).append(pf_parent)
479
480            # Even boxes in groups of 4
481            for d, boxes in list(even_dict.items()):
482                if d < max_int_dimension:
483                    boxes.sort(key=lambda a: len(a.children))
484
485                    while len(boxes) >= 4:
486                        # print("bar", len(boxes))
487                        ok = True
488                        c += 1
489
490                        pf_parent = prettyface([boxes.pop(), boxes.pop(), boxes.pop(), boxes.pop()])
491                        pretty_faces.append(pf_parent)
492                        w = pf_parent.width  # width and weight are the same
493                        even_dict.setdefault(w, []).append(pf_parent)
494
495        del even_dict
496        del odd_dict
497
498        # orig = len(pretty_faces)
499
500        pretty_faces = [pf for pf in pretty_faces if not pf.has_parent]
501
502        # spin every second pretty-face
503        # if there all vertical you get less efficiently used texture space
504        i = len(pretty_faces)
505        d = 0
506        while i:
507            i -= 1
508            pf = pretty_faces[i]
509            if pf.width != pf.height:
510                d += 1
511                if d % 2:  # only pack every second
512                    pf.spin()
513                    # pass
514
515        print("Consolidated", c, "boxes, done")
516        # print("done", orig, len(pretty_faces))
517
518        # boxes2Pack.append([islandIdx, w,h])
519        print("\tPacking Boxes", len(pretty_faces), end="...")
520        boxes2Pack = [[0.0, 0.0, pf.width, pf.height, i] for i, pf in enumerate(pretty_faces)]
521        packWidth, packHeight = mathutils.geometry.box_pack_2d(boxes2Pack)
522
523        # print(packWidth, packHeight)
524
525        packWidth = float(packWidth)
526        packHeight = float(packHeight)
527
528        margin_w = ((packWidth) / PREF_MARGIN_DIV) / packWidth
529        margin_h = ((packHeight) / PREF_MARGIN_DIV) / packHeight
530
531        # print(margin_w, margin_h)
532        print("done")
533
534        # Apply the boxes back to the UV coords.
535        print("\twriting back UVs", end="")
536        for i, box in enumerate(boxes2Pack):
537            pretty_faces[i].place(box[0], box[1], packWidth, packHeight, margin_w, margin_h)
538            # pf.place(box[1][1], box[1][2], packWidth, packHeight, margin_w, margin_h)
539        print("done")
540
541        if PREF_APPLY_IMAGE:
542            pass
543            # removed with texface
544            '''
545            if not PREF_PACK_IN_ONE:
546                image = bpy.data.images.new(name="lightmap",
547                                            width=PREF_IMG_PX_SIZE,
548                                            height=PREF_IMG_PX_SIZE,
549                                            )
550
551            for f in face_sel:
552                f.image = image
553            '''
554
555    for me in meshes:
556        me.update()
557
558    print("finished all %.2f " % (time.time() - t))
559
560
561def unwrap(operator, context, **kwargs):
562    # switch to object mode
563    is_editmode = context.object and context.object.mode == 'EDIT'
564    if is_editmode:
565        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
566
567    # define list of meshes
568    meshes = list({
569        me for obj in context.selected_objects
570        if obj.type == 'MESH'
571        for me in (obj.data,)
572        if me.polygons and me.library is None
573    })
574
575    if not meshes:
576        operator.report({'ERROR'}, "No mesh object")
577        return {'CANCELLED'}
578
579    lightmap_uvpack(meshes, **kwargs)
580
581    # switch back to edit mode
582    if is_editmode:
583        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
584
585    return {'FINISHED'}
586
587
588from bpy.props import BoolProperty, FloatProperty, IntProperty
589
590
591class LightMapPack(Operator):
592    """Pack each faces UV's into the UV bounds"""
593    bl_idname = "uv.lightmap_pack"
594    bl_label = "Lightmap Pack"
595
596    # Disable REGISTER flag for now because this operator might create new
597    # images. This leads to non-proper operator redo because current undo
598    # stack is local for edit mode and can not remove images created by this
599    # operator.
600    # Proper solution would be to make undo stack aware of such things,
601    # but for now just disable redo. Keep undo here so unwanted changes to uv
602    # coords might be undone.
603    # This fixes infinite image creation reported there T30968 (sergey)
604    bl_options = {'UNDO'}
605
606    PREF_CONTEXT: bpy.props.EnumProperty(
607        name="Selection",
608        items=(
609            ('SEL_FACES', "Selected Faces", "Space all UVs evenly"),
610            ('ALL_FACES', "All Faces", "Average space UVs edge length of each loop"),
611        ),
612    )
613
614    # Image & UVs...
615    PREF_PACK_IN_ONE: BoolProperty(
616        name="Share Texture Space",
617        description=(
618            "Objects Share texture space, map all objects "
619            "into 1 uvmap"
620        ),
621        default=True,
622    )
623    PREF_NEW_UVLAYER: BoolProperty(
624        name="New UV Map",
625        description="Create a new UV map for every mesh packed",
626        default=False,
627    )
628    PREF_APPLY_IMAGE: BoolProperty(
629        name="New Image",
630        description=(
631            "Assign new images for every mesh (only one if "
632            "shared tex space enabled)"
633        ),
634        default=False,
635    )
636    PREF_IMG_PX_SIZE: IntProperty(
637        name="Image Size",
638        description="Width and Height for the new image",
639        min=64, max=5000,
640        default=512,
641    )
642    # UV Packing...
643    PREF_BOX_DIV: IntProperty(
644        name="Pack Quality",
645        description="Pre Packing before the complex boxpack",
646        min=1, max=48,
647        default=12,
648    )
649    PREF_MARGIN_DIV: FloatProperty(
650        name="Margin",
651        description="Size of the margin as a division of the UV",
652        min=0.001, max=1.0,
653        default=0.1,
654    )
655
656    def draw(self, context):
657        layout = self.layout
658        layout.use_property_split = True
659        layout.use_property_decorate = False
660
661        is_editmode = context.active_object.mode == 'EDIT'
662        if is_editmode:
663            layout.prop(self, "PREF_CONTEXT")
664
665        layout.prop(self, "PREF_PACK_IN_ONE")
666        layout.prop(self, "PREF_NEW_UVLAYER")
667        layout.prop(self, "PREF_APPLY_IMAGE")
668        layout.prop(self, "PREF_IMG_PX_SIZE")
669        layout.prop(self, "PREF_BOX_DIV")
670        layout.prop(self, "PREF_MARGIN_DIV")
671
672    @classmethod
673    def poll(cls, context):
674        ob = context.active_object
675        return ob and ob.type == 'MESH'
676
677    def execute(self, context):
678        kwargs = self.as_keywords()
679        PREF_CONTEXT = kwargs.pop("PREF_CONTEXT")
680
681        is_editmode = context.active_object.mode == 'EDIT'
682
683        if not is_editmode:
684            kwargs["PREF_SEL_ONLY"] = False
685        elif PREF_CONTEXT == 'SEL_FACES':
686            kwargs["PREF_SEL_ONLY"] = True
687        elif PREF_CONTEXT == 'ALL_FACES':
688            kwargs["PREF_SEL_ONLY"] = False
689        else:
690            raise Exception("invalid context")
691
692        kwargs["PREF_MARGIN_DIV"] = int(1.0 / (kwargs["PREF_MARGIN_DIV"] / 100.0))
693
694        return unwrap(self, context, **kwargs)
695
696    def invoke(self, context, _event):
697        wm = context.window_manager
698        return wm.invoke_props_dialog(self)
699
700
701classes = (
702    LightMapPack,
703)
704