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
2 changed files with 581 additions and 0 deletions
Showing only changes of commit 9a85307848 - Show all commits

3
README.md Normal file
View File

@ -0,0 +1,3 @@
Camera Lattice lets you easily create a lattice in a camera's view frame and deform a character (or any collection) with said lattice.
Note: If you want to delete a lattice, make sure to do it through the addon's UI, which is in the sidebar. This will make sure to delete all modifiers, drivers and animation datablocks that were created along with the lattice.

578
camera_lattice.py Normal file
View File

@ -0,0 +1,578 @@
# Copyright (C) 2020 Demeter Dzadik
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
bl_info = {
"name": "Camera Lattice",
"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.",
"category": "Rigging",
"doc_url": "",
"tracker_url": "",
}
# Inspired by https://animplay.wordpress.com/2015/11/18/smear-frame-script-maya/.
# This addon allows the user to specify a camera and a collection,
# and create a 2D lattice that fills the camera's view,
# to deform the mesh objects in that collection.
# TODO:
# 3D Lattices: Need to have a distance, thickness and Z resolution parameter.
import bpy
from typing import Tuple, List
import math
from mathutils import Vector
from bpy.props import BoolProperty, PointerProperty, CollectionProperty, IntProperty, EnumProperty, FloatProperty
from mathutils.geometry import intersect_point_line
def bounding_box(points) -> Tuple[Vector, Vector]:
""" Return two vectors representing the lowest and highest coordinates of
a the bounding box of the passed points.
"""
lowest = points[0].copy()
highest = points[0].copy()
for p in points:
for i in range(len(p)):
if p[i] < lowest[i]:
lowest[i] = p[i]
if p[i] > highest[i]:
highest[i] = p[i]
return lowest, highest
def bounding_box_center(points) -> Vector:
"""Find the bounding box center of some points."""
bbox_low, bbox_high = bounding_box(points)
return bbox_low + (bbox_high-bbox_low)/2
class CAMLAT_UL_lattice_slots(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
lattice_slots = context.scene.lattice_slots
active_slot = lattice_slots[context.scene.active_lattice_index]
current_slot = item
if self.layout_type in {'DEFAULT', 'COMPACT'}:
if current_slot.collection:
row = layout.row()
icon = 'OUTLINER_COLLECTION' if current_slot.enabled else 'COLLECTION_COLOR_07'
row.prop(current_slot.collection, 'name', text="", emboss=False, icon=icon)
row.enabled = current_slot.enabled
layout.prop(current_slot, 'strength', text="", slider=True, emboss=False)
icon = 'CHECKBOX_HLT' if current_slot.enabled else 'CHECKBOX_DEHLT'
layout.prop(current_slot, 'enabled', text="", icon=icon, emboss=False)
else:
layout.label(text="", translate=False, icon='COLLECTION_NEW')
elif self.layout_type in {'GRID'}:
layout.alignment = 'CENTER'
layout.label(text="", icon_value=icon)
class LatticeSlot(bpy.types.PropertyGroup):
enabled: BoolProperty(
name = "Enabled"
,description = "Whether the Lattice has an effect or not"
,default = True
)
strength: FloatProperty(
name = "Strength"
,description = "Strength of the lattice effect"
,min = 0
,max = 1
,default = 1
)
lattice: PointerProperty(
name = "Lattice"
,type = bpy.types.Object
,description = "Lattice object generated by this LatticeSlot. This cannot be specified manually, use the Generate or Delete operator below"
)
def is_camera(self, obj):
return obj.type=='CAMERA'
camera: PointerProperty(
name = "Camera"
,type = bpy.types.Object
,description = "Camera used by this LatticeSlot"
,poll = is_camera
)
collection: PointerProperty(
name = "Collection"
,type = bpy.types.Collection
,description = "Collection affected by this LatticeSlot"
)
resolution: IntProperty(
name = "Resolution"
,description = "Resolution of the lattice grid"
,min = 5
,max = 64
,default = 10
,options = set()
)
class CAMLAT_OT_Remove(bpy.types.Operator):
"""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'}
index: IntProperty()
@classmethod
def poll(cls, context):
scene = context.scene
return len(scene.lattice_slots) > 0
def execute(self, context):
scene = context.scene
lattice_slots = scene.lattice_slots
active_index = scene.active_lattice_index
# This behaviour is inconsistent with other UILists in Blender, but I am right and they are wrong!
active_slot = lattice_slots[active_index]
if active_slot.lattice:
bpy.ops.lattice.delete_lattice_from_slot()
to_index = active_index
if to_index > len(lattice_slots)-2:
to_index = len(lattice_slots)-2
scene.lattice_slots.remove(self.index)
scene.active_lattice_index = to_index
return { 'FINISHED' }
class CAMLAT_OT_Add(bpy.types.Operator):
bl_idname = "lattice.add_slot"
bl_label = "Add Lattice Slot"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context):
scene = context.scene
lattice_slots = scene.lattice_slots
active_index = scene.active_lattice_index
to_index = active_index + 1
if len(lattice_slots)==0:
to_index = 0
scene.lattice_slots.add()
scene.lattice_slots.move(len(scene.lattice_slots)-1, to_index)
scene.active_lattice_index = to_index
return { 'FINISHED' }
class CAMLAT_OT_Move(bpy.types.Operator):
bl_idname = "lattice.move_slot"
bl_label = "Move Lattice Slot"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
direction: EnumProperty(
name = "Direction"
,items = [
('UP', 'UP', 'UP'),
('DOWN', 'DOWN', 'DOWN'),
]
,default = 'UP'
)
@classmethod
def poll(cls, context):
scene = context.scene
return len(scene.lattice_slots) > 1
def execute(self, context):
scene = context.scene
lattice_slots = scene.lattice_slots
active_index = scene.active_lattice_index
to_index = active_index + (1 if self.direction=='DOWN' else -1)
if to_index > len(lattice_slots)-1:
to_index = 0
if to_index < 0:
to_index = len(lattice_slots)-1
scene.lattice_slots.move(active_index, to_index)
scene.active_lattice_index = to_index
return { 'FINISHED' }
class CAMLAT_OT_Generate(bpy.types.Operator):
"""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'}
@classmethod
def poll(cls, context):
scene = context.scene
active_slot = scene.lattice_slots[scene.active_lattice_index]
return active_slot.collection and active_slot.camera and not active_slot.lattice
def execute(self, context):
scene = context.scene
active_slot = scene.lattice_slots[scene.active_lattice_index]
collection = active_slot.collection
camera = active_slot.camera
resolution = active_slot.resolution
# Create a lattice object.
lattice_name = "Lattice_" + collection.name
lattice = bpy.data.lattices.new(lattice_name)
lattice_ob = bpy.data.objects.new(lattice_name, lattice)
scene.collection.objects.link(lattice_ob)
active_slot.lattice = lattice_ob
bpy.ops.object.select_all(action='DESELECT')
context.view_layer.objects.active = lattice_ob
lattice_ob.select_set(True)
# Align to camera (not really needed).
lattice_ob.rotation_euler = camera.matrix_world.to_euler()
# Parent to camera.
lattice_ob.parent = camera
lattice_ob.matrix_parent_inverse = camera.matrix_world.inverted()
# Constrain to camera.
constraint = lattice_ob.constraints.new('DAMPED_TRACK')
constraint.target = camera
constraint.track_axis = 'TRACK_Z'
### Placing the Lattice in the center of the camera's view, at the bounding box center of the collection's objects.
# Find the bounding box center of the collection of objects
all_meshes = []
all_points = []
for ob in collection.all_objects:
if ob.type=='MESH' and ob not in all_meshes:
all_meshes.append(ob)
for p in ob.bound_box:
all_points.append(ob.matrix_world @ Vector(p))
center = bounding_box_center(all_points)
# Define a line from the camera towards the camera's view direction
cam_vec = Vector((0, 0, -1)) # Default aim vector of a camera (they point straight down)
# Rotate the default vector by the camera's rotation
cam_vec.rotate(camera.matrix_world.to_euler())
cam_world_pos = camera.matrix_world.to_translation()
cam_target_pos = cam_world_pos + cam_vec
# Find the nearest point on this line to the bounding box center
intersect = intersect_point_line(center, cam_world_pos, cam_target_pos)[0]
# This is where the Lattice is placed!
lattice_ob.location = intersect
# Scale the lattice so that it fills up the camera's view
# based on the distance of this point from the camera and the scene's aspect ratio.
# https://fullpipeumbrella.com/en/blender-python-script-how-to-position/
distance = (intersect - cam_world_pos).length
fov = camera.data.angle
scale_x = distance * math.sin(fov/2) / math.cos(fov/2) * 2
aspect_ratio = (scene.render.resolution_x * scene.render.pixel_aspect_x) / (scene.render.resolution_y * scene.render.pixel_aspect_y)
scale_y = scale_x / aspect_ratio
lattice_ob.scale = [scale_x, scale_y, 1]
# Set lattice resolution
lattice.points_u = resolution
lattice.points_v = round(resolution / aspect_ratio)
lattice.points_w = 1
# Create two shape keys.
bpy.ops.lattice.smear_add_shape()
bpy.ops.lattice.smear_add_shape()
# Add Lattice modifiers
for ob in all_meshes:
# Skip those meshes which are already being deformed by another mesh in the same collection.
skip=False
for m in ob.modifiers:
if m.type == 'MESH_DEFORM' and m.object in all_meshes:
skip = True
break
if m.type == 'SURFACE_DEFORM' and m.target in all_meshes:
skip = True
break
if skip: continue
mod = ob.modifiers.new(name=lattice_ob.name, type='LATTICE')
mod.object = lattice_ob
# Add drivers for easy disabling
index = len(scene.lattice_slots)-1
driver = ob.driver_add(f'modifiers["{lattice_ob.name}"].strength').driver
driver.type = 'SUM'
var = driver.variables.new()
var.targets[0].id_type = 'SCENE'
var.targets[0].id = scene
var.targets[0].data_path = f'lattice_slots[{index}].strength'
driver = ob.driver_add(f'modifiers["{lattice_ob.name}"].show_viewport').driver
driver.type = 'SUM'
var = driver.variables.new()
var.targets[0].id_type = 'SCENE'
var.targets[0].id = scene
var.targets[0].data_path = f'lattice_slots[{index}].enabled'
driver = ob.driver_add(f'modifiers["{lattice_ob.name}"].show_render').driver
driver.type = 'SUM'
var = driver.variables.new()
var.targets[0].id_type = 'SCENE'
var.targets[0].id = scene
var.targets[0].data_path = f'lattice_slots[{index}].enabled'
return { 'FINISHED' }
class CAMLAT_OT_Delete(bpy.types.Operator):
"""Delete Lattice object, its animation and modifiers that target it in the selected collection's objects"""
bl_idname = "lattice.delete_lattice_from_slot"
bl_label = "Delete Lattice"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context):
scene = context.scene
active_slot = scene.lattice_slots[scene.active_lattice_index]
return active_slot.lattice
def execute(self, context):
scene = context.scene
active_slot = scene.lattice_slots[scene.active_lattice_index]
lattice_ob = active_slot.lattice
lattice = lattice_ob.data
# Delete modifiers and their drivers
collection = active_slot.collection
for ob in collection.all_objects:
if not ob.type=='MESH': continue
for m in ob.modifiers[:]:
if not (m.type=='LATTICE' and m.object==lattice_ob): continue
ob.driver_remove(f'modifiers["{m.name}"].strength')
ob.driver_remove(f'modifiers["{m.name}"].show_viewport')
ob.driver_remove(f'modifiers["{m.name}"].show_render')
ob.modifiers.remove(m)
# Delete animation datablocks
datablocks = [lattice, lattice_ob, lattice.shape_keys]
for datablock in datablocks:
if not datablock: continue
if not datablock.animation_data: continue
if not datablock.animation_data.action: continue
bpy.data.actions.remove(datablock.animation_data.action)
# Delte Lattice datablock
bpy.data.objects.remove(lattice_ob)
# Delete Object datablock
bpy.data.lattices.remove(lattice)
return { 'FINISHED' }
class ShapeKey_OT_Reset(bpy.types.Operator):
"""Reset shape of the active shape key of the active object"""
bl_idname = "object.reset_shape_key"
bl_label = "Reset Shape Key"
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context):
ob = context.object
if not ob:
return False
if not ob.data.shape_keys:
return False
if len(ob.data.shape_keys.key_blocks)<2:
return False
if ob.active_shape_key_index==0:
return False
return True
def execute(self, context):
ob = context.object
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]
for i, skp in enumerate(active_block.data):
skp.co = basis_block.data[i].co
return { 'FINISHED' }
def draw_shape_key_reset(self, context):
self.layout.operator(ShapeKey_OT_Reset.bl_idname, text="Reset Shape Key", icon='FILE_REFRESH')
class CAMLAT_OT_ShapeKey_Add(bpy.types.Operator):
"""Add a shape key to the active Lattice Slot's lattice, named after the current frame number"""
bl_idname = "lattice.smear_add_shape"
bl_label = "Add Smear Shape"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context):
scene = context.scene
active_slot = scene.lattice_slots[scene.active_lattice_index]
lattice_ob = active_slot.lattice
lattice = lattice_ob.data
name = "Basis"
if lattice.shape_keys:
name = "Frame " + str(scene.frame_current)
lattice_ob.shape_key_add(name=name, from_mix=False)
lattice_ob.active_shape_key_index = len(lattice.shape_keys.key_blocks)-1
block = lattice.shape_keys.key_blocks[-1]
block.value = 1
return {'FINISHED'}
class CAMLAT_PT_main(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Camera Lattice'
bl_label = "Lattice Slots"
@classmethod
def poll(cls, context):
return True
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
scene = context.scene
active_index = scene.active_lattice_index
row = layout.row()
row.template_list(
'CAMLAT_UL_lattice_slots',
'',
scene,
'lattice_slots',
scene,
'active_lattice_index',
)
col = row.column()
col.operator(CAMLAT_OT_Add.bl_idname, text="", icon='ADD')
remove_op = col.operator(CAMLAT_OT_Remove.bl_idname, text="", icon='REMOVE')
remove_op.index = active_index
col.separator()
move_up_op = col.operator(CAMLAT_OT_Move.bl_idname, text="", icon='TRIA_UP')
move_up_op.direction='UP'
move_down_op = col.operator(CAMLAT_OT_Move.bl_idname, text="", icon='TRIA_DOWN')
move_down_op.direction='DOWN'
if len(scene.lattice_slots)==0:
return
active_slot = scene.lattice_slots[scene.active_lattice_index]
col = layout.column()
if active_slot.lattice:
col.enabled=False
row = col.row()
if not active_slot.collection:
row.alert=True
row.prop(active_slot, 'collection')
row = col.row()
if not active_slot.camera:
row.alert=True
row.prop(active_slot, 'camera', icon='OUTLINER_OB_CAMERA')
col.prop(active_slot, 'resolution')
layout.separator()
if not active_slot.lattice:
layout.operator(CAMLAT_OT_Generate.bl_idname, icon='OUTLINER_OB_LATTICE')
else:
layout.operator(CAMLAT_OT_Delete.bl_idname, icon='TRASH')
row = layout.row()
row.enabled=False
row.prop(active_slot, 'lattice')
if not active_slot.lattice:
return
lattice_ob = active_slot.lattice
lattice = lattice_ob.data
col = layout.column()
row = layout.row()
# Display the lattice's Shape Keys in a less cluttered way than in the Properties editor.
row.template_list(
'MESH_UL_shape_keys',
'',
lattice.shape_keys,
'key_blocks',
lattice_ob,
'active_shape_key_index',
)
col = row.column()
col.operator(CAMLAT_OT_ShapeKey_Add.bl_idname, text="", icon='ADD')
remove_op = col.operator('object.shape_key_remove', text="", icon='REMOVE')
col.operator(ShapeKey_OT_Reset.bl_idname, text="", icon='RECOVER_LAST')
col.separator()
move_up_op = col.operator('object.shape_key_move', text="", icon='TRIA_UP')
move_up_op.type='UP'
move_down_op = col.operator('object.shape_key_move', text="", icon='TRIA_DOWN')
move_down_op.type='DOWN'
classes = [
LatticeSlot
,CAMLAT_UL_lattice_slots
,CAMLAT_PT_main
,CAMLAT_OT_Remove
,CAMLAT_OT_Add
,CAMLAT_OT_Move
,CAMLAT_OT_Generate
,CAMLAT_OT_Delete
,ShapeKey_OT_Reset
,CAMLAT_OT_ShapeKey_Add
]
def register():
from bpy.utils import register_class
for c in classes:
register_class(c)
bpy.types.Scene.lattice_slots = CollectionProperty(type=LatticeSlot)
bpy.types.Scene.active_lattice_index = IntProperty()
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)
del bpy.types.Scene.lattice_slots
del bpy.types.Scene.active_lattice_index
bpy.types.MESH_MT_shape_key_context_menu.remove(draw_shape_key_reset)