Files
blender-portable-repo/scripts/addons/Lip Sync Visemes.py
T
2026-03-17 14:30:01 -06:00

211 lines
7.7 KiB
Python

bl_info = {
"name": "Lip Sync",
"description": "A tool for generating lip-sync animations using poses from the Asset Browser.",
"author": "Blackout Creatively",
"version": (1, 0, 3),
"blender": (4, 3, 0),
"location": "View3D > Tool > My Lipsync",
"category": "Animation",
}
import bpy
class LipSyncProperties(bpy.types.PropertyGroup):
"""Properties for Lip Sync Tool"""
prefix: bpy.props.StringProperty(name="Pose Prefix", default="") # Prefix for pose names
pose_0: bpy.props.StringProperty(name="Rest", default="Rest")
pose_1: bpy.props.StringProperty(name="Aa", default="Aa")
pose_2: bpy.props.StringProperty(name="D", default="D")
pose_3: bpy.props.StringProperty(name="Ee", default="Ee")
pose_4: bpy.props.StringProperty(name="F", default="F")
pose_5: bpy.props.StringProperty(name="L", default="L")
pose_6: bpy.props.StringProperty(name="M", default="M")
pose_7: bpy.props.StringProperty(name="Oh", default="Oh")
pose_8: bpy.props.StringProperty(name="R", default="R")
pose_9: bpy.props.StringProperty(name="S", default="S")
pose_10: bpy.props.StringProperty(name="Uh", default="Uh")
pose_11: bpy.props.StringProperty(name="W-Oo", default="W-Oo")
holdPoseOffset: bpy.props.IntProperty(name="Hold Pose Frame Offset", default=2)
class LipSyncPluginOperator(bpy.types.Operator):
"""Lip Sync Animation Plugin"""
bl_idname = "object.lip_sync_plugin"
bl_label = "Lip Sync Animation"
filepath: bpy.props.StringProperty(subtype="FILE_PATH")
def execute(self, context):
props = context.scene.lip_sync_properties
prefix = props.prefix.strip() # Get prefix, if any
value_to_string = {
0: f"{prefix}{props.pose_0}",
1: f"{prefix}{props.pose_1}",
2: f"{prefix}{props.pose_2}",
3: f"{prefix}{props.pose_3}",
4: f"{prefix}{props.pose_4}",
5: f"{prefix}{props.pose_5}",
6: f"{prefix}{props.pose_6}",
7: f"{prefix}{props.pose_7}",
8: f"{prefix}{props.pose_8}",
9: f"{prefix}{props.pose_9}",
10: f"{prefix}{props.pose_10}",
11: f"{prefix}{props.pose_11}",
}
hold_offset = props.holdPoseOffset
try:
with open(self.filepath, 'r') as file:
lines = file.readlines()
except Exception as e:
self.report({'ERROR'}, f"Failed to read file: {e}")
return {'CANCELLED'}
action_name = "Lip Sync"
action = bpy.data.actions.get(action_name)
if not action:
action = bpy.data.actions.new(name=action_name)
obj = context.object
if not obj or obj.type != 'ARMATURE':
self.report({'ERROR'}, "No armature selected.")
return {'CANCELLED'}
obj.animation_data_create()
obj.animation_data.action = action
bpy.ops.object.mode_set(mode='POSE')
keyframe_data = []
for line in lines:
try:
parts = line.strip().split('\t')
if len(parts) != 2:
self.report({'WARNING'}, f"Skipping malformed line: '{line.strip()}'")
continue
time = round(float(parts[0]))
int_value = int(parts[1])
pose_name = value_to_string.get(int_value, None)
if pose_name is None:
self.report({'WARNING'}, f"Integer value {int_value} is not mapped. Skipping.")
continue
keyframe_data.append((time, pose_name))
except Exception as e:
self.report({'WARNING'}, f"Error processing line '{line.strip()}': {e}")
continue
keyframe_data.sort()
selected_bones = [bone.name for bone in obj.pose.bones if bone.bone.select]
for i, (time, pose_name) in enumerate(keyframe_data):
# Apply the pose
context.scene.frame_set(time)
pose_asset = next(
(action for action in bpy.data.actions if action.asset_data and action.name == pose_name),
None
)
if not pose_asset:
self.report({'WARNING'}, f"Pose '{pose_name}' not found in the asset library. Skipping.")
continue
for fcurve in pose_asset.fcurves:
data_path = fcurve.data_path
bone_name = data_path.split('"')[1] if '"' in data_path else None
if not bone_name or bone_name not in selected_bones:
continue
pose_bone = obj.pose.bones.get(bone_name)
if not pose_bone:
continue
for keyframe in fcurve.keyframe_points:
frame, value = keyframe.co
if "location" in data_path:
pose_bone.location[fcurve.array_index] = value
elif "rotation_quaternion" in data_path:
pose_bone.rotation_quaternion[fcurve.array_index] = value
elif "scale" in data_path:
pose_bone.scale[fcurve.array_index] = value
for bone_name in selected_bones:
bone = obj.pose.bones.get(bone_name)
if bone:
bone.keyframe_insert(data_path="location", frame=time)
bone.keyframe_insert(data_path="rotation_quaternion", frame=time)
bone.keyframe_insert(data_path="scale", frame=time)
# Duplicate the pose if the gap between keyframes is too large and holdPoseOffset > 0
if hold_offset > 0 and i < len(keyframe_data) - 1:
next_time, _ = keyframe_data[i + 1]
if next_time - time > hold_offset + 1:
hold_time = next_time - hold_offset
context.scene.frame_set(hold_time)
for bone_name in selected_bones:
bone = obj.pose.bones.get(bone_name)
if bone:
bone.keyframe_insert(data_path="location", frame=hold_time)
bone.keyframe_insert(data_path="rotation_quaternion", frame=hold_time)
bone.keyframe_insert(data_path="scale", frame=hold_time)
bpy.ops.object.mode_set(mode='OBJECT')
self.report({'INFO'}, "Lip Sync animation created successfully.")
return {'FINISHED'}
def invoke(self, context, event):
context.window_manager.fileselect_add(self)
return {'RUNNING_MODAL'}
class LipSyncPluginPanel(bpy.types.Panel):
"""Creates a Panel in the Tool shelf"""
bl_label = "Lip Sync Tool"
bl_idname = "OBJECT_PT_lip_sync_tool"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "Lipsync"
def draw(self, context):
layout = self.layout
props = context.scene.lip_sync_properties
layout.label(text="Pose Prefix:")
layout.prop(props, "prefix")
layout.label(text="Pose Mappings:")
for i in range(12):
layout.prop(props, f"pose_{i}")
layout.prop(props, "holdPoseOffset")
layout.operator(LipSyncPluginOperator.bl_idname, text="Run Lip Sync Tool")
def register():
bpy.utils.register_class(LipSyncProperties)
bpy.types.Scene.lip_sync_properties = bpy.props.PointerProperty(type=LipSyncProperties)
bpy.utils.register_class(LipSyncPluginOperator)
bpy.utils.register_class(LipSyncPluginPanel)
def unregister():
bpy.utils.unregister_class(LipSyncPluginPanel)
bpy.utils.unregister_class(LipSyncPluginOperator)
del bpy.types.Scene.lip_sync_properties
bpy.utils.unregister_class(LipSyncProperties)
if __name__ == "__main__":
register()