- 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
259 lines
8.1 KiB
Python
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,
|
|
)
|
|
)
|