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

259 lines
8.1 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import functools
from dataclasses import dataclass
from typing import Final, Optional
import bpy
from bpy.types import Armature, Context
from ..common import ops
from ..common.logger import get_logger
from ..common.preferences import get_preferences
from ..common.version import get_addon_version
from .extension import (
VrmAddonArmatureExtensionPropertyGroup,
VrmAddonSceneExtensionPropertyGroup,
get_armature_extension,
)
from .mtoon1 import migration as mtoon1_migration
from .spring_bone1 import migration as spring_bone1_migration
from .vrm0 import migration as vrm0_migration
from .vrm1 import migration as vrm1_migration
logger = get_logger(__name__)
@dataclass
class State:
blend_file_compatibility_warning_shown: bool = False
blend_file_addon_compatibility_warning_shown: bool = False
state: Final = State()
def is_unnecessary(armature_data: Armature) -> bool:
ext = get_armature_extension(armature_data)
return tuple(
ext.addon_version
) >= get_addon_version() and vrm0_migration.is_unnecessary(ext.vrm0)
def defer_migrate(armature_object_name: str) -> bool:
context = bpy.context
armature = context.blend_data.objects.get(armature_object_name)
if not armature:
return False
armature_data = armature.data
if not isinstance(armature_data, Armature):
return False
if is_unnecessary(armature_data):
return True
bpy.app.timers.register(
functools.partial(
migrate_timer_callback,
armature_object_name,
)
)
return False
def migrate_timer_callback(armature_object_name: str) -> None:
"""Match the type of migrate() to bpy.app.timers.register."""
context = bpy.context # Context cannot span frames, so get it anew
migrate(context, armature_object_name)
def migrate(context: Optional[Context], armature_object_name: str) -> bool:
if context is None:
context = bpy.context
armature = context.blend_data.objects.get(armature_object_name)
if not armature:
return False
armature_data = armature.data
if not isinstance(armature_data, Armature):
return False
if is_unnecessary(armature_data):
return True
ext = get_armature_extension(armature_data)
vrm0_migration.migrate(context, ext.vrm0, armature)
vrm1_migration.migrate(context, ext.vrm1, armature)
spring_bone1_migration.migrate(context, armature)
if (
ext.has_vrm_model_metadata(armature)
and tuple(ext.addon_version) < (3, 14, 0)
and not ext.get("spec_version")
):
ext.spec_version = ext.SPEC_VERSION_VRM0
updated_addon_version = get_addon_version()
logger.info(
"Upgrade armature %s %s to %s",
armature_object_name,
tuple(ext.addon_version),
updated_addon_version,
)
ext.addon_version = updated_addon_version
return True
def migrate_all_objects(
context: Context,
*,
skip_non_migrated_armatures: bool = False,
show_progress: bool = False,
) -> None:
for obj in context.blend_data.objects:
if obj.type == "ARMATURE" and isinstance(armature_data := obj.data, Armature):
if skip_non_migrated_armatures:
ext = get_armature_extension(armature_data)
if (
tuple(ext.addon_version)
== VrmAddonArmatureExtensionPropertyGroup.INITIAL_ADDON_VERSION
):
continue
migrate(context, obj.name)
VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(
context, context.scene.name
)
mtoon1_migration.migrate(context, show_progress=show_progress)
validate_blend_file_compatibility(context)
validate_blend_file_addon_compatibility(context)
preferences = get_preferences(context)
updated_addon_version = get_addon_version()
logger.debug(
"Upgrade preferences %s to %s",
tuple(preferences.addon_version),
updated_addon_version,
)
if tuple(preferences.addon_version) != preferences.INITIAL_ADDON_VERSION and tuple(
preferences.addon_version
) < (2, 34, 0):
preferences.enable_advanced_preferences = True
preferences.export_gltf_animations = True
preferences.addon_version = updated_addon_version
def validate_blend_file_compatibility(context: Context) -> None:
"""Warn when attempting to edit a file created in newer Blender with older Blender.
Due to add-on version support issues, there are often reports of users trying to
edit files created in newer Blender with older Blender, which can cause shape keys
to break. This warning alerts users to be careful.
"""
if not context.blend_data.filepath:
return
if not have_vrm_model(context):
return
blend_file_major_minor_version = (
context.blend_data.version[0],
context.blend_data.version[1],
)
current_major_minor_version = (bpy.app.version[0], bpy.app.version[1])
if blend_file_major_minor_version <= current_major_minor_version:
return
file_version_str = ".".join(map(str, blend_file_major_minor_version))
app_version_str = ".".join(map(str, current_major_minor_version))
logger.error(
"Opening incompatible file: file_blender_version=%s running_blender_version=%s",
file_version_str,
app_version_str,
)
if not state.blend_file_compatibility_warning_shown:
state.blend_file_compatibility_warning_shown = True
# Use timer because dialog disappears automatically if not executed with
# timer in Blender 4.2.0
bpy.app.timers.register(
functools.partial(
show_blend_file_compatibility_warning,
file_version_str,
app_version_str,
),
first_interval=0.1,
)
def show_blend_file_compatibility_warning(file_version: str, app_version: str) -> None:
ops.vrm.show_blend_file_compatibility_warning(
"INVOKE_DEFAULT",
file_version=file_version,
app_version=app_version,
)
def validate_blend_file_addon_compatibility(context: Context) -> None:
"""Warn when attempting to edit a file created with a newer VRM add-on.
This warning is for files using an older VRM add-on.
"""
if not context.blend_data.filepath:
return
installed_addon_version = get_addon_version()
# TODO: It might be better to store the version in Scene or similar
up_to_date = True
file_addon_version: tuple[int, ...] = (0, 0, 0)
for armature in context.blend_data.armatures:
file_addon_version = tuple(get_armature_extension(armature).addon_version)
if file_addon_version > installed_addon_version:
up_to_date = False
break
if up_to_date:
return
file_addon_version_str = ".".join(map(str, file_addon_version))
installed_addon_version_str = ".".join(map(str, installed_addon_version))
logger.error(
"Opening incompatible VRM add-on version: file=%s installed=%s",
file_addon_version_str,
installed_addon_version_str,
)
if not state.blend_file_compatibility_warning_shown:
state.blend_file_compatibility_warning_shown = True
# Use timer because dialog disappears automatically if not executed with
# timer in Blender 4.2.0
bpy.app.timers.register(
functools.partial(
show_blend_file_addon_compatibility_warning,
file_addon_version_str,
installed_addon_version_str,
),
first_interval=0.1,
)
def show_blend_file_addon_compatibility_warning(
file_addon_version: str, installed_addon_version: str
) -> None:
ops.vrm.show_blend_file_addon_compatibility_warning(
"INVOKE_DEFAULT",
file_addon_version=file_addon_version,
installed_addon_version=installed_addon_version,
)
def have_vrm_model(context: Context) -> bool:
return any(
map(
VrmAddonArmatureExtensionPropertyGroup.has_vrm_model_metadata,
context.blend_data.objects,
)
)