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
10 changed files with 1762 additions and 0 deletions
Showing only changes of commit 89f355ce5a - Show all commits

View File

@ -0,0 +1,141 @@
*.pyc
# vscode
*.code-workspace
.vscode/
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
__pypackages__/
#mypy
.mypy_cache
# celery beat schedule file
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
blender_addons.code-workspace
rigs/cloud_face_wrap_chain.py

View File

@ -0,0 +1,63 @@
This addon adds some Lattice-based utilities to Blender.
Install like any other Blender addon: Download this repository as a .zip archive, then in Blender go to Edit->Preferences->Addons->Install Addon from File, and browse the zip you downloaded, and enable the checkbox for the addon.
After that, you can find the Lattice Magic panel in the 3D Viewport's Sidebar, which you can bring up by pressing the N key.
![](docs/lattice_magic.png)
# Tweak Lattice
Tweak Lattice lets you create a lattice setup at the 3D cursor to make deformation adjustments to the selected objects.
![](docs/tweak_lattice.gif)
### Parenting
This is meant to be possible to be used in conjunction with a character rig: Before pressing the "Create Tweak Lattice" button, simply select the desired parent rig object and bone in the UI.
### Deletion
If you want to delete a lattice, don't just delete the empty object that was created for you. This would leave behind a big mess of broken modifiers and drivers which will cause tremendous error printing spam in your console/terminal. Instead, use the "Delete Tweak Lattice" button.
### Adding/Removing meshes
When creating a lattice, it will affect all mesh objects which were selected at the moment of its creation.
If you want more meshes to be influenced by a lattice, you don't need to delete it and re-create it with a different selection. Just select the objects you want to add to or remove from the lattice's influence, then finally select the lattice control. There will now be an "Add Selected Objects" and "Remove Selected Objects" button.
### Going under the hood
With the lattice control selected, you can see a "Helper Objects" section in the UI. This lists two objects which are taking care of things under the hood. If you want, you can enable them with the screen icon, which will let you mess with them. This should rarely be necessary though, and you should only do it at your own risk, since there's no way to get these back to their original states once modified.
# Camera Lattice
Camera Lattice lets you create a lattice in a camera's view frame and deform a character (or any collection) with the lattice.
![](docs/camera_lattice.gif)
### Creation
Add an entry to the Camera Lattice list with the + icon. Each entry corresponds to deforming a single collection with a single lattice object from the perspective of a single camera.
You must select a collection and a camera, then hit Generate Lattice. Note that you cannot change the resolution after the fact, so find a resolution that you're happy with, as you will be locked into that.
### Parenting
On creation, the lattice is parented to the camera. You can feel free to remove or change this parenting to your heart's desire, it shouldn't cause any issues. The lattice object also has a Damped Track constraint, the same applies there: You can remove it if you want.
Just remember, there's no reset button for these sort of things.
### Animation
Feel free to animate the lattice in object mode as you wish, although unless the above mentioned Damped Track constraint is enabled, you will only be able to rotate it on one axis.
Animating the lattice's vertices is possible using shape keys. The addon provides some UI and helper operators for this, but at the end of the day it's up to you how you organize and keyframe these shape keys.
The intended workflow is that a shape key should only be active for a single frame. To help with this, shape keys are named when they are added, according to the current frame. There are also some buttons above the list:
- Zero All Shape Keys: Operator to set all shape key values to 0.0. This does not insert a keyframe!
- Keyframe All Shape Keys: Operator to insert a keyframe for all shape keys on the current frame with their current value.
- Update Active Shape Key: Toggle to automatically change the active shape key based on the current frame. Useful when switching into edit mode quickly on different frames.
Note that Blender is not capable of displaying the effect of multiple shape keys on a lattice at the same time, which is another reason to go with the intended workflow, since that will always only have one shape key active at a time.
### Deletion
Similar to Tweak Lattice, never ever delete a lattice setup by simply pressing the X or Del keys, as this will leave behind a huge mess. Instead, use the "Delete Lattice" button, or the "-" button in the top list.
### TODO
Some ideas that could be implemented for Camera Lattice:
- Automatically inserting new shape key in the correct place in the list. Eg., when Frame 1 and Frame 10 already exist, creating a shape key on Frame 5 should insert it in between them.
- Adding or removing objects to the influence of the lattice is not currently possible.

View File

@ -0,0 +1,67 @@
# 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": "Lattice Magic",
"author": "Demeter Dzadik",
"version": (1,0),
"blender": (2, 90, 0),
"location": "View3D > Sidebar > Lattice Magic",
"description": "Various Lattice-based tools to smear or adjust geometry.",
"category": "Rigging",
"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
from . import operators
from . import utils # Just for importlib.reload()
import importlib
import bpy
from bpy.types import AddonPreferences
from bpy.props import BoolProperty
class LatticeMagicPreferences(AddonPreferences):
bl_idname = __name__
update_active_shape_key: BoolProperty(
name = 'Update Active Shape Key',
description = "Update the active shape key on frame change based on the current frame and the shape key's name",
default = False
)
modules = [
camera_lattice
,tweak_lattice
,operators
,utils
]
def register():
from bpy.utils import register_class
register_class(LatticeMagicPreferences)
for m in modules:
importlib.reload(m)
if hasattr(m, 'register'):
m.register()
def unregister():
from bpy.utils import unregister_class
unregister_class(LatticeMagicPreferences)
for m in modules:
if hasattr(m, 'unregister'):
m.unregister()

View File

@ -0,0 +1,595 @@
# 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
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
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_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):
"""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_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
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
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 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 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'}
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 CAMLAT_OT_ShapeKey_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 CAMLAT_OT_ShapeKey_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 CAMLAT_PT_Main(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(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(align=True)
row.operator(CAMLAT_OT_ShapeKey_Zero_All.bl_idname, text="", icon='RADIOBUT_OFF')
row.operator(CAMLAT_OT_ShapeKey_Keyframe_All.bl_idname, text="", icon='HANDLETYPE_FREE_VEC')
row.separator()
prefs = context.preferences.addons[__package__].preferences
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(CAMLAT_OT_ShapeKey_Add.bl_idname, text="", icon='ADD')
remove_op = col.operator('object.shape_key_remove', text="", icon='REMOVE')
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 = context.preferences.addons[__package__].preferences
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
classes = [
LatticeSlot
,CAMLAT_UL_lattice_slots
,CAMLAT_OT_Add
,CAMLAT_OT_Remove
,CAMLAT_OT_Move
,CAMLAT_OT_Generate
,CAMLAT_OT_Delete
,CAMLAT_OT_ShapeKey_Add
,CAMLAT_OT_ShapeKey_Zero_All
,CAMLAT_OT_ShapeKey_Keyframe_All
,CAMLAT_PT_Main
]
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.app.handlers.frame_change_post.append(camera_lattice_frame_change)
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.app.handlers.frame_change_post.remove(camera_lattice_frame_change)

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 MiB

View File

@ -0,0 +1,90 @@
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):
if not ob.data.points[i].select: continue
skp.co = skp.co.lerp(basis_block.data[i].co, self.factor)
continue
else:
for i, skp in enumerate(active_block.data):
if not ob.data.points[i].select: continue
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 = mix
bpy.ops.object.mode_set(mode='EDIT')
return {'FINISHED'}
def draw_shape_key_reset(self, context):
layout = self.layout
ob = context.object
if ob.type=='MESH':
op = layout.operator('mesh.blend_from_shape', text='Reset Shape Key', icon='FILE_REFRESH')
op.shape = ob.data.shape_keys.key_blocks[0].name
op.blend = 1
op.add = False
else:
layout.operator(LATTICE_OT_Reset.bl_idname, text="Reset Shape Key", icon='FILE_REFRESH')
def draw_lattice_reset(self, context):
self.layout.operator(LATTICE_OT_Reset.bl_idname, text="Reset Point Positions", icon='FILE_REFRESH')
classes = [
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)
bpy.types.VIEW3D_MT_edit_lattice.append(draw_lattice_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)
bpy.types.VIEW3D_MT_edit_lattice.remove(draw_lattice_reset)

View File

@ -0,0 +1,702 @@
# 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.
import bpy
from bpy.props import FloatProperty, IntVectorProperty, FloatVectorProperty, BoolProperty, PointerProperty, StringProperty, EnumProperty
from bpy.types import Operator, Object, VertexGroup, Scene, Collection, Modifier, Panel
from typing import List, Tuple
from mathutils import Vector
from rna_prop_ui import rna_idprop_ui_create
from .utils import clamp, get_lattice_vertex_index, simple_driver, bounding_box_center_of_objects
TWEAKLAT_COLL_NAME = 'Tweak Lattices'
class TWEAKLAT_OT_Create(Operator):
"""Create a lattice setup to deform selected objects"""
bl_idname = "lattice.create_tweak_lattice"
bl_label = "Create Tweak Lattice"
bl_options = {'REGISTER', 'UNDO'}
resolution: IntVectorProperty(
name="Resolution",
default=(12, 12, 12),
min=6,
max=64
)
location: EnumProperty(name="Location", items=[
('CURSOR', "3D Cursor", "Create at the location and orientation of the 3D cursor."),
('CENTER', "Center", "Create at the bounding box center of all selected objects."),
('PARENT', "Parent", "Create at the location of the parent object or bone.")
])
radius: FloatProperty(
name="Radius",
description="Radius of influence of this lattice. Can be changed later",
default=0.1,
min=0.0001,
max=1000,
soft_max=2
)
parent_bone: StringProperty(
name="Bone", description="Bone to use as parent")
@classmethod
def poll(cls, context):
for ob in context.selected_objects:
if ob.type == 'MESH':
return True
return False
def invoke(self, context, _event):
parent_obj = context.object
for m in parent_obj.modifiers:
if m.type == 'ARMATURE' and m.object:
parent_obj = m.object
if self.parent_bone not in parent_obj.data.bones:
self.parent_bone = ""
break
context.scene.tweak_lattice_parent_ob = parent_obj
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(self, 'location', expand=True)
layout.prop(self, 'radius', slider=True)
layout.separator()
col = layout.column(align=True)
col.prop(context.scene, 'tweak_lattice_parent_ob')
scene = context.scene
if scene.tweak_lattice_parent_ob and scene.tweak_lattice_parent_ob.type == 'ARMATURE':
col.prop_search(self, 'parent_bone',
scene.tweak_lattice_parent_ob.data, 'bones')
def execute(self, context):
scene = context.scene
# 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.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 falloff vertex group
vg = ensure_falloff_vgroup(lattice_ob, vg_name="Hook")
# 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
coll.objects.link(hook)
# Create some custom properties
hook['Lattice'] = lattice_ob
lattice_ob['Hook'] = hook
hook['Multiplier'] = 1.0
hook['Expression'] = 'x'
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=0.2, 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['Hook'] = hook
root.empty_display_type = 'CUBE'
root.empty_display_size = 0.5
if self.location == 'CENTER':
meshes = [o for o in context.selected_objects if o.type == 'MESH']
root.matrix_world.translation = bounding_box_center_of_objects(
meshes)
elif self.location == 'CURSOR':
root.matrix_world = context.scene.cursor.matrix
elif self.location == 'PARENT':
matrix_of_parent = scene.tweak_lattice_parent_ob.matrix_world
if self.parent_bone:
matrix_of_parent = scene.tweak_lattice_parent_ob.matrix_world @ scene.tweak_lattice_parent_ob.pose.bones[
self.parent_bone].matrix
root.matrix_world = matrix_of_parent.copy()
coll.objects.link(root)
root.hide_viewport = True
hook['Root'] = root
# Parent the root
scene = context.scene
matrix_backup = root.matrix_world.copy()
root.parent = scene.tweak_lattice_parent_ob
if root.parent and root.parent.type == 'ARMATURE':
bone = root.parent.pose.bones.get(self.parent_bone)
if bone:
root.parent_type = 'BONE'
root.parent_bone = bone.name
root.matrix_world = matrix_backup
# Parent lattice and hook to root
lattice_ob.parent = root
hook.parent = root
# 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
add_objects_to_lattice(hook, context.selected_objects)
# Set up Radius control.
add_radius_constraint(hook, hook, root)
add_radius_constraint(lattice_ob, hook, root)
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
scene.tweak_lattice_parent_ob = None
return {'FINISHED'}
class TWEAKLAT_OT_Duplicate(Operator):
"""Duplicate this Tweak Lattice set-up"""
bl_idname = "lattice.duplicate_tweak_setup"
bl_label = "Duplicate Tweak Lattice"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def execute(self, context):
hook, lattice, root = get_tweak_setup(context.object)
bpy.ops.object.select_all(action='DESELECT')
affected_objects = get_objects_of_lattice(hook)
visibilities = {}
for ob in [hook, lattice, root]:
ob.hide_set(False)
visibilities[ob] = ob.hide_viewport
ob.hide_viewport = False
if not ob.visible_get():
self.report({'ERROR'}, f'Object "{ob.name}" could not be made visible, cancelling.')
return {'CANCELLED'}
ob.select_set(True)
context.view_layer.objects.active = hook
bpy.ops.object.duplicate()
new_hook, new_lattice, new_root = get_tweak_setup(context.object)
for key, value in list(new_hook.items()):
if key.startswith("object_"):
del new_hook[key]
add_objects_to_lattice(new_hook, affected_objects)
# Restore visibilities
for ob, new_ob in zip((hook, lattice, root), (new_hook, new_lattice, new_root)):
ob.hide_viewport = new_ob.hide_viewport = visibilities[ob]
return {'FINISHED'}
class TWEAKLAT_OT_Falloff(Operator):
"""Adjust falloff of the hook vertex group of a Tweak Lattice"""
bl_idname = "lattice.tweak_lattice_adjust_falloww"
bl_label = "Adjust Falloff"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
def update_falloff(self, context):
if self.doing_invoke:
return
hook, lattice, _root = get_tweak_setup(context.object)
ret = ensure_falloff_vgroup(
lattice, 'Hook', multiplier=self.multiplier, expression=self.expression)
self.is_expression_valid = ret != None
if ret:
hook['Expression'] = self.expression
hook['Multiplier'] = self.multiplier
is_expression_valid: BoolProperty(
name="Error",
description="Used to notify user if their expression is invalid",
default=True
)
# Actual parameters
multiplier: FloatProperty(
name="Multiplier",
description="Multiplier on the weight values",
default=1,
update=update_falloff,
min=0,
soft_max=2
)
expression: StringProperty(
name="Expression",
default="x",
description="Expression to calculate the weight values where 'x' is a 0-1 value representing a point's closeness to the lattice center",
update=update_falloff,
)
# Storage to share info between Invoke and Update
lattice_start_scale: FloatVectorProperty()
hook_start_scale: FloatVectorProperty()
doing_invoke: BoolProperty(default=True)
@classmethod
def poll(cls, context):
hook, lattice, root = get_tweak_setup(context.object)
return hook and lattice and root
def invoke(self, context, event):
hook, _lattice, _root = get_tweak_setup(context.object)
self.multiplier = hook['Multiplier']
self.hook_start_scale = hook.scale.copy()
lattice_ob = hook['Lattice']
self.lattice_start_scale = lattice_ob.scale.copy()
if 'Expression' not in hook:
# Back-comp for Tweak Lattices created with older versions of the add-on.
hook['Expression'] = 'x'
self.expression = hook['Expression']
self.doing_invoke = False
wm = context.window_manager
return wm.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
layout.prop(self, 'expression', text="Expression", slider=True)
if not self.is_expression_valid:
row = layout.row()
row.alert = True
row.label(text="Invalid expression.", icon='ERROR')
layout.prop(self, 'multiplier', text="Strength", slider=True)
def execute(self, context):
return {'FINISHED'}
class TWEAKLAT_OT_Delete(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):
hook, lattice, root = get_tweak_setup(context.object)
return hook and lattice and root
def execute(self, context):
hook, lattice, root = get_tweak_setup(context.object)
# Remove Lattice modifiers and their drivers.
remove_all_objects_from_lattice(hook)
# 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(TWEAKLAT_COLL_NAME)
if coll and len(coll.all_objects) == 0:
bpy.data.collections.remove(coll)
return {'FINISHED'}
class TWEAKLAT_OT_Add_Objects(Operator):
"""Add selected objects to this tweak lattice"""
bl_idname = "lattice.add_selected_objects"
bl_label = "Add Selected Objects"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context):
hook, _lattice, _root = get_tweak_setup(context.object)
if not hook:
return False
values = hook.values()
for sel_o in context.selected_objects:
if sel_o == hook or sel_o.type != 'MESH':
continue
if sel_o not in values:
return True
return False
def execute(self, context):
hook, _lattice, _root = get_tweak_setup(context.object)
# Add Lattice modifier to the selected objects
add_objects_to_lattice(hook, context.selected_objects)
return {'FINISHED'}
class TWEAKLAT_OT_Remove_Selected_Objects(Operator):
"""Remove selected objects from this tweak lattice"""
bl_idname = "lattice.remove_selected_objects"
bl_label = "Remove Selected Objects"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
@classmethod
def poll(cls, context):
hook, _lattice, _root = get_tweak_setup(context.object)
if not hook:
return False
values = hook.values()
for sel_o in context.selected_objects:
if sel_o == hook or sel_o.type != 'MESH':
continue
if sel_o in values:
return True
return False
def execute(self, context):
hook, _lattice, _root = get_tweak_setup(context.object)
# Add Lattice modifier to the selected objects
remove_objects_from_lattice(hook, context.selected_objects)
return {'FINISHED'}
class TWEAKLAT_OT_Remove_Object(Operator):
"""Remove this object from the tweak lattice"""
bl_idname = "lattice.remove_object"
bl_label = "Remove Object"
bl_options = {'REGISTER', 'UNDO', 'INTERNAL'}
ob_pointer_prop_name: StringProperty(
description="Name of the custom property that references the object that should be removed")
def execute(self, context):
hook, _lattice, _root = get_tweak_setup(context.object)
target = hook[self.ob_pointer_prop_name]
# Add Lattice modifier to the selected objects
remove_objects_from_lattice(hook, [target])
return {'FINISHED'}
class TWEAKLAT_PT_Main(Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = 'Lattice Magic'
bl_label = "Tweak Lattice"
@classmethod
def poll(cls, context):
hook, _lattice, _root = get_tweak_setup(context.object)
return context.object and context.object.type == 'MESH' or hook
def draw(self, context):
layout = self.layout
layout.use_property_split = True
layout.use_property_decorate = False
hook, lattice, root = get_tweak_setup(context.object)
layout = layout.column()
if not 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.operator(TWEAKLAT_OT_Falloff.bl_idname, text="Adjust Falloff")
layout.separator()
layout.operator(TWEAKLAT_OT_Delete.bl_idname,
text='Delete Tweak Lattice', icon='TRASH')
layout.operator(TWEAKLAT_OT_Duplicate.bl_idname,
text='Duplicate Tweak Lattice', icon='DUPLICATE')
layout.separator()
layout.label(text="Helper Objects")
lattice_row = layout.row()
lattice_row.prop(hook, '["Lattice"]', text="Lattice")
lattice_row.prop(lattice, 'hide_viewport',
text="", emboss=False)
root_row = layout.row()
root_row.prop(hook, '["Root"]', text="Root")
root_row.prop(root, 'hide_viewport', text="", emboss=False)
layout.separator()
layout.label(text="Parenting")
col = layout.column()
col.enabled = False
col.prop(root, 'parent')
if root.parent and root.parent.type == 'ARMATURE':
col.prop(root, 'parent_bone', icon='BONE_DATA')
layout.separator()
layout.label(text="Affected Objects")
num_to_add = 0
for o in context.selected_objects:
if o == hook or o.type != 'MESH':
continue
if o in hook.values():
continue
num_to_add += 1
if num_to_add == 1:
text = f"Add {o.name}"
if num_to_add:
if num_to_add > 1:
text = f"Add {num_to_add} Objects"
layout.operator(TWEAKLAT_OT_Add_Objects.bl_idname,
icon='ADD', text=text)
layout.separator()
num_to_remove = False
for o in context.selected_objects:
if o == hook or o.type != 'MESH':
continue
if o not in hook.values():
continue
num_to_remove += 1
if num_to_remove == 1:
text = f"Remove {o.name}"
if num_to_remove:
if num_to_remove > 1:
text = f"Remove {num_to_remove} Objects"
layout.operator(
TWEAKLAT_OT_Remove_Selected_Objects.bl_idname, icon='REMOVE', text=text)
objects_and_keys = [(hook[key], key)
for key in hook.keys() if "object_" in key]
objects_and_keys.sort(key=lambda o_and_k: o_and_k[1])
for ob, key in objects_and_keys:
row = layout.row(align=True)
row.prop(hook, f'["{key}"]', text="")
mod = get_lattice_modifier_of_object(ob, lattice)
row.prop_search(mod, 'vertex_group', ob,
'vertex_groups', text="", icon='GROUP_VERTEX')
op = row.operator(
TWEAKLAT_OT_Remove_Object.bl_idname, text="", icon='X')
op.ob_pointer_prop_name = key
def get_tweak_setup(obj: Object) -> Tuple[Object, Object, Object]:
"""Based on either the hook, lattice or root, return all three."""
if not obj:
return [None, None, None]
if obj.type == 'EMPTY':
if 'Root' and 'Lattice' in obj:
return obj, obj['Lattice'], obj['Root']
elif 'Hook' in obj:
return obj['Hook'], obj['Hook']['Lattice'], obj
elif obj.type == 'LATTICE' and 'Hook' in obj:
return obj['Hook'], obj, obj['Hook']['Root']
return [None, None, None]
def ensure_tweak_lattice_collection(scene: Scene) -> Collection:
coll = bpy.data.collections.get(TWEAKLAT_COLL_NAME)
if not coll:
coll = bpy.data.collections.new(TWEAKLAT_COLL_NAME)
scene.collection.children.link(coll)
return coll
def ensure_falloff_vgroup(
lattice_ob: Object,
vg_name="Group", multiplier=1, expression="x") -> VertexGroup:
lattice = lattice_ob.data
res_x, res_y, res_z = lattice.points_u, lattice.points_v, lattice.points_w
vg = lattice_ob.vertex_groups.get(vg_name)
center = Vector((res_x-1, res_y-1, res_z-1))/2
max_res = max(res_x, res_y, res_z)
if not vg:
vg = lattice_ob.vertex_groups.new(name=vg_name)
for x in range(res_x-4):
for y in range(res_y-4):
for z in range(res_z-4):
index = get_lattice_vertex_index(lattice, (x+2, y+2, z+2))
coord = Vector((x+2, y+2, z+2))
distance_from_center = (coord-center).length
distance_factor = 1 - (distance_from_center / max_res * 2)
try:
influence = eval(expression.replace(
"x", "distance_factor"))
except:
return None
vg.add([index], influence * multiplier, 'REPLACE')
return vg
def add_radius_constraint(obj, hook, target):
trans_con = obj.constraints.new(type='TRANSFORM')
trans_con.target = target
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"]')
return trans_con
def get_objects_of_lattice(hook: Object) -> List[Object]:
objs = []
for key, value in hook.items():
if key.startswith("object_") and value:
objs.append(value)
return objs
def get_lattice_modifier_of_object(obj, lattice) -> Modifier:
"""Find the lattice modifier on the object that uses this lattice"""
for m in obj.modifiers:
if m.type == 'LATTICE' and m.object == lattice:
return m
def add_objects_to_lattice(
hook: Object,
objects: List[Object]):
lattice_ob = hook['Lattice']
for i, o in enumerate(objects):
o.select_set(False)
if o.type != 'MESH' or o in hook.values():
continue
m = o.modifiers.new(name=lattice_ob.name, type='LATTICE')
m.object = lattice_ob
# Make sure the property name is available.
offset = 0
while "object_"+str(offset) in hook:
offset += 1
hook["object_"+str(i+offset)] = o
# Add driver to the modifier influence.
simple_driver(m, 'strength', hook, '["Tweak Lattice"]')
def remove_object_from_lattice(hook: Object, obj: Object):
"""Cleanly remove an object from a Tweak Lattice set-up's influence."""
hook, lattice, root = get_tweak_setup(hook)
# Remove the custom property pointing from the Hook to the Object.
for key, value in list(hook.items()):
if value == obj:
del hook[key]
break
# Remove the Lattice modifier (and its driver) deforming the Object.
for m in obj.modifiers:
if m.type != 'LATTICE':
continue
if m.object == lattice:
m.driver_remove('strength')
obj.modifiers.remove(m)
break
def remove_objects_from_lattice(hook: Object, objects_to_remove: List[Object]) -> List[Object]:
"""Cleanly remove several objects from a Tweak Lattice set-up's influence."""
objs_removed = []
for key, value in list(hook.items()):
if value in objects_to_remove:
remove_object_from_lattice(hook, value)
objs_removed.append(value)
return objs_removed
def remove_all_objects_from_lattice(hook: Object) -> List[Object]:
"""Cleanly remove all objects from a Tweak Lattice set-up's influence."""
objs_to_remove = []
for key, value in list(hook.items()):
if key.startswith("object_"):
objs_to_remove.append(value)
return remove_objects_from_lattice(hook, objs_to_remove)
classes = [
TWEAKLAT_OT_Create,
TWEAKLAT_OT_Duplicate,
TWEAKLAT_OT_Delete,
TWEAKLAT_OT_Falloff,
TWEAKLAT_OT_Add_Objects,
TWEAKLAT_OT_Remove_Selected_Objects,
TWEAKLAT_OT_Remove_Object,
TWEAKLAT_PT_Main
]
def register():
from bpy.utils import register_class
for c in classes:
register_class(c)
Scene.tweak_lattice_parent_ob = PointerProperty(
type=Object, name="Parent")
def unregister():
from bpy.utils import unregister_class
for c in reversed(classes):
unregister_class(c)
del Scene.tweak_lattice_parent_ob

View File

@ -0,0 +1,104 @@
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-0.99)
unit_v = 1/(lattice.points_v-0.99)
unit_w = 1/(lattice.points_w-0.99)
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
def bounding_box_center_of_objects(objects) -> Vector:
"""Find the bounding box center of some objects."""
all_points = []
for o in objects:
for p in o.bound_box:
all_points.append(o.matrix_world @ Vector(p))
return bounding_box_center(all_points)