Add Lattice Magic
to Addons
#48
26
__init__.py
26
__init__.py
@ -14,23 +14,33 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
bl_info = {
|
||||
"name": "Camera Lattice",
|
||||
"name": "Lattice Magic",
|
||||
"author": "Demeter Dzadik",
|
||||
"version": (1,0),
|
||||
"blender": (2, 90, 0),
|
||||
"location": "View3D > Sidebar > Camera Lattice",
|
||||
"description": "Create a lattice in the camera view to smear geometry.",
|
||||
"location": "View3D > Sidebar > Lattice Magic",
|
||||
"description": "Various Lattice-based tools to smear or adjust geometry.",
|
||||
"category": "Rigging",
|
||||
"doc_url": "",
|
||||
"tracker_url": "",
|
||||
"doc_url": "https://gitlab.com/blender/lattice_magic/-/wikis/home",
|
||||
"tracker_url": "https://gitlab.com/blender/lattice_magic/-/issues/new",
|
||||
}
|
||||
|
||||
from . import camera_lattice
|
||||
from . import tweak_lattice
|
||||
import importlib
|
||||
|
||||
modules = [
|
||||
camera_lattice
|
||||
,tweak_lattice
|
||||
]
|
||||
|
||||
def register():
|
||||
importlib.reload(camera_lattice)
|
||||
camera_lattice.register()
|
||||
for m in modules:
|
||||
importlib.reload(m)
|
||||
if hasattr(m, 'register'):
|
||||
m.register()
|
||||
|
||||
def unregister():
|
||||
camera_lattice.unregister()
|
||||
for m in modules:
|
||||
if hasattr(m, 'unregister'):
|
||||
m.unregister()
|
@ -103,7 +103,7 @@ class LatticeSlot(bpy.types.PropertyGroup):
|
||||
|
||||
|
||||
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_label = "Remove Lattice Slot"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
@ -133,6 +133,7 @@ class CAMLAT_OT_Remove(bpy.types.Operator):
|
||||
return { 'FINISHED' }
|
||||
|
||||
class CAMLAT_OT_Add(bpy.types.Operator):
|
||||
"""Add a Camera Lattice Slot"""
|
||||
bl_idname = "lattice.add_slot"
|
||||
bl_label = "Add Lattice Slot"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
@ -152,6 +153,7 @@ class CAMLAT_OT_Add(bpy.types.Operator):
|
||||
return { 'FINISHED' }
|
||||
|
||||
class CAMLAT_OT_Move(bpy.types.Operator):
|
||||
"""Move Lattice Slot"""
|
||||
bl_idname = "lattice.move_slot"
|
||||
bl_label = "Move Lattice Slot"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
@ -188,7 +190,7 @@ class CAMLAT_OT_Move(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_label = "Generate Lattice"
|
||||
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
||||
@ -423,8 +425,8 @@ class CAMLAT_OT_ShapeKey_Add(bpy.types.Operator):
|
||||
class CAMLAT_PT_main(bpy.types.Panel):
|
||||
bl_space_type = 'VIEW_3D'
|
||||
bl_region_type = 'UI'
|
||||
bl_category = 'Camera Lattice'
|
||||
bl_label = "Lattice Slots"
|
||||
bl_category = 'Lattice Magic'
|
||||
bl_label = "Camera Lattice Slots"
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context):
|
||||
|
398
tweak_lattice.py
Normal file
398
tweak_lattice.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user