868 lines
28 KiB
Python
868 lines
28 KiB
Python
# SPDX-FileCopyrightText: 2025 Blender Studio Tools Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
# Inspired by https://twitter.com/soyposmoderno/status/1307222594047758337
|
|
|
|
# This lets you create an empty hooked up to a Lattice to deform all selected
|
|
# objects. A root empty is also created that can be (manually) parented to a
|
|
# rig in order to use this for animation.
|
|
|
|
import bpy
|
|
from bpy.props import (
|
|
FloatProperty,
|
|
IntVectorProperty,
|
|
PointerProperty,
|
|
StringProperty,
|
|
EnumProperty,
|
|
)
|
|
from bpy.types import (
|
|
Operator,
|
|
Object,
|
|
Lattice,
|
|
VertexGroup,
|
|
Scene,
|
|
Collection,
|
|
Modifier,
|
|
Panel,
|
|
PropertyGroup,
|
|
)
|
|
from typing import List, Tuple
|
|
|
|
from mathutils import Matrix, Vector, kdtree
|
|
from math import *
|
|
|
|
from rna_prop_ui import rna_idprop_ui_create
|
|
|
|
from .utils import clamp, get_lattice_vertex_index, simple_driver, bounding_box_center_of_objects
|
|
|
|
TWEAKLAT_COLL_NAME = 'Tweak Lattices'
|
|
|
|
FALLOFF_TYPES = {
|
|
# Since these expressions manipulate the shape of the lattice,
|
|
# which then manipulates the shape of the mesh,
|
|
# it's hard to come up with these functions in any meaningful way.
|
|
# So, it was done more so with artistic trial and error.
|
|
'LINEAR': lambda x: x + x * 0.1,
|
|
'CONSTANT': lambda x: 1,
|
|
'SHARP': lambda x: pow(x, 2) * 1.25,
|
|
'ROOT': lambda x: pow(x, 0.5) * 1.05,
|
|
'SMOOTH': lambda x: (1 - cos(x * pi)) / 2,
|
|
'SPHERE': lambda x: sin(pow(x, 0.5)) * 1.25,
|
|
'DONUT': lambda x: (sin(pow(x, 0.5)) - pow(x, 2)) * 2.5,
|
|
}
|
|
|
|
|
|
class OBJECT_OT_tweaklattice_create(Operator):
|
|
"""Create a lattice setup to deform selected objects"""
|
|
|
|
bl_idname = "lattice.create_tweak_lattice"
|
|
bl_label = "Create Tweak Lattice"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
name: StringProperty(
|
|
name="Name",
|
|
description="A unique name to identify this tweak lattice. Used in helper objects and modifiers",
|
|
default="Tweak",
|
|
)
|
|
|
|
resolution: IntVectorProperty(name="Resolution", default=(25, 25, 25), min=6, max=64)
|
|
|
|
location: EnumProperty(
|
|
name="Location",
|
|
items=[
|
|
('CURSOR', "3D Cursor", "Create at the location and orientation of the 3D cursor."),
|
|
('CENTER', "Center", "Create at the bounding box center of all selected objects."),
|
|
('PARENT', "Parent", "Create at the location of the parent object or bone."),
|
|
],
|
|
)
|
|
radius: FloatProperty(
|
|
name="Radius",
|
|
description="Radius of influence of this lattice. Can be changed later",
|
|
default=0.1,
|
|
min=0.0001,
|
|
max=1000,
|
|
soft_max=2,
|
|
)
|
|
|
|
parent_method: EnumProperty(
|
|
name="Parent Method",
|
|
description="How to parent the tweak lattice",
|
|
items=[
|
|
(
|
|
'AUTO',
|
|
'Automatic',
|
|
"Parent using an Armature constraint which mimics the deformation of the nearest vertex to the cursor",
|
|
),
|
|
(
|
|
'MANUAL',
|
|
'Manual',
|
|
"Manually select a single object or bone as the tweak lattice parent",
|
|
),
|
|
],
|
|
default='AUTO',
|
|
)
|
|
parent_bone: StringProperty(name="Bone", description="Bone to use as parent")
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
for ob in context.selected_objects:
|
|
if ob.type == 'MESH':
|
|
return True
|
|
|
|
cls.poll_message_set("No selected mesh objects.")
|
|
return False
|
|
|
|
def invoke(self, context, _event):
|
|
parent_obj = context.object
|
|
for m in parent_obj.modifiers:
|
|
if m.type == 'ARMATURE' and m.object:
|
|
parent_obj = m.object
|
|
if self.parent_bone not in parent_obj.data.bones:
|
|
self.parent_bone = ""
|
|
break
|
|
|
|
context.scene.tweak_lattice_parent_ob = parent_obj
|
|
|
|
wm = context.window_manager
|
|
return wm.invoke_props_dialog(self, width=400)
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
layout.prop(self, 'name')
|
|
layout.separator()
|
|
|
|
layout.prop(self, 'location', expand=True)
|
|
layout.prop(self, 'radius', slider=True)
|
|
layout.separator()
|
|
|
|
layout.prop(self, 'parent_method', expand=True)
|
|
|
|
if self.parent_method == 'MANUAL':
|
|
col = layout.column(align=True)
|
|
col.prop(context.scene, 'tweak_lattice_parent_ob')
|
|
|
|
scene = context.scene
|
|
if scene.tweak_lattice_parent_ob and scene.tweak_lattice_parent_ob.type == 'ARMATURE':
|
|
col.prop_search(self, 'parent_bone', scene.tweak_lattice_parent_ob.data, 'bones')
|
|
|
|
def execute(self, context):
|
|
scene = context.scene
|
|
|
|
# Ensure a collection to organize all our objects in.
|
|
coll = ensure_tweak_lattice_collection(context.scene)
|
|
|
|
# Create a lattice object at the 3D cursor.
|
|
lattice_name = f"LTC-{self.name}"
|
|
lattice = bpy.data.lattices.new(lattice_name)
|
|
lattice_ob = bpy.data.objects.new(lattice_name, lattice)
|
|
coll.objects.link(lattice_ob)
|
|
|
|
# Set resolution
|
|
set_lattice_resolution(lattice_ob, *self.resolution)
|
|
lattice_ob.hide_viewport = True
|
|
|
|
# Create a falloff vertex group.
|
|
vg = ensure_falloff_vgroup(lattice_ob, vg_name="Hook", func=FALLOFF_TYPES['SMOOTH'])
|
|
|
|
# Create the Hook Empty.
|
|
hook_name = "Hook_" + lattice_ob.name
|
|
hook = bpy.data.objects.new(hook_name, None)
|
|
hook.empty_display_type = 'SPHERE'
|
|
hook.empty_display_size = 0.5
|
|
coll.objects.link(hook)
|
|
|
|
# Create some custom properties.
|
|
hook['Lattice'] = lattice_ob
|
|
lattice_ob['Hook'] = hook
|
|
hook['Strength'] = 1.0
|
|
|
|
rna_idprop_ui_create(
|
|
hook,
|
|
"Influence",
|
|
default=1.0,
|
|
min=0,
|
|
max=1,
|
|
description="Influence of this lattice on all of its target objects",
|
|
)
|
|
rna_idprop_ui_create(
|
|
hook,
|
|
"Radius",
|
|
default=self.radius,
|
|
min=0,
|
|
soft_max=0.2,
|
|
max=100,
|
|
description="Size of the influenced area",
|
|
)
|
|
|
|
# Create a Root Empty to parent both the hook and the lattice to.
|
|
# This will allow pressing Alt+G/R/S on the hook to reset its transforms.
|
|
root_name = "Root_" + hook.name
|
|
root = bpy.data.objects.new(root_name, None)
|
|
root['Hook'] = hook
|
|
root.empty_display_type = 'CUBE'
|
|
root.empty_display_size = 0.5
|
|
coll.objects.link(root)
|
|
hook['Root'] = root
|
|
|
|
self.set_parent_and_transform(context, root)
|
|
|
|
# Disable the root from view.
|
|
# NOTE: This must be done AFTER any reliance on view_layer.update() calls!
|
|
# They don't work when the object is disabled!
|
|
root.hide_viewport = True
|
|
|
|
# Parent lattice and hook to root.
|
|
lattice_ob.parent = root
|
|
hook.parent = root
|
|
|
|
# Add Hook modifier to the lattice.
|
|
hook_mod = lattice_ob.modifiers.new(name="Hook", type='HOOK')
|
|
hook_mod.object = hook
|
|
hook_mod.vertex_group = vg.name
|
|
|
|
# Add Lattice modifier to the selected objects
|
|
add_objects_to_lattice(hook, context.selected_objects)
|
|
|
|
# Set up Radius control.
|
|
add_radius_constraint(hook, hook, root)
|
|
add_radius_constraint(lattice_ob, hook, root)
|
|
|
|
root_drv = simple_driver(root, 'empty_display_size', hook, '["Radius"]')
|
|
root_drv.expression = 'var/2'
|
|
|
|
# Deselect everything, select the hook and make it active
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
hook.select_set(True)
|
|
context.view_layer.objects.active = hook
|
|
|
|
# Clear the parent selector helper property.
|
|
scene.tweak_lattice_parent_ob = None
|
|
return {'FINISHED'}
|
|
|
|
def set_parent_and_transform(self, context, root):
|
|
scene = context.scene
|
|
depsgraph = context.evaluated_depsgraph_get()
|
|
|
|
matrix_of_parent = self.get_lattice_parent_matrix(context)
|
|
|
|
root.matrix_world = matrix_of_parent.copy()
|
|
context.view_layer.update()
|
|
mat_pre_arm_con = root.matrix_world.copy()
|
|
|
|
if self.parent_method == 'AUTO':
|
|
# Parent to the nearest deforming vertex.
|
|
nearest_vertex = get_nearest_evaluated_vertex(
|
|
dg=depsgraph,
|
|
coord=matrix_of_parent.copy().to_translation(),
|
|
objs=context.selected_objects,
|
|
)
|
|
obj, eval_obj, vert_idx, _vert_co = nearest_vertex
|
|
|
|
root.parent = obj.find_armature() or obj
|
|
weights = get_deforming_weights(obj, eval_obj, vert_idx)
|
|
else:
|
|
root.parent = scene.tweak_lattice_parent_ob
|
|
weights = {}
|
|
if self.parent_bone:
|
|
weights = {self.parent_bone: 1.0}
|
|
|
|
if weights:
|
|
arm_con = root.constraints.new(type='ARMATURE')
|
|
for bone_name, weight in weights.items():
|
|
tgt = arm_con.targets.new()
|
|
tgt.target = root.parent
|
|
tgt.subtarget = bone_name
|
|
tgt.weight = weight
|
|
|
|
context.view_layer.update()
|
|
mat_post_arm_con = root.matrix_world.copy()
|
|
|
|
delta = mat_pre_arm_con.inverted() @ mat_post_arm_con
|
|
|
|
root.matrix_world = matrix_of_parent @ delta.inverted()
|
|
|
|
if self.parent_method != 'AUTO' or self.location != 'CURSOR':
|
|
root.rotation_euler = 0, 0, 0
|
|
|
|
def get_lattice_parent_matrix(self, context):
|
|
location = self.location
|
|
parent_bone = self.parent_bone
|
|
scene = context.scene
|
|
|
|
if location == 'CENTER':
|
|
meshes = [o for o in context.selected_objects if o.type == 'MESH']
|
|
mat = Matrix.Identity((4))
|
|
mat.translation = bounding_box_center_of_objects(meshes)
|
|
return mat
|
|
elif location == 'CURSOR':
|
|
return context.scene.cursor.matrix.copy()
|
|
elif location == 'PARENT':
|
|
ob_mat = scene.tweak_lattice_parent_ob.matrix_world
|
|
if parent_bone:
|
|
bone_mat = scene.tweak_lattice_parent_ob.pose.bones[parent_bone].matrix
|
|
return ob_mat @ bone_mat
|
|
else:
|
|
return ob_mat
|
|
|
|
|
|
class OBJECT_OT_tweaklattice_duplicate(Operator):
|
|
"""Duplicate this Tweak Lattice set-up"""
|
|
|
|
bl_idname = "lattice.duplicate_tweak_setup"
|
|
bl_label = "Duplicate Tweak Lattice"
|
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
|
|
def execute(self, context):
|
|
hook, lattice, root = get_tweak_setup(context.object)
|
|
bpy.ops.object.select_all(action='DESELECT')
|
|
|
|
affected_objects = get_objects_of_lattice(hook)
|
|
|
|
visibilities = {}
|
|
for ob in [hook, lattice, root]:
|
|
ob.hide_set(False)
|
|
visibilities[ob] = ob.hide_viewport
|
|
ob.hide_viewport = False
|
|
if not ob.visible_get():
|
|
self.report({'ERROR'}, f'Object "{ob.name}" could not be made visible, cancelling.')
|
|
return {'CANCELLED'}
|
|
ob.select_set(True)
|
|
|
|
context.view_layer.objects.active = hook
|
|
|
|
bpy.ops.object.duplicate()
|
|
new_hook, new_lattice, new_root = get_tweak_setup(context.object)
|
|
|
|
for key, value in list(new_hook.items()):
|
|
if key.startswith("object_"):
|
|
del new_hook[key]
|
|
|
|
add_objects_to_lattice(new_hook, affected_objects)
|
|
|
|
# Restore visibilities
|
|
for ob, new_ob in zip((hook, lattice, root), (new_hook, new_lattice, new_root)):
|
|
ob.hide_viewport = new_ob.hide_viewport = visibilities[ob]
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class TweakLatticeProperties(PropertyGroup):
|
|
def update_falloff(self, context):
|
|
falloff_func = FALLOFF_TYPES[self.falloff_type]
|
|
|
|
hook, lattice, _root = get_tweak_setup(context.active_object)
|
|
ensure_falloff_vgroup(lattice, 'Hook', multiplier=self.strength, func=falloff_func)
|
|
hook['Strength'] = self.strength
|
|
|
|
strength: FloatProperty(
|
|
name="Strength",
|
|
description="A multiplier on the weight values",
|
|
default=1,
|
|
update=update_falloff,
|
|
min=0,
|
|
soft_max=2,
|
|
)
|
|
falloff_type: EnumProperty(
|
|
name="Falloff Shape",
|
|
description="Choose shape of influence for this lattice",
|
|
items=[
|
|
('LINEAR', 'Linear', 'Linear'),
|
|
('CONSTANT', 'Constant', 'Constant'),
|
|
('SHARP', 'Sharp', 'Sharp'),
|
|
('ROOT', 'Root', 'Root'),
|
|
('SMOOTH', 'Smooth', 'Smooth'),
|
|
('SPHERE', 'Sphere', 'Sphere'),
|
|
('DONUT', 'Donut', 'Donut'),
|
|
],
|
|
default='SMOOTH',
|
|
update=update_falloff,
|
|
)
|
|
|
|
|
|
class OBJECT_OT_tweaklattice_delete(Operator):
|
|
"""Delete a tweak lattice setup with all its helper objects, drivers, etc"""
|
|
|
|
bl_idname = "lattice.delete_tweak_lattice"
|
|
bl_label = "Delete Tweak Lattice"
|
|
bl_options = {'REGISTER', 'UNDO'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
hook, lattice, root = get_tweak_setup(context.object)
|
|
if hook and lattice and root:
|
|
return True
|
|
|
|
cls.poll_message_set("Tweak Lattice set-up is incomplete. Some objects were manually deleted.")
|
|
return False
|
|
|
|
def execute(self, context):
|
|
hook, lattice, root = get_tweak_setup(context.object)
|
|
|
|
# Remove Lattice modifiers and their drivers.
|
|
remove_all_objects_from_lattice(hook)
|
|
|
|
# Remove hook Action if exists.
|
|
if hook.animation_data and hook.animation_data.action:
|
|
bpy.data.actions.remove(hook.animation_data.action)
|
|
|
|
# Remove objects and Lattice datablock.
|
|
bpy.data.objects.remove(hook)
|
|
lattice_data = lattice.data
|
|
bpy.data.objects.remove(lattice)
|
|
bpy.data.lattices.remove(lattice_data)
|
|
bpy.data.objects.remove(root)
|
|
|
|
# Remove the collection if it's empty.
|
|
coll = bpy.data.collections.get(TWEAKLAT_COLL_NAME)
|
|
if coll and len(coll.all_objects) == 0:
|
|
bpy.data.collections.remove(coll)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_tweaklattice_objects_add(Operator):
|
|
"""Add selected objects to this tweak lattice"""
|
|
|
|
bl_idname = "lattice.add_selected_objects"
|
|
bl_label = "Add Selected Objects"
|
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
hook, _lattice, _root = get_tweak_setup(context.object)
|
|
if not hook:
|
|
cls.poll_message_set("Cannot find hook object of this Tweak Lattice set-up. Perhaps it was deleted?")
|
|
return False
|
|
|
|
values = hook.values()
|
|
for sel_o in context.selected_objects:
|
|
if sel_o == hook or sel_o.type != 'MESH':
|
|
continue
|
|
if sel_o not in values:
|
|
return True
|
|
|
|
cls.poll_message_set("No selected objects to add to this Tweak Lattice.")
|
|
return False
|
|
|
|
def execute(self, context):
|
|
hook, _lattice, _root = get_tweak_setup(context.object)
|
|
|
|
# Add Lattice modifier to the selected objects
|
|
add_objects_to_lattice(hook, context.selected_objects)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_tweaklattice_objects_remove(Operator):
|
|
"""Remove selected objects from this tweak lattice"""
|
|
|
|
bl_idname = "lattice.remove_selected_objects"
|
|
bl_label = "Remove Selected Objects"
|
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
hook, _lattice, _root = get_tweak_setup(context.object)
|
|
if not hook:
|
|
cls.poll_message_set("Cannot find hook object of this Tweak Lattice set-up. Perhaps it was deleted?")
|
|
return False
|
|
|
|
values = hook.values()
|
|
for sel_o in context.selected_objects:
|
|
if sel_o == hook or sel_o.type != 'MESH':
|
|
continue
|
|
if sel_o in values:
|
|
return True
|
|
|
|
cls.poll_message_set("No selected objects to remove from this Tweak Lattice.")
|
|
return False
|
|
|
|
def execute(self, context):
|
|
hook, _lattice, _root = get_tweak_setup(context.object)
|
|
|
|
# Add Lattice modifier to the selected objects
|
|
remove_objects_from_lattice(hook, context.selected_objects)
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_tweaklattice_object_remove_single(Operator):
|
|
"""Remove this object from the tweak lattice"""
|
|
|
|
bl_idname = "lattice.remove_object"
|
|
bl_label = "Remove Object"
|
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
|
|
ob_pointer_prop_name: StringProperty(
|
|
description="Name of the custom property that references the object that should be removed"
|
|
)
|
|
|
|
def execute(self, context):
|
|
hook, _lattice, _root = get_tweak_setup(context.object)
|
|
target = hook[self.ob_pointer_prop_name]
|
|
|
|
# Add Lattice modifier to the selected objects
|
|
remove_objects_from_lattice(hook, [target])
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class VIEW3D_PT_tweak_lattice(Panel):
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'Lattice Magic'
|
|
bl_label = "Tweak Lattice"
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
hook, _lattice, _root = get_tweak_setup(context.object)
|
|
|
|
return context.object and context.object.type == 'MESH' or hook
|
|
|
|
def draw(self, context):
|
|
layout = self.layout
|
|
layout.use_property_split = True
|
|
layout.use_property_decorate = False
|
|
|
|
hook, lattice, root = get_tweak_setup(context.object)
|
|
|
|
layout = layout.column()
|
|
if not hook:
|
|
layout.operator(OBJECT_OT_tweaklattice_create.bl_idname, icon='OUTLINER_OB_LATTICE')
|
|
return
|
|
|
|
layout.prop(hook, '["Influence"]', slider=True, text="Influence")
|
|
layout.prop(hook["Lattice"].data.lattice_magic, 'strength')
|
|
layout.separator()
|
|
|
|
layout.prop(hook["Lattice"].data.lattice_magic, 'falloff_type')
|
|
layout.prop(hook, '["Radius"]', slider=True)
|
|
|
|
layout.separator()
|
|
layout.operator(
|
|
OBJECT_OT_tweaklattice_delete.bl_idname, text='Delete Tweak Lattice', icon='TRASH'
|
|
)
|
|
layout.operator(
|
|
OBJECT_OT_tweaklattice_duplicate.bl_idname,
|
|
text='Duplicate Tweak Lattice',
|
|
icon='DUPLICATE',
|
|
)
|
|
|
|
layout.separator()
|
|
layout.label(text="Helper Objects")
|
|
lattice_row = layout.row()
|
|
lattice_row.prop(hook, '["Lattice"]', text="Lattice")
|
|
lattice_row.prop(lattice, 'hide_viewport', text="", emboss=False)
|
|
|
|
root_row = layout.row()
|
|
root_row.prop(hook, '["Root"]', text="Root")
|
|
root_row.prop(root, 'hide_viewport', text="", emboss=False)
|
|
|
|
layout.separator()
|
|
layout.label(text="Parenting")
|
|
col = layout.column()
|
|
col.enabled = False
|
|
col.prop(root, 'parent')
|
|
if root.parent and root.parent.type == 'ARMATURE':
|
|
col.prop(root, 'parent_bone', icon='BONE_DATA')
|
|
|
|
layout.separator()
|
|
layout.label(text="Affected Objects")
|
|
|
|
num_to_add = 0
|
|
for o in context.selected_objects:
|
|
if o == hook or o.type != 'MESH':
|
|
continue
|
|
if o in hook.values():
|
|
continue
|
|
num_to_add += 1
|
|
if num_to_add == 1:
|
|
text = f"Add {o.name}"
|
|
if num_to_add:
|
|
if num_to_add > 1:
|
|
text = f"Add {num_to_add} Objects"
|
|
layout.operator(OBJECT_OT_tweaklattice_objects_add.bl_idname, icon='ADD', text=text)
|
|
|
|
layout.separator()
|
|
num_to_remove = False
|
|
for o in context.selected_objects:
|
|
if o == hook or o.type != 'MESH':
|
|
continue
|
|
if o not in hook.values():
|
|
continue
|
|
num_to_remove += 1
|
|
if num_to_remove == 1:
|
|
text = f"Remove {o.name}"
|
|
if num_to_remove:
|
|
if num_to_remove > 1:
|
|
text = f"Remove {num_to_remove} Objects"
|
|
layout.operator(
|
|
OBJECT_OT_tweaklattice_objects_remove.bl_idname, icon='REMOVE', text=text
|
|
)
|
|
|
|
objects_and_keys = [(hook[key], key) for key in hook.keys() if "object_" in key]
|
|
objects_and_keys.sort(key=lambda o_and_k: o_and_k[1])
|
|
for ob, key in objects_and_keys:
|
|
row = layout.row(align=True)
|
|
row.prop(hook, f'["{key}"]', text="")
|
|
mod = get_lattice_modifier_of_object(ob, lattice)
|
|
if not mod:
|
|
continue
|
|
row.prop_search(mod, 'vertex_group', ob, 'vertex_groups', text="", icon='GROUP_VERTEX')
|
|
op = row.operator(
|
|
OBJECT_OT_tweaklattice_object_remove_single.bl_idname, text="", icon='X'
|
|
)
|
|
op.ob_pointer_prop_name = key
|
|
|
|
|
|
def set_lattice_resolution(lat_ob: Object, res_u, res_v=None, res_w=None):
|
|
assert lat_ob.type == 'LATTICE', f"This isn't a lattice object: {lat_ob.name}"
|
|
|
|
if not res_v:
|
|
res_v = res_u
|
|
if not res_w:
|
|
res_w = res_u
|
|
|
|
lat_ob.data.points_u = res_u
|
|
lat_ob.data.points_v = res_v
|
|
lat_ob.data.points_w = res_w
|
|
|
|
|
|
def build_kdtree(obj):
|
|
# Get the vertices of the mesh in world coordinates
|
|
vertices = [obj.matrix_world @ v.co for v in obj.data.vertices]
|
|
|
|
# Build KD-Tree from the vertices
|
|
size = len(vertices)
|
|
kd = kdtree.KDTree(size)
|
|
|
|
for i, vertex in enumerate(vertices):
|
|
kd.insert(vertex, i)
|
|
|
|
kd.balance()
|
|
return kd
|
|
|
|
|
|
def get_nearest_evaluated_vertex(
|
|
dg, coord: Vector, objs: list[Object]
|
|
) -> tuple[Object, int, Vector]:
|
|
"""Get nearest EVALUATED vertex to a coordinate out of a list of passed mesh objects.
|
|
Return the original object, and the evaluated object, vertex index, and coordinate.
|
|
"""
|
|
nearest_vertex = None
|
|
nearest_distance = float('inf')
|
|
|
|
for obj in objs:
|
|
if obj.type != 'MESH':
|
|
continue
|
|
|
|
eval_ob = obj.evaluated_get(dg)
|
|
|
|
kd = build_kdtree(eval_ob)
|
|
|
|
# Find the nearest vertex to the 3D cursor
|
|
|
|
eval_co, eval_idx, dist = kd.find(coord)
|
|
|
|
# If this vertex is closer than any previously found, store it
|
|
if dist < nearest_distance:
|
|
nearest_distance = dist
|
|
nearest_vertex = (obj, eval_ob, eval_idx, eval_co)
|
|
|
|
return nearest_vertex
|
|
|
|
|
|
def get_deforming_weights(obj: Object, eval_obj, vert_idx: int) -> dict[str, float] | None:
|
|
armature = obj.find_armature()
|
|
|
|
if not armature:
|
|
return
|
|
|
|
vertex = eval_obj.data.vertices[vert_idx]
|
|
|
|
weights = {}
|
|
|
|
# Loop through the vertex groups the vertex belongs to
|
|
for group in vertex.groups:
|
|
group_index = group.group
|
|
group_weight = group.weight
|
|
group_name = obj.vertex_groups[group_index].name
|
|
pbone = armature.pose.bones.get(group_name)
|
|
if pbone and pbone.bone.use_deform:
|
|
weights[group_name] = group_weight
|
|
|
|
return weights
|
|
|
|
|
|
def get_tweak_setup(obj: Object) -> Tuple[Object, Object, Object]:
|
|
"""Based on either the hook, lattice or root, return all three."""
|
|
if not obj:
|
|
return [None, None, None]
|
|
|
|
if obj.type == 'EMPTY':
|
|
if 'Root' and 'Lattice' in obj:
|
|
return obj, obj['Lattice'], obj['Root']
|
|
elif 'Hook' in obj:
|
|
return obj['Hook'], obj['Hook']['Lattice'], obj
|
|
elif obj.type == 'LATTICE' and 'Hook' in obj:
|
|
return obj['Hook'], obj, obj['Hook']['Root']
|
|
|
|
return [None, None, None]
|
|
|
|
|
|
def ensure_tweak_lattice_collection(scene: Scene) -> Collection:
|
|
coll = bpy.data.collections.get(TWEAKLAT_COLL_NAME)
|
|
if not coll:
|
|
coll = bpy.data.collections.new(TWEAKLAT_COLL_NAME)
|
|
scene.collection.children.link(coll)
|
|
|
|
return coll
|
|
|
|
|
|
def ensure_falloff_vgroup(
|
|
lattice_ob: Object, vg_name="Group", multiplier=1, func=lambda x: x
|
|
) -> VertexGroup:
|
|
lattice = lattice_ob.data
|
|
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
|
|
|
|
vg = lattice_ob.vertex_groups.get(vg_name)
|
|
|
|
center = Vector((res_x - 1, res_y - 1, res_z - 1)) / 2
|
|
max_res = max(res_x, res_y, res_z)
|
|
|
|
if not vg:
|
|
vg = lattice_ob.vertex_groups.new(name=vg_name)
|
|
for x in range(res_x - 4):
|
|
for y in range(res_y - 4):
|
|
for z in range(res_z - 4):
|
|
index = get_lattice_vertex_index(lattice, (x + 2, y + 2, z + 2))
|
|
|
|
coord = Vector((x + 2, y + 2, z + 2))
|
|
distance_from_center = (coord - center).length
|
|
distance_factor = clamp(1 - (distance_from_center / max_res * 2), 0, 1)
|
|
influence = func(distance_factor)
|
|
|
|
vg.add([index], influence * multiplier, 'REPLACE')
|
|
return vg
|
|
|
|
|
|
def add_radius_constraint(obj, hook, target):
|
|
trans_con = obj.constraints.new(type='TRANSFORM')
|
|
trans_con.name += " (Radius Scaling)"
|
|
trans_con.target = target
|
|
trans_con.map_to = 'SCALE'
|
|
trans_con.mix_mode_scale = 'MULTIPLY'
|
|
for prop in ['to_min_x_scale', 'to_min_y_scale', 'to_min_z_scale']:
|
|
simple_driver(trans_con, prop, hook, '["Radius"]')
|
|
return trans_con
|
|
|
|
|
|
def get_objects_of_lattice(hook: Object) -> List[Object]:
|
|
objs = []
|
|
for key, value in hook.items():
|
|
if key.startswith("object_") and value:
|
|
objs.append(value)
|
|
|
|
return objs
|
|
|
|
|
|
def get_lattice_modifier_of_object(obj, lattice) -> Modifier:
|
|
"""Find the lattice modifier on the object that uses this lattice"""
|
|
if not obj:
|
|
return
|
|
for m in obj.modifiers:
|
|
if m.type == 'LATTICE' and m.object == lattice:
|
|
return m
|
|
|
|
|
|
def add_objects_to_lattice(hook: Object, objects: List[Object]):
|
|
lattice_ob = hook['Lattice']
|
|
|
|
for i, obj in enumerate(objects):
|
|
obj.select_set(False)
|
|
if obj.type != 'MESH' or obj in hook.values():
|
|
continue
|
|
|
|
# Make sure overridden object is editable.
|
|
if obj.override_library:
|
|
obj.override_library.is_system_override = False
|
|
|
|
mod = obj.modifiers.new(name=lattice_ob.name, type='LATTICE')
|
|
mod.object = lattice_ob
|
|
|
|
# Make sure the property name is available.
|
|
offset = 0
|
|
while "object_" + str(offset) in hook:
|
|
offset += 1
|
|
hook["object_" + str(i + offset)] = obj
|
|
|
|
# Add driver to the modifier influence.
|
|
simple_driver(mod, 'strength', hook, '["Influence"]')
|
|
|
|
|
|
def remove_object_from_lattice(hook: Object, obj: Object):
|
|
"""Cleanly remove an object from a Tweak Lattice set-up's influence."""
|
|
hook, lattice, root = get_tweak_setup(hook)
|
|
|
|
# Remove the custom property pointing from the Hook to the Object.
|
|
for key, value in list(hook.items()):
|
|
if value == obj:
|
|
del hook[key]
|
|
break
|
|
|
|
# Remove the Lattice modifier (and its driver) deforming the Object.
|
|
for m in obj.modifiers:
|
|
if m.type != 'LATTICE':
|
|
continue
|
|
if m.object == lattice:
|
|
m.driver_remove('strength')
|
|
obj.modifiers.remove(m)
|
|
break
|
|
|
|
|
|
def remove_objects_from_lattice(hook: Object, objects_to_remove: List[Object]) -> List[Object]:
|
|
"""Cleanly remove several objects from a Tweak Lattice set-up's influence."""
|
|
objs_removed = []
|
|
for key, value in list(hook.items()):
|
|
if value in objects_to_remove:
|
|
remove_object_from_lattice(hook, value)
|
|
objs_removed.append(value)
|
|
|
|
return objs_removed
|
|
|
|
|
|
def remove_all_objects_from_lattice(hook: Object) -> List[Object]:
|
|
"""Cleanly remove all objects from a Tweak Lattice set-up's influence."""
|
|
objs_to_remove = []
|
|
for key, value in list(hook.items()):
|
|
if key.startswith("object_"):
|
|
objs_to_remove.append(value)
|
|
|
|
return remove_objects_from_lattice(hook, objs_to_remove)
|
|
|
|
|
|
registry = [
|
|
OBJECT_OT_tweaklattice_create,
|
|
OBJECT_OT_tweaklattice_duplicate,
|
|
OBJECT_OT_tweaklattice_delete,
|
|
OBJECT_OT_tweaklattice_objects_add,
|
|
OBJECT_OT_tweaklattice_objects_remove,
|
|
OBJECT_OT_tweaklattice_object_remove_single,
|
|
VIEW3D_PT_tweak_lattice,
|
|
TweakLatticeProperties,
|
|
]
|
|
|
|
|
|
def register():
|
|
Scene.tweak_lattice_parent_ob = PointerProperty(type=Object, name="Parent")
|
|
Lattice.lattice_magic = PointerProperty(type=TweakLatticeProperties)
|
|
|
|
|
|
def unregister():
|
|
del Scene.tweak_lattice_parent_ob
|
|
del Lattice.lattice_magic
|