Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'release/scripts/startup/bl_operators/uvcalc_lightmap.py')
-rw-r--r--release/scripts/startup/bl_operators/uvcalc_lightmap.py582
1 files changed, 582 insertions, 0 deletions
diff --git a/release/scripts/startup/bl_operators/uvcalc_lightmap.py b/release/scripts/startup/bl_operators/uvcalc_lightmap.py
new file mode 100644
index 00000000000..fedc8b15161
--- /dev/null
+++ b/release/scripts/startup/bl_operators/uvcalc_lightmap.py
@@ -0,0 +1,582 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+# <pep8 compliant>
+
+import bpy
+import mathutils
+
+from math import sqrt, pi
+
+
+class prettyface(object):
+ __slots__ = "uv", "width", "height", "children", "xoff", "yoff", "has_parent", "rot"
+
+ def __init__(self, data):
+ self.has_parent = False
+ self.rot = False # only used for triables
+ self.xoff = 0
+ self.yoff = 0
+
+ if type(data) == list: # list of data
+ self.uv = None
+
+ # join the data
+ if len(data) == 2:
+ # 2 vertical blocks
+ data[1].xoff = data[0].width
+ self.width = data[0].width * 2
+ self.height = data[0].height
+
+ elif len(data) == 4:
+ # 4 blocks all the same size
+ d = data[0].width # dimension x/y are the same
+
+ data[1].xoff += d
+ data[2].yoff += d
+
+ data[3].xoff += d
+ data[3].yoff += d
+
+ self.width = self.height = d * 2
+
+ #else:
+ # print(len(data), data)
+ # raise "Error"
+
+ for pf in data:
+ pf.has_parent = True
+
+ self.children = data
+
+ elif type(data) == tuple:
+ # 2 blender faces
+ # f, (len_min, len_mid, len_max)
+ self.uv = data
+
+ f1, lens1, lens1ord = data[0]
+ if data[1]:
+ f2, lens2, lens2ord = data[1]
+ self.width = (lens1[lens1ord[0]] + lens2[lens2ord[0]]) / 2.0
+ self.height = (lens1[lens1ord[1]] + lens2[lens2ord[1]]) / 2.0
+ else: # 1 tri :/
+ self.width = lens1[0]
+ self.height = lens1[1]
+
+ self.children = []
+
+ else: # blender face
+ # self.uv = data.uv
+ self.uv = data.id_data.uv_textures.active.data[data.index].uv # XXX25
+
+ # cos = [v.co for v in data]
+ cos = [data.id_data.vertices[v].co for v in data.vertices] # XXX25
+
+ self.width = ((cos[0] - cos[1]).length + (cos[2] - cos[3]).length) / 2.0
+ self.height = ((cos[1] - cos[2]).length + (cos[0] - cos[3]).length) / 2.0
+
+ self.children = []
+
+ def spin(self):
+ if self.uv and len(self.uv) == 4:
+ self.uv = self.uv[1], self.uv[2], self.uv[3], self.uv[0]
+
+ self.width, self.height = self.height, self.width
+ self.xoff, self.yoff = self.yoff, self.xoff # not needed?
+ self.rot = not self.rot # only for tri pairs.
+ # print("spinning")
+ for pf in self.children:
+ pf.spin()
+
+ def place(self, xoff, yoff, xfac, yfac, margin_w, margin_h):
+
+ xoff += self.xoff
+ yoff += self.yoff
+
+ for pf in self.children:
+ pf.place(xoff, yoff, xfac, yfac, margin_w, margin_h)
+
+ uv = self.uv
+ if not uv:
+ return
+
+ x1 = xoff
+ y1 = yoff
+ x2 = xoff + self.width
+ y2 = yoff + self.height
+
+ # Scale the values
+ x1 = x1 / xfac + margin_w
+ x2 = x2 / xfac - margin_w
+ y1 = y1 / yfac + margin_h
+ y2 = y2 / yfac - margin_h
+
+ # 2 Tri pairs
+ if len(uv) == 2:
+ # match the order of angle sizes of the 3d verts with the UV angles and rotate.
+ def get_tri_angles(v1, v2, v3):
+ a1 = (v2 - v1).angle(v3 - v1, pi)
+ a2 = (v1 - v2).angle(v3 - v2, pi)
+ a3 = pi - (a1 + a2) # a3= (v2 - v3).angle(v1 - v3)
+
+ return [(a1, 0), (a2, 1), (a3, 2)]
+
+ def set_uv(f, p1, p2, p3):
+
+ # cos =
+ #v1 = cos[0]-cos[1]
+ #v2 = cos[1]-cos[2]
+ #v3 = cos[2]-cos[0]
+
+ # angles_co = get_tri_angles(*[v.co for v in f])
+ angles_co = get_tri_angles(*[f.id_data.vertices[v].co for v in f.vertices]) # XXX25
+
+ angles_co.sort()
+ I = [i for a, i in angles_co]
+
+ # fuv = f.uv
+ fuv = f.id_data.uv_textures.active.data[f.index].uv # XXX25
+
+ if self.rot:
+ fuv[I[2]] = p1
+ fuv[I[1]] = p2
+ fuv[I[0]] = p3
+ else:
+ fuv[I[2]] = p1
+ fuv[I[0]] = p2
+ fuv[I[1]] = p3
+
+ f, lens, lensord = uv[0]
+
+ set_uv(f, (x1, y1), (x1, y2 - margin_h), (x2 - margin_w, y1))
+
+ if uv[1]:
+ f, lens, lensord = uv[1]
+ set_uv(f, (x2, y2), (x2, y1 + margin_h), (x1 + margin_w, y2))
+
+ else: # 1 QUAD
+ uv[1][0], uv[1][1] = x1, y1
+ uv[2][0], uv[2][1] = x1, y2
+ uv[3][0], uv[3][1] = x2, y2
+ uv[0][0], uv[0][1] = x2, y1
+
+ def __hash__(self):
+ # None unique hash
+ return self.width, self.height
+
+
+def lightmap_uvpack(meshes,
+ PREF_SEL_ONLY=True,
+ PREF_NEW_UVLAYER=False,
+ PREF_PACK_IN_ONE=False,
+ PREF_APPLY_IMAGE=False,
+ PREF_IMG_PX_SIZE=512,
+ PREF_BOX_DIV=8,
+ PREF_MARGIN_DIV=512
+ ):
+ '''
+ BOX_DIV if the maximum division of the UV map that
+ a box may be consolidated into.
+ Basicly, a lower value will be slower but waist less space
+ and a higher value will have more clumpy boxes but more waisted space
+ '''
+ import time
+
+ if not meshes:
+ return
+
+ t = time.time()
+
+ if PREF_PACK_IN_ONE:
+ if PREF_APPLY_IMAGE:
+ image = bpy.data.images.new(name="lightmap", width=PREF_IMG_PX_SIZE, height=PREF_IMG_PX_SIZE, alpha=False)
+ face_groups = [[]]
+ else:
+ face_groups = []
+
+ for me in meshes:
+ # Add face UV if it does not exist.
+ # All new faces are selected.
+ if not me.uv_textures:
+ me.uv_textures.new()
+
+ if PREF_SEL_ONLY:
+ faces = [f for f in me.faces if f.select]
+ else:
+ faces = me.faces[:]
+
+ if PREF_PACK_IN_ONE:
+ face_groups[0].extend(faces)
+ else:
+ face_groups.append(faces)
+
+ if PREF_NEW_UVLAYER:
+ me.uv_textures.new()
+
+ for face_sel in face_groups:
+ print("\nStarting unwrap")
+
+ if len(face_sel) < 4:
+ print("\tWarning, less then 4 faces, skipping")
+ continue
+
+ pretty_faces = [prettyface(f) for f in face_sel if len(f.vertices) == 4]
+
+ # Do we have any tri's
+ if len(pretty_faces) != len(face_sel):
+
+ # Now add tri's, not so simple because we need to pair them up.
+ def trylens(f):
+ # f must be a tri
+
+ # cos = [v.co for v in f]
+ cos = [f.id_data.vertices[v].co for v in f.vertices] # XXX25
+
+ lens = [(cos[0] - cos[1]).length, (cos[1] - cos[2]).length, (cos[2] - cos[0]).length]
+
+ lens_min = lens.index(min(lens))
+ lens_max = lens.index(max(lens))
+ for i in range(3):
+ if i != lens_min and i != lens_max:
+ lens_mid = i
+ break
+ lens_order = lens_min, lens_mid, lens_max
+
+ return f, lens, lens_order
+
+ tri_lengths = [trylens(f) for f in face_sel if len(f.vertices) == 3]
+ del trylens
+
+ def trilensdiff(t1, t2):
+ return\
+ abs(t1[1][t1[2][0]] - t2[1][t2[2][0]]) + \
+ abs(t1[1][t1[2][1]] - t2[1][t2[2][1]]) + \
+ abs(t1[1][t1[2][2]] - t2[1][t2[2][2]])
+
+ while tri_lengths:
+ tri1 = tri_lengths.pop()
+
+ if not tri_lengths:
+ pretty_faces.append(prettyface((tri1, None)))
+ break
+
+ best_tri_index = -1
+ best_tri_diff = 100000000.0
+
+ for i, tri2 in enumerate(tri_lengths):
+ diff = trilensdiff(tri1, tri2)
+ if diff < best_tri_diff:
+ best_tri_index = i
+ best_tri_diff = diff
+
+ pretty_faces.append(prettyface((tri1, tri_lengths.pop(best_tri_index))))
+
+ # Get the min, max and total areas
+ max_area = 0.0
+ min_area = 100000000.0
+ tot_area = 0
+ for f in face_sel:
+ area = f.area
+ if area > max_area:
+ max_area = area
+ if area < min_area:
+ min_area = area
+ tot_area += area
+
+ max_len = sqrt(max_area)
+ min_len = sqrt(min_area)
+ side_len = sqrt(tot_area)
+
+ # Build widths
+
+ curr_len = max_len
+
+ print("\tGenerating lengths...", end="")
+
+ lengths = []
+ while curr_len > min_len:
+ lengths.append(curr_len)
+ curr_len = curr_len / 2.0
+
+ # Dont allow boxes smaller then the margin
+ # since we contract on the margin, boxes that are smaller will create errors
+ # print(curr_len, side_len/MARGIN_DIV)
+ if curr_len / 4.0 < side_len / PREF_MARGIN_DIV:
+ break
+
+ if not lengths:
+ lengths.append(curr_len)
+
+ # convert into ints
+ lengths_to_ints = {}
+
+ l_int = 1
+ for l in reversed(lengths):
+ lengths_to_ints[l] = l_int
+ l_int *= 2
+
+ lengths_to_ints = list(lengths_to_ints.items())
+ lengths_to_ints.sort()
+ print("done")
+
+ # apply quantized values.
+
+ for pf in pretty_faces:
+ w = pf.width
+ h = pf.height
+ bestw_diff = 1000000000.0
+ besth_diff = 1000000000.0
+ new_w = 0.0
+ new_h = 0.0
+ for l, i in lengths_to_ints:
+ d = abs(l - w)
+ if d < bestw_diff:
+ bestw_diff = d
+ new_w = i # assign the int version
+
+ d = abs(l - h)
+ if d < besth_diff:
+ besth_diff = d
+ new_h = i # ditto
+
+ pf.width = new_w
+ pf.height = new_h
+
+ if new_w > new_h:
+ pf.spin()
+
+ print("...done")
+
+ # Since the boxes are sized in powers of 2, we can neatly group them into bigger squares
+ # this is done hierarchily, so that we may avoid running the pack function
+ # on many thousands of boxes, (under 1k is best) because it would get slow.
+ # Using an off and even dict us usefull because they are packed differently
+ # where w/h are the same, their packed in groups of 4
+ # where they are different they are packed in pairs
+ #
+ # After this is done an external pack func is done that packs the whole group.
+
+ print("\tConsolidating Boxes...", end="")
+ even_dict = {} # w/h are the same, the key is an int (w)
+ odd_dict = {} # w/h are different, the key is the (w,h)
+
+ for pf in pretty_faces:
+ w, h = pf.width, pf.height
+ if w == h:
+ even_dict.setdefault(w, []).append(pf)
+ else:
+ odd_dict.setdefault((w, h), []).append(pf)
+
+ # Count the number of boxes consolidated, only used for stats.
+ c = 0
+
+ # This is tricky. the total area of all packed boxes, then squt that to get an estimated size
+ # this is used then converted into out INT space so we can compare it with
+ # the ints assigned to the boxes size
+ # and divided by BOX_DIV, basicly if BOX_DIV is 8
+ # ...then the maximum box consolidataion (recursive grouping) will have a max width & height
+ # ...1/8th of the UV size.
+ # ...limiting this is needed or you end up with bug unused texture spaces
+ # ...however if its too high, boxpacking is way too slow for high poly meshes.
+ float_to_int_factor = lengths_to_ints[0][0]
+ if float_to_int_factor > 0:
+ max_int_dimension = int(((side_len / float_to_int_factor)) / PREF_BOX_DIV)
+ ok = True
+ else:
+ max_int_dimension = 0.0 # wont be used
+ ok = False
+
+ # RECURSIVE prettyface grouping
+ while ok:
+ ok = False
+
+ # Tall boxes in groups of 2
+ for d, boxes in odd_dict.items():
+ if d[1] < max_int_dimension:
+ #\boxes.sort(key = lambda a: len(a.children))
+ while len(boxes) >= 2:
+ # print("foo", len(boxes))
+ ok = True
+ c += 1
+ pf_parent = prettyface([boxes.pop(), boxes.pop()])
+ pretty_faces.append(pf_parent)
+
+ w, h = pf_parent.width, pf_parent.height
+
+ if w > h:
+ raise "error"
+
+ if w == h:
+ even_dict.setdefault(w, []).append(pf_parent)
+ else:
+ odd_dict.setdefault((w, h), []).append(pf_parent)
+
+ # Even boxes in groups of 4
+ for d, boxes in even_dict.items():
+ if d < max_int_dimension:
+ boxes.sort(key=lambda a: len(a.children))
+
+ while len(boxes) >= 4:
+ # print("bar", len(boxes))
+ ok = True
+ c += 1
+
+ pf_parent = prettyface([boxes.pop(), boxes.pop(), boxes.pop(), boxes.pop()])
+ pretty_faces.append(pf_parent)
+ w = pf_parent.width # width and weight are the same
+ even_dict.setdefault(w, []).append(pf_parent)
+
+ del even_dict
+ del odd_dict
+
+ orig = len(pretty_faces)
+
+ pretty_faces = [pf for pf in pretty_faces if not pf.has_parent]
+
+ # spin every second prettyface
+ # if there all vertical you get less efficiently used texture space
+ i = len(pretty_faces)
+ d = 0
+ while i:
+ i -= 1
+ pf = pretty_faces[i]
+ if pf.width != pf.height:
+ d += 1
+ if d % 2: # only pack every second
+ pf.spin()
+ # pass
+
+ print("Consolidated", c, "boxes, done")
+ # print("done", orig, len(pretty_faces))
+
+ # boxes2Pack.append([islandIdx, w,h])
+ print("\tPacking Boxes", len(pretty_faces), end="...")
+ boxes2Pack = [[0.0, 0.0, pf.width, pf.height, i] for i, pf in enumerate(pretty_faces)]
+ packWidth, packHeight = mathutils.geometry.box_pack_2d(boxes2Pack)
+
+ # print(packWidth, packHeight)
+
+ packWidth = float(packWidth)
+ packHeight = float(packHeight)
+
+ margin_w = ((packWidth) / PREF_MARGIN_DIV) / packWidth
+ margin_h = ((packHeight) / PREF_MARGIN_DIV) / packHeight
+
+ # print(margin_w, margin_h)
+ print("done")
+
+ # Apply the boxes back to the UV coords.
+ print("\twriting back UVs", end="")
+ for i, box in enumerate(boxes2Pack):
+ pretty_faces[i].place(box[0], box[1], packWidth, packHeight, margin_w, margin_h)
+ # pf.place(box[1][1], box[1][2], packWidth, packHeight, margin_w, margin_h)
+ print("done")
+
+ if PREF_APPLY_IMAGE:
+ if not PREF_PACK_IN_ONE:
+ image = Image.New("lightmap", PREF_IMG_PX_SIZE, PREF_IMG_PX_SIZE, 24)
+
+ for f in face_sel:
+ # f.image = image
+ f.id_data.uv_textures.active.data[f.index].image = image # XXX25
+
+ for me in meshes:
+ me.update()
+
+ print("finished all %.2f " % (time.time() - t))
+
+ # Window.RedrawAll()
+
+
+def unwrap(operator, context, **kwargs):
+
+ is_editmode = (bpy.context.object.mode == 'EDIT')
+ if is_editmode:
+ bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
+
+ PREF_ACT_ONLY = kwargs.pop("PREF_ACT_ONLY")
+
+ meshes = []
+ if PREF_ACT_ONLY:
+ obj = context.scene.objects.active
+ if obj and obj.type == 'MESH':
+ meshes = [obj.data]
+ else:
+ meshes = {me.name: me for obj in context.selected_objects if obj.type == 'MESH' for me in (obj.data,) if not me.library if len(me.faces)}.values()
+
+ if not meshes:
+ operator.report({'ERROR'}, "No mesh object.")
+ return {'CANCELLED'}
+
+ lightmap_uvpack(meshes, **kwargs)
+
+ if is_editmode:
+ bpy.ops.object.mode_set(mode='EDIT', toggle=False)
+
+ return {'FINISHED'}
+
+from bpy.props import BoolProperty, FloatProperty, IntProperty, EnumProperty
+
+
+class LightMapPack(bpy.types.Operator):
+ '''Follow UVs from active quads along continuous face loops'''
+ bl_idname = "uv.lightmap_pack"
+ bl_label = "Lightmap Pack"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ PREF_CONTEXT = bpy.props.EnumProperty(
+ items=(("SEL_FACES", "Selected Faces", "Space all UVs evently"),
+ ("ALL_FACES", "All Faces", "Average space UVs edge length of each loop"),
+ ("ALL_OBJECTS", "Selected Mesh Object", "Average space UVs edge length of each loop")
+ ),
+ name="Selection",
+ description="")
+
+ # Image & UVs...
+ PREF_PACK_IN_ONE = BoolProperty(name="Share Tex Space", default=True, description="Objects Share texture space, map all objects into 1 uvmap")
+ PREF_NEW_UVLAYER = BoolProperty(name="New UV Layer", default=False, description="Create a new UV layer for every mesh packed")
+ PREF_APPLY_IMAGE = BoolProperty(name="New Image", default=False, description="Assign new images for every mesh (only one if shared tex space enabled)")
+ PREF_IMG_PX_SIZE = IntProperty(name="Image Size", min=64, max=5000, default=512, description="Width and Height for the new image")
+
+ # UV Packing...
+ PREF_BOX_DIV = IntProperty(name="Pack Quality", min=1, max=48, default=12, description="Pre Packing before the complex boxpack")
+ PREF_MARGIN_DIV = FloatProperty(name="Margin", min=0.001, max=1.0, default=0.1, description="Size of the margin as a division of the UV")
+
+ def execute(self, context):
+ kwargs = self.as_keywords()
+ PREF_CONTEXT = kwargs.pop("PREF_CONTEXT")
+
+ if PREF_CONTEXT == 'SEL_FACES':
+ kwargs["PREF_ACT_ONLY"] = True
+ kwargs["PREF_SEL_ONLY"] = True
+ elif PREF_CONTEXT == 'ALL_FACES':
+ kwargs["PREF_ACT_ONLY"] = True
+ kwargs["PREF_SEL_ONLY"] = False
+ elif PREF_CONTEXT == 'ALL_OBJECTS':
+ kwargs["PREF_ACT_ONLY"] = False
+ kwargs["PREF_SEL_ONLY"] = False
+ else:
+ raise Exception("invalid context")
+
+ kwargs["PREF_MARGIN_DIV"] = int(1.0 / (kwargs["PREF_MARGIN_DIV"] / 100.0))
+
+ return unwrap(self, context, **kwargs)
+
+ def invoke(self, context, event):
+ wm = context.window_manager
+ return wm.invoke_props_dialog(self)