Add Lattice Magic
to Addons
#48
3
README.md
Normal file
3
README.md
Normal 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
578
camera_lattice.py
Normal 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)
|
Loading…
Reference in New Issue
Block a user