1 # ##### BEGIN GPL LICENSE BLOCK #####
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.
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.
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.
17 # ##### END GPL LICENSE BLOCK #####
22 from bpy.types import Operator
26 class prettyface(object):
37 def __init__(self, data):
38 self.has_parent = False
39 self.rot = False # only used for triables
43 if type(data) == list: # list of data
49 data[1].xoff = data[0].width
50 self.width = data[0].width * 2
51 self.height = data[0].height
54 # 4 blocks all the same size
55 d = data[0].width # dimension x/y are the same
63 self.width = self.height = d * 2
66 # print(len(data), data)
74 elif type(data) == tuple:
76 # f, (len_min, len_mid, len_max)
79 f1, lens1, lens1ord = data[0]
81 f2, lens2, lens2ord = data[1]
82 self.width = (lens1[lens1ord[0]] + lens2[lens2ord[0]]) / 2.0
83 self.height = (lens1[lens1ord[1]] + lens2[lens2ord[1]]) / 2.0
86 self.height = lens1[1]
92 self.uv = data.id_data.uv_textures.active.data[data.index].uv # XXX25
94 # cos = [v.co for v in data]
95 cos = [data.id_data.vertices[v].co for v in data.vertices] # XXX25
97 self.width = ((cos[0] - cos[1]).length + (cos[2] - cos[3]).length) / 2.0
98 self.height = ((cos[1] - cos[2]).length + (cos[0] - cos[3]).length) / 2.0
103 if self.uv and len(self.uv) == 4:
104 self.uv = self.uv[1], self.uv[2], self.uv[3], self.uv[0]
106 self.width, self.height = self.height, self.width
107 self.xoff, self.yoff = self.yoff, self.xoff # not needed?
108 self.rot = not self.rot # only for tri pairs.
110 for pf in self.children:
113 def place(self, xoff, yoff, xfac, yfac, margin_w, margin_h):
119 for pf in self.children:
120 pf.place(xoff, yoff, xfac, yfac, margin_w, margin_h)
128 x2 = xoff + self.width
129 y2 = yoff + self.height
132 x1 = x1 / xfac + margin_w
133 x2 = x2 / xfac - margin_w
134 y1 = y1 / yfac + margin_h
135 y2 = y2 / yfac - margin_h
139 # match the order of angle sizes of the 3d verts with the UV angles and rotate.
140 def get_tri_angles(v1, v2, v3):
141 a1 = (v2 - v1).angle(v3 - v1, pi)
142 a2 = (v1 - v2).angle(v3 - v2, pi)
143 a3 = pi - (a1 + a2) # a3= (v2 - v3).angle(v1 - v3)
145 return [(a1, 0), (a2, 1), (a3, 2)]
147 def set_uv(f, p1, p2, p3):
154 # angles_co = get_tri_angles(*[v.co for v in f])
155 angles_co = get_tri_angles(*[f.id_data.vertices[v].co for v in f.vertices]) # XXX25
158 I = [i for a, i in angles_co]
161 fuv = f.id_data.uv_textures.active.data[f.index].uv # XXX25
172 f, lens, lensord = uv[0]
174 set_uv(f, (x1, y1), (x1, y2 - margin_h), (x2 - margin_w, y1))
177 f, lens, lensord = uv[1]
178 set_uv(f, (x2, y2), (x2, y1 + margin_h), (x1 + margin_w, y2))
181 uv[1][0], uv[1][1] = x1, y1
182 uv[2][0], uv[2][1] = x1, y2
183 uv[3][0], uv[3][1] = x2, y2
184 uv[0][0], uv[0][1] = x2, y1
188 return self.width, self.height
191 def lightmap_uvpack(meshes,
193 PREF_NEW_UVLAYER=False,
194 PREF_PACK_IN_ONE=False,
195 PREF_APPLY_IMAGE=False,
196 PREF_IMG_PX_SIZE=512,
201 BOX_DIV if the maximum division of the UV map that
202 a box may be consolidated into.
203 Basicly, a lower value will be slower but waist less space
204 and a higher value will have more clumpy boxes but more waisted space
207 from math import sqrt
216 image = bpy.data.images.new(name="lightmap", width=PREF_IMG_PX_SIZE, height=PREF_IMG_PX_SIZE, alpha=False)
222 # Add face UV if it does not exist.
223 # All new faces are selected.
224 if not me.uv_textures:
228 faces = [f for f in me.faces if f.select]
233 face_groups[0].extend(faces)
235 face_groups.append(faces)
240 for face_sel in face_groups:
241 print("\nStarting unwrap")
243 if len(face_sel) < 4:
244 print("\tWarning, less then 4 faces, skipping")
247 pretty_faces = [prettyface(f) for f in face_sel if len(f.vertices) == 4]
249 # Do we have any tri's
250 if len(pretty_faces) != len(face_sel):
252 # Now add tri's, not so simple because we need to pair them up.
256 # cos = [v.co for v in f]
257 cos = [f.id_data.vertices[v].co for v in f.vertices] # XXX25
259 lens = [(cos[0] - cos[1]).length, (cos[1] - cos[2]).length, (cos[2] - cos[0]).length]
261 lens_min = lens.index(min(lens))
262 lens_max = lens.index(max(lens))
264 if i != lens_min and i != lens_max:
267 lens_order = lens_min, lens_mid, lens_max
269 return f, lens, lens_order
271 tri_lengths = [trylens(f) for f in face_sel if len(f.vertices) == 3]
274 def trilensdiff(t1, t2):
275 return (abs(t1[1][t1[2][0]] - t2[1][t2[2][0]]) +
276 abs(t1[1][t1[2][1]] - t2[1][t2[2][1]]) +
277 abs(t1[1][t1[2][2]] - t2[1][t2[2][2]]))
280 tri1 = tri_lengths.pop()
283 pretty_faces.append(prettyface((tri1, None)))
287 best_tri_diff = 100000000.0
289 for i, tri2 in enumerate(tri_lengths):
290 diff = trilensdiff(tri1, tri2)
291 if diff < best_tri_diff:
295 pretty_faces.append(prettyface((tri1, tri_lengths.pop(best_tri_index))))
297 # Get the min, max and total areas
299 min_area = 100000000.0
309 max_len = sqrt(max_area)
310 min_len = sqrt(min_area)
311 side_len = sqrt(tot_area)
317 print("\tGenerating lengths...", end="")
320 while curr_len > min_len:
321 lengths.append(curr_len)
322 curr_len = curr_len / 2.0
324 # Dont allow boxes smaller then the margin
325 # since we contract on the margin, boxes that are smaller will create errors
326 # print(curr_len, side_len/MARGIN_DIV)
327 if curr_len / 4.0 < side_len / PREF_MARGIN_DIV:
331 lengths.append(curr_len)
337 for l in reversed(lengths):
338 lengths_to_ints[l] = l_int
341 lengths_to_ints = list(lengths_to_ints.items())
342 lengths_to_ints.sort()
345 # apply quantized values.
347 for pf in pretty_faces:
350 bestw_diff = 1000000000.0
351 besth_diff = 1000000000.0
354 for l, i in lengths_to_ints:
358 new_w = i # assign the int version
373 # Since the boxes are sized in powers of 2, we can neatly group them into bigger squares
374 # this is done hierarchily, so that we may avoid running the pack function
375 # on many thousands of boxes, (under 1k is best) because it would get slow.
376 # Using an off and even dict us usefull because they are packed differently
377 # where w/h are the same, their packed in groups of 4
378 # where they are different they are packed in pairs
380 # After this is done an external pack func is done that packs the whole group.
382 print("\tConsolidating Boxes...", end="")
383 even_dict = {} # w/h are the same, the key is an int (w)
384 odd_dict = {} # w/h are different, the key is the (w,h)
386 for pf in pretty_faces:
387 w, h = pf.width, pf.height
389 even_dict.setdefault(w, []).append(pf)
391 odd_dict.setdefault((w, h), []).append(pf)
393 # Count the number of boxes consolidated, only used for stats.
396 # This is tricky. the total area of all packed boxes, then squt that to get an estimated size
397 # this is used then converted into out INT space so we can compare it with
398 # the ints assigned to the boxes size
399 # and divided by BOX_DIV, basicly if BOX_DIV is 8
400 # ...then the maximum box consolidataion (recursive grouping) will have a max width & height
401 # ...1/8th of the UV size.
402 # ...limiting this is needed or you end up with bug unused texture spaces
403 # ...however if its too high, boxpacking is way too slow for high poly meshes.
404 float_to_int_factor = lengths_to_ints[0][0]
405 if float_to_int_factor > 0:
406 max_int_dimension = int(((side_len / float_to_int_factor)) / PREF_BOX_DIV)
409 max_int_dimension = 0.0 # wont be used
412 # RECURSIVE prettyface grouping
416 # Tall boxes in groups of 2
417 for d, boxes in list(odd_dict.items()):
418 if d[1] < max_int_dimension:
419 #\boxes.sort(key = lambda a: len(a.children))
420 while len(boxes) >= 2:
421 # print("foo", len(boxes))
424 pf_parent = prettyface([boxes.pop(), boxes.pop()])
425 pretty_faces.append(pf_parent)
427 w, h = pf_parent.width, pf_parent.height
433 even_dict.setdefault(w, []).append(pf_parent)
435 odd_dict.setdefault((w, h), []).append(pf_parent)
437 # Even boxes in groups of 4
438 for d, boxes in list(even_dict.items()):
439 if d < max_int_dimension:
440 boxes.sort(key=lambda a: len(a.children))
442 while len(boxes) >= 4:
443 # print("bar", len(boxes))
447 pf_parent = prettyface([boxes.pop(), boxes.pop(), boxes.pop(), boxes.pop()])
448 pretty_faces.append(pf_parent)
449 w = pf_parent.width # width and weight are the same
450 even_dict.setdefault(w, []).append(pf_parent)
455 # orig = len(pretty_faces)
457 pretty_faces = [pf for pf in pretty_faces if not pf.has_parent]
459 # spin every second prettyface
460 # if there all vertical you get less efficiently used texture space
461 i = len(pretty_faces)
466 if pf.width != pf.height:
468 if d % 2: # only pack every second
472 print("Consolidated", c, "boxes, done")
473 # print("done", orig, len(pretty_faces))
475 # boxes2Pack.append([islandIdx, w,h])
476 print("\tPacking Boxes", len(pretty_faces), end="...")
477 boxes2Pack = [[0.0, 0.0, pf.width, pf.height, i] for i, pf in enumerate(pretty_faces)]
478 packWidth, packHeight = mathutils.geometry.box_pack_2d(boxes2Pack)
480 # print(packWidth, packHeight)
482 packWidth = float(packWidth)
483 packHeight = float(packHeight)
485 margin_w = ((packWidth) / PREF_MARGIN_DIV) / packWidth
486 margin_h = ((packHeight) / PREF_MARGIN_DIV) / packHeight
488 # print(margin_w, margin_h)
491 # Apply the boxes back to the UV coords.
492 print("\twriting back UVs", end="")
493 for i, box in enumerate(boxes2Pack):
494 pretty_faces[i].place(box[0], box[1], packWidth, packHeight, margin_w, margin_h)
495 # pf.place(box[1][1], box[1][2], packWidth, packHeight, margin_w, margin_h)
499 if not PREF_PACK_IN_ONE:
500 image = bpy.data.images.new(name="lightmap",
501 width=PREF_IMG_PX_SIZE,
502 height=PREF_IMG_PX_SIZE,
507 f.id_data.uv_textures.active.data[f.index].image = image # XXX25
512 print("finished all %.2f " % (time.time() - t))
517 def unwrap(operator, context, **kwargs):
519 is_editmode = (bpy.context.object.mode == 'EDIT')
521 bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
523 PREF_ACT_ONLY = kwargs.pop("PREF_ACT_ONLY")
527 obj = context.scene.objects.active
528 if obj and obj.type == 'MESH':
531 meshes = list({me for obj in context.selected_objects if obj.type == 'MESH' for me in (obj.data,) if me.faces and me.library is None})
534 operator.report({'ERROR'}, "No mesh object.")
537 lightmap_uvpack(meshes, **kwargs)
540 bpy.ops.object.mode_set(mode='EDIT', toggle=False)
544 from bpy.props import BoolProperty, FloatProperty, IntProperty
547 class LightMapPack(Operator):
548 '''Follow UVs from active quads along continuous face loops'''
549 bl_idname = "uv.lightmap_pack"
550 bl_label = "Lightmap Pack"
551 bl_options = {'REGISTER', 'UNDO'}
553 PREF_CONTEXT = bpy.props.EnumProperty(
556 items=(("SEL_FACES", "Selected Faces", "Space all UVs evently"),
557 ("ALL_FACES", "All Faces", "Average space UVs edge length of each loop"),
558 ("ALL_OBJECTS", "Selected Mesh Object", "Average space UVs edge length of each loop")
563 PREF_PACK_IN_ONE = BoolProperty(
564 name="Share Tex Space",
565 description=("Objects Share texture space, map all objects "
569 PREF_NEW_UVLAYER = BoolProperty(
571 description="Create a new UV layer for every mesh packed",
574 PREF_APPLY_IMAGE = BoolProperty(
576 description=("Assign new images for every mesh (only one if "
577 "shared tex space enabled)"),
580 PREF_IMG_PX_SIZE = IntProperty(
582 description="Width and Height for the new image",
587 PREF_BOX_DIV = IntProperty(
589 description="Pre Packing before the complex boxpack",
593 PREF_MARGIN_DIV = FloatProperty(
595 description="Size of the margin as a division of the UV",
600 def execute(self, context):
601 kwargs = self.as_keywords()
602 PREF_CONTEXT = kwargs.pop("PREF_CONTEXT")
604 if PREF_CONTEXT == 'SEL_FACES':
605 kwargs["PREF_ACT_ONLY"] = True
606 kwargs["PREF_SEL_ONLY"] = True
607 elif PREF_CONTEXT == 'ALL_FACES':
608 kwargs["PREF_ACT_ONLY"] = True
609 kwargs["PREF_SEL_ONLY"] = False
610 elif PREF_CONTEXT == 'ALL_OBJECTS':
611 kwargs["PREF_ACT_ONLY"] = False
612 kwargs["PREF_SEL_ONLY"] = False
614 raise Exception("invalid context")
616 kwargs["PREF_MARGIN_DIV"] = int(1.0 / (kwargs["PREF_MARGIN_DIV"] / 100.0))
618 return unwrap(self, context, **kwargs)
620 def invoke(self, context, event):
621 wm = context.window_manager
622 return wm.invoke_props_dialog(self)