211 lines
7.7 KiB
Python
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()
|