From de570dc87ed17cae2d2d1ed4347793c440266b4b Mon Sep 17 00:00:00 2001 From: Chris Blackbourn Date: Thu, 25 Aug 2022 17:59:39 +1200 Subject: Fix T78406: create uv randomize islands operator Implement a new operator to randomize the scale, rotation and offset of selected UV islands. --- release/scripts/modules/bpy_extras/bmesh_utils.py | 58 ++++++ release/scripts/startup/bl_operators/__init__.py | 1 + .../bl_operators/uvcalc_randomize_transform.py | 210 +++++++++++++++++++++ release/scripts/startup/bl_ui/space_image.py | 4 + 4 files changed, 273 insertions(+) create mode 100644 release/scripts/modules/bpy_extras/bmesh_utils.py create mode 100644 release/scripts/startup/bl_operators/uvcalc_randomize_transform.py diff --git a/release/scripts/modules/bpy_extras/bmesh_utils.py b/release/scripts/modules/bpy_extras/bmesh_utils.py new file mode 100644 index 00000000000..baf1f9d863f --- /dev/null +++ b/release/scripts/modules/bpy_extras/bmesh_utils.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +__all__ = ( + "bmesh_linked_uv_islands", +) + +import bmesh + +def match_uv(face, vert, uv, uv_layer): + for loop in face.loops: + if loop.vert == vert: + return uv == loop[uv_layer].uv + return False + + +def bmesh_linked_uv_islands(bm, uv_layer): + """ + Returns lists of face indices connected by UV islands. + + For `bpy.types.Mesh`, use `mesh_linked_uv_islands` instead. + + :arg bm: the bmesh used to group with. + :type bmesh: :class: `BMesh` + :arg uv_layer: the UV layer to source UVs from. + :type bmesh: :class: `BMLayerItem` + :return: list of lists containing polygon indices + :rtype: list + """ + + result = [] + bm.faces.ensure_lookup_table() + + used = {} + for seed_face in bm.faces: + seed_index = seed_face.index + if used.get(seed_index): + continue # Face has already been processed. + used[seed_index] = True + island = [seed_index] + stack = [seed_face] # Faces still to consider on this island. + while stack: + current_face = stack.pop() + for loop in current_face.loops: + v = loop.vert + uv = loop[uv_layer].uv + for f in v.link_faces: + if used.get(f.index): + continue + if not match_uv(f, v, uv, uv_layer): + continue + + # `f` is part of island, add to island and stack + used[f.index] = True + island.append(f.index) + stack.append(f) + result.append(island) + + return result diff --git a/release/scripts/startup/bl_operators/__init__.py b/release/scripts/startup/bl_operators/__init__.py index 14dc72336f6..6f61d7e7129 100644 --- a/release/scripts/startup/bl_operators/__init__.py +++ b/release/scripts/startup/bl_operators/__init__.py @@ -31,6 +31,7 @@ _modules = [ "userpref", "uvcalc_follow_active", "uvcalc_lightmap", + "uvcalc_randomize_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 new file mode 100644 index 00000000000..22ae5ed9a6f --- /dev/null +++ b/release/scripts/startup/bl_operators/uvcalc_randomize_transform.py @@ -0,0 +1,210 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +from bpy.types import Operator +from mathutils import Vector + +import bpy.ops +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[0] + scale_v *= 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): + entropy = min(faces) # Ensure consistent random values for island, regardless of selection etc. + transform = get_random_transform(transform_params, entropy) + + # Find bounding box. + minmax = [1e30, 1e30, -1e30, -1e30] + for face_index in faces: + face = bm.faces[face_index] + 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_index in faces: + face = bm.faces[face_index] + 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_index in island: + if is_face_uv_selected(bm.faces[face_index], 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) + bm.faces.ensure_lookup_table() + if bm.loops.layers.uv: + 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_ui/space_image.py b/release/scripts/startup/bl_ui/space_image.py index 0f51c3830eb..4165f6ab0cf 100644 --- a/release/scripts/startup/bl_ui/space_image.py +++ b/release/scripts/startup/bl_ui/space_image.py @@ -292,6 +292,10 @@ class IMAGE_MT_uvs_transform(Menu): layout.operator("transform.shear") + layout.separator() + + layout.operator("uv.randomize_uv_transform") + class IMAGE_MT_uvs_snap(Menu): bl_label = "Snap" -- cgit v1.2.3