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
5 changed files with 248 additions and 225 deletions
Showing only changes of commit cbf891eba5 - Show all commits

View File

@ -27,20 +27,20 @@ bl_info = {
from . import camera_lattice from . import camera_lattice
from . import tweak_lattice from . import tweak_lattice
from . import operators
import importlib import importlib
modules = [ modules = [
camera_lattice camera_lattice
,tweak_lattice ,tweak_lattice
,operators
] ]
def register(): def register():
for m in modules: for m in modules:
importlib.reload(m) importlib.reload(m)
if hasattr(m, 'register'): m.register()
m.register()
def unregister(): def unregister():
for m in modules: for m in modules:
if hasattr(m, 'unregister'): m.unregister()
m.unregister()

View File

@ -8,33 +8,12 @@
# 3D Lattices: Need to have a distance, thickness and Z resolution parameter. # 3D Lattices: Need to have a distance, thickness and Z resolution parameter.
import bpy import bpy
from typing import Tuple, List
import math import math
from mathutils import Vector from mathutils import Vector
from bpy.props import BoolProperty, PointerProperty, CollectionProperty, IntProperty, EnumProperty, FloatProperty from bpy.props import BoolProperty, PointerProperty, CollectionProperty, IntProperty, EnumProperty, FloatProperty
from mathutils.geometry import intersect_point_line from mathutils.geometry import intersect_point_line
def bounding_box(points) -> Tuple[Vector, Vector]: from .utils import bounding_box_center, bounding_box
""" 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): class CAMLAT_UL_lattice_slots(bpy.types.UIList):
def draw_item(self, context, layout, data, item, icon, active_data, active_propname): def draw_item(self, context, layout, data, item, icon, active_data, active_propname):
@ -102,6 +81,26 @@ class LatticeSlot(bpy.types.PropertyGroup):
) )
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'}
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_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"
@ -132,26 +131,6 @@ class CAMLAT_OT_Remove(bpy.types.Operator):
return { 'FINISHED' } 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'}
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): class CAMLAT_OT_Move(bpy.types.Operator):
"""Move Lattice Slot""" """Move Lattice Slot"""
bl_idname = "lattice.move_slot" bl_idname = "lattice.move_slot"
@ -364,41 +343,6 @@ class CAMLAT_OT_Delete(bpy.types.Operator):
return { 'FINISHED' } 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): 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""" """Add a shape key to the active Lattice Slot's lattice, named after the current frame number"""
@ -422,11 +366,12 @@ class CAMLAT_OT_ShapeKey_Add(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
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 = 'Lattice Magic' bl_category = 'Lattice Magic'
bl_label = "Camera Lattice Slots" bl_label = "Camera Lattice"
@classmethod @classmethod
def poll(cls, context): def poll(cls, context):
@ -523,15 +468,16 @@ class CAMLAT_PT_main(bpy.types.Panel):
classes = [ classes = [
LatticeSlot LatticeSlot
,CAMLAT_UL_lattice_slots ,CAMLAT_UL_lattice_slots
,CAMLAT_PT_main
,CAMLAT_OT_Remove
,CAMLAT_OT_Add ,CAMLAT_OT_Add
,CAMLAT_OT_Remove
,CAMLAT_OT_Move ,CAMLAT_OT_Move
,CAMLAT_OT_Generate ,CAMLAT_OT_Generate
,CAMLAT_OT_Delete ,CAMLAT_OT_Delete
,ShapeKey_OT_Reset
,CAMLAT_OT_ShapeKey_Add ,CAMLAT_OT_ShapeKey_Add
,CAMLAT_PT_Main
] ]
def register(): def register():
@ -541,7 +487,6 @@ def register():
bpy.types.Scene.lattice_slots = CollectionProperty(type=LatticeSlot) bpy.types.Scene.lattice_slots = CollectionProperty(type=LatticeSlot)
bpy.types.Scene.active_lattice_index = IntProperty() bpy.types.Scene.active_lattice_index = IntProperty()
bpy.types.MESH_MT_shape_key_context_menu.append(draw_shape_key_reset)
def unregister(): def unregister():
from bpy.utils import unregister_class from bpy.utils import unregister_class
@ -550,4 +495,3 @@ def unregister():
del bpy.types.Scene.lattice_slots del bpy.types.Scene.lattice_slots
del bpy.types.Scene.active_lattice_index del bpy.types.Scene.active_lattice_index
bpy.types.MESH_MT_shape_key_context_menu.remove(draw_shape_key_reset)

113
operators.py Normal file
View File

@ -0,0 +1,113 @@
import bpy
from bpy.props import FloatProperty
from .utils import get_lattice_point_original_position
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 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')
classes = [
ShapeKey_OT_Reset
,LATTICE_OT_Reset
]
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)

View File

@ -21,79 +21,10 @@ from typing import List
from mathutils import Vector from mathutils import Vector
from rna_prop_ui import rna_idprop_ui_create from rna_prop_ui import rna_idprop_ui_create
from .utils import clamp, get_lattice_vertex_index, simple_driver
coll_name = 'Tweak Lattices' 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): def ensure_tweak_lattice_collection(scene):
coll = bpy.data.collections.get(coll_name) coll = bpy.data.collections.get(coll_name)
if not coll: if not coll:
@ -102,62 +33,6 @@ def ensure_tweak_lattice_collection(scene):
return 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): class TWEAKLAT_OT_Create(bpy.types.Operator):
"""Create a lattice setup at the 3D cursor to deform selected objects.""" """Create a lattice setup at the 3D cursor to deform selected objects."""
bl_idname = "lattice.create_tweak_lattice" bl_idname = "lattice.create_tweak_lattice"
@ -330,7 +205,7 @@ class TWEAKLAT_OT_Delete(bpy.types.Operator):
return {'FINISHED'} return {'FINISHED'}
class TWEAKLAT_PT_main(bpy.types.Panel): class TWEAKLAT_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 = 'Lattice Magic' bl_category = 'Lattice Magic'
@ -377,10 +252,9 @@ class TWEAKLAT_PT_main(bpy.types.Panel):
ob_prop_name = "object_"+str(ob_count) ob_prop_name = "object_"+str(ob_count)
classes = [ classes = [
LATTICE_OT_Reset TWEAKLAT_OT_Create
,TWEAKLAT_OT_Create
,TWEAKLAT_OT_Delete ,TWEAKLAT_OT_Delete
,TWEAKLAT_PT_main ,TWEAKLAT_PT_Main
] ]
def register(): def register():
@ -388,11 +262,7 @@ def register():
for c in classes: for c in classes:
register_class(c) register_class(c)
# bpy.types.MESH_MT_shape_key_context_menu.append(draw_shape_key_reset)
def unregister(): def unregister():
from bpy.utils import unregister_class from bpy.utils import unregister_class
for c in reversed(classes): for c in reversed(classes):
unregister_class(c) unregister_class(c)
# bpy.types.MESH_MT_shape_key_context_menu.remove(draw_shape_key_reset)

96
utils.py Normal file
View File

@ -0,0 +1,96 @@
import bpy
from mathutils import Vector
from typing import List, Tuple
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 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