- 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
420 lines
12 KiB
Python
420 lines
12 KiB
Python
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
|
from bpy.types import Armature, Context, Mesh, UILayout, UIList
|
|
|
|
from ...common.logger import get_logger
|
|
from ..extension import get_armature_extension
|
|
from ..property_group import BonePropertyGroup, StringPropertyGroup
|
|
from .property_group import (
|
|
Vrm0BlendShapeBindPropertyGroup,
|
|
Vrm0BlendShapeGroupPropertyGroup,
|
|
Vrm0FirstPersonPropertyGroup,
|
|
Vrm0MaterialValueBindPropertyGroup,
|
|
Vrm0MeshAnnotationPropertyGroup,
|
|
Vrm0SecondaryAnimationColliderGroupPropertyGroup,
|
|
Vrm0SecondaryAnimationColliderPropertyGroup,
|
|
Vrm0SecondaryAnimationGroupPropertyGroup,
|
|
)
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
class VRM_UL_vrm0_first_person_mesh_annotation(UIList):
|
|
bl_idname = "VRM_UL_vrm0_first_person_mesh_annotation"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
first_person: object,
|
|
mesh_annotation: object,
|
|
_icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
if not isinstance(first_person, Vrm0FirstPersonPropertyGroup):
|
|
return
|
|
if not isinstance(mesh_annotation, Vrm0MeshAnnotationPropertyGroup):
|
|
return
|
|
|
|
icon = "OUTLINER_OB_MESH"
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
row = layout.split(factor=0.6, align=True)
|
|
if index == first_person.active_mesh_annotation_index:
|
|
row.prop(
|
|
mesh_annotation.mesh,
|
|
"bpy_object",
|
|
icon=icon,
|
|
text="",
|
|
translate=False,
|
|
)
|
|
else:
|
|
row.label(
|
|
text=mesh_annotation.mesh.mesh_object_name,
|
|
translate=False,
|
|
icon=icon,
|
|
)
|
|
row.prop(mesh_annotation, "first_person_flag", text="", translate=False)
|
|
|
|
|
|
class VRM_UL_vrm0_secondary_animation_group(UIList):
|
|
bl_idname = "VRM_UL_vrm0_secondary_animation_group"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
_data: object,
|
|
bone_group: object,
|
|
_icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
_index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
if not isinstance(bone_group, Vrm0SecondaryAnimationGroupPropertyGroup):
|
|
return
|
|
|
|
icon = "BONE_DATA"
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
text = ""
|
|
if bone_group.bones:
|
|
text = (
|
|
"(" + ", ".join(str(bone.bone_name) for bone in bone_group.bones) + ")"
|
|
)
|
|
|
|
if bone_group.center.bone_name:
|
|
if text:
|
|
text = " - " + text
|
|
text = bone_group.center.bone_name + text
|
|
|
|
if bone_group.comment:
|
|
if text:
|
|
text = " / " + text
|
|
text = bone_group.comment + text
|
|
|
|
if not text:
|
|
text = "(EMPTY)"
|
|
|
|
layout.label(text=text, translate=False, icon=icon)
|
|
|
|
|
|
class VRM_UL_vrm0_secondary_animation_group_bone(UIList):
|
|
bl_idname = "VRM_UL_vrm0_secondary_animation_group_bone"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
bone_group: object,
|
|
bone: object,
|
|
_icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
if not isinstance(bone_group, Vrm0SecondaryAnimationGroupPropertyGroup):
|
|
return
|
|
if not isinstance(bone, BonePropertyGroup):
|
|
return
|
|
armature = bone.find_armature()
|
|
|
|
icon = "BONE_DATA"
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
if index == bone_group.active_bone_index:
|
|
layout.prop_search(
|
|
bone,
|
|
"bone_name",
|
|
armature,
|
|
"bones",
|
|
text="",
|
|
translate=False,
|
|
icon=icon,
|
|
)
|
|
else:
|
|
layout.label(text=bone.bone_name, translate=False, icon=icon)
|
|
|
|
|
|
class VRM_UL_vrm0_secondary_animation_group_collider_group(UIList):
|
|
bl_idname = "VRM_UL_vrm0_secondary_animation_group_collider_group"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
bone_group: object,
|
|
collider_group: object,
|
|
_icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
if not isinstance(bone_group, Vrm0SecondaryAnimationGroupPropertyGroup):
|
|
return
|
|
if not isinstance(collider_group, StringPropertyGroup):
|
|
return
|
|
|
|
armature_data = bone_group.id_data
|
|
if not isinstance(armature_data, Armature):
|
|
logger.error("Failed to find armature")
|
|
return
|
|
|
|
secondary_animation = get_armature_extension(
|
|
armature_data
|
|
).vrm0.secondary_animation
|
|
|
|
icon = "PIVOT_INDIVIDUAL"
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
if index == bone_group.active_collider_group_index:
|
|
layout.prop_search(
|
|
collider_group,
|
|
"value",
|
|
secondary_animation,
|
|
"collider_groups",
|
|
text="",
|
|
translate=False,
|
|
icon=icon,
|
|
)
|
|
else:
|
|
layout.label(text=collider_group.value, translate=False, icon=icon)
|
|
|
|
|
|
class VRM_UL_vrm0_secondary_animation_collider_group(UIList):
|
|
bl_idname = "VRM_UL_vrm0_secondary_animation_collider_group"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
_data: object,
|
|
collider_group: object,
|
|
_icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
_index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
if not isinstance(
|
|
collider_group, Vrm0SecondaryAnimationColliderGroupPropertyGroup
|
|
):
|
|
return
|
|
|
|
icon = "SPHERE"
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
layout.label(text=collider_group.name, translate=False, icon=icon)
|
|
|
|
|
|
class VRM_UL_vrm0_secondary_animation_collider_group_collider(UIList):
|
|
bl_idname = "VRM_UL_vrm0_secondary_animation_collider_group_collider"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
collider_group: object,
|
|
collider: object,
|
|
_icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
if not isinstance(
|
|
collider_group, Vrm0SecondaryAnimationColliderGroupPropertyGroup
|
|
):
|
|
return
|
|
if not isinstance(collider, Vrm0SecondaryAnimationColliderPropertyGroup):
|
|
return
|
|
|
|
icon = "MESH_UVSPHERE"
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
bpy_object = collider.bpy_object
|
|
if bpy_object is None:
|
|
return
|
|
|
|
row = layout.split(align=True, factor=0.7)
|
|
if index == collider_group.active_collider_index:
|
|
row.prop(
|
|
bpy_object,
|
|
"name",
|
|
icon=icon,
|
|
translate=False,
|
|
text="",
|
|
)
|
|
row.prop(bpy_object, "empty_display_size", text="")
|
|
else:
|
|
row.label(text=bpy_object.name, icon=icon, translate=False)
|
|
row.prop(bpy_object, "empty_display_size", text="", emboss=False)
|
|
|
|
|
|
class VRM_UL_vrm0_blend_shape_group(UIList):
|
|
bl_idname = "VRM_UL_vrm0_blend_shape_group"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
_data: object,
|
|
item: object,
|
|
_icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
_index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
blend_shape_group = item
|
|
if not isinstance(blend_shape_group, Vrm0BlendShapeGroupPropertyGroup):
|
|
return
|
|
|
|
preset = next(
|
|
(
|
|
preset
|
|
for preset in blend_shape_group.preset_name_enum
|
|
if preset.identifier == blend_shape_group.preset_name
|
|
),
|
|
None,
|
|
)
|
|
if not preset:
|
|
return
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon=preset.icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
text = blend_shape_group.name + " / " + preset.name
|
|
split = layout.split(align=True, factor=0.55)
|
|
split.label(text=text, translate=False, icon=preset.icon)
|
|
split.prop(blend_shape_group, "preview", text="Preview")
|
|
|
|
|
|
class VRM_UL_vrm0_blend_shape_bind(UIList):
|
|
bl_idname = "VRM_UL_vrm0_blend_shape_bind"
|
|
|
|
def draw_item(
|
|
self,
|
|
context: Context,
|
|
layout: UILayout,
|
|
_data: object,
|
|
item: object,
|
|
icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
_index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
blend_data = context.blend_data
|
|
blend_shape_bind = item
|
|
if not isinstance(blend_shape_bind, Vrm0BlendShapeBindPropertyGroup):
|
|
return
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon_value=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
name = blend_shape_bind.mesh.mesh_object_name
|
|
mesh_object = blend_data.objects.get(blend_shape_bind.mesh.mesh_object_name)
|
|
if mesh_object:
|
|
mesh_data = mesh_object.data
|
|
if isinstance(mesh_data, Mesh):
|
|
shape_keys = mesh_data.shape_keys
|
|
if shape_keys:
|
|
keys = shape_keys.key_blocks.keys()
|
|
if blend_shape_bind.index in keys:
|
|
name += " / " + blend_shape_bind.index
|
|
layout.label(text=name, translate=False, icon="MESH_DATA")
|
|
|
|
|
|
class VRM_UL_vrm0_material_value_bind(UIList):
|
|
bl_idname = "VRM_UL_vrm0_material_value_bind"
|
|
|
|
def draw_item(
|
|
self,
|
|
_context: Context,
|
|
layout: UILayout,
|
|
_data: object,
|
|
item: object,
|
|
icon: int,
|
|
_active_data: object,
|
|
_active_prop_name: str,
|
|
_index: int,
|
|
_flt_flag: int,
|
|
) -> None:
|
|
material_value_bind = item
|
|
if not isinstance(material_value_bind, Vrm0MaterialValueBindPropertyGroup):
|
|
return
|
|
|
|
if self.layout_type == "GRID":
|
|
layout.alignment = "CENTER"
|
|
layout.label(text="", translate=False, icon_value=icon)
|
|
return
|
|
|
|
if self.layout_type not in {"DEFAULT", "COMPACT"}:
|
|
return
|
|
|
|
name = ""
|
|
material = material_value_bind.material
|
|
if material:
|
|
name = material.name
|
|
if material_value_bind.property_name:
|
|
name += " / " + material_value_bind.property_name
|
|
layout.label(text=name, translate=False, icon="MATERIAL")
|