Files
blender-vrm/editor/vrm1/migration.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

282 lines
11 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from bpy.types import Armature, Context, Material, Object
from idprop.types import IDPropertyGroup
from mathutils import Vector
from ...common import convert, ops, shader
from ..extension import get_armature_extension
from .property_group import (
Vrm1ExpressionPropertyGroup,
Vrm1ExpressionsPropertyGroup,
Vrm1HumanBonesPropertyGroup,
Vrm1MaterialColorBindPropertyGroup,
Vrm1PropertyGroup,
)
def migrate_old_expression_layout(
old_expression: object, expression: Vrm1ExpressionPropertyGroup
) -> None:
if not isinstance(old_expression, IDPropertyGroup):
return
old_morph_target_binds = convert.iterator_or_none(
old_expression.get("morph_target_binds")
)
if old_morph_target_binds is not None:
for old_morph_target_bind in old_morph_target_binds:
if not isinstance(old_morph_target_bind, IDPropertyGroup):
continue
morph_target_bind = expression.morph_target_binds.add()
old_node = old_morph_target_bind.get("node")
if isinstance(old_node, IDPropertyGroup):
old_bpy_object = old_node.get("bpy_object")
if isinstance(old_bpy_object, Object) and old_bpy_object.type == "MESH":
morph_target_bind.node.mesh_object_name = old_bpy_object.name
old_node.clear()
old_index = old_morph_target_bind.get("index")
if isinstance(old_index, str):
morph_target_bind.index = old_index
old_weight = old_morph_target_bind.get("weight")
if isinstance(old_weight, (float, int)):
morph_target_bind.weight = old_weight
old_morph_target_bind.clear()
old_material_color_binds = convert.iterator_or_none(
old_expression.get("material_color_binds")
)
if old_material_color_binds is not None:
for old_material_color_bind in old_material_color_binds:
if not isinstance(old_material_color_bind, IDPropertyGroup):
continue
material_color_bind = expression.material_color_binds.add()
old_material = old_material_color_bind.get("material")
if isinstance(old_material, Material):
material_color_bind.material = old_material
old_type = next(
(
enum.identifier
for enum in Vrm1MaterialColorBindPropertyGroup.type_enum
if old_material_color_bind.get("type") == enum.value
),
None,
)
if old_type is not None:
material_color_bind.type = old_type
old_target_value = shader.rgba_or_none(
old_material_color_bind.get("target_value"), 0.0, 1.0
)
if old_target_value:
material_color_bind.target_value = old_target_value
old_material_color_bind.clear()
old_texture_transform_binds = convert.iterator_or_none(
old_expression.get("texture_transform_binds")
)
if old_texture_transform_binds is not None:
for old_texture_transform_bind in old_texture_transform_binds:
if not isinstance(old_texture_transform_bind, IDPropertyGroup):
continue
texture_transform_bind = expression.texture_transform_binds.add()
old_material = old_texture_transform_bind.get("material")
if isinstance(old_material, Material):
texture_transform_bind.material = old_material
old_scale = convert.float2_or_none(old_texture_transform_bind.get("scale"))
if old_scale:
texture_transform_bind.scale = old_scale
old_offset = convert.float2_or_none(
old_texture_transform_bind.get("offset")
)
if old_offset:
texture_transform_bind.offset = old_offset
old_texture_transform_bind.clear()
old_is_binary = old_expression.get("is_binary")
if isinstance(old_is_binary, int):
expression.is_binary = bool(old_is_binary)
old_override_blink = next(
(
enum.identifier
for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
if old_expression.get("override_blink") == enum.value
),
None,
)
if old_override_blink is not None:
expression.override_blink = old_override_blink
old_override_look_at = next(
(
enum.identifier
for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
if old_expression.get("override_look_at") == enum.value
),
None,
)
if old_override_look_at is not None:
expression.override_look_at = old_override_look_at
old_override_mouth = next(
(
enum.identifier
for enum in Vrm1ExpressionPropertyGroup.expression_override_type_enum
if old_expression.get("override_mouth") == enum.value
),
None,
)
if old_override_mouth is not None:
expression.override_mouth = old_override_mouth
def migrate_old_expressions_layout(expressions: Vrm1ExpressionsPropertyGroup) -> None:
for name, expression in expressions.preset.name_to_expression_dict().items():
property_name = {
"blinkLeft": "blink_left",
"blinkRight": "blink_right",
"lookUp": "look_up",
"lookDown": "look_down",
"lookLeft": "look_left",
"lookRight": "look_right",
}.get(name)
if property_name is None:
property_name = name
old_expression = expressions.get(property_name)
migrate_old_expression_layout(old_expression, expression)
expression.name = name
for expression in expressions.custom:
old_expression = expression.get("expression")
migrate_old_expression_layout(old_expression, expression)
def migrate_pose(context: Context, armature: Object, armature_data: Armature) -> None:
ext = get_armature_extension(armature_data)
if tuple(ext.addon_version) >= (2, 20, 34):
return
humanoid = ext.vrm1.humanoid
if isinstance(humanoid.get("pose"), int):
return
if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION:
if ext.has_vrm_model_metadata(armature):
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
return
action = humanoid.pose_library
if action and action.name in context.blend_data.actions:
humanoid.pose = humanoid.POSE_CUSTOM_POSE.identifier
elif armature_data.pose_position == "REST":
humanoid.pose = humanoid.POSE_REST_POSITION_POSE.identifier
else:
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
def migrate_auto_pose(_context: Context, armature_data: Armature) -> None:
ext = get_armature_extension(armature_data)
if tuple(ext.addon_version) == ext.INITIAL_ADDON_VERSION or tuple(
ext.addon_version
) >= (2, 20, 81):
return
humanoid = ext.vrm1.humanoid
if not isinstance(humanoid.get("pose"), int):
humanoid.pose = humanoid.POSE_CURRENT_POSE.identifier
def migrate(context: Context, vrm1: Vrm1PropertyGroup, armature: Object) -> None:
armature_data = armature.data
if not isinstance(armature_data, Armature):
return
human_bones = vrm1.humanoid.human_bones
human_bones.last_bone_names_str = ""
Vrm1HumanBonesPropertyGroup.fixup_human_bones(armature)
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(context, armature_data.name)
if human_bones.initial_automatic_bone_assignment:
human_bones.initial_automatic_bone_assignment = False
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
if all(not b.node.bone_name for b in human_bone_name_to_human_bone.values()):
ops.vrm.assign_vrm1_humanoid_human_bones_automatically(
armature_object_name=armature.name
)
if tuple(get_armature_extension(armature_data).addon_version) <= (2, 14, 10):
ext = get_armature_extension(armature_data)
head_bone_name = ext.vrm1.humanoid.human_bones.head.node.bone_name
head_bone = armature_data.bones.get(head_bone_name)
if head_bone:
look_at = get_armature_extension(armature_data).vrm1.look_at
world_translation = (
armature.matrix_world @ head_bone.matrix_local
).to_quaternion() @ Vector(look_at.offset_from_head_bone)
look_at.offset_from_head_bone = list(world_translation)
if tuple(get_armature_extension(armature_data).addon_version) <= (2, 15, 5):
# Apply lower limit value
look_at = get_armature_extension(armature_data).vrm1.look_at
look_at.range_map_horizontal_inner.input_max_value = (
look_at.range_map_horizontal_inner.input_max_value
)
look_at.range_map_horizontal_outer.input_max_value = (
look_at.range_map_horizontal_outer.input_max_value
)
look_at.range_map_vertical_down.input_max_value = (
look_at.range_map_vertical_down.input_max_value
)
look_at.range_map_vertical_up.input_max_value = (
look_at.range_map_vertical_up.input_max_value
)
if tuple(get_armature_extension(armature_data).addon_version) < (2, 18, 0):
migrate_old_expressions_layout(
get_armature_extension(armature_data).vrm1.expressions
)
if tuple(get_armature_extension(armature_data).addon_version) < (2, 20, 0):
look_at = get_armature_extension(armature_data).vrm1.look_at
look_at.offset_from_head_bone = (
look_at.offset_from_head_bone[0],
look_at.offset_from_head_bone[2],
-look_at.offset_from_head_bone[1],
)
migrate_pose(context, armature, armature_data)
migrate_auto_pose(context, armature_data)
# Set a name for the expression preset.
# It's not necessary for management, but I want to set it because it's
# displayed in the animation keyframes.
expressions = get_armature_extension(armature_data).vrm1.expressions
preset_name_to_expression_dict = expressions.preset.name_to_expression_dict()
for preset_name, preset_expression in preset_name_to_expression_dict.items():
if preset_expression.name != preset_name:
preset_expression.name = preset_name
if tuple(get_armature_extension(armature_data).addon_version) < (3, 9, 0):
for expression in expressions.preset.name_to_expression_dict().values():
for morph_target_bind in expression.morph_target_binds:
morph_target_bind.node.saved_mesh_object_name_to_restore = (
morph_target_bind.node.mesh_object_name
)
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
context,
armature_data.name,
force=True,
)
ops.vrm.update_vrm1_expression_ui_list_elements()