Files
blender-vrm/common/human_bone_mapper/human_bone_mapper.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

180 lines
5.8 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import re
from collections.abc import Mapping
from typing import Optional
from bpy.types import Armature, Object
from ..char import FULLWIDTH_ASCII_TO_ASCII_MAP
from ..logger import get_logger
from ..vrm1.human_bone import HumanBoneSpecification
from . import (
biped_mapping,
cats_blender_plugin_fix_model_mapping,
microsoft_rocketbox_mapping,
mixamo_mapping,
mmd_mapping,
ready_player_me_mapping,
rigify_meta_rig_mapping,
unreal_mapping,
vrm_addon_mapping,
vroid_mapping,
)
logger = get_logger(__name__)
def canonicalize_bone_name(bone_name: str) -> str:
bone_name = "".join(FULLWIDTH_ASCII_TO_ASCII_MAP.get(c, c) for c in bone_name)
bone_name = re.sub(r"([a-z])([A-Z])", r"\1.\2", bone_name)
bone_name = bone_name.lower()
bone_name = "".join(" " if c.isspace() else c for c in bone_name)
bone_name = re.sub(r"(\d+)", r".\1.", bone_name).strip(".")
bone_name_components = re.split(r"[-._: (){}[\]<>]+", bone_name)
for patterns, replacement in {
("l", ""): "left",
("r", ""): "right",
}.items():
bone_name_components = [
replacement if bone_name_component in patterns else bone_name_component
for bone_name_component in bone_name_components
]
return ".".join(bone_name_components)
def match_bone_name(bone_name1: str, bone_name2: str) -> bool:
return canonicalize_bone_name(bone_name1) == canonicalize_bone_name(bone_name2)
def match_count(
armature: Armature, mapping: Mapping[str, HumanBoneSpecification]
) -> int:
count = 0
mapping = {
bpy_name: specification
for bpy_name, specification in mapping.items()
if any(match_bone_name(bpy_name, bone.name) for bone in armature.bones)
}
# Validate bone ordering
for bpy_name, specification in mapping.items():
bone = next(
(bone for bone in armature.bones if match_bone_name(bpy_name, bone.name)),
None,
)
if not bone:
continue
parent_specification: Optional[HumanBoneSpecification] = None
search_parent_specification = specification.parent()
while search_parent_specification:
if search_parent_specification in mapping.values():
parent_specification = search_parent_specification
break
search_parent_specification = search_parent_specification.parent()
found = False
bone = bone.parent
while bone:
search_specification = next(
(
search_specification
for bpy_name, search_specification in mapping.items()
if match_bone_name(bpy_name, bone.name)
),
None,
)
if search_specification:
found = search_specification == parent_specification
break
bone = bone.parent
if found or not parent_specification:
count += 1
continue
return count
def match_counts(
armature: object, mapping: Mapping[str, HumanBoneSpecification]
) -> tuple[int, int]:
if not isinstance(armature, Armature):
message = f"{type(armature)} is not an Armature"
raise TypeError(message)
required_mapping = {
bpy_name: required_specification
for bpy_name, required_specification in mapping.items()
if required_specification.requirement
}
return (match_count(armature, required_mapping), match_count(armature, mapping))
def sorted_required_first(
armature: Armature,
mapping: Mapping[str, HumanBoneSpecification],
) -> dict[str, HumanBoneSpecification]:
bpy_bone_name_mapping: dict[str, HumanBoneSpecification] = {
bpy_bone_name: specification
for original_bone_name, specification in mapping.items()
if (
bpy_bone_name := next(
(
bone.name
for bone in armature.bones
if match_bone_name(original_bone_name, bone.name)
),
None,
)
)
}
sorted_mapping: dict[str, HumanBoneSpecification] = {}
sorted_mapping.update(
{
bpy_name: specification
for bpy_name, specification in bpy_bone_name_mapping.items()
if specification.requirement
}
)
sorted_mapping.update(
{
bpy_name: specification
for bpy_name, specification in bpy_bone_name_mapping.items()
if not specification.requirement
}
)
return sorted_mapping
def create_human_bone_mapping(
armature: Object,
) -> dict[str, HumanBoneSpecification]:
armature_data = armature.data
if not isinstance(armature_data, Armature):
raise TypeError
((required_count, _all_count), name, mapping) = sorted(
[
(match_counts(armature_data, mapping), name, mapping)
for name, mapping in [
mmd_mapping.create_config(armature),
biped_mapping.create_config(armature),
mixamo_mapping.CONFIG,
unreal_mapping.CONFIG,
ready_player_me_mapping.CONFIG,
cats_blender_plugin_fix_model_mapping.CONFIG,
microsoft_rocketbox_mapping.CONFIG_BIP01,
microsoft_rocketbox_mapping.CONFIG_BIP02,
rigify_meta_rig_mapping.CONFIG,
vroid_mapping.CONFIG,
vroid_mapping.CONFIG_SYMMETRICAL,
vrm_addon_mapping.CONFIG_VRM1,
vrm_addon_mapping.CONFIG_VRM0,
]
]
)[-1]
if required_count:
logger.debug('Treat as "%s" bone mappings', name)
return sorted_required_first(armature_data, mapping)
return {}