diff options
-rw-r--r-- | release/scripts/startup/bl_operators/__init__.py | 1 | ||||
-rw-r--r-- | release/scripts/startup/bl_operators/uvcalc_smart_project.py | 1053 | ||||
-rw-r--r-- | source/blender/editors/uvedit/uvedit_intern.h | 1 | ||||
-rw-r--r-- | source/blender/editors/uvedit/uvedit_ops.c | 1 | ||||
-rw-r--r-- | source/blender/editors/uvedit/uvedit_unwrap_ops.c | 385 |
5 files changed, 380 insertions, 1061 deletions
diff --git a/release/scripts/startup/bl_operators/__init__.py b/release/scripts/startup/bl_operators/__init__.py index c39a7afcff9..c927cc184a3 100644 --- a/release/scripts/startup/bl_operators/__init__.py +++ b/release/scripts/startup/bl_operators/__init__.py @@ -46,7 +46,6 @@ _modules = [ "userpref", "uvcalc_follow_active", "uvcalc_lightmap", - "uvcalc_smart_project", "vertexpaint_dirt", "view3d", "gpencil_mesh_bake", diff --git a/release/scripts/startup/bl_operators/uvcalc_smart_project.py b/release/scripts/startup/bl_operators/uvcalc_smart_project.py deleted file mode 100644 index 1f56cbe6d57..00000000000 --- a/release/scripts/startup/bl_operators/uvcalc_smart_project.py +++ /dev/null @@ -1,1053 +0,0 @@ -# ##### 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 ##### - -# TODO <pep8 compliant> - -from mathutils import ( - Matrix, - Vector, - geometry, -) -import bpy -from bpy.types import Operator - -DEG_TO_RAD = 0.017453292519943295 # pi/180.0 -# see bugs: -# - T31598 (when too small). -# - T48086 (when too big). -SMALL_NUM = 1e-12 - - -global USER_FILL_HOLES -global USER_FILL_HOLES_QUALITY -USER_FILL_HOLES = None -USER_FILL_HOLES_QUALITY = None - - -def pointInTri2D(v, v1, v2, v3): - key = v1.x, v1.y, v2.x, v2.y, v3.x, v3.y - - # Commented because its slower to do the bounds check, we should really cache the bounds info for each face. - ''' - # BOUNDS CHECK - xmin= 1000000 - ymin= 1000000 - - xmax= -1000000 - ymax= -1000000 - - for i in (0,2,4): - x= key[i] - y= key[i+1] - - if xmax<x: xmax= x - if ymax<y: ymax= y - if xmin>x: xmin= x - if ymin>y: ymin= y - - x= v.x - y= v.y - - if x<xmin or x>xmax or y < ymin or y > ymax: - return False - # Done with bounds check - ''' - try: - mtx = dict_matrix[key] - if not mtx: - return False - except: - side1 = v2 - v1 - side2 = v3 - v1 - - nor = side1.cross(side2) - - mtx = Matrix((side1, side2, nor)) - - # Zero area 2d tri, even tho we throw away zero area faces - # the projection UV can result in a zero area UV. - if not mtx.determinant(): - dict_matrix[key] = None - return False - - mtx.invert() - - dict_matrix[key] = mtx - - uvw = (v - v1) @ mtx - return 0 <= uvw[0] and 0 <= uvw[1] and uvw[0] + uvw[1] <= 1 - - -def boundsIsland(faces): - minx = maxx = faces[0].uv[0][0] # Set initial bounds. - miny = maxy = faces[0].uv[0][1] - # print len(faces), minx, maxx, miny , maxy - for f in faces: - for uv in f.uv: - x = uv.x - y = uv.y - if x < minx: - minx = x - if y < miny: - miny = y - if x > maxx: - maxx = x - if y > maxy: - maxy = y - - return minx, miny, maxx, maxy - - -""" -def boundsEdgeLoop(edges): - minx = maxx = edges[0][0] # Set initial bounds. - miny = maxy = edges[0][1] - # print len(faces), minx, maxx, miny , maxy - for ed in edges: - for pt in ed: - x= pt[0] - y= pt[1] - if x<minx: x= minx - if y<miny: y= miny - if x>maxx: x= maxx - if y>maxy: y= maxy - - return minx, miny, maxx, maxy -""" - -# Turns the islands into a list of unpordered edges (Non internal) -# Only for UV's -# only returns outline edges for intersection tests. and unique points. - - -def island2Edge(island): - - # Vert index edges - edges = {} - - unique_points = {} - - for f in island: - f_uvkey = list(map(tuple, f.uv)) - - for vIdx in range(len(f_uvkey)): - unique_points[f_uvkey[vIdx]] = f.uv[vIdx] - - if f.v[vIdx].index > f.v[vIdx - 1].index: - i1 = vIdx - 1 - i2 = vIdx - else: - i1 = vIdx - i2 = vIdx - 1 - - try: - edges[f_uvkey[i1], f_uvkey[i2]] *= 0 # sets any edge with more than 1 user to 0 are not returned. - except: - edges[f_uvkey[i1], f_uvkey[i2]] = (f.uv[i1] - f.uv[i2]).length - - # If 2 are the same then they will be together, but full [a,b] order is not correct. - - # Sort by length - length_sorted_edges = [(Vector(key[0]), Vector(key[1]), value) for key, value in edges.items() if value != 0] - - length_sorted_edges.sort(key=lambda a: -a[2]) # largest first - - # Its okay to leave the length in there. - # for e in length_sorted_edges: - # e.pop(2) - - # return edges and unique points - return length_sorted_edges, [v.to_3d() for v in unique_points.values()] - - -def pointInIsland(pt, island): - vec1, vec2, vec3 = Vector(), Vector(), Vector() - for f in island: - vec1.x, vec1.y = f.uv[0] - vec2.x, vec2.y = f.uv[1] - vec3.x, vec3.y = f.uv[2] - - if pointInTri2D(pt, vec1, vec2, vec3): - return True - - if len(f.v) == 4: - vec1.x, vec1.y = f.uv[0] - vec2.x, vec2.y = f.uv[2] - vec3.x, vec3.y = f.uv[3] - if pointInTri2D(pt, vec1, vec2, vec3): - return True - return False - - -# box is (left,bottom, right, top) -def islandIntersectUvIsland(source, target, SourceOffset): - # Is 1 point in the box, inside the vertLoops - edgeLoopsSource = source[6] # Pretend this is offset - edgeLoopsTarget = target[6] - - # Edge intersect test - for ed in edgeLoopsSource: - for seg in edgeLoopsTarget: - i = geometry.intersect_line_line_2d(seg[0], - seg[1], - SourceOffset + ed[0], - SourceOffset + ed[1], - ) - if i: - return 1 # LINE INTERSECTION - - # 1 test for source being totally inside target - SourceOffset.resize_3d() - for pv in source[7]: - if pointInIsland(pv + SourceOffset, target[0]): - return 2 # SOURCE INSIDE TARGET - - # 2 test for a part of the target being totally inside the source. - for pv in target[7]: - if pointInIsland(pv - SourceOffset, source[0]): - return 3 # PART OF TARGET INSIDE SOURCE. - - return 0 # NO INTERSECTION - - -def rotate_uvs(uv_points, angle): - - if angle != 0.0: - mat = Matrix.Rotation(angle, 2) - for uv in uv_points: - uv[:] = mat @ uv - - -def optiRotateUvIsland(faces): - uv_points = [uv for f in faces for uv in f.uv] - angle = geometry.box_fit_2d(uv_points) - - if angle != 0.0: - rotate_uvs(uv_points, angle) - - # orient them vertically (could be an option) - minx, miny, maxx, maxy = boundsIsland(faces) - w, h = maxx - minx, maxy - miny - # use epsilon so we don't randomly rotate (almost) perfect squares. - if h + 0.00001 < w: - from math import pi - angle = pi / 2.0 - rotate_uvs(uv_points, angle) - - -# Takes an island list and tries to find concave, hollow areas to pack smaller islands into. -def mergeUvIslands(islandList): - global USER_FILL_HOLES - global USER_FILL_HOLES_QUALITY - - # Pack islands to bottom LHS - # Sync with island - - # islandTotFaceArea = [] # A list of floats, each island area - # islandArea = [] # a list of tuples ( area, w,h) - - decoratedIslandList = [] - - islandIdx = len(islandList) - while islandIdx: - islandIdx -= 1 - minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx]) - w, h = maxx - minx, maxy - miny - - totFaceArea = 0 - offset = Vector((minx, miny)) - for f in islandList[islandIdx]: - for uv in f.uv: - uv -= offset - - totFaceArea += f.area - - islandBoundsArea = w * h - efficiency = abs(islandBoundsArea - totFaceArea) - - # UV Edge list used for intersections as well as unique points. - edges, uniqueEdgePoints = island2Edge(islandList[islandIdx]) - - decoratedIslandList.append([ - islandList[islandIdx], - totFaceArea, - efficiency, - islandBoundsArea, - w, - h, - edges, - uniqueEdgePoints, - ]) - - # Sort by island bounding box area, smallest face area first. - # no.. chance that to most simple edge loop first. - decoratedIslandListAreaSort = decoratedIslandList[:] - - decoratedIslandListAreaSort.sort(key=lambda A: A[3]) - - # sort by efficiency, Least Efficient first. - decoratedIslandListEfficSort = decoratedIslandList[:] - # decoratedIslandListEfficSort.sort(lambda A, B: cmp(B[2], A[2])) - - decoratedIslandListEfficSort.sort(key=lambda A: -A[2]) - - # ================================================== THESE CAN BE TWEAKED. - # This is a quality value for the number of tests. - # from 1 to 4, generic quality value is from 1 to 100 - USER_STEP_QUALITY = ((USER_FILL_HOLES_QUALITY - 1) / 25.0) + 1 - - # If 100 will test as long as there is enough free space. - # this is rarely enough, and testing takes a while, so lower quality speeds this up. - - # 1 means they have the same quality - USER_FREE_SPACE_TO_TEST_QUALITY = 1 + (((100 - USER_FILL_HOLES_QUALITY) / 100.0) * 5) - - # print 'USER_STEP_QUALITY', USER_STEP_QUALITY - # print 'USER_FREE_SPACE_TO_TEST_QUALITY', USER_FREE_SPACE_TO_TEST_QUALITY - - removedCount = 0 - - areaIslandIdx = 0 - ctrl = Window.Qual.CTRL - BREAK = False - while areaIslandIdx < len(decoratedIslandListAreaSort) and not BREAK: - sourceIsland = decoratedIslandListAreaSort[areaIslandIdx] - # Already packed? - if not sourceIsland[0]: - areaIslandIdx += 1 - else: - efficIslandIdx = 0 - while efficIslandIdx < len(decoratedIslandListEfficSort) and not BREAK: - - if Window.GetKeyQualifiers() & ctrl: - BREAK = True - break - - # Now we have 2 islands, if the efficiency of the islands lowers there's an - # increasing likely hood that we can fit merge into the bigger UV island. - # this ensures a tight fit. - - # Just use figures we have about user/unused area to see if they might fit. - - targetIsland = decoratedIslandListEfficSort[efficIslandIdx] - - if sourceIsland[0] == targetIsland[0] or\ - not targetIsland[0] or\ - not sourceIsland[0]: - pass - else: - - #~ ([island, totFaceArea, efficiency, islandArea, w,h]) - # Wasted space on target is greater then UV bounding island area. - - #~ if targetIsland[3] > (sourceIsland[2]) and\ # - # ~ print USER_FREE_SPACE_TO_TEST_QUALITY - if targetIsland[2] > (sourceIsland[1] * USER_FREE_SPACE_TO_TEST_QUALITY) and\ - targetIsland[4] > sourceIsland[4] and\ - targetIsland[5] > sourceIsland[5]: - - # DEBUG # print '%.10f %.10f' % (targetIsland[3], sourceIsland[1]) - - # These enough spare space lets move the box until it fits - - # How many times does the source fit into the target x/y - blockTestXUnit = targetIsland[4] / sourceIsland[4] - blockTestYUnit = targetIsland[5] / sourceIsland[5] - - boxLeft = 0 - - # Distance we can move between whilst staying inside the targets bounds. - testWidth = targetIsland[4] - sourceIsland[4] - testHeight = targetIsland[5] - sourceIsland[5] - - # Increment we move each test. x/y - xIncrement = (testWidth / (blockTestXUnit * ((USER_STEP_QUALITY / 50) + 0.1))) - yIncrement = (testHeight / (blockTestYUnit * ((USER_STEP_QUALITY / 50) + 0.1))) - - # Make sure were not moving less then a 3rg of our width/height - if xIncrement < sourceIsland[4] / 3: - xIncrement = sourceIsland[4] - if yIncrement < sourceIsland[5] / 3: - yIncrement = sourceIsland[5] - - boxLeft = 0 # Start 1 back so we can jump into the loop. - boxBottom = 0 # -yIncrement - - # ~ testcount= 0 - - while boxBottom <= testHeight: - # Should we use this? - not needed for now. - # ~ if Window.GetKeyQualifiers() & ctrl: - # ~ BREAK= True - # ~ break - - # testcount+=1 - # print 'Testing intersect' - Intersect = islandIntersectUvIsland( - sourceIsland, targetIsland, Vector((boxLeft, boxBottom))) - # print 'Done', Intersect - if Intersect == 1: # Line intersect, don't bother with this any more - pass - - if Intersect == 2: # Source inside target - """ - We have an intersection, if we are inside the target - then move us 1 whole width across, - Its possible this is a bad idea since 2 skinny Angular faces - could join without 1 whole move, but its a lot more optimal to speed this up - since we have already tested for it. - - It gives about 10% speedup with minimal errors. - """ - # Move the test along its width + SMALL_NUM - #boxLeft += sourceIsland[4] + SMALL_NUM - boxLeft += sourceIsland[4] - elif Intersect == 0: # No intersection?? Place it. - # Progress - removedCount += 1 -# XXX Window.DrawProgressBar(0.0, 'Merged: %i islands, Ctrl to finish early.' % removedCount) - - # Move faces into new island and offset - targetIsland[0].extend(sourceIsland[0]) - offset = Vector((boxLeft, boxBottom)) - - for f in sourceIsland[0]: - for uv in f.uv: - uv += offset - - del sourceIsland[0][:] # Empty - - # Move edge loop into new and offset. - # targetIsland[6].extend(sourceIsland[6]) - # while sourceIsland[6]: - targetIsland[6].extend([( - (e[0] + offset, e[1] + offset, e[2]) - ) for e in sourceIsland[6]]) - - del sourceIsland[6][:] # Empty - - # Sort by edge length, reverse so biggest are first. - - try: - targetIsland[6].sort(key=lambda A: A[2]) - except: - targetIsland[6].sort(lambda B, A: cmp(A[2], B[2])) - - targetIsland[7].extend(sourceIsland[7]) - offset = Vector((boxLeft, boxBottom, 0.0)) - for p in sourceIsland[7]: - p += offset - - del sourceIsland[7][:] - - # Decrement the efficiency - targetIsland[1] += sourceIsland[1] # Increment totFaceArea - targetIsland[2] -= sourceIsland[1] # Decrement efficiency - # IF we ever used these again, should set to 0, eg - sourceIsland[2] = 0 # No area if anyone wants to know - - break - - # INCREMENT NEXT LOCATION - if boxLeft > testWidth: - boxBottom += yIncrement - boxLeft = 0.0 - else: - boxLeft += xIncrement - # print testcount - - efficIslandIdx += 1 - areaIslandIdx += 1 - - # Remove empty islands - i = len(islandList) - while i: - i -= 1 - if not islandList[i]: - del islandList[i] # Can increment islands removed here. - -# Takes groups of faces. assumes face groups are UV groups. - - -def getUvIslands(faceGroups, me): - - # Get seams so we don't cross over seams - edge_seams = {} # should be a set - for ed in me.edges: - if ed.use_seam: - edge_seams[ed.key] = None # dummy var- use sets! - # Done finding seams - - islandList = [] - -# XXX Window.DrawProgressBar(0.0, 'Splitting %d projection groups into UV islands:' % len(faceGroups)) - # print '\tSplitting %d projection groups into UV islands:' % len(faceGroups), - # Find grouped faces - - faceGroupIdx = len(faceGroups) - - while faceGroupIdx: - faceGroupIdx -= 1 - faces = faceGroups[faceGroupIdx] - - if not faces: - continue - - # Build edge dict - edge_users = {} - - for i, f in enumerate(faces): - for ed_key in f.edge_keys: - if ed_key in edge_seams: # DELIMIT SEAMS! ;) - edge_users[ed_key] = [] # so as not to raise an error - else: - try: - edge_users[ed_key].append(i) - except: - edge_users[ed_key] = [i] - - # Modes - # 0 - face not yet touched. - # 1 - added to island list, and need to search - # 2 - touched and searched - don't touch again. - face_modes = [0] * len(faces) # initialize zero - untested. - - face_modes[0] = 1 # start the search with face 1 - - newIsland = [] - - newIsland.append(faces[0]) - - ok = True - while ok: - - ok = True - while ok: - ok = False - for i in range(len(faces)): - if face_modes[i] == 1: # search - for ed_key in faces[i].edge_keys: - for ii in edge_users[ed_key]: - if i != ii and face_modes[ii] == 0: - face_modes[ii] = ok = 1 # mark as searched - newIsland.append(faces[ii]) - - # mark as searched, don't look again. - face_modes[i] = 2 - - islandList.append(newIsland) - - ok = False - for i in range(len(faces)): - if face_modes[i] == 0: - newIsland = [] - newIsland.append(faces[i]) - - face_modes[i] = ok = 1 - break - # if not ok will stop looping - -# XXX Window.DrawProgressBar(0.1, 'Optimizing Rotation for %i UV Islands' % len(islandList)) - - for island in islandList: - optiRotateUvIsland(island) - - return islandList - - -def packIslands(islandList): - if USER_FILL_HOLES: - # XXX Window.DrawProgressBar(0.1, 'Merging Islands (Ctrl: skip merge)...') - mergeUvIslands(islandList) # Modify in place - - # Now we have UV islands, we need to pack them. - - # Make a synchronized list with the islands - # so we can box pack the islands. - packBoxes = [] - - # Keep a list of X/Y offset so we can save time by writing the - # uv's and packed data in one pass. - islandOffsetList = [] - - islandIdx = 0 - - while islandIdx < len(islandList): - minx, miny, maxx, maxy = boundsIsland(islandList[islandIdx]) - - w, h = maxx - minx, maxy - miny - - if USER_ISLAND_MARGIN: - minx -= USER_ISLAND_MARGIN * w / 2 - miny -= USER_ISLAND_MARGIN * h / 2 - maxx += USER_ISLAND_MARGIN * w / 2 - maxy += USER_ISLAND_MARGIN * h / 2 - - # recalc width and height - w, h = maxx - minx, maxy - miny - - if w < SMALL_NUM: - w = SMALL_NUM - if h < SMALL_NUM: - h = SMALL_NUM - - """Save the offset to be applied later, - we could apply to the UVs now and align them to the bottom left hand area - of the UV coords like the box packer imagines they are - but, its quicker just to remember their offset and - apply the packing and offset in 1 pass """ - islandOffsetList.append((minx, miny)) - - # Add to boxList. use the island idx for the BOX id. - packBoxes.append([0, 0, w, h]) - islandIdx += 1 - - # Now we have a list of boxes to pack that syncs - # with the islands. - - # print '\tPacking UV Islands...' -# XXX Window.DrawProgressBar(0.7, "Packing %i UV Islands..." % len(packBoxes) ) - - # time1 = time.time() - packWidth, packHeight = geometry.box_pack_2d(packBoxes) - - # print 'Box Packing Time:', time.time() - time1 - - # if len(packedLs) != len(islandList): - # raise ValueError("Packed boxes differs from original length") - - # print '\tWriting Packed Data to faces' -# XXX Window.DrawProgressBar(0.8, "Writing Packed Data to faces") - - # Sort by ID, so there in sync again - islandIdx = len(islandList) - # Having these here avoids divide by 0 - if islandIdx: - - if USER_STRETCH_ASPECT: - # Maximize to uv area?? Will write a normalize function. - xfactor = 1.0 / packWidth - yfactor = 1.0 / packHeight - else: - # Keep proportions. - xfactor = yfactor = 1.0 / max(packWidth, packHeight) - - while islandIdx: - islandIdx -= 1 - # Write the packed values to the UV's - - xoffset = packBoxes[islandIdx][0] - islandOffsetList[islandIdx][0] - yoffset = packBoxes[islandIdx][1] - islandOffsetList[islandIdx][1] - - for f in islandList[islandIdx]: # Offsetting the UV's so they fit in there packed box - for uv in f.uv: - uv.x = (uv.x + xoffset) * xfactor - uv.y = (uv.y + yoffset) * yfactor - - -def VectoQuat(vec): - vec = vec.normalized() - return vec.to_track_quat('Z', 'X' if abs(vec.x) > 0.5 else 'Y').inverted() - - -class thickface: - __slost__ = "v", "uv", "no", "area", "edge_keys" - - def __init__(self, face, uv_layer, mesh_verts): - self.v = [mesh_verts[i] for i in face.vertices] - self.uv = [uv_layer[i].uv for i in face.loop_indices] - - self.no = face.normal.copy() - self.area = face.area - self.edge_keys = face.edge_keys - - -def main_consts(): - from math import radians - - global ROTMAT_2D_POS_90D - global ROTMAT_2D_POS_45D - global RotMatStepRotation - - ROTMAT_2D_POS_90D = Matrix.Rotation(radians(90.0), 2) - ROTMAT_2D_POS_45D = Matrix.Rotation(radians(45.0), 2) - - RotMatStepRotation = [] - rot_angle = 22.5 # 45.0/2 - while rot_angle > 0.1: - RotMatStepRotation.append([ - Matrix.Rotation(radians(+rot_angle), 2), - Matrix.Rotation(radians(-rot_angle), 2), - ]) - - rot_angle = rot_angle / 2.0 - - -global ob -ob = None - - -def main(context, - island_margin, - projection_limit, - user_area_weight, - use_aspect, - stretch_to_bounds, - ): - global USER_FILL_HOLES - global USER_FILL_HOLES_QUALITY - global USER_STRETCH_ASPECT - global USER_ISLAND_MARGIN - - from math import cos - import time - - global dict_matrix - dict_matrix = {} - - # Constants: - # Takes a list of faces that make up a UV island and rotate - # until they optimally fit inside a square. - global ROTMAT_2D_POS_90D - global ROTMAT_2D_POS_45D - global RotMatStepRotation - main_consts() - - # Create the variables. - USER_PROJECTION_LIMIT = projection_limit - USER_ONLY_SELECTED_FACES = True - USER_SHARE_SPACE = 1 # Only for hole filling. - USER_STRETCH_ASPECT = stretch_to_bounds - USER_ISLAND_MARGIN = island_margin # Only for hole filling. - USER_FILL_HOLES = 0 - USER_FILL_HOLES_QUALITY = 50 # Only for hole filling. - USER_VIEW_INIT = 0 # Only for hole filling. - - is_editmode = (context.mode == 'EDIT_MESH') - if is_editmode: - obList = context.objects_in_mode_unique_data - else: - obList = [ - ob for ob in context.selected_editable_objects - if ob.type == 'MESH' and ob.data.library is None - ] - - if not is_editmode: - USER_ONLY_SELECTED_FACES = False - - if not obList: - raise Exception("error, no selected mesh objects") - - # Convert from being button types - USER_PROJECTION_LIMIT_CONVERTED = cos(USER_PROJECTION_LIMIT * DEG_TO_RAD) - USER_PROJECTION_LIMIT_HALF_CONVERTED = cos((USER_PROJECTION_LIMIT / 2) * DEG_TO_RAD) - - # Toggle Edit mode - if is_editmode: - bpy.ops.object.mode_set(mode='OBJECT') - # Assume face select mode! an annoying hack to toggle face select mode because Mesh doesn't like faceSelectMode. - - if USER_SHARE_SPACE: - # Sort by data name so we get consistent results - obList.sort(key=lambda ob: ob.data.name) - collected_islandList = [] - - time1 = time.time() - - # Tag as False so we don't operate on the same mesh twice. - for me in bpy.data.meshes: - me.tag = False - - for ob in obList: - me = ob.data - - if me.tag or me.library: - continue - - # Tag as used - me.tag = True - - if not me.uv_layers: # Mesh has no UV Coords, don't bother. - me.uv_layers.new() - - uv_layer = me.uv_layers.active.data - me_verts = list(me.vertices) - - if USER_ONLY_SELECTED_FACES: - meshFaces = [thickface(f, uv_layer, me_verts) for i, f in enumerate(me.polygons) if f.select] - else: - meshFaces = [thickface(f, uv_layer, me_verts) for i, f in enumerate(me.polygons)] - - # ======= - # Generate a projection list from face normals, this is meant to be smart :) - - # make a list of face props that are in sync with meshFaces - # Make a Face List that is sorted by area. - # meshFaces = [] - - # meshFaces.sort( lambda a, b: cmp(b.area , a.area) ) # Biggest first. - meshFaces.sort(key=lambda a: -a.area) - - # remove all zero area faces - while meshFaces and meshFaces[-1].area <= SMALL_NUM: - # Set their UV's to 0,0 - for uv in meshFaces[-1].uv: - uv.zero() - meshFaces.pop() - - if not meshFaces: - continue - - # Smallest first is slightly more efficient, - # but if the user cancels early then its better we work on the larger data. - - # Generate Projection Vecs - # 0d is 1.0 - # 180 IS -0.59846 - - # Initialize projectVecs - if USER_VIEW_INIT: - # Generate Projection - - # We add to this along the way - projectVecs = [Vector(Window.GetViewVector()) @ ob.matrix_world.inverted().to_3x3()] - else: - projectVecs = [] - - newProjectVec = meshFaces[0].no - newProjectMeshFaces = [] # Popping stuffs it up. - - # Pretend that the most unique angle is ages away to start the loop off - mostUniqueAngle = -1.0 - - # This is popped - tempMeshFaces = meshFaces[:] - - # This while only gathers projection vecs, faces are assigned later on. - while 1: - # If there's none there then start with the largest face - - # add all the faces that are close. - for fIdx in range(len(tempMeshFaces) - 1, -1, -1): - # Use half the angle limit so we don't overweight faces towards this - # normal and hog all the faces. - if newProjectVec.dot(tempMeshFaces[fIdx].no) > USER_PROJECTION_LIMIT_HALF_CONVERTED: - newProjectMeshFaces.append(tempMeshFaces.pop(fIdx)) - - # Add the average of all these faces normals as a projectionVec - averageVec = Vector((0.0, 0.0, 0.0)) - if user_area_weight == 0.0: - for fprop in newProjectMeshFaces: - averageVec += fprop.no - elif user_area_weight == 1.0: - for fprop in newProjectMeshFaces: - averageVec += fprop.no * fprop.area - else: - for fprop in newProjectMeshFaces: - averageVec += fprop.no * ((fprop.area * user_area_weight) + (1.0 - user_area_weight)) - - if averageVec.x != 0 or averageVec.y != 0 or averageVec.z != 0: # Avoid NAN - projectVecs.append(averageVec.normalized()) - - # Get the next vec! - # Pick the face that's most different to all existing angles :) - mostUniqueAngle = 1.0 # 1.0 is 0d. no difference. - mostUniqueIndex = 0 # dummy - - for fIdx in range(len(tempMeshFaces) - 1, -1, -1): - angleDifference = -1.0 # 180d difference. - - # Get the closest vec angle we are to. - for p in projectVecs: - temp_angle_diff = p.dot(tempMeshFaces[fIdx].no) - - if angleDifference < temp_angle_diff: - angleDifference = temp_angle_diff - - if angleDifference < mostUniqueAngle: - # We have a new most different angle - mostUniqueIndex = fIdx - mostUniqueAngle = angleDifference - - if mostUniqueAngle < USER_PROJECTION_LIMIT_CONVERTED: - # print 'adding', mostUniqueAngle, USER_PROJECTION_LIMIT, len(newProjectMeshFaces) - # Now weight the vector to all its faces, will give a more direct projection - # if the face its self was not representative of the normal from surrounding faces. - - newProjectVec = tempMeshFaces[mostUniqueIndex].no - newProjectMeshFaces = [tempMeshFaces.pop(mostUniqueIndex)] - - else: - if len(projectVecs) >= 1: # Must have at least 2 projections - break - - # If there are only zero area faces then its possible - # there are no projectionVecs - if not len(projectVecs): - Draw.PupMenu('error, no projection vecs where generated, 0 area faces can cause this.') - return - - faceProjectionGroupList = [[] for i in range(len(projectVecs))] - - # MAP and Arrange # We know there are 3 or 4 faces here - - for fIdx in range(len(meshFaces) - 1, -1, -1): - fvec = meshFaces[fIdx].no - i = len(projectVecs) - - # Initialize first - bestAng = fvec.dot(projectVecs[0]) - bestAngIdx = 0 - - # Cycle through the remaining, first already done - while i - 1: - i -= 1 - - newAng = fvec.dot(projectVecs[i]) - if newAng > bestAng: # Reverse logic for dotvecs - bestAng = newAng - bestAngIdx = i - - # Store the area for later use. - faceProjectionGroupList[bestAngIdx].append(meshFaces[fIdx]) - - # Cull faceProjectionGroupList, - - # Now faceProjectionGroupList is full of faces that face match the project Vecs list - for i in range(len(projectVecs)): - # Account for projectVecs having no faces. - if not faceProjectionGroupList[i]: - continue - - # Make a projection matrix from a unit length vector. - MatQuat = VectoQuat(projectVecs[i]) - - # Get the faces UV's from the projected vertex. - for f in faceProjectionGroupList[i]: - f_uv = f.uv - for j, v in enumerate(f.v): - f_uv[j][:] = (MatQuat @ v.co).xy - - if USER_SHARE_SPACE: - # Should we collect and pack later? - islandList = getUvIslands(faceProjectionGroupList, me) - collected_islandList.extend(islandList) - - else: - # Should we pack the islands for this 1 object? - islandList = getUvIslands(faceProjectionGroupList, me) - packIslands(islandList) - - # update the mesh here if we need to. - - # We want to pack all in 1 go, so pack now - if USER_SHARE_SPACE: - packIslands(collected_islandList) - - print("Smart Projection time: %.2f" % (time.time() - time1)) - - # aspect correction is only done in edit mode - and only smart unwrap supports currently - if is_editmode: - bpy.ops.object.mode_set(mode='EDIT') - - if use_aspect: - import bmesh - aspect = context.scene.uvedit_aspect(context.active_object) - if aspect[0] > aspect[1]: - aspect[0] = aspect[1] / aspect[0] - aspect[1] = 1.0 - else: - aspect[1] = aspect[0] / aspect[1] - aspect[0] = 1.0 - - bm = bmesh.from_edit_mesh(me) - - uv_act = bm.loops.layers.uv.active - - faces = [f for f in bm.faces if f.select] - - for f in faces: - for l in f.loops: - l[uv_act].uv[0] *= aspect[0] - l[uv_act].uv[1] *= aspect[1] - - dict_matrix.clear() - - -from bpy.props import FloatProperty, BoolProperty - - -class SmartProject(Operator): - """This script projection unwraps the selected faces of a mesh """ \ - """(it operates on all selected mesh objects, and can be used """ \ - """to unwrap selected faces, or all faces)""" - bl_idname = "uv.smart_project" - bl_label = "Smart UV Project" - bl_options = {'REGISTER', 'UNDO'} - - angle_limit: FloatProperty( - name="Angle Limit", - description="Lower for more projection groups, higher for less distortion", - min=1.0, max=89.0, - default=66.0, - ) - island_margin: FloatProperty( - name="Island Margin", - description="Margin to reduce bleed from adjacent islands", - min=0.0, max=1.0, - default=0.0, - ) - user_area_weight: FloatProperty( - name="Area Weight", - description="Weight projections vector by faces with larger areas", - min=0.0, max=1.0, - default=0.0, - ) - use_aspect: BoolProperty( - name="Correct Aspect", - description="Map UVs taking image aspect ratio into account", - default=True, - ) - stretch_to_bounds: BoolProperty( - name="Stretch to UV Bounds", - description="Stretch the final output to texture bounds", - default=True, - ) - - @classmethod - def poll(cls, context): - return context.active_object is not None - - def execute(self, context): - main(context, - self.island_margin, - self.angle_limit, - self.user_area_weight, - self.use_aspect, - self.stretch_to_bounds, - ) - return {'FINISHED'} - - def invoke(self, context, _event): - wm = context.window_manager - return wm.invoke_props_dialog(self) - - -classes = ( - SmartProject, -) diff --git a/source/blender/editors/uvedit/uvedit_intern.h b/source/blender/editors/uvedit/uvedit_intern.h index d5e7dd08fd1..d4f284a1619 100644 --- a/source/blender/editors/uvedit/uvedit_intern.h +++ b/source/blender/editors/uvedit/uvedit_intern.h @@ -108,6 +108,7 @@ void UV_OT_sphere_project(struct wmOperatorType *ot); void UV_OT_unwrap(struct wmOperatorType *ot); void UV_OT_rip(struct wmOperatorType *ot); void UV_OT_stitch(struct wmOperatorType *ot); +void UV_OT_smart_project(struct wmOperatorType *ot); /* uvedit_path.c */ void UV_OT_shortest_path_pick(struct wmOperatorType *ot); diff --git a/source/blender/editors/uvedit/uvedit_ops.c b/source/blender/editors/uvedit/uvedit_ops.c index e2368ae6ba8..ad98321c458 100644 --- a/source/blender/editors/uvedit/uvedit_ops.c +++ b/source/blender/editors/uvedit/uvedit_ops.c @@ -2094,6 +2094,7 @@ void ED_operatortypes_uvedit(void) WM_operatortype_append(UV_OT_reset); WM_operatortype_append(UV_OT_sphere_project); WM_operatortype_append(UV_OT_unwrap); + WM_operatortype_append(UV_OT_smart_project); WM_operatortype_append(UV_OT_reveal); WM_operatortype_append(UV_OT_hide); diff --git a/source/blender/editors/uvedit/uvedit_unwrap_ops.c b/source/blender/editors/uvedit/uvedit_unwrap_ops.c index 6e11f4f5be9..2f30ef1caa6 100644 --- a/source/blender/editors/uvedit/uvedit_unwrap_ops.c +++ b/source/blender/editors/uvedit/uvedit_unwrap_ops.c @@ -35,7 +35,10 @@ #include "DNA_scene_types.h" #include "BLI_alloca.h" +#include "BLI_array.h" +#include "BLI_linklist.h" #include "BLI_math.h" +#include "BLI_memarena.h" #include "BLI_string.h" #include "BLI_utildefines.h" #include "BLI_uvproject.h" @@ -1486,18 +1489,21 @@ static void correct_uv_aspect(Object *ob, BMEditMesh *em) /** \name UV Map Clip & Correct * \{ */ -static void uv_map_clip_correct_properties(wmOperatorType *ot) +static void uv_map_clip_correct_properties_ex(wmOperatorType *ot, bool clip_to_bounds) { RNA_def_boolean(ot->srna, "correct_aspect", 1, "Correct Aspect", "Map UVs taking image aspect ratio into account"); - RNA_def_boolean(ot->srna, - "clip_to_bounds", - 0, - "Clip to Bounds", - "Clip UV coordinates to bounds after unwrapping"); + /* Optional, since not all unwrapping types need to be clipped. */ + if (clip_to_bounds) { + RNA_def_boolean(ot->srna, + "clip_to_bounds", + 0, + "Clip to Bounds", + "Clip UV coordinates to bounds after unwrapping"); + } RNA_def_boolean(ot->srna, "scale_to_bounds", 0, @@ -1505,6 +1511,11 @@ static void uv_map_clip_correct_properties(wmOperatorType *ot) "Scale UV coordinates to bounds after unwrapping"); } +static void uv_map_clip_correct_properties(wmOperatorType *ot) +{ + uv_map_clip_correct_properties_ex(ot, true); +} + static void uv_map_clip_correct_multi(Object **objects, uint objects_len, wmOperator *op) { BMFace *efa; @@ -1513,7 +1524,8 @@ static void uv_map_clip_correct_multi(Object **objects, uint objects_len, wmOper MLoopUV *luv; float dx, dy, min[2], max[2]; const bool correct_aspect = RNA_boolean_get(op->ptr, "correct_aspect"); - const bool clip_to_bounds = RNA_boolean_get(op->ptr, "clip_to_bounds"); + const bool clip_to_bounds = (RNA_struct_find_property(op->ptr, "clip_to_bounds") && + RNA_boolean_get(op->ptr, "clip_to_bounds")); const bool scale_to_bounds = RNA_boolean_get(op->ptr, "scale_to_bounds"); INIT_MINMAX2(min, max); @@ -1846,6 +1858,365 @@ void UV_OT_unwrap(wmOperatorType *ot) /** \} */ /* -------------------------------------------------------------------- */ +/** \name Smart UV Project Operator + * \{ */ + +/* Ignore all areas below this, as the UV's get zeroed. */ +static const float smart_uv_project_area_ignore = 1e-12f; + +typedef struct ThickFace { + float area; + BMFace *efa; +} ThickFace; + +static int smart_uv_project_thickface_area_cmp_fn(const void *tf_a_p, const void *tf_b_p) +{ + + const ThickFace *tf_a = (ThickFace *)tf_a_p; + const ThickFace *tf_b = (ThickFace *)tf_b_p; + + /* Ignore the area of small faces. + * Also, order checks so `!isfinite(...)` values are counted as zero area. */ + if (!((tf_a->area > smart_uv_project_area_ignore) || + (tf_b->area > smart_uv_project_area_ignore))) { + return 0; + } + + if (tf_a->area < tf_b->area) { + return 1; + } + else if (tf_a->area > tf_b->area) { + return -1; + } + else { + return 0; + } +} + +static uint smart_uv_project_calculate_project_normals(const ThickFace *thick_faces, + const uint thick_faces_len, + BMesh *bm, + const float project_angle_limit_half_cos, + const float project_angle_limit_cos, + const float area_weight, + float (**r_project_normal_array)[3]) +{ + if (UNLIKELY(thick_faces_len == 0)) { + *r_project_normal_array = NULL; + return 0; + } + + const float *project_normal = thick_faces[0].efa->no; + + const ThickFace **project_thick_faces = NULL; + BLI_array_declare(project_thick_faces); + + float(*project_normal_array)[3] = NULL; + BLI_array_declare(project_normal_array); + + BM_mesh_elem_hflag_disable_all(bm, BM_FACE, BM_ELEM_TAG, false); + + while (true) { + for (int f_index = thick_faces_len - 1; f_index >= 0; f_index--) { + if (BM_elem_flag_test(thick_faces[f_index].efa, BM_ELEM_TAG)) { + continue; + } + + if (dot_v3v3(thick_faces[f_index].efa->no, project_normal) > project_angle_limit_half_cos) { + BLI_array_append(project_thick_faces, &thick_faces[f_index]); + BM_elem_flag_set(thick_faces[f_index].efa, BM_ELEM_TAG, true); + } + } + + float average_normal[3] = {0.0f, 0.0f, 0.0f}; + + if (area_weight <= 0.0f) { + for (int f_proj_index = 0; f_proj_index < BLI_array_len(project_thick_faces); + f_proj_index++) { + const ThickFace *tf = project_thick_faces[f_proj_index]; + add_v3_v3(average_normal, tf->efa->no); + } + } + else if (area_weight >= 1.0f) { + for (int f_proj_index = 0; f_proj_index < BLI_array_len(project_thick_faces); + f_proj_index++) { + const ThickFace *tf = project_thick_faces[f_proj_index]; + madd_v3_v3fl(average_normal, tf->efa->no, tf->area); + } + } + else { + for (int f_proj_index = 0; f_proj_index < BLI_array_len(project_thick_faces); + f_proj_index++) { + const ThickFace *tf = project_thick_faces[f_proj_index]; + const float area_blend = (tf->area * area_weight) + (1.0f - area_weight); + madd_v3_v3fl(average_normal, tf->efa->no, area_blend); + } + } + + /* Avoid NAN. */ + if (normalize_v3(average_normal) != 0.0f) { + float(*normal)[3] = BLI_array_append_ret(project_normal_array); + copy_v3_v3(*normal, average_normal); + } + + /* Find the most unique angle that points away from other normals. */ + float anble_best = 1.0f; + uint angle_best_index = 0; + + for (int f_index = thick_faces_len - 1; f_index >= 0; f_index--) { + if (BM_elem_flag_test(thick_faces[f_index].efa, BM_ELEM_TAG)) { + continue; + } + + float angle_test = -1.0f; + for (int p_index = 0; p_index < BLI_array_len(project_normal_array); p_index++) { + angle_test = max_ff(angle_test, + dot_v3v3(project_normal_array[p_index], thick_faces[f_index].efa->no)); + } + + if (angle_test < anble_best) { + anble_best = angle_test; + angle_best_index = f_index; + } + } + + if (anble_best < project_angle_limit_cos) { + project_normal = thick_faces[angle_best_index].efa->no; + BLI_array_clear(project_thick_faces); + BLI_array_append(project_thick_faces, &thick_faces[angle_best_index]); + BM_elem_flag_enable(thick_faces[angle_best_index].efa, BM_ELEM_TAG); + } + else { + if (BLI_array_len(project_normal_array) >= 1) { + break; + } + } + } + + BLI_array_free(project_thick_faces); + BM_mesh_elem_hflag_disable_all(bm, BM_FACE, BM_ELEM_TAG, false); + + *r_project_normal_array = project_normal_array; + return BLI_array_len(project_normal_array); +} + +static int smart_project_exec(bContext *C, wmOperator *op) +{ + Scene *scene = CTX_data_scene(C); + ViewLayer *view_layer = CTX_data_view_layer(C); + + /* May be NULL. */ + View3D *v3d = CTX_wm_view3d(C); + + const float project_angle_limit = RNA_float_get(op->ptr, "angle_limit"); + const float island_margin = RNA_float_get(op->ptr, "island_margin"); + const float area_weight = RNA_float_get(op->ptr, "area_weight"); + + const float project_angle_limit_cos = cosf(project_angle_limit); + const float project_angle_limit_half_cos = cosf(project_angle_limit / 2); + + /* Memory arena for list links (cleared for each object). */ + MemArena *arena = BLI_memarena_new(BLI_MEMARENA_STD_BUFSIZE, __func__); + + uint objects_len = 0; + Object **objects = BKE_view_layer_array_from_objects_in_edit_mode_unique_data_with_uvs( + view_layer, v3d, &objects_len); + + Object **objects_changed = MEM_mallocN(sizeof(*objects_changed) * objects_len, __func__); + uint object_changed_len = 0; + + BMFace *efa; + BMIter iter; + uint ob_index; + + for (ob_index = 0; ob_index < objects_len; ob_index++) { + Object *obedit = objects[ob_index]; + BMEditMesh *em = BKE_editmesh_from_object(obedit); + bool changed = false; + + const uint cd_loop_uv_offset = CustomData_get_offset(&em->bm->ldata, CD_MLOOPUV); + ThickFace *thick_faces = MEM_mallocN(sizeof(*thick_faces) * em->bm->totface, __func__); + + uint thick_faces_len = 0; + BM_ITER_MESH (efa, &iter, em->bm, BM_FACES_OF_MESH) { + if (!BM_elem_flag_test(efa, BM_ELEM_SELECT)) { + continue; + } + thick_faces[thick_faces_len].area = BM_face_calc_area(efa); + thick_faces[thick_faces_len].efa = efa; + thick_faces_len++; + } + + qsort(thick_faces, thick_faces_len, sizeof(ThickFace), smart_uv_project_thickface_area_cmp_fn); + + /* Remove all zero area faces. */ + while ((thick_faces_len > 0) && + !(thick_faces[thick_faces_len - 1].area > smart_uv_project_area_ignore)) { + + /* Zero UV's so they don't overlap with other faces being unwrapped. */ + BMIter liter; + BMLoop *l; + BM_ITER_ELEM (l, &liter, thick_faces[thick_faces_len - 1].efa, BM_LOOPS_OF_FACE) { + MLoopUV *luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset); + zero_v2(luv->uv); + changed = true; + } + + thick_faces_len -= 1; + } + + float(*project_normal_array)[3] = NULL; + int project_normals_len = smart_uv_project_calculate_project_normals( + thick_faces, + thick_faces_len, + em->bm, + project_angle_limit_half_cos, + project_angle_limit_cos, + area_weight, + &project_normal_array); + + if (project_normals_len == 0) { + MEM_freeN(thick_faces); + BLI_assert(project_normal_array == NULL); + continue; + } + + /* After finding projection vectors, we find the uv positions. */ + LinkNode **thickface_project_groups = MEM_callocN( + sizeof(*thickface_project_groups) * project_normals_len, __func__); + + BLI_memarena_clear(arena); + + for (int f_index = thick_faces_len - 1; f_index >= 0; f_index--) { + const float *f_normal = thick_faces[f_index].efa->no; + + float angle_best = dot_v3v3(f_normal, project_normal_array[0]); + uint angle_best_index = 0; + + for (int p_index = 1; p_index < project_normals_len; p_index++) { + const float angle_test = dot_v3v3(f_normal, project_normal_array[p_index]); + if (angle_test > angle_best) { + angle_best = angle_test; + angle_best_index = p_index; + } + } + + BLI_linklist_prepend_arena( + &thickface_project_groups[angle_best_index], &thick_faces[f_index], arena); + } + + for (int p_index = 0; p_index < project_normals_len; p_index++) { + if (thickface_project_groups[p_index] == NULL) { + continue; + } + + float axis_mat[3][3]; + axis_dominant_v3_to_m3_negate(axis_mat, project_normal_array[p_index]); + + for (LinkNode *list = thickface_project_groups[p_index]; list; list = list->next) { + ThickFace *tf = list->link; + BMIter liter; + BMLoop *l; + BM_ITER_ELEM (l, &liter, tf->efa, BM_LOOPS_OF_FACE) { + MLoopUV *luv = BM_ELEM_CD_GET_VOID_P(l, cd_loop_uv_offset); + mul_v2_m3v3(luv->uv, axis_mat, l->v->co); + } + changed = true; + } + } + + MEM_freeN(thick_faces); + MEM_freeN(project_normal_array); + + /* No need to free the lists in 'thickface_project_groups' values as the 'arena' is used. */ + MEM_freeN(thickface_project_groups); + + if (changed) { + objects_changed[object_changed_len] = objects[ob_index]; + object_changed_len += 1; + } + } + + BLI_memarena_free(arena); + + MEM_freeN(objects); + + /* Pack islands & Stretch to UV bounds */ + if (object_changed_len > 0) { + + scene->toolsettings->uvcalc_margin = island_margin; + const UnwrapOptions options = { + .topology_from_uvs = true, + .only_selected_faces = true, + .only_selected_uvs = false, + .fill_holes = true, + .correct_aspect = false, + }; + + /* Depsgraph refresh functions are called here. */ + uvedit_pack_islands_multi(scene, objects_changed, object_changed_len, &options, true, false); + uv_map_clip_correct_multi(objects_changed, object_changed_len, op); + } + + MEM_freeN(objects_changed); + + return OPERATOR_FINISHED; +} + +void UV_OT_smart_project(wmOperatorType *ot) +{ + PropertyRNA *prop; + + /* identifiers */ + ot->name = "Smart UV Project"; + ot->idname = "UV_OT_smart_project"; + ot->description = "Projection unwraps the selected faces of mesh objects"; + + ot->flag = OPTYPE_REGISTER | OPTYPE_UNDO; + + /* api callbacks */ + ot->exec = smart_project_exec; + ot->poll = ED_operator_uvmap; + ot->invoke = WM_operator_props_popup_confirm; + + /* properties */ + prop = RNA_def_float_rotation(ot->srna, + "angle_limit", + 0, + NULL, + DEG2RADF(0.0f), + DEG2RADF(90.0f), + "Angle Limit", + "Lower for more projection groups, higher for less distortion", + DEG2RADF(0.0f), + DEG2RADF(89.0f)); + RNA_def_property_float_default(prop, DEG2RADF(66.0f)); + + RNA_def_float(ot->srna, + "island_margin", + 0.0f, + 0.0f, + 1.0f, + "Island Margin", + "Margin to reduce bleed from adjacent islands", + 0.0f, + 1.0f); + RNA_def_float(ot->srna, + "area_weight", + 0.0f, + 0.0f, + 1.0f, + "Area Weight", + "Weight projections vector by faces with larger areas", + 0.0f, + 1.0f); + + uv_map_clip_correct_properties_ex(ot, false); +} + +/** \} */ + +/* -------------------------------------------------------------------- */ /** \name Project UV From View Operator * \{ */ |