641 lines
21 KiB
Python
641 lines
21 KiB
Python
# SPDX-FileCopyrightText: 2015 Blender Studio Tools Authors
|
|
#
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
# 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.
|
|
|
|
import bpy
|
|
import math
|
|
from bpy.app.handlers import persistent
|
|
from mathutils import Vector
|
|
from bpy.props import (
|
|
BoolProperty,
|
|
PointerProperty,
|
|
CollectionProperty,
|
|
IntProperty,
|
|
EnumProperty,
|
|
FloatProperty,
|
|
)
|
|
from mathutils.geometry import intersect_point_line
|
|
|
|
from .utils import bounding_box_center_of_objects
|
|
from .prefs import get_addon_prefs
|
|
|
|
|
|
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 OBJECT_OT_camlattice_add(bpy.types.Operator):
|
|
"""Add a Camera Lattice Slot"""
|
|
|
|
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 OBJECT_OT_camlattice_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
|
|
if len(scene.lattice_slots) > 0:
|
|
return True
|
|
|
|
cls.poll_message_set("No slots to remove.")
|
|
return False
|
|
|
|
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 OBJECT_OT_camlattice_move(bpy.types.Operator):
|
|
"""Move Lattice Slot"""
|
|
|
|
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
|
|
if len(scene.lattice_slots) > 1:
|
|
return True
|
|
|
|
cls.poll_message_set("No slots to re-order.")
|
|
return False
|
|
|
|
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 OBJECT_OT_camlattice_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]
|
|
|
|
if not active_slot.collection:
|
|
cls.poll_message_set("A collection must be selected above.")
|
|
return False
|
|
if not active_slot.camera:
|
|
cls.poll_message_set("A camera must be selected above.")
|
|
return False
|
|
if active_slot.lattice:
|
|
cls.poll_message_set("This slot already has a lattice generated for it. You can delete it above if you wish to re-generate it.")
|
|
return False
|
|
return True
|
|
|
|
|
|
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
|
|
|
|
if context.active_object and context.active_object.mode != 'OBJECT':
|
|
bpy.ops.object.mode_set(mode='OBJECT')
|
|
|
|
# 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 = [o for o in collection.all_objects if o.type == 'MESH']
|
|
center = bounding_box_center_of_objects(all_meshes)
|
|
|
|
# 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 not ob.visible_get():
|
|
skip = True
|
|
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 OBJECT_OT_camlattice_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]
|
|
|
|
if not active_slot.lattice:
|
|
cls.poll_message_set("This slot has no lattice to delete.")
|
|
return False
|
|
|
|
return True
|
|
|
|
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 OBJECT_OT_camlattice_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'}
|
|
|
|
|
|
def shape_key_poll(context):
|
|
ob = context.object
|
|
if not ob or ob.type != 'LATTICE':
|
|
return False
|
|
if not ob.data.shape_keys or len(ob.data.shape_keys.key_blocks) < 2:
|
|
return False
|
|
return True
|
|
|
|
|
|
class OBJECT_OT_camlattice_zero_all(bpy.types.Operator):
|
|
"""Set all shape key values to 0"""
|
|
|
|
bl_idname = "lattice.shape_keys_zero_all"
|
|
bl_label = "Zero All Shape Keys"
|
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return shape_key_poll(context)
|
|
|
|
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
|
|
|
|
for sk in lattice.shape_keys.key_blocks:
|
|
sk.value = 0
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class OBJECT_OT_camlattice_keyframe_all(bpy.types.Operator):
|
|
"""Insert a keyframe on the current frame for all shape key values"""
|
|
|
|
bl_idname = "lattice.shape_keys_keyframe_all"
|
|
bl_label = "Keyframe All Shape Keys"
|
|
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
|
|
|
|
@classmethod
|
|
def poll(cls, context):
|
|
return shape_key_poll(context)
|
|
|
|
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
|
|
|
|
for sk in lattice.shape_keys.key_blocks:
|
|
sk.keyframe_insert('value')
|
|
|
|
return {'FINISHED'}
|
|
|
|
|
|
class VIEW3D_PT_camlattice_panel(bpy.types.Panel):
|
|
bl_space_type = 'VIEW_3D'
|
|
bl_region_type = 'UI'
|
|
bl_category = 'Lattice Magic'
|
|
bl_label = "Camera Lattice"
|
|
|
|
@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(OBJECT_OT_camlattice_add.bl_idname, text="", icon='ADD')
|
|
remove_op = col.operator(OBJECT_OT_camlattice_remove.bl_idname, text="", icon='REMOVE')
|
|
remove_op.index = active_index
|
|
col.separator()
|
|
move_up_op = col.operator(OBJECT_OT_camlattice_move.bl_idname, text="", icon='TRIA_UP')
|
|
move_up_op.direction = 'UP'
|
|
move_down_op = col.operator(OBJECT_OT_camlattice_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(OBJECT_OT_camlattice_generate.bl_idname, icon='OUTLINER_OB_LATTICE')
|
|
else:
|
|
layout.operator(OBJECT_OT_camlattice_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(align=True)
|
|
row.operator(OBJECT_OT_camlattice_zero_all.bl_idname, text="", icon='RADIOBUT_OFF')
|
|
row.operator(OBJECT_OT_camlattice_keyframe_all.bl_idname, text="", icon='HANDLETYPE_FREE_VEC')
|
|
row.separator()
|
|
prefs = get_addon_prefs(context)
|
|
row.prop(prefs, 'update_active_shape_key', toggle=True, text="", icon='TIME')
|
|
|
|
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(OBJECT_OT_camlattice_shapekey_add.bl_idname, text="", icon='ADD')
|
|
remove_op = col.operator('object.shape_key_remove', text="", icon='REMOVE')
|
|
|
|
col.separator()
|
|
col.menu("MESH_MT_shape_key_context_menu", icon='DOWNARROW_HLT', text="")
|
|
|
|
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'
|
|
|
|
|
|
@persistent
|
|
def camera_lattice_frame_change(scene):
|
|
"""On frame change, set the active shape key of the active lattice object to the most recent frame
|
|
|
|
(Assuming the shape keys are named after the frame on which they are used)
|
|
"""
|
|
|
|
# I wonder why this function doesn't recieve a context... should it not be relied on from here? o.0
|
|
context = bpy.context
|
|
|
|
prefs = get_addon_prefs(context)
|
|
if not prefs.update_active_shape_key:
|
|
return
|
|
|
|
ob = context.object
|
|
if not shape_key_poll(context):
|
|
return
|
|
|
|
key_blocks = ob.data.shape_keys.key_blocks
|
|
|
|
current_frame = scene.frame_current
|
|
most_recent_number = 0
|
|
most_recent_index = 1
|
|
for i, kb in enumerate(key_blocks):
|
|
if not kb.name.startswith('Frame '):
|
|
continue
|
|
number_str = kb.name[5:].split(".")[0]
|
|
if number_str == "":
|
|
continue
|
|
number = int(number_str)
|
|
if number <= current_frame and number >= most_recent_number:
|
|
most_recent_number = number
|
|
most_recent_index = i
|
|
if most_recent_number == current_frame:
|
|
break
|
|
|
|
if ob.active_shape_key_index != most_recent_index:
|
|
ob.active_shape_key_index = most_recent_index
|
|
|
|
|
|
registry = [
|
|
LatticeSlot,
|
|
CAMLAT_UL_lattice_slots,
|
|
OBJECT_OT_camlattice_add,
|
|
OBJECT_OT_camlattice_remove,
|
|
OBJECT_OT_camlattice_move,
|
|
OBJECT_OT_camlattice_generate,
|
|
OBJECT_OT_camlattice_delete,
|
|
OBJECT_OT_camlattice_shapekey_add,
|
|
OBJECT_OT_camlattice_zero_all,
|
|
OBJECT_OT_camlattice_keyframe_all,
|
|
VIEW3D_PT_camlattice_panel,
|
|
]
|
|
|
|
|
|
def register():
|
|
bpy.types.Scene.lattice_slots = CollectionProperty(type=LatticeSlot)
|
|
bpy.types.Scene.active_lattice_index = IntProperty()
|
|
bpy.app.handlers.frame_change_post.append(camera_lattice_frame_change)
|
|
|
|
|
|
def unregister():
|
|
del bpy.types.Scene.lattice_slots
|
|
del bpy.types.Scene.active_lattice_index
|
|
|
|
bpy.app.handlers.frame_change_post.remove(camera_lattice_frame_change)
|