Files
blender-vrm/editor/spring_bone1/panel.py
小煜 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

600 lines
20 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from collections.abc import Set as AbstractSet
from typing import Optional
from bpy.types import Armature, Context, Object, Panel, UILayout
from .. import search
from ..extension import get_armature_extension
from ..migration import defer_migrate
from ..panel import VRM_PT_vrm_armature_object_property, draw_template_list
from ..search import active_object_is_vrm1_armature
from . import ops
from .property_group import (
SpringBone1ColliderGroupPropertyGroup,
SpringBone1ColliderPropertyGroup,
SpringBone1JointPropertyGroup,
SpringBone1SpringBonePropertyGroup,
SpringBone1SpringPropertyGroup,
)
from .ui_list import (
VRM_UL_spring_bone1_collider,
VRM_UL_spring_bone1_collider_group,
VRM_UL_spring_bone1_collider_group_collider,
VRM_UL_spring_bone1_joint,
VRM_UL_spring_bone1_spring,
VRM_UL_spring_bone1_spring_collider_group,
)
def draw_spring_bone1_collider_sphere_layout(
layout: UILayout,
armature_data: Armature,
collider: SpringBone1ColliderPropertyGroup,
) -> None:
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
layout.prop(collider, "ui_collider_type")
bpy_object = collider.bpy_object
if bpy_object:
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Name")
layout.prop(collider.shape.sphere, "offset", text="Offset")
layout.separator(factor=0.5)
layout.prop(collider.shape.sphere, "radius", slider=True)
layout.prop(collider.extensions.vrmc_spring_bone_extended_collider, "enabled")
def draw_spring_bone1_collider_capsule_layout(
layout: UILayout,
armature_data: Armature,
collider: SpringBone1ColliderPropertyGroup,
) -> None:
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
layout.prop(collider, "ui_collider_type")
bpy_object = collider.bpy_object
if bpy_object:
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Head")
layout.prop(collider.shape.capsule, "offset", text="")
layout.separator(factor=0.5)
layout.prop(collider.shape.capsule, "radius", slider=True)
layout.separator(factor=0.5)
if bpy_object and bpy_object.children:
layout.prop(
bpy_object.children[0],
"name",
icon="MESH_UVSPHERE",
text="Tail",
)
layout.prop(collider.shape.capsule, "tail", text="")
layout.prop(collider.extensions.vrmc_spring_bone_extended_collider, "enabled")
def draw_spring_bone1_collider_extended_fallback_layout(
layout: UILayout,
_armature_data: Armature,
collider: SpringBone1ColliderPropertyGroup,
) -> None:
extended = collider.extensions.vrmc_spring_bone_extended_collider
layout.prop(extended, "automatic_fallback_generation")
if extended.automatic_fallback_generation:
return
layout.prop(collider, "shape_type", text="Fallback")
if collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
layout.prop(collider.shape.sphere, "fallback_offset")
layout.separator(factor=0.5)
layout.prop(collider.shape.sphere, "fallback_radius", slider=True)
elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
layout.prop(collider.shape.capsule, "fallback_offset")
layout.separator(factor=0.5)
layout.prop(collider.shape.capsule, "fallback_radius", slider=True)
layout.separator(factor=0.5)
layout.prop(collider.shape.capsule, "fallback_tail")
def draw_spring_bone1_collider_extended_sphere_layout(
layout: UILayout,
armature_data: Armature,
collider: SpringBone1ColliderPropertyGroup,
) -> None:
extended = collider.extensions.vrmc_spring_bone_extended_collider
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
layout.prop(collider, "ui_collider_type")
bpy_object = collider.bpy_object
if bpy_object:
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Offset")
layout.prop(extended.shape.sphere, "offset", text="")
layout.separator(factor=0.5)
layout.prop(extended.shape.sphere, "radius", slider=True)
if not (extended.enabled and extended.shape.sphere.inside):
layout.prop(extended, "enabled")
draw_spring_bone1_collider_extended_fallback_layout(
layout,
armature_data,
collider,
)
def draw_spring_bone1_collider_extended_capsule_layout(
layout: UILayout,
armature_data: Armature,
collider: SpringBone1ColliderPropertyGroup,
) -> None:
extended = collider.extensions.vrmc_spring_bone_extended_collider
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
layout.prop(collider, "ui_collider_type")
bpy_object = collider.bpy_object
if bpy_object:
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Head")
layout.prop(extended.shape.capsule, "offset", text="")
layout.separator(factor=0.5)
layout.prop(extended.shape.capsule, "radius", slider=True)
layout.separator(factor=0.5)
if bpy_object and bpy_object.children:
layout.prop(
bpy_object.children[0],
"name",
icon="MESH_UVSPHERE",
text="Tail",
)
layout.prop(extended.shape.capsule, "tail", text="")
if not (extended.enabled and extended.shape.capsule.inside):
layout.prop(extended, "enabled")
draw_spring_bone1_collider_extended_fallback_layout(
layout,
armature_data,
collider,
)
def draw_spring_bone1_collider_extended_plane_layout(
layout: UILayout,
armature_data: Armature,
collider: SpringBone1ColliderPropertyGroup,
) -> None:
extended = collider.extensions.vrmc_spring_bone_extended_collider
layout.prop_search(collider.node, "bone_name", armature_data, "bones")
layout.prop(collider, "ui_collider_type")
bpy_object = collider.bpy_object
if bpy_object:
layout.prop(bpy_object, "name", icon="MESH_UVSPHERE", text="Offset")
layout.prop(extended.shape.plane, "offset", text="")
layout.separator(factor=0.5)
layout.prop(extended.shape.plane, "normal")
draw_spring_bone1_collider_extended_fallback_layout(
layout,
armature_data,
collider,
)
def draw_spring_bone1_collider_layout(
armature: Object,
layout: UILayout,
collider: SpringBone1ColliderPropertyGroup,
) -> None:
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
extended = collider.extensions.vrmc_spring_bone_extended_collider
if extended.enabled:
if extended.shape_type == extended.SHAPE_TYPE_EXTENDED_PLANE.identifier:
draw_spring_bone1_collider_extended_plane_layout(
layout, armature_data, collider
)
elif extended.shape_type == extended.SHAPE_TYPE_EXTENDED_SPHERE.identifier:
draw_spring_bone1_collider_extended_sphere_layout(
layout, armature_data, collider
)
elif extended.shape_type == extended.SHAPE_TYPE_EXTENDED_CAPSULE.identifier:
draw_spring_bone1_collider_extended_capsule_layout(
layout, armature_data, collider
)
elif collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
draw_spring_bone1_collider_sphere_layout(layout, armature_data, collider)
elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
draw_spring_bone1_collider_capsule_layout(layout, armature_data, collider)
layout.separator(factor=0.5)
def draw_spring_bone1_spring_bone_layout(
armature: Object,
layout: UILayout,
spring_bone: SpringBone1SpringBonePropertyGroup,
) -> None:
defer_migrate(armature.name)
layout.prop(spring_bone, "enable_animation")
# layout.operator(ops.VRM_OT_reset_spring_bone1_animation_state.bl_idname)
draw_spring_bone1_colliders_layout(armature, layout, spring_bone)
draw_spring_bone1_collider_groups_layout(armature, layout, spring_bone)
draw_spring_bone1_springs_layout(armature, layout, spring_bone)
def draw_spring_bone1_colliders_layout(
armature: Object,
layout: UILayout,
spring_bone: SpringBone1SpringBonePropertyGroup,
) -> None:
colliders_box = layout.box()
colliders_header_row = colliders_box.row()
colliders_header_row.alignment = "LEFT"
colliders_header_row.prop(
spring_bone,
"show_expanded_colliders",
icon="TRIA_DOWN" if spring_bone.show_expanded_colliders else "TRIA_RIGHT",
emboss=False,
)
if not spring_bone.show_expanded_colliders:
return
(
collider_collection_ops,
collider_collection_item_ops,
collider_index,
collider,
_,
) = draw_template_list(
colliders_box,
VRM_UL_spring_bone1_collider,
spring_bone,
"colliders",
"active_collider_index",
ops.VRM_OT_add_spring_bone1_collider,
ops.VRM_OT_remove_spring_bone1_collider,
ops.VRM_OT_move_up_spring_bone1_collider,
ops.VRM_OT_move_down_spring_bone1_collider,
)
for collider_collection_op in collider_collection_ops:
collider_collection_op.armature_object_name = armature.name
for collider_collection_item_op in collider_collection_item_ops:
collider_collection_item_op.collider_index = collider_index
if not isinstance(collider, SpringBone1ColliderPropertyGroup):
return
draw_spring_bone1_collider_layout(armature, colliders_box.column(), collider)
def draw_spring_bone1_collider_groups_layout(
armature: Object,
layout: UILayout,
spring_bone: SpringBone1SpringBonePropertyGroup,
) -> None:
collider_groups_box = layout.box()
collider_groups_header_row = collider_groups_box.row()
collider_groups_header_row.alignment = "LEFT"
collider_groups_header_row.prop(
spring_bone,
"show_expanded_collider_groups",
icon="TRIA_DOWN" if spring_bone.show_expanded_collider_groups else "TRIA_RIGHT",
emboss=False,
)
if not spring_bone.show_expanded_collider_groups:
return
(
collider_group_collection_ops,
collider_group_collection_item_ops,
collider_group_index,
collider_group,
_,
) = draw_template_list(
collider_groups_box,
VRM_UL_spring_bone1_collider_group,
spring_bone,
"collider_groups",
"active_collider_group_index",
ops.VRM_OT_add_spring_bone1_collider_group,
ops.VRM_OT_remove_spring_bone1_collider_group,
ops.VRM_OT_move_up_spring_bone1_collider_group,
ops.VRM_OT_move_down_spring_bone1_collider_group,
)
for collider_group_collection_op in collider_group_collection_ops:
collider_group_collection_op.armature_object_name = armature.name
for collider_group_collection_item_op in collider_group_collection_item_ops:
collider_group_collection_item_op.collider_group_index = collider_group_index
if not isinstance(collider_group, SpringBone1ColliderGroupPropertyGroup):
return
collider_group_column = collider_groups_box.column()
collider_group_column.prop(
collider_group,
"vrm_name",
)
colliders_box = collider_group_column.box()
colliders_column = colliders_box.column()
colliders_column.label(text="Colliders:")
(
collider_collection_ops,
collider_collection_item_ops,
collider_index,
_collider,
_,
) = draw_template_list(
colliders_column,
VRM_UL_spring_bone1_collider_group_collider,
collider_group,
"colliders",
"active_collider_index",
ops.VRM_OT_add_spring_bone1_collider_group_collider,
ops.VRM_OT_remove_spring_bone1_collider_group_collider,
ops.VRM_OT_move_up_spring_bone1_collider_group_collider,
ops.VRM_OT_move_down_spring_bone1_collider_group_collider,
)
for collider_collection_op in collider_collection_ops:
collider_collection_op.armature_object_name = armature.name
collider_collection_op.collider_group_index = collider_group_index
for collider_collection_item_op in collider_collection_item_ops:
collider_collection_item_op.collider_index = collider_index
def draw_spring_bone1_springs_layout(
armature: Object,
layout: UILayout,
spring_bone: SpringBone1SpringBonePropertyGroup,
) -> None:
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
springs_box = layout.box()
springs_header_row = springs_box.row()
springs_header_row.alignment = "LEFT"
springs_header_row.prop(
spring_bone,
"show_expanded_springs",
icon="TRIA_DOWN" if spring_bone.show_expanded_springs else "TRIA_RIGHT",
emboss=False,
)
if not spring_bone.show_expanded_springs:
return
(
spring_collection_ops,
spring_collection_item_ops,
spring_index,
spring,
_,
) = draw_template_list(
springs_box,
VRM_UL_spring_bone1_spring,
spring_bone,
"springs",
"active_spring_index",
ops.VRM_OT_add_spring_bone1_spring,
ops.VRM_OT_remove_spring_bone1_spring,
ops.VRM_OT_move_up_spring_bone1_spring,
ops.VRM_OT_move_down_spring_bone1_spring,
)
for spring_collection_op in spring_collection_ops:
spring_collection_op.armature_object_name = armature.name
for spring_collection_item_op in spring_collection_item_ops:
spring_collection_item_op.spring_index = spring_index
if not isinstance(spring, SpringBone1SpringPropertyGroup):
return
spring_column = springs_box.column()
spring_column.prop(
spring,
"vrm_name",
)
spring_column.prop_search(
spring.center,
"bone_name",
armature_data,
"bones",
text="Center",
)
joints_box = spring_column.box()
joints_column = joints_box.column()
joints_column.label(text="Joints:")
(
joint_collection_ops,
joint_collection_item_ops,
joint_index,
joint,
(add_joint_op, _, _, _),
) = draw_template_list(
joints_column,
VRM_UL_spring_bone1_joint,
spring,
"joints",
"active_joint_index",
ops.VRM_OT_add_spring_bone1_joint,
ops.VRM_OT_remove_spring_bone1_joint,
ops.VRM_OT_move_up_spring_bone1_joint,
ops.VRM_OT_move_down_spring_bone1_joint,
)
for joint_collection_op in joint_collection_ops:
joint_collection_op.armature_object_name = armature.name
joint_collection_op.spring_index = spring_index
for joint_collection_item_op in joint_collection_item_ops:
joint_collection_item_op.joint_index = joint_index
add_joint_op.guess_properties = True
if isinstance(joint, SpringBone1JointPropertyGroup):
joints_column.prop_search(joint.node, "bone_name", armature_data, "bones")
joints_column.prop(joint, "stiffness", slider=True)
joints_column.prop(joint, "gravity_power", slider=True)
joints_column.prop(joint, "gravity_dir")
joints_column.prop(joint, "drag_force", slider=True)
joints_column.prop(joint, "hit_radius", slider=True)
collider_groups_box = spring_column.box()
collider_groups_column = collider_groups_box.column()
collider_groups_column.label(text="Collider Groups:")
(
collider_group_collection_ops,
collider_group_collection_item_ops,
collider_group_index,
_collider_group,
_,
) = draw_template_list(
collider_groups_column,
VRM_UL_spring_bone1_spring_collider_group,
spring,
"collider_groups",
"active_collider_group_index",
ops.VRM_OT_add_spring_bone1_spring_collider_group,
ops.VRM_OT_remove_spring_bone1_spring_collider_group,
ops.VRM_OT_move_up_spring_bone1_spring_collider_group,
ops.VRM_OT_move_down_spring_bone1_spring_collider_group,
)
for collider_group_collection_op in collider_group_collection_ops:
collider_group_collection_op.armature_object_name = armature.name
collider_group_collection_op.spring_index = spring_index
for collider_group_collection_item_op in collider_group_collection_item_ops:
collider_group_collection_item_op.collider_group_index = collider_group_index
class VRM_PT_spring_bone1_armature_object_property(Panel):
bl_idname = "VRM_PT_vrm1_spring_bone_armature_object_property"
bl_label = "Spring Bone"
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="PHYSICS")
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_spring_bone1_spring_bone_layout(
active_object,
self.layout,
get_armature_extension(armature_data).spring_bone1,
)
class VRM_PT_spring_bone1_ui(Panel):
bl_idname = "VRM_PT_vrm1_spring_bone_ui"
bl_label = "Spring Bone"
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="PHYSICS")
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_spring_bone1_spring_bone_layout(
armature,
self.layout,
get_armature_extension(armature_data).spring_bone1,
)
class VRM_PT_spring_bone1_collider_property(Panel):
bl_idname = "VRM_PT_spring_bone1_collider_property"
bl_label = "VRM Spring Bone Collider"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "data"
@classmethod
def active_armature_and_collider(
cls, context: Context
) -> Optional[tuple[Object, SpringBone1ColliderPropertyGroup]]:
active_object = context.active_object
if not active_object:
return None
if active_object.type != "EMPTY":
return None
parent = active_object.parent
if not parent:
return None
if active_object.parent_type == "BONE":
collider_object = context.active_object
elif active_object.parent_type == "OBJECT":
if parent.type == "ARMATURE":
collider_object = active_object
elif parent.parent_type == "BONE" or (
parent.parent_type == "OBJECT"
and parent.parent
and parent.parent.type == "ARMATURE"
):
collider_object = parent
else:
return None
else:
return None
for obj in context.blend_data.objects:
if obj.type != "ARMATURE":
continue
armature_data = obj.data
if not isinstance(armature_data, Armature):
continue
for collider in get_armature_extension(
armature_data
).spring_bone1.colliders:
if collider.bpy_object == collider_object:
return (obj, collider)
return None
@classmethod
def poll(cls, context: Context) -> bool:
return cls.active_armature_and_collider(context) is not None
def draw(self, context: Context) -> None:
armature_and_collider = self.active_armature_and_collider(context)
if armature_and_collider is None:
return
armature, collider = armature_and_collider
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
if get_armature_extension(armature_data).is_vrm1():
draw_spring_bone1_collider_layout(armature, self.layout.column(), collider)
return
self.layout.label(text="This is a VRM 1.0 Spring Bone Collider", icon="INFO")