Files
小煜 091ad6a49a feat: Add VRM Blender addon with complete import/export functionality
- Add core VRM addon infrastructure with manifest and registration
- Add common utilities module with file system, logging, and conversion helpers
- Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.)
- Add VRM 0.x and 1.x format support with property groups and handlers
- Add editor UI panels for VRM metadata, spring bones, and MToon materials
- Add exporter with glTF2 extension support for VRM format serialization
- Add importer with scene reconstruction and armature generation
- Add MToon shader support with auto-setup and material migration
- Add spring bone physics simulation with constraint handling
- Add node constraint editor for advanced rigging control
- Add comprehensive validation and error handling with user dialogs
- Add scene watcher for real-time property synchronization
- Add workspace management and preference system
- Include Python cache files and Blender manifest configuration
- This is the initial commit establishing the complete VRM addon ecosystem for Blender
2026-01-01 14:21:56 +08:00

317 lines
11 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from collections.abc import Set as AbstractSet
from bpy.app.translations import pgettext
from bpy.types import (
Armature,
Context,
CopyRotationConstraint,
DampedTrackConstraint,
Object,
Panel,
UILayout,
)
from ...common.preferences import get_preferences
from .. import search
from ..extension import get_armature_extension
from ..panel import VRM_PT_vrm_armature_object_property
from ..search import active_object_is_vrm1_armature
from .property_group import NodeConstraint1NodeConstraintPropertyGroup
def draw_roll_constraint_layout(
layout: UILayout,
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
object_constraints: dict[str, CopyRotationConstraint],
bone_constraints: dict[str, CopyRotationConstraint],
) -> None:
constraints_box = layout.box()
constraints_row = constraints_box.row()
constraints_row.alignment = "LEFT"
constraints_row.prop(
node_constraint,
"show_expanded_roll_constraints",
icon="TRIA_DOWN"
if node_constraint.show_expanded_roll_constraints
else "TRIA_RIGHT",
emboss=False,
)
if not node_constraint.show_expanded_roll_constraints:
return
constraints_column = constraints_box.column()
if object_constraints or bone_constraints:
constraints_expanded_column = constraints_column.column()
for object_name, constraint in object_constraints.items():
constraint_row = constraints_expanded_column.row()
constraint_row.alignment = "LEFT"
constraint_row.label(
text=object_name + ": " + constraint.name,
icon="CONSTRAINT",
translate=False,
)
for bone_name, constraint in bone_constraints.items():
constraint_row = constraints_expanded_column.row()
constraint_row.alignment = "LEFT"
constraint_row.label(
text=bone_name + ": " + constraint.name,
icon="CONSTRAINT_BONE",
translate=False,
)
constraints_help_column = constraints_column.box().column(align=True)
help_message = pgettext(
"Conditions exported as Roll Constraint\n"
+ " - {copy_rotation}\n"
+ " - Enabled\n"
+ " - No Vertex Group\n"
+ " - {axis} is one of X, Y and Z\n"
+ " - No Inverted\n"
+ " - {mix} is {add}\n"
+ " - {target} is {local_space}\n"
+ " - {owner} is {local_space}\n"
+ " - No circular dependencies\n"
+ " - The one at the top of the list of\n"
+ " those that meet all the conditions\n",
).format(
copy_rotation=pgettext("Copy Rotation"),
axis=pgettext("Axis"),
mix=pgettext("Mix"),
add=pgettext("Add"),
target=pgettext("Target"),
local_space=pgettext("Local Space"),
owner=pgettext("Owner"),
)
for index, help_line in enumerate(help_message.splitlines()):
constraints_help_column.label(
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
)
def draw_aim_constraint_layout(
layout: UILayout,
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
object_constraints: dict[str, DampedTrackConstraint],
bone_constraints: dict[str, DampedTrackConstraint],
) -> None:
constraints_box = layout.box()
constraints_row = constraints_box.row()
constraints_row.alignment = "LEFT"
constraints_row.prop(
node_constraint,
"show_expanded_aim_constraints",
icon="TRIA_DOWN"
if node_constraint.show_expanded_aim_constraints
else "TRIA_RIGHT",
emboss=False,
)
if not node_constraint.show_expanded_aim_constraints:
return
constraints_column = constraints_box.column()
if object_constraints or bone_constraints:
constraints_expanded_column = constraints_column.column()
for object_name, constraint in object_constraints.items():
constraint_row = constraints_expanded_column.row()
constraint_row.alignment = "LEFT"
constraint_row.label(
text=object_name + ": " + constraint.name,
icon="CONSTRAINT",
translate=False,
)
for bone_name, constraint in bone_constraints.items():
constraint_row = constraints_expanded_column.row()
constraint_row.alignment = "LEFT"
constraint_row.label(
text=bone_name + ": " + constraint.name,
icon="CONSTRAINT_BONE",
translate=False,
)
constraints_help_column = constraints_column.box().column(align=True)
help_message = pgettext(
"Conditions exported as Aim Constraint\n"
+ " - {damped_track}\n"
+ " - Enabled\n"
+ " - Target Bone {head_tail} is 0\n"
+ " - No Follow Target Bone B-Bone\n"
+ " - No circular dependencies\n"
+ " - The one at the top of the list of\n"
+ " those that meet all the conditions\n"
).format(
damped_track=pgettext("Damped Track"),
head_tail=pgettext("Head/Tail"),
)
for index, help_line in enumerate(help_message.splitlines()):
constraints_help_column.label(
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
)
def draw_rotation_constraint_layout(
layout: UILayout,
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
object_constraints: dict[str, CopyRotationConstraint],
bone_constraints: dict[str, CopyRotationConstraint],
) -> None:
constraints_box = layout.box()
constraints_row = constraints_box.row()
constraints_row.alignment = "LEFT"
constraints_row.prop(
node_constraint,
"show_expanded_rotation_constraints",
icon="TRIA_DOWN"
if node_constraint.show_expanded_rotation_constraints
else "TRIA_RIGHT",
emboss=False,
)
if not node_constraint.show_expanded_rotation_constraints:
return
constraints_column = constraints_box.column()
if object_constraints or bone_constraints:
constraints_expanded_column = constraints_column.column()
for object_name, constraint in object_constraints.items():
constraint_row = constraints_expanded_column.row()
constraint_row.alignment = "LEFT"
constraint_row.label(
text=object_name + ": " + constraint.name,
icon="CONSTRAINT",
translate=False,
)
for bone_name, constraint in bone_constraints.items():
constraint_row = constraints_expanded_column.row()
constraint_row.alignment = "LEFT"
constraint_row.label(
text=bone_name + ": " + constraint.name,
icon="CONSTRAINT_BONE",
translate=False,
)
constraints_help_column = constraints_column.box().column(align=True)
help_message = pgettext(
"Conditions exported as Rotation Constraint\n"
+ " - {copy_rotation}\n"
+ " - Enabled\n"
+ " - No Vertex Group\n"
+ " - {axis} is X, Y and Z\n"
+ " - No Inverted\n"
+ " - {mix} is {add}\n"
+ " - {target} is {local_space}\n"
+ " - {owner} is {local_space}\n"
+ " - No circular dependencies\n"
+ " - The one at the top of the list of\n"
+ " those that meet all the conditions\n"
).format(
copy_rotation=pgettext("Copy Rotation"),
axis=pgettext("Axis"),
mix=pgettext("Mix"),
add=pgettext("Add"),
target=pgettext("Target"),
local_space=pgettext("Local Space"),
owner=pgettext("Owner"),
)
for index, help_line in enumerate(help_message.splitlines()):
constraints_help_column.label(
text=help_line, translate=False, icon="HELP" if index == 0 else "NONE"
)
def draw_node_constraint1_layout(
context: Context,
armature: Object,
layout: UILayout,
node_constraint: NodeConstraint1NodeConstraintPropertyGroup,
) -> None:
preferences = get_preferences(context)
objs = search.export_objects(
context,
armature_object_name=armature.name,
export_invisibles=preferences.export_invisibles,
export_only_selections=False,
export_lights=False,
)
object_constraints, bone_constraints, _ = search.export_constraints(objs, armature)
draw_roll_constraint_layout(
layout,
node_constraint,
object_constraints.roll_constraints,
bone_constraints.roll_constraints,
)
draw_aim_constraint_layout(
layout,
node_constraint,
object_constraints.aim_constraints,
bone_constraints.aim_constraints,
)
draw_rotation_constraint_layout(
layout,
node_constraint,
object_constraints.rotation_constraints,
bone_constraints.rotation_constraints,
)
class VRM_PT_node_constraint1_armature_object_property(Panel):
bl_idname = "VRM_PT_node_constraint1_armature_object_property"
bl_label = "Node Constraint"
bl_translation_context = "VRM"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
bl_options: AbstractSet[str] = {"DEFAULT_CLOSED"}
bl_parent_id = VRM_PT_vrm_armature_object_property.bl_idname
@classmethod
def poll(cls, context: Context) -> bool:
return active_object_is_vrm1_armature(context)
def draw_header(self, _context: Context) -> None:
self.layout.label(icon="CONSTRAINT")
def draw(self, context: Context) -> None:
active_object = context.active_object
if not active_object:
return
armature_data = active_object.data
if not isinstance(armature_data, Armature):
return
draw_node_constraint1_layout(
context,
active_object,
self.layout,
get_armature_extension(armature_data).node_constraint1,
)
class VRM_PT_node_constraint1_ui(Panel):
bl_idname = "VRM_PT_node_constraint1_ui"
bl_label = "Node Constraint"
bl_translation_context = "VRM"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "VRM"
bl_options: AbstractSet[str] = {"DEFAULT_CLOSED"}
@classmethod
def poll(cls, context: Context) -> bool:
return search.current_armature_is_vrm1(context)
def draw_header(self, _context: Context) -> None:
self.layout.label(icon="CONSTRAINT")
def draw(self, context: Context) -> None:
armature = search.current_armature(context)
if not armature:
return
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
draw_node_constraint1_layout(
context,
armature,
self.layout,
get_armature_extension(armature_data).node_constraint1,
)