From 20daaeffce4cf9bfe48ab7c84cb9e2b1d71d2c91 Mon Sep 17 00:00:00 2001 From: Chris Blackbourn Date: Wed, 7 Sep 2022 13:22:45 +1200 Subject: UV: add new operator, uvcalc_align_rotation Adds a new operator to automatically rotate UV Islands into alignment. Modes: * Auto (All edges) * Geometry (V direction will point in geometry direction) [1] * Edge (Rotate until selected edge is in V direction) Also adds uv_sync_selection support to UV Randomize Transform. Resolves: T78399 Differential Revision: https://developer.blender.org/D15820 [1] Listed as "World" in Task description. --- release/scripts/startup/bl_operators/__init__.py | 2 +- .../bl_operators/uvcalc_randomize_transform.py | 212 ---------- .../startup/bl_operators/uvcalc_transform.py | 436 +++++++++++++++++++++ release/scripts/startup/bl_ui/space_image.py | 1 + 4 files changed, 438 insertions(+), 213 deletions(-) delete mode 100644 release/scripts/startup/bl_operators/uvcalc_randomize_transform.py create mode 100644 release/scripts/startup/bl_operators/uvcalc_transform.py diff --git a/release/scripts/startup/bl_operators/__init__.py b/release/scripts/startup/bl_operators/__init__.py index 6f61d7e7129..de0b7798072 100644 --- a/release/scripts/startup/bl_operators/__init__.py +++ b/release/scripts/startup/bl_operators/__init__.py @@ -31,7 +31,7 @@ _modules = [ "userpref", "uvcalc_follow_active", "uvcalc_lightmap", - "uvcalc_randomize_transform", + "uvcalc_transform", "vertexpaint_dirt", "view3d", "wm", diff --git a/release/scripts/startup/bl_operators/uvcalc_randomize_transform.py b/release/scripts/startup/bl_operators/uvcalc_randomize_transform.py deleted file mode 100644 index 2867164a72e..00000000000 --- a/release/scripts/startup/bl_operators/uvcalc_randomize_transform.py +++ /dev/null @@ -1,212 +0,0 @@ -# SPDX-License-Identifier: GPL-2.0-or-later - -from bpy.types import Operator -from mathutils import Vector - -import math - - -def get_random_transform(transform_params, entropy): - from random import uniform - from random import seed as random_seed - - (seed, loc, rot, scale, scale_even) = transform_params - - # First, seed the RNG. - random_seed(seed + entropy) - - # Next, call uniform a known number of times. - offset_u = uniform(0, 1) - offset_v = uniform(0, 1) - angle = uniform(0, 1) - scale_u = uniform(0, 1) - scale_v = uniform(0, 1) - - # Apply the transform_params. - if loc: - offset_u *= loc[0] - offset_v *= loc[1] - else: - offset_u = 0 - offset_v = 0 - - if rot: - angle *= rot - else: - angle = 0 - - if scale: - scale_u = scale_u * (2 * scale[0] - 2.0) + 2.0 - scale[0] - scale_v = scale_v * (2 * scale[1] - 2.0) + 2.0 - scale[1] - else: - scale_u = 1 - scale_v = 1 - - if scale_even: - scale_v = scale_u - - # Results in homogenous co-ordinates. - return [[scale_u * math.cos(angle), -scale_v * math.sin(angle), offset_u], - [scale_u * math.sin(angle), scale_v * math.cos(angle), offset_v]] - - -def randomize_uv_transform_island(bm, uv_layer, faces, transform_params): - # Ensure consistent random values for island, regardless of selection etc. - entropy = min(f.index for f in faces) - - transform = get_random_transform(transform_params, entropy) - - # Find bounding box. - minmax = [1e30, 1e30, -1e30, -1e30] - for face in faces: - for loop in face.loops: - u, v = loop[uv_layer].uv - minmax[0] = min(minmax[0], u) - minmax[1] = min(minmax[1], v) - minmax[2] = max(minmax[2], u) - minmax[3] = max(minmax[3], v) - - mid_u = (minmax[0] + minmax[2]) / 2 - mid_v = (minmax[1] + minmax[3]) / 2 - - del_u = transform[0][2] + mid_u - transform[0][0] * mid_u - transform[0][1] * mid_v - del_v = transform[1][2] + mid_v - transform[1][0] * mid_u - transform[1][1] * mid_v - - # Apply transform. - for face in faces: - for loop in face.loops: - pre_uv = loop[uv_layer].uv - u = transform[0][0] * pre_uv[0] + transform[0][1] * pre_uv[1] + del_u - v = transform[1][0] * pre_uv[0] + transform[1][1] * pre_uv[1] + del_v - loop[uv_layer].uv = (u, v) - - -def is_face_uv_selected(face, uv_layer): - for loop in face.loops: - if not loop[uv_layer].select: - return False - return True - - -def is_island_uv_selected(bm, island, uv_layer): - for face in island: - if is_face_uv_selected(face, uv_layer): - return True - return False - - -def randomize_uv_transform_bmesh(mesh, bm, transform_params): - import bpy_extras.bmesh_utils - uv_layer = bm.loops.layers.uv.verify() - islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer) - for island in islands: - if is_island_uv_selected(bm, island, uv_layer): - randomize_uv_transform_island(bm, uv_layer, island, transform_params) - - -def randomize_uv_transform(context, transform_params): - import bmesh - ob_list = context.objects_in_mode_unique_data - for ob in ob_list: - bm = bmesh.from_edit_mesh(ob.data) - if not bm.loops.layers.uv: - continue - - # Only needed to access the minimum face index of each island. - bm.faces.index_update() - randomize_uv_transform_bmesh(ob.data, bm, transform_params) - - for ob in ob_list: - bmesh.update_edit_mesh(ob.data) - - return {'FINISHED'} - - -from bpy.props import ( - BoolProperty, - FloatProperty, - FloatVectorProperty, - IntProperty, -) - - -class RandomizeUVTransform(Operator): - """Randomize uv island's location, rotation, and scale""" - bl_idname = "uv.randomize_uv_transform" - bl_label = "Randomize" - bl_options = {'REGISTER', 'UNDO'} - - random_seed: IntProperty( - name="Random Seed", - description="Seed value for the random generator", - min=0, - max=10000, - default=0, - ) - use_loc: BoolProperty( - name="Randomize Location", - description="Randomize the location values", - default=True, - ) - loc: FloatVectorProperty( - name="Location", - description=("Maximum distance the objects " - "can spread over each axis"), - min=-100.0, - max=100.0, - size=2, - subtype='TRANSLATION', - default=(0.0, 0.0), - ) - use_rot: BoolProperty( - name="Randomize Rotation", - description="Randomize the rotation value", - default=True, - ) - rot: FloatProperty( - name="Rotation", - description="Maximum rotation", - min=-2 * math.pi, - max=2 * math.pi, - subtype='ANGLE', - default=0.0, - ) - use_scale: BoolProperty( - name="Randomize Scale", - description="Randomize the scale values", - default=True, - ) - scale_even: BoolProperty( - name="Scale Even", - description="Use the same scale value for both axes", - default=False, - ) - - scale: FloatVectorProperty( - name="Scale", - description="Maximum scale randomization over each axis", - min=-100.0, - max=100.0, - default=(1.0, 1.0), - size=2, - ) - - @classmethod - def poll(cls, context): - return context.mode == 'EDIT_MESH' - - def execute(self, context): - seed = self.random_seed - - loc = [0, 0] if not self.use_loc else self.loc - rot = 0 if not self.use_rot else self.rot - scale = None if not self.use_scale else self.scale - scale_even = self.scale_even - - transformParams = [seed, loc, rot, scale, scale_even] - return randomize_uv_transform(context, transformParams) - - -classes = ( - RandomizeUVTransform, -) diff --git a/release/scripts/startup/bl_operators/uvcalc_transform.py b/release/scripts/startup/bl_operators/uvcalc_transform.py new file mode 100644 index 00000000000..093aea4eaa8 --- /dev/null +++ b/release/scripts/startup/bl_operators/uvcalc_transform.py @@ -0,0 +1,436 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +from bpy.types import Operator +from mathutils import Matrix,Vector + +import math + +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, + FloatVectorProperty, + IntProperty, +) + + +def is_face_uv_selected(face, uv_layer, any_edge): + """ + Returns True if the face is UV selected. + + :arg face: the face to query. + :type bmesh: :class:`BMFace` + :arg uv_layer: the UV layer to source UVs from. + :type bmesh: :class:`BMLayerItem` + :arg any_edge: use edge selection instead of vertex selection. + :type any_edge: bool + :return: True if the face is UV selected. + :rtype: bool + """ + + if not face.select: # Geometry selection + return False + + import bpy + if bpy.context.tool_settings.use_uv_select_sync: + # In sync selection mode, UV selection comes solely from geometry selection. + return True + + if any_edge: + for loop in face.loops: + if loop[uv_layer].select_edge: + return True + return False + + for loop in face.loops: + if not loop[uv_layer].select: + return False + return True + + +def is_island_uv_selected(island, uv_layer, any_edge): + """ + Returns True if the island is UV selected. + + :arg island: list of faces to query. + :arg uv_layer: the UV layer to source UVs from. + :type bmesh: :class:`BMLayerItem` + :arg any_edge: use edge selection instead of vertex selection. + :type any_edge: bool + :return: list of lists containing polygon indices. + :rtype: bool + """ + for face in island: + if is_face_uv_selected(face, uv_layer, any_edge): + return True + return False + + +def find_rotation_auto(bm, uv_layer, faces): + sum_u = 0.0 + sum_v = 0.0 + for face in faces: + prev_uv = face.loops[-1][uv_layer].uv + for loop in face.loops: + uv = loop[uv_layer].uv + du = uv[0] - prev_uv[0] + dv = uv[1] - prev_uv[1] + edge_angle = math.atan2(dv, du) + edge_angle *= 4.0 # Wrap 4 times around the circle + sum_u += math.cos(edge_angle) + sum_v += math.sin(edge_angle) + prev_uv = uv + + # Compute angle. + return -math.atan2(sum_v, sum_u) / 4.0 + + +def find_rotation_edge(bm, uv_layer, faces): + sum_u = 0.0 + sum_v = 0.0 + for face in faces: + prev_uv = face.loops[-1][uv_layer].uv + prev_select = face.loops[-1][uv_layer].select_edge + for loop in face.loops: + uv = loop[uv_layer].uv + if prev_select: + du = uv[0] - prev_uv[0] + dv = uv[1] - prev_uv[1] + edge_angle = math.atan2(dv, du) + edge_angle *= 2.0 # Wrap 2 times around the circle + sum_u += math.cos(edge_angle) + sum_v += math.sin(edge_angle) + + prev_uv = uv + prev_select = loop[uv_layer].select_edge + + # Add 90 degrees to align along V co-ordinate. + # Twice, because we divide by two. + sum_u, sum_v = -sum_u, -sum_v + + # Compute angle. + return -math.atan2(sum_v, sum_u) / 2.0 + + +def find_rotation_geometry(bm, uv_layer, faces, method, axis): + sum_u_co = Vector((0.0, 0.0, 0.0)) + sum_v_co = Vector((0.0, 0.0, 0.0)) + for face in faces: + # Triangulate. + for fan in range(2, len(face.loops)): + delta_uv0 = face.loops[fan - 1][uv_layer].uv - face.loops[0][uv_layer].uv + delta_uv1 = face.loops[fan][uv_layer].uv - face.loops[0][uv_layer].uv + + mat = Matrix((delta_uv0, delta_uv1)) + mat.invert_safe() + + delta_co0 = face.loops[fan - 1].vert.co - face.loops[0].vert.co + delta_co1 = face.loops[fan].vert.co - face.loops[0].vert.co + w = delta_co0.cross(delta_co1).length + # U direction in geometry co-ordinates. + sum_u_co += (delta_co0 * mat[0][0] + delta_co1 * mat[0][1]) * w + # V direction in geometry co-ordinates. + sum_v_co += (delta_co0 * mat[1][0] + delta_co1 * mat[1][1]) * w + + if axis == 'X': + axis_index = 0 + elif axis == 'Y': + axis_index = 1 + elif axis == 'Z': + axis_index = 2 + + # Compute angle. + return math.atan2(sum_u_co[axis_index], sum_v_co[axis_index]) + + +def align_uv_rotation_island(bm, uv_layer, faces, method, axis): + angle = 0.0 + if method == 'AUTO': + angle = find_rotation_auto(bm, uv_layer, faces) + elif method == 'EDGE': + angle = find_rotation_edge(bm, uv_layer, faces) + elif method == 'GEOMETRY': + angle = find_rotation_geometry(bm, uv_layer, faces, method, axis) + + if angle == 0.0: + return False # No change. + + # Find bounding box. + minmax = [1e30, 1e30, -1e30, -1e30] + for face in faces: + for loop in face.loops: + u, v = loop[uv_layer].uv + minmax[0] = min(minmax[0], u) + minmax[1] = min(minmax[1], v) + minmax[2] = max(minmax[2], u) + minmax[3] = max(minmax[3], v) + + mid_u = (minmax[0] + minmax[2]) / 2.0 + mid_v = (minmax[1] + minmax[3]) / 2.0 + + cos_angle = math.cos(angle) + sin_angle = math.sin(angle) + + delta_u = mid_u - cos_angle * mid_u + sin_angle * mid_v + delta_v = mid_v - sin_angle * mid_u - cos_angle * mid_v + + # Apply transform. + for face in faces: + for loop in face.loops: + pre_uv = loop[uv_layer].uv + u = cos_angle * pre_uv[0] - sin_angle * pre_uv[1] + delta_u + v = sin_angle * pre_uv[0] + cos_angle * pre_uv[1] + delta_v + loop[uv_layer].uv = u, v + + return True + + +def align_uv_rotation_bmesh(mesh, bm, method, axis): + import bpy_extras.bmesh_utils + + uv_layer = bm.loops.layers.uv.active + if not uv_layer: + return False + + islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer) + changed = False + for island in islands: + if is_island_uv_selected(island, uv_layer, method == 'EDGE'): + if align_uv_rotation_island(bm, uv_layer, island, method, axis): + changed = True + return changed + + +def align_uv_rotation(context, method, axis): + import bmesh + ob_list = context.objects_in_mode_unique_data + for ob in ob_list: + bm = bmesh.from_edit_mesh(ob.data) + if bm.loops.layers.uv: + if align_uv_rotation_bmesh(ob.data, bm, method, axis): + bmesh.update_edit_mesh(ob.data) + + return {'FINISHED'} + + +class AlignUVRotation(Operator): + """Align uv island's rotation""" + bl_idname = "uv.align_rotation" + bl_label = "Align Rotation" + bl_options = {'REGISTER', 'UNDO'} + + method: EnumProperty( + name="Method", description="Method to calculate rotation angle", + items=( + ('AUTO', "Auto", "Align from all edges"), + ('EDGE', "Edge", "Only selected edges"), + ('GEOMETRY', "Geometry", "Align to Geometry axis"), + ), + ) + + axis: EnumProperty( + name="Axis", description="Axis to align to", + items=( + ('X', "X", "X axis"), + ('Y', "Y", "Y axis"), + ('Z', "Z", "Z axis"), + ), + ) + + def execute(self, context): + return align_uv_rotation(context, self.method, self.axis) + + def draw(self, _context): + layout = self.layout + layout.prop(self, "method") + if self.method == 'GEOMETRY': + layout.prop(self, "axis") + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + +def get_random_transform(transform_params, entropy): + from random import uniform + from random import seed as random_seed + + (seed, loc, rot, scale, scale_even) = transform_params + + # First, seed the RNG. + random_seed(seed + entropy) + + # Next, call uniform a known number of times. + offset_u = uniform(0, 1) + offset_v = uniform(0, 1) + angle = uniform(0, 1) + scale_u = uniform(0, 1) + scale_v = uniform(0, 1) + + # Apply the transform_params. + if loc: + offset_u *= loc[0] + offset_v *= loc[1] + else: + offset_u = 0 + offset_v = 0 + + if rot: + angle *= rot + else: + angle = 0 + + if scale: + scale_u = scale_u * (2 * scale[0] - 2.0) + 2.0 - scale[0] + scale_v = scale_v * (2 * scale[1] - 2.0) + 2.0 - scale[1] + else: + scale_u = 1 + scale_v = 1 + + if scale_even: + scale_v = scale_u + + # Results in homogenous co-ordinates. + return [[scale_u * math.cos(angle), -scale_v * math.sin(angle), offset_u], + [scale_u * math.sin(angle), scale_v * math.cos(angle), offset_v]] + + +def randomize_uv_transform_island(bm, uv_layer, faces, transform_params): + # Ensure consistent random values for island, regardless of selection etc. + entropy = min(f.index for f in faces) + + transform = get_random_transform(transform_params, entropy) + + # Find bounding box. + minmax = [1e30, 1e30, -1e30, -1e30] + for face in faces: + for loop in face.loops: + u, v = loop[uv_layer].uv + minmax[0] = min(minmax[0], u) + minmax[1] = min(minmax[1], v) + minmax[2] = max(minmax[2], u) + minmax[3] = max(minmax[3], v) + + mid_u = (minmax[0] + minmax[2]) / 2 + mid_v = (minmax[1] + minmax[3]) / 2 + + del_u = transform[0][2] + mid_u - transform[0][0] * mid_u - transform[0][1] * mid_v + del_v = transform[1][2] + mid_v - transform[1][0] * mid_u - transform[1][1] * mid_v + + # Apply transform. + for face in faces: + for loop in face.loops: + pre_uv = loop[uv_layer].uv + u = transform[0][0] * pre_uv[0] + transform[0][1] * pre_uv[1] + del_u + v = transform[1][0] * pre_uv[0] + transform[1][1] * pre_uv[1] + del_v + loop[uv_layer].uv = (u, v) + + +def randomize_uv_transform_bmesh(mesh, bm, transform_params): + import bpy_extras.bmesh_utils + uv_layer = bm.loops.layers.uv.verify() + islands = bpy_extras.bmesh_utils.bmesh_linked_uv_islands(bm, uv_layer) + for island in islands: + if is_island_uv_selected(island, uv_layer, False): + randomize_uv_transform_island(bm, uv_layer, island, transform_params) + + +def randomize_uv_transform(context, transform_params): + import bmesh + ob_list = context.objects_in_mode_unique_data + for ob in ob_list: + bm = bmesh.from_edit_mesh(ob.data) + if not bm.loops.layers.uv: + continue + + # Only needed to access the minimum face index of each island. + bm.faces.index_update() + randomize_uv_transform_bmesh(ob.data, bm, transform_params) + + for ob in ob_list: + bmesh.update_edit_mesh(ob.data) + + return {'FINISHED'} + + +class RandomizeUVTransform(Operator): + """Randomize uv island's location, rotation, and scale""" + bl_idname = "uv.randomize_uv_transform" + bl_label = "Randomize" + bl_options = {'REGISTER', 'UNDO'} + + random_seed: IntProperty( + name="Random Seed", + description="Seed value for the random generator", + min=0, + max=10000, + default=0, + ) + use_loc: BoolProperty( + name="Randomize Location", + description="Randomize the location values", + default=True, + ) + loc: FloatVectorProperty( + name="Location", + description=("Maximum distance the objects " + "can spread over each axis"), + min=-100.0, + max=100.0, + size=2, + subtype='TRANSLATION', + default=(0.0, 0.0), + ) + use_rot: BoolProperty( + name="Randomize Rotation", + description="Randomize the rotation value", + default=True, + ) + rot: FloatProperty( + name="Rotation", + description="Maximum rotation", + min=-2 * math.pi, + max=2 * math.pi, + subtype='ANGLE', + default=0.0, + ) + use_scale: BoolProperty( + name="Randomize Scale", + description="Randomize the scale values", + default=True, + ) + scale_even: BoolProperty( + name="Scale Even", + description="Use the same scale value for both axes", + default=False, + ) + + scale: FloatVectorProperty( + name="Scale", + description="Maximum scale randomization over each axis", + min=-100.0, + max=100.0, + default=(1.0, 1.0), + size=2, + ) + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def execute(self, context): + seed = self.random_seed + + loc = [0, 0] if not self.use_loc else self.loc + rot = 0 if not self.use_rot else self.rot + scale = None if not self.use_scale else self.scale + scale_even = self.scale_even + + transformParams = [seed, loc, rot, scale, scale_even] + return randomize_uv_transform(context, transformParams) + + +classes = ( + AlignUVRotation, + RandomizeUVTransform, +) diff --git a/release/scripts/startup/bl_ui/space_image.py b/release/scripts/startup/bl_ui/space_image.py index 4165f6ab0cf..0b26f0b1203 100644 --- a/release/scripts/startup/bl_ui/space_image.py +++ b/release/scripts/startup/bl_ui/space_image.py @@ -436,6 +436,7 @@ class IMAGE_MT_uvs(Menu): layout.operator("uv.minimize_stretch") layout.operator("uv.stitch") layout.menu("IMAGE_MT_uvs_align") + layout.operator("uv.align_rotation") layout.separator() -- cgit v1.2.3