Fix T78406: create uv randomize islands operator

Implement a new operator to randomize the scale, rotation and offset
of selected UV islands.
This commit is contained in:
Chris Blackbourn 2022-08-25 17:59:39 +12:00
parent f36d8d59c2
commit de570dc87e
Notes: blender-bot 2023-02-14 11:24:03 +01:00
Referenced by issue #78406, Randomize UV islands
4 changed files with 273 additions and 0 deletions

View File

@ -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

View File

@ -31,6 +31,7 @@ _modules = [
"userpref",
"uvcalc_follow_active",
"uvcalc_lightmap",
"uvcalc_randomize_transform",
"vertexpaint_dirt",
"view3d",
"wm",

View File

@ -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,
)

View File

@ -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"