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

540 lines
19 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import math
from collections.abc import Sequence
from typing import TYPE_CHECKING, Optional, TypeVar
import bpy
from bpy.props import (
CollectionProperty,
EnumProperty,
IntVectorProperty,
PointerProperty,
StringProperty,
)
from bpy.types import (
Armature,
Bone,
Context,
Material,
NodeTree,
Object,
PropertyGroup,
Scene,
)
from mathutils import Matrix, Quaternion
from ..common.logger import get_logger
from ..common.preferences import VrmAddonPreferences
from .mtoon1.property_group import Mtoon1MaterialPropertyGroup
from .node_constraint1.property_group import NodeConstraint1NodeConstraintPropertyGroup
from .property_group import StringPropertyGroup, property_group_enum
from .spring_bone1.property_group import SpringBone1SpringBonePropertyGroup
from .vrm0.property_group import Vrm0HumanoidPropertyGroup, Vrm0PropertyGroup
from .vrm1.property_group import Vrm1HumanBonesPropertyGroup, Vrm1PropertyGroup
if TYPE_CHECKING:
from .property_group import CollectionPropertyProtocol
logger = get_logger(__name__)
class VrmAddonSceneExtensionPropertyGroup(PropertyGroup):
vrm0_material_gltf_property_names: CollectionProperty( # type: ignore[valid-type]
type=StringPropertyGroup
)
vrm0_material_mtoon0_property_names: CollectionProperty( # type: ignore[valid-type]
type=StringPropertyGroup
)
@staticmethod
def update_vrm0_material_property_names_timer_callback() -> Optional[float]:
context = bpy.context
VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(context)
return None
@staticmethod
def defer_update_vrm0_material_property_names() -> None:
s = VrmAddonSceneExtensionPropertyGroup
bpy.app.timers.register(s.update_vrm0_material_property_names_timer_callback)
@staticmethod
def update_vrm0_material_property_names(
context: Context, scene_name: Optional[str] = None
) -> None:
# Unity 2022.3.4 + UniVRM 0.112.0
gltf_property_names = [
"_Color",
"_MainTex_ST",
"_MainTex_ST_S",
"_MainTex_ST_T",
"_MetallicGlossMap_ST",
"_MetallicGlossMap_ST_S",
"_MetallicGlossMap_ST_T",
"_BumpMap_ST",
"_BumpMap_ST_S",
"_BumpMap_ST_T",
"_ParallaxMap_ST",
"_ParallaxMap_ST_S",
"_ParallaxMap_ST_T",
"_OcclusionMap_ST",
"_OcclusionMap_ST_S",
"_OcclusionMap_ST_T",
"_EmissionColor",
"_EmissionMap_ST",
"_EmissionMap_ST_S",
"_EmissionMap_ST_T",
"_DetailMask_ST",
"_DetailMask_ST_S",
"_DetailMask_ST_T",
"_DetailAlbedoMap_ST",
"_DetailAlbedoMap_ST_S",
"_DetailAlbedoMap_ST_T",
"_DetailNormalMap_ST",
"_DetailNormalMap_ST_S",
"_DetailNormalMap_ST_T",
]
# UniVRM 0.112.0
mtoon0_property_names = [
"_Color",
"_ShadeColor",
"_MainTex_ST",
"_MainTex_ST_S",
"_MainTex_ST_T",
"_ShadeTexture_ST",
"_ShadeTexture_ST_S",
"_ShadeTexture_ST_T",
"_BumpMap_ST",
"_BumpMap_ST_S",
"_BumpMap_ST_T",
"_ReceiveShadowTexture_ST",
"_ReceiveShadowTexture_ST_S",
"_ReceiveShadowTexture_ST_T",
"_ShadingGradeTexture_ST",
"_ShadingGradeTexture_ST_S",
"_ShadingGradeTexture_ST_T",
"_RimColor",
"_RimTexture_ST",
"_RimTexture_ST_S",
"_RimTexture_ST_T",
"_SphereAdd_ST",
"_SphereAdd_ST_S",
"_SphereAdd_ST_T",
"_EmissionColor",
"_EmissionMap_ST",
"_EmissionMap_ST_S",
"_EmissionMap_ST_T",
"_OutlineWidthTexture_ST",
"_OutlineWidthTexture_ST_S",
"_OutlineWidthTexture_ST_T",
"_OutlineColor",
"_UvAnimMaskTexture_ST",
"_UvAnimMaskTexture_ST_S",
"_UvAnimMaskTexture_ST_T",
]
if scene_name is None:
scenes: Sequence[Scene] = list(context.blend_data.scenes)
else:
scene = context.blend_data.scenes.get(scene_name)
if not scene:
logger.error('No scene "%s"', scene_name)
return
scenes = [scene]
for scene in scenes:
ext = get_scene_extension(scene)
if gltf_property_names != [
n.value for n in ext.vrm0_material_gltf_property_names
]:
ext.vrm0_material_gltf_property_names.clear()
for gltf_property_name in gltf_property_names:
n = ext.vrm0_material_gltf_property_names.add()
n.value = gltf_property_name
if mtoon0_property_names != [
n.value for n in ext.vrm0_material_mtoon0_property_names
]:
ext.vrm0_material_mtoon0_property_names.clear()
for mtoon0_property_name in mtoon0_property_names:
n = ext.vrm0_material_mtoon0_property_names.add()
n.value = mtoon0_property_name
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
vrm0_material_gltf_property_names: CollectionPropertyProtocol[ # type: ignore[no-redef]
StringPropertyGroup
]
vrm0_material_mtoon0_property_names: CollectionPropertyProtocol[ # type: ignore[no-redef]
StringPropertyGroup
]
class VrmAddonBoneExtensionPropertyGroup(PropertyGroup):
uuid: StringProperty() # type: ignore[valid-type]
(
axis_translation_enum,
(
AXIS_TRANSLATION_AUTO,
AXIS_TRANSLATION_NONE,
AXIS_TRANSLATION_X_TO_Y,
AXIS_TRANSLATION_MINUS_X_TO_Y,
AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z,
AXIS_TRANSLATION_Z_TO_Y,
AXIS_TRANSLATION_MINUS_Z_TO_Y,
),
) = property_group_enum(
("AUTO", "Auto", "", "NONE", 0),
("NONE", "None", "", "NONE", 1),
("X_TO_Y", "X,Y to Y,-X", "", "NONE", 2),
("MINUS_X_TO_Y", "X,Y to -Y,X", "", "NONE", 3),
("MINUS_Y_TO_Y_AROUND_Z", "X,Y to -X,-Y", "", "NONE", 4),
("Z_TO_Y", "Y,Z to -Z,Y", "", "NONE", 5),
("MINUS_Z_TO_Y", "Y,Z to Z,-Y", "", "NONE", 6),
)
@classmethod
def reverse_axis_translation(cls, axis_translation: str) -> str:
return {
cls.AXIS_TRANSLATION_AUTO.identifier: cls.AXIS_TRANSLATION_AUTO,
cls.AXIS_TRANSLATION_NONE.identifier: cls.AXIS_TRANSLATION_NONE,
cls.AXIS_TRANSLATION_X_TO_Y.identifier: (cls.AXIS_TRANSLATION_MINUS_X_TO_Y),
cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: (cls.AXIS_TRANSLATION_X_TO_Y),
cls.AXIS_TRANSLATION_Z_TO_Y.identifier: (cls.AXIS_TRANSLATION_MINUS_Z_TO_Y),
cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: (cls.AXIS_TRANSLATION_Z_TO_Y),
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: (
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z
),
}[axis_translation].identifier
@classmethod
def node_constraint_roll_axis_translation(
cls, axis_translation: str, roll_axis: Optional[str]
) -> Optional[str]:
if roll_axis is None:
return None
return {
cls.AXIS_TRANSLATION_AUTO.identifier: {"X": "X", "Y": "Y", "Z": "Z"},
cls.AXIS_TRANSLATION_NONE.identifier: {"X": "X", "Y": "Y", "Z": "Z"},
cls.AXIS_TRANSLATION_X_TO_Y.identifier: {"X": "Y", "Y": "X", "Z": "Z"},
cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: {
"X": "Y",
"Y": "X",
"Z": "Z",
},
cls.AXIS_TRANSLATION_Z_TO_Y.identifier: {"X": "X", "Y": "Z", "Z": "Y"},
cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: {
"X": "X",
"Y": "Z",
"Z": "Y",
},
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: {
"X": "X",
"Y": "Y",
"Z": "Z",
},
}[axis_translation][roll_axis]
@classmethod
def node_constraint_aim_axis_translation(
cls, axis_translation: str, aim_axis: Optional[str]
) -> Optional[str]:
if aim_axis is None:
return None
return {
cls.AXIS_TRANSLATION_AUTO.identifier: {
"PositiveX": "PositiveX",
"PositiveY": "PositiveY",
"PositiveZ": "PositiveZ",
"NegativeX": "NegativeX",
"NegativeY": "NegativeY",
"NegativeZ": "NegativeZ",
},
cls.AXIS_TRANSLATION_NONE.identifier: {
"PositiveX": "PositiveX",
"PositiveY": "PositiveY",
"PositiveZ": "PositiveZ",
"NegativeX": "NegativeX",
"NegativeY": "NegativeY",
"NegativeZ": "NegativeZ",
},
cls.AXIS_TRANSLATION_X_TO_Y.identifier: {
"PositiveX": "PositiveY",
"PositiveY": "NegativeX",
"PositiveZ": "PositiveZ",
"NegativeX": "NegativeY",
"NegativeY": "PositiveX",
"NegativeZ": "NegativeZ",
},
cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier: {
"PositiveY": "PositiveX",
"NegativeX": "PositiveY",
"PositiveZ": "PositiveZ",
"NegativeY": "NegativeX",
"PositiveX": "NegativeY",
"NegativeZ": "NegativeZ",
},
cls.AXIS_TRANSLATION_Z_TO_Y.identifier: {
"PositiveX": "PositiveX",
"PositiveY": "NegativeZ",
"PositiveZ": "PositiveY",
"NegativeX": "NegativeX",
"NegativeY": "PositiveZ",
"NegativeZ": "NegativeY",
},
cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier: {
"PositiveX": "PositiveX",
"NegativeZ": "PositiveY",
"PositiveY": "PositiveZ",
"NegativeX": "NegativeX",
"PositiveZ": "NegativeY",
"NegativeY": "NegativeZ",
},
cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier: {
"PositiveX": "NegativeX",
"PositiveY": "NegativeY",
"PositiveZ": "PositiveZ",
"NegativeX": "PositiveX",
"NegativeY": "PositiveY",
"NegativeZ": "NegativeZ",
},
}[axis_translation][aim_axis]
@classmethod
def translate_axis(cls, matrix: Matrix, axis_translation: str) -> Matrix:
location, rotation, scale = matrix.decompose()
if axis_translation == cls.AXIS_TRANSLATION_X_TO_Y.identifier:
rotation @= Quaternion((0, 0, 1), -math.pi / 2)
elif axis_translation == cls.AXIS_TRANSLATION_MINUS_X_TO_Y.identifier:
rotation @= Quaternion((0, 0, 1), math.pi / 2)
elif axis_translation == cls.AXIS_TRANSLATION_MINUS_Y_TO_Y_AROUND_Z.identifier:
rotation @= Quaternion((0, 0, 1), math.pi)
elif axis_translation == cls.AXIS_TRANSLATION_Z_TO_Y.identifier:
rotation @= Quaternion((1, 0, 0), math.pi / 2)
elif axis_translation == cls.AXIS_TRANSLATION_MINUS_Z_TO_Y.identifier:
rotation @= Quaternion((1, 0, 0), -math.pi / 2)
# return Matrix.LocRotScale(location, rotation, scale)
return (
Matrix.Translation(location)
@ rotation.to_matrix().to_4x4()
@ Matrix.Diagonal(scale).to_4x4()
)
axis_translation: EnumProperty( # type: ignore[valid-type]
items=axis_translation_enum.items(),
name="Axis Translation on Export",
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
uuid: str # type: ignore[no-redef]
axis_translation: str # type: ignore[no-redef]
class VrmAddonObjectExtensionPropertyGroup(PropertyGroup):
axis_translation: EnumProperty( # type: ignore[valid-type]
items=VrmAddonBoneExtensionPropertyGroup.axis_translation_enum.items(),
name="Axis Translation on Export",
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
axis_translation: str # type: ignore[no-redef]
class VrmAddonArmatureExtensionPropertyGroup(PropertyGroup):
INITIAL_ADDON_VERSION = VrmAddonPreferences.INITIAL_ADDON_VERSION
addon_version: IntVectorProperty( # type: ignore[valid-type]
size=3,
default=INITIAL_ADDON_VERSION,
)
vrm0: PointerProperty( # type: ignore[valid-type]
type=Vrm0PropertyGroup
)
vrm1: PointerProperty( # type: ignore[valid-type]
type=Vrm1PropertyGroup
)
spring_bone1: PointerProperty( # type: ignore[valid-type]
type=SpringBone1SpringBonePropertyGroup
)
node_constraint1: PointerProperty( # type: ignore[valid-type]
type=NodeConstraint1NodeConstraintPropertyGroup
)
SPEC_VERSION_VRM0 = "0.0"
SPEC_VERSION_VRM1 = "1.0"
spec_version_items = (
(SPEC_VERSION_VRM0, "VRM 0.0", "", "NONE", 0),
(SPEC_VERSION_VRM1, "VRM 1.0", "", "NONE", 1),
)
def update_spec_version(self, _context: Context) -> None:
for blend_shape_group in self.vrm0.blend_shape_master.blend_shape_groups:
blend_shape_group.preview = 0
if self.spec_version == self.SPEC_VERSION_VRM0:
vrm0_hidden = False
vrm1_hidden = True
elif self.spec_version == self.SPEC_VERSION_VRM1:
vrm0_hidden = True
vrm1_hidden = False
else:
return
for vrm0_collider in [
collider.bpy_object
for collider_group in self.vrm0.secondary_animation.collider_groups
for collider in collider_group.colliders
if collider.bpy_object
]:
vrm0_collider.hide_set(vrm0_hidden)
for vrm1_collider in [
collider.bpy_object
for collider in self.spring_bone1.colliders
if collider.bpy_object
]:
vrm1_collider.hide_set(vrm1_hidden)
for child in vrm1_collider.children:
child.hide_set(vrm1_hidden)
spec_version: EnumProperty( # type: ignore[valid-type]
items=spec_version_items,
name="Spec Version",
update=update_spec_version,
default=SPEC_VERSION_VRM1,
)
def is_vrm0(self) -> bool:
return str(self.spec_version) == self.SPEC_VERSION_VRM0
def is_vrm1(self) -> bool:
return str(self.spec_version) == self.SPEC_VERSION_VRM1
@staticmethod
def has_vrm_model_metadata(obj: Object) -> bool:
if obj.type != "ARMATURE":
return False
armature = obj.data
if not isinstance(armature, Armature):
return False
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/2_0_3/io_scene_vrm/editor/migration.py#L372-L373
ext = get_armature_extension(armature)
if tuple(ext.addon_version) > (2, 0, 1):
return True
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/0_79/importer/model_build.py#L731
humanoid_params_key = obj.get("humanoid_params")
if not isinstance(humanoid_params_key, str):
return False
# https://github.com/saturday06/VRM-Addon-for-Blender/blob/0_79/importer/model_build.py#L706
return "hips" in armature
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
addon_version: Sequence[int] # type: ignore[no-redef]
vrm0: Vrm0PropertyGroup # type: ignore[no-redef]
vrm1: Vrm1PropertyGroup # type: ignore[no-redef]
spring_bone1: SpringBone1SpringBonePropertyGroup # type: ignore[no-redef]
node_constraint1: ( # type: ignore[no-redef]
NodeConstraint1NodeConstraintPropertyGroup
)
spec_version: str # type: ignore[no-redef]
def update_internal_cache(context: Context) -> None:
for armature in context.blend_data.armatures:
Vrm0HumanoidPropertyGroup.update_all_node_candidates(context, armature.name)
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(context, armature.name)
VrmAddonSceneExtensionPropertyGroup.update_vrm0_material_property_names(context)
class VrmAddonMaterialExtensionPropertyGroup(PropertyGroup):
mtoon1: PointerProperty( # type: ignore[valid-type]
type=Mtoon1MaterialPropertyGroup
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
mtoon1: Mtoon1MaterialPropertyGroup # type: ignore[no-redef]
class VrmAddonNodeTreeExtensionPropertyGroup(PropertyGroup):
INITIAL_ADDON_VERSION = VrmAddonPreferences.INITIAL_ADDON_VERSION
addon_version: IntVectorProperty( # type: ignore[valid-type]
size=3,
default=INITIAL_ADDON_VERSION,
)
if TYPE_CHECKING:
# This code is auto generated.
# To regenerate, run the `uv run tools/property_typing.py` command.
addon_version: Sequence[int] # type: ignore[no-redef]
__Extension = TypeVar("__Extension")
def get_vrm_addon_extension_or_raise(
obj: object, expected_type: type[__Extension]
) -> __Extension:
extension = getattr(obj, "vrm_addon_extension", None)
if isinstance(extension, expected_type):
return extension
message = f"{extension} is not a {expected_type} but {type(extension)}"
raise TypeError(message)
def get_material_extension(
material: Material,
) -> VrmAddonMaterialExtensionPropertyGroup:
return get_vrm_addon_extension_or_raise(
material, VrmAddonMaterialExtensionPropertyGroup
)
def get_armature_extension(
armature: Armature,
) -> VrmAddonArmatureExtensionPropertyGroup:
return get_vrm_addon_extension_or_raise(
armature, VrmAddonArmatureExtensionPropertyGroup
)
def get_node_tree_extension(
node_tree: NodeTree,
) -> VrmAddonNodeTreeExtensionPropertyGroup:
return get_vrm_addon_extension_or_raise(
node_tree, VrmAddonNodeTreeExtensionPropertyGroup
)
def get_scene_extension(scene: Scene) -> VrmAddonSceneExtensionPropertyGroup:
return get_vrm_addon_extension_or_raise(scene, VrmAddonSceneExtensionPropertyGroup)
def get_bone_extension(bone: Bone) -> VrmAddonBoneExtensionPropertyGroup:
return get_vrm_addon_extension_or_raise(bone, VrmAddonBoneExtensionPropertyGroup)
def get_object_extension(obj: Object) -> VrmAddonObjectExtensionPropertyGroup:
return get_vrm_addon_extension_or_raise(obj, VrmAddonObjectExtensionPropertyGroup)