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
This commit is contained in:
258
editor/migration.py
Normal file
258
editor/migration.py
Normal file
@@ -0,0 +1,258 @@
|
||||
# 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,
|
||||
)
|
||||
)
|
||||
Reference in New Issue
Block a user