Add Lattice Magic to Addons #48

Merged
Nick Alberelli merged 36 commits from feature/lattice_magic into main 2023-05-17 20:48:52 +02:00
3 changed files with 422 additions and 12 deletions
Showing only changes of commit d354469bba - Show all commits

View File

@ -14,23 +14,33 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
bl_info = { bl_info = {
"name": "Camera Lattice", "name": "Lattice Magic",
"author": "Demeter Dzadik", "author": "Demeter Dzadik",
"version": (1,0), "version": (1,0),
"blender": (2, 90, 0), "blender": (2, 90, 0),
"location": "View3D > Sidebar > Camera Lattice", "location": "View3D > Sidebar > Lattice Magic",
"description": "Create a lattice in the camera view to smear geometry.", "description": "Various Lattice-based tools to smear or adjust geometry.",
"category": "Rigging", "category": "Rigging",
"doc_url": "", "doc_url": "https://gitlab.com/blender/lattice_magic/-/wikis/home",
"tracker_url": "", "tracker_url": "https://gitlab.com/blender/lattice_magic/-/issues/new",
} }
from . import camera_lattice from . import camera_lattice
from . import tweak_lattice
import importlib import importlib
modules = [
camera_lattice
,tweak_lattice
]
def register(): def register():
importlib.reload(camera_lattice) for m in modules:
camera_lattice.register() importlib.reload(m)
if hasattr(m, 'register'):
m.register()
def unregister(): def unregister():
camera_lattice.unregister() for m in modules:
if hasattr(m, 'unregister'):
m.unregister()

View File

@ -103,7 +103,7 @@ class LatticeSlot(bpy.types.PropertyGroup):
class CAMLAT_OT_Remove(bpy.types.Operator): class CAMLAT_OT_Remove(bpy.types.Operator):
"""Remove Lattice Slot along with its Lattice object, animation and modifiers.""" """Remove Lattice Slot along with its Lattice object, animation and modifiers"""
bl_idname = "lattice.remove_slot" bl_idname = "lattice.remove_slot"
bl_label = "Remove Lattice Slot" bl_label = "Remove Lattice Slot"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@ -133,6 +133,7 @@ class CAMLAT_OT_Remove(bpy.types.Operator):
return { 'FINISHED' } return { 'FINISHED' }
class CAMLAT_OT_Add(bpy.types.Operator): class CAMLAT_OT_Add(bpy.types.Operator):
"""Add a Camera Lattice Slot"""
bl_idname = "lattice.add_slot" bl_idname = "lattice.add_slot"
bl_label = "Add Lattice Slot" bl_label = "Add Lattice Slot"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@ -152,6 +153,7 @@ class CAMLAT_OT_Add(bpy.types.Operator):
return { 'FINISHED' } return { 'FINISHED' }
class CAMLAT_OT_Move(bpy.types.Operator): class CAMLAT_OT_Move(bpy.types.Operator):
"""Move Lattice Slot"""
bl_idname = "lattice.move_slot" bl_idname = "lattice.move_slot"
bl_label = "Move Lattice Slot" bl_label = "Move Lattice Slot"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@ -188,7 +190,7 @@ class CAMLAT_OT_Move(bpy.types.Operator):
class CAMLAT_OT_Generate(bpy.types.Operator): class CAMLAT_OT_Generate(bpy.types.Operator):
"""Generate a lattice to smear the selected collection from the selected camera.""" """Generate a lattice to smear the selected collection from the selected camera"""
bl_idname = "lattice.generate_lattice_for_slot" bl_idname = "lattice.generate_lattice_for_slot"
bl_label = "Generate Lattice" bl_label = "Generate Lattice"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'} bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@ -423,8 +425,8 @@ class CAMLAT_OT_ShapeKey_Add(bpy.types.Operator):
class CAMLAT_PT_main(bpy.types.Panel): class CAMLAT_PT_main(bpy.types.Panel):
bl_space_type = 'VIEW_3D' bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' bl_region_type = 'UI'
bl_category = 'Camera Lattice' bl_category = 'Lattice Magic'
bl_label = "Lattice Slots" bl_label = "Camera Lattice Slots"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):

398
tweak_lattice.py Normal file
View File

@ -0,0 +1,398 @@
# Another lattice addon, this time inspired by https://twitter.com/soyposmoderno/status/1307222594047758337
# This one 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.
# TODO:
# Merge this and Camera Lattice into one addon
# Re-organize the code
# Automagically determining the initial radius based on the selected objects' bounding box would be interesting
# More things:
# Mirror selected positions(along any axis, with pickable left/right up/down front/back preference)
# Grow selection
# Add operators to the UI wherever possible.
import bpy
from bpy.props import FloatProperty, IntVectorProperty
from typing import List
from mathutils import Vector
from rna_prop_ui import rna_idprop_ui_create
coll_name = 'Tweak Lattices'
def clamp(val, _min=0, _max=1) -> float or int:
if val < _min:
return _min
if val > _max:
return _max
return val
def get_lattice_vertex_index(lattice: bpy.types.Lattice, xyz: List[int], do_clamp=True) -> int:
"""Get the index of a lattice vertex based on its position on the XYZ axes."""
# The lattice vertex indicies start in the -Y, -X, -Z corner,
# increase on X+, then moves to the next row on Y+, then moves up on Z+.
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
x, y, z = xyz[:]
if do_clamp:
x = clamp(x, 0, res_x)
y = clamp(y, 0, res_y)
z = clamp(z, 0, res_z)
assert x < res_x and y < res_y and z < res_z, "Error: Lattice vertex xyz index out of bounds"
index = (z * res_y*res_x) + (y * res_x) + x
return index
def get_lattice_vertex_xyz_position(lattice: bpy.types.Lattice, index: int) -> (int, int, int):
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
x = 0
remaining = index
z = int(remaining / (res_x*res_y))
remaining -= z*(res_x*res_y)
y = int(remaining / res_x)
remaining -= y*res_x
x = remaining # Maybe need to add or subtract 1 here?
return (x, y, z)
def get_lattice_point_original_position(lattice: bpy.types.Lattice, index: int) -> Vector:
"""Reset a lattice vertex to its original position."""
start_vec = Vector((-0.5, -0.5, -0.5))
if lattice.points_u == 1:
start_vec[0] = 0
if lattice.points_v == 1:
start_vec[1] = 0
if lattice.points_w == 1:
start_vec[2] = 0
unit_u = 1/(lattice.points_u-1)
unit_v = 1/(lattice.points_v-1)
unit_w = 1/(lattice.points_w-1)
unit_vec = Vector((unit_u, unit_v, unit_w))
xyz_vec = Vector(get_lattice_vertex_xyz_position(lattice, index))
return start_vec + xyz_vec*unit_vec
def simple_driver(owner: bpy.types.ID, driver_path: str, target_ob: bpy.types.Object, data_path: str, array_index=-1) -> bpy.types.Driver:
if array_index > -1:
owner.driver_remove(driver_path, array_index)
driver = owner.driver_add(driver_path, array_index).driver
else:
owner.driver_remove(driver_path)
driver = owner.driver_add(driver_path).driver
driver.expression = 'var'
var = driver.variables.new()
var.targets[0].id = target_ob
var.targets[0].data_path = data_path
return driver
def ensure_tweak_lattice_collection(scene):
coll = bpy.data.collections.get(coll_name)
if not coll:
coll = bpy.data.collections.new(coll_name)
scene.collection.children.link(coll)
return coll
class LATTICE_OT_Reset(bpy.types.Operator):
"""Reset selected lattice points to their default position."""
bl_idname = "lattice.reset_points"
bl_label = "Reset Lattice Points"
bl_options = {'REGISTER', 'UNDO'}
factor: FloatProperty(name="Factor", min=0, max=1, default=1)
@classmethod
def poll(cls, context):
return len(context.selected_objects)>0 and context.mode=='EDIT_LATTICE'
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.prop(self, 'factor', slider=True)
def execute(self, context):
bpy.ops.object.mode_set(mode='OBJECT')
for ob in context.selected_objects:
if ob.type!='LATTICE':
continue
# Resetting shape key or Basis shape
if ob.data.shape_keys:
active_index = ob.active_shape_key_index
key_blocks = ob.data.shape_keys.key_blocks
active_block = key_blocks[active_index]
basis_block = key_blocks[0]
if active_index > 0:
for i, skp in enumerate(active_block.data):
point = ob.data.points[i]
if not point.select: continue
mix = skp.co.lerp(basis_block.data[i].co, self.factor)
skp.co = mix
continue
else:
for i, skp in enumerate(active_block.data):
base = get_lattice_point_original_position(ob.data, i)
# Resetting the Basis shape
mix = basis_block.data[i].co.lerp(base, self.factor)
basis_block.data[i].co = mix
continue
# Otherwise, reset the actual points.
for i in range(len(ob.data.points)):
point = ob.data.points[i]
if not point.select:
continue
base = get_lattice_point_original_position(ob.data, i)
mix = point.co_deform.lerp(base, self.factor)
point.co_deform = base
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
class TWEAKLAT_OT_Create(bpy.types.Operator):
"""Create a lattice setup at the 3D cursor to deform selected objects."""
bl_idname = "lattice.create_tweak_lattice"
bl_label = "Create Tweak Lattice"
bl_options = {'REGISTER', 'UNDO'}
radius: FloatProperty(name="Radius", default=0.5)
resolution: IntVectorProperty(name="Resolution", default=(12, 12, 12), min=6, max=64)
@classmethod
def poll(cls, context):
for ob in context.selected_objects:
if ob.type=='MESH':
return True
return False
def execute(self, context):
# 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 = "LTC-Tweak"
lattice = bpy.data.lattices.new(lattice_name)
lattice_ob = bpy.data.objects.new(lattice_name, lattice)
coll.objects.link(lattice_ob)
lattice_ob.location = context.scene.cursor.location
lattice_ob.hide_viewport = True
# Set resolution
lattice.points_u, lattice.points_v, lattice.points_w, = self.resolution
lattice.points_u = clamp(lattice.points_u, 6, 64)
lattice.points_v = clamp(lattice.points_v, 6, 64)
lattice.points_w = clamp(lattice.points_w, 6, 64)
# Create a vertex group.
vg = lattice_ob.vertex_groups.new(name="Hook")
indices = []
for x in range(lattice.points_u-4):
for y in range(lattice.points_v-4):
for z in range(lattice.points_w-4):
indices.append( get_lattice_vertex_index(lattice, (x+2, y+2, z+2)) )
# Assign weights to the group.
vg.add(indices, 1, 'REPLACE')
# Create an Empty at the 3D cursor
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
hook.location = context.scene.cursor.location
coll.objects.link(hook)
# Create some custom properties
hook['Lattice'] = lattice_ob
rna_idprop_ui_create(
hook, "Tweak Lattice", 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 = 1, 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 Ctrl+G/R/S on the hook to reset its transforms.
root_name = "Root_" + hook.name
root = bpy.data.objects.new(root_name, None)
root.empty_display_type = 'CUBE'
root.empty_display_size = 0.5
root.location = context.scene.cursor.location
coll.objects.link(root)
root.hide_viewport = True
hook['Root'] = root
# Parent lattice and hook to root
lattice_ob.parent = root
lattice_ob.matrix_parent_inverse = root.matrix_world.inverted()
lattice_ob.location = Vector()
hook.parent = root
hook.matrix_parent_inverse = root.matrix_world.inverted()
hook.location = Vector()
# 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
for i, o in enumerate(context.selected_objects):
if o.type!='MESH': continue
m = o.modifiers.new(name="Tweak Lattice", type='LATTICE')
m.object = lattice_ob
hook["object_"+str(i)] = o
# Add driver to the modifier influence
simple_driver(m, 'strength', hook, '["Tweak Lattice"]')
# Set up Radius control.
trans_con = hook.constraints.new(type='TRANSFORM')
trans_con.target = root
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"]')
for i in range(3):
simple_driver(lattice_ob, 'scale', hook, '["Radius"]', i)
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
return {'FINISHED'}
class TWEAKLAT_OT_Delete(bpy.types.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):
ob = context.object
return ob.type=='EMPTY' and 'Tweak Lattice' in ob
def execute(self, context):
hook = context.object
lattice = hook['Lattice']
root = hook['Root']
# Remove Lattice modifiers and their drivers.
ob_count = 0
ob_prop_name = "object_"+str(ob_count)
while ob_prop_name in hook:
ob = hook[ob_prop_name]
for m in ob.modifiers:
if m.type!='LATTICE': continue
if m.object == lattice:
m.driver_remove('strength')
ob.modifiers.remove(m)
break
ob_count += 1
ob_prop_name = "object_"+str(ob_count)
# 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(coll_name)
if coll and len(coll.all_objects)==0:
bpy.data.collections.remove(coll)
return {'FINISHED'}
class TWEAKLAT_PT_main(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Lattice Magic'
bl_label = "Tweak Lattice"
@classmethod
def poll(cls, context):
return context.object
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
hook = context.object
if hook.type!='EMPTY' or 'Tweak Lattice' not in hook:
layout.operator(TWEAKLAT_OT_Create.bl_idname, icon='OUTLINER_OB_LATTICE')
return
layout.prop(hook, '["Tweak Lattice"]', slider=True, text="Influence")
layout.prop(hook, '["Radius"]', slider=True)
layout.separator()
layout.operator(TWEAKLAT_OT_Delete.bl_idname, text='Delete Tweak Lattice', icon='TRASH')
layout.separator()
layout.label(text="Helper Objects")
lattice_row = layout.row()
lattice_row.prop(hook, '["Lattice"]', text="Lattice")
lattice_row.prop(hook['Lattice'], 'hide_viewport', text="", emboss=False)
root_row = layout.row()
root_row.prop(hook, '["Root"]', text="Root")
root_row.prop(hook['Root'], 'hide_viewport', text="", emboss=False)
layout.separator()
layout.label(text="Affected Objects")
ob_count = 0
ob_prop_name = "object_"+str(ob_count)
while ob_prop_name in hook:
layout.prop(hook, f'["{ob_prop_name}"]', text="")
ob_count += 1
ob_prop_name = "object_"+str(ob_count)
classes = [
LATTICE_OT_Reset
,TWEAKLAT_OT_Create
,TWEAKLAT_OT_Delete
,TWEAKLAT_PT_main
]
def register():
from bpy.utils import register_class
for c in classes:
register_class(c)
# bpy.types.MESH_MT_shape_key_context_menu.append(draw_shape_key_reset)
def unregister():
from bpy.utils import unregister_class
for c in reversed(classes):
unregister_class(c)
# bpy.types.MESH_MT_shape_key_context_menu.remove(draw_shape_key_reset)