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:
2026-01-01 14:21:56 +08:00
commit 091ad6a49a
243 changed files with 60636 additions and 0 deletions

375
external/io_scene_gltf2_support.py vendored Normal file
View File

@@ -0,0 +1,375 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import dataclasses
import datetime
import logging
from collections.abc import Set as AbstractSet
from typing import Optional
import bpy
from bpy.types import Context, Event, Image, Material, Operator
from io_scene_gltf2.io.com import gltf2_io
from ..common.logger import get_logger
logger = get_logger(__name__)
class WM_OT_vrm_io_scene_gltf2_disabled_warning(Operator):
bl_label = "glTF 2.0 add-on is disabled"
bl_idname = "wm.vrm_gltf2_addon_disabled_warning"
bl_options: AbstractSet[str] = {"REGISTER"}
def execute(self, _context: Context) -> set[str]:
return {"FINISHED"}
def invoke(self, context: Context, _event: Event) -> set[str]:
return context.window_manager.invoke_props_dialog(self, width=500)
def draw(self, _context: Context) -> None:
self.layout.label(
text='Official add-on "glTF 2.0 format" is required. Please enable it.'
)
def image_to_image_bytes(
image: Image, export_settings: dict[str, object]
) -> tuple[bytes, str]:
mime_type = "image/jpeg" if image.file_format == "JPEG" else "image/png"
if bpy.app.version < (3, 6, 0):
from io_scene_gltf2.blender.exp.gltf2_blender_image import (
ExportImage as ExportImage_Before_3_6,
)
export_image_before_3_6 = ExportImage_Before_3_6.from_blender_image(image)
if bpy.app.version < (3, 5, 0):
encoded = export_image_before_3_6.encode(mime_type)
if isinstance(encoded, bytes):
# bpy.app.version < (3, 3, 0)
return encoded, mime_type
image_bytes, _specular_color_factor = encoded
return image_bytes, mime_type
image_bytes, _specular_color_factor = export_image_before_3_6.encode(
mime_type, export_settings
)
return image_bytes, mime_type
if bpy.app.version < (4, 3):
from io_scene_gltf2.blender.exp.material.extensions.gltf2_blender_image import (
ExportImage as ExportImage_Before_4_3,
)
export_image_before_4_3 = ExportImage_Before_4_3.from_blender_image(image)
image_bytes, _specular_color_factor = export_image_before_4_3.encode(
mime_type, export_settings
)
return image_bytes, mime_type
from io_scene_gltf2.blender.exp.material.encode_image import ExportImage
export_image = ExportImage.from_blender_image(image)
image_bytes, _specular_color_factor = export_image.encode(
mime_type, export_settings
)
return image_bytes, mime_type
def init_extras_export() -> None:
try:
if bpy.app.version < (4, 5):
from io_scene_gltf2.blender.com.gltf2_blender_extras import BLACK_LIST
else:
from io_scene_gltf2.blender.com.extras import BLACK_LIST
except ImportError:
return
key = "vrm_addon_extension"
if key not in BLACK_LIST:
BLACK_LIST.append(key)
def create_export_settings() -> dict[str, object]:
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/b9bdc358ebf41e5f14be397d0d612cc8d645a09e/addons/io_scene_gltf2/__init__.py#L1054
loglevel = logging.INFO
export_settings: dict[str, object] = {
"loglevel": loglevel,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L522
"timestamp": datetime.datetime.now(datetime.timezone.utc),
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L258-L268
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L552
"gltf_materials": True,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L120-L137
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L532
"gltf_format": "GLB",
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L154-L168
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L533
"gltf_image_format": "AUTO",
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L329-L333
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L569
"gltf_extras": True,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L611-L633
"gltf_user_extensions": [],
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L606
"gltf_binary": bytearray(),
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L176-L184
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/67b2ed150b0eba08129b970dbe1116c633a77d24/addons/io_scene_gltf2/__init__.py#L530
"gltf_keep_original_textures": False,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/bfe4ff8b1b5c26ba17b0531b67798376147d9fa7/addons/io_scene_gltf2/__init__.py#L273-L279
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/bfe4ff8b1b5c26ba17b0531b67798376147d9fa7/addons/io_scene_gltf2/__init__.py#L579
"gltf_original_specular": False,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/e662c281fc830d7ad3ea918d38c6a1881ee143c5/addons/io_scene_gltf2/__init__.py#L208-L214
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/e662c281fc830d7ad3ea918d38c6a1881ee143c5/addons/io_scene_gltf2/__init__.py#L617
"gltf_jpeg_quality": 75,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/a2eac6c4b4ef957b654b61970dc554e3803a642e/addons/io_scene_gltf2/__init__.py#L233-L240
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/a2eac6c4b4ef957b654b61970dc554e3803a642e/addons/io_scene_gltf2/__init__.py#L787
"gltf_image_quality": 75,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/765c1bd8f59ce34d6e346147f379af191969777f/addons/io_scene_gltf2/__init__.py#L785
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/765c1bd8f59ce34d6e346147f379af191969777f/addons/io_scene_gltf2/__init__.py#L201-L208
"gltf_add_webp": False,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/2debd75ace303f3a3b00a43e9d7a9507af32f194/addons/io_scene_gltf2/__init__.py#L941
"exported_images": {},
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/2debd75ace303f3a3b00a43e9d7a9507af32f194/addons/io_scene_gltf2/__init__.py#L942
"exported_texture_nodes": [],
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/2debd75ace303f3a3b00a43e9d7a9507af32f194/addons/io_scene_gltf2/__init__.py#L943
"additional_texture_export": [],
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/2debd75ace303f3a3b00a43e9d7a9507af32f194/addons/io_scene_gltf2/__init__.py#L944
"additional_texture_export_current_idx": 0,
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/2debd75ace303f3a3b00a43e9d7a9507af32f194/addons/io_scene_gltf2/__init__.py#L985-L986
"gltf_unused_textures": False,
"gltf_unused_images": False,
# for https://github.com/KhronosGroup/glTF-Blender-IO/blob/06f0f908e883add2767fde828f52a013086a17c3/addons/io_scene_gltf2/blender/exp/material/extensions/gltf2_blender_gather_materials_emission.py#L72
"current_paths": {},
# for https://github.com/KhronosGroup/glTF-Blender-IO/blob/06f0f908e883add2767fde828f52a013086a17c3/addons/io_scene_gltf2/blender/exp/material/gltf2_blender_gather_materials.py#L171
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/06f0f908e883add2767fde828f52a013086a17c3/addons/io_scene_gltf2/blender/exp/gltf2_blender_gather.py#L62-L66
"KHR_animation_pointer": {"materials": {}, "lights": {}, "cameras": {}},
}
if bpy.app.version < (4, 2):
return export_settings
if bpy.app.version < (4, 3):
from io_scene_gltf2.io.com.gltf2_io_debug import Log as Log_Before_4_3
export_settings["log"] = Log_Before_4_3(loglevel)
else:
from io_scene_gltf2.io.com.debug import Log
export_settings["log"] = Log(loglevel)
return export_settings
@dataclasses.dataclass
class ImportSceneGltfArguments:
filepath: str
import_pack_images: bool
bone_heuristic: str
guess_original_bind_pose: bool
disable_bone_shape: bool
import_scene_as_collection: bool
def import_scene_gltf(arguments: ImportSceneGltfArguments) -> set[str]:
if bpy.app.version < (4, 2):
return bpy.ops.import_scene.gltf(
filepath=arguments.filepath,
import_pack_images=arguments.import_pack_images,
bone_heuristic=arguments.bone_heuristic,
guess_original_bind_pose=arguments.guess_original_bind_pose,
)
if bpy.app.version < (4, 5):
return bpy.ops.import_scene.gltf(
filepath=arguments.filepath,
import_pack_images=arguments.import_pack_images,
bone_heuristic=arguments.bone_heuristic,
guess_original_bind_pose=arguments.guess_original_bind_pose,
disable_bone_shape=arguments.disable_bone_shape,
)
return bpy.ops.import_scene.gltf(
filepath=arguments.filepath,
import_pack_images=arguments.import_pack_images,
bone_heuristic=arguments.bone_heuristic,
guess_original_bind_pose=arguments.guess_original_bind_pose,
disable_bone_shape=arguments.disable_bone_shape,
import_scene_as_collection=arguments.import_scene_as_collection,
)
@dataclasses.dataclass
class ExportSceneGltfArguments:
filepath: str
check_existing: bool
export_format: str
export_extras: bool
export_def_bones: bool
export_current_frame: bool
use_selection: bool
use_active_scene: bool
export_animations: bool
export_armature_object_remove: bool
export_rest_position_armature: bool
export_all_influences: bool
export_vertex_color: str
export_lights: bool
export_try_sparse_sk: bool
export_apply: bool
def __invoke_export_scene_gltf(arguments: ExportSceneGltfArguments) -> set[str]:
if bpy.app.version < (3, 2):
return bpy.ops.export_scene.gltf(
filepath=arguments.filepath,
check_existing=arguments.check_existing,
export_format=arguments.export_format,
export_extras=arguments.export_extras,
export_def_bones=False,
export_current_frame=arguments.export_current_frame,
use_selection=arguments.use_selection,
export_animations=arguments.export_animations,
export_all_influences=arguments.export_all_influences,
export_lights=arguments.export_lights,
export_apply=arguments.export_apply,
)
if bpy.app.version < (3, 6):
return bpy.ops.export_scene.gltf(
filepath=arguments.filepath,
check_existing=arguments.check_existing,
export_format=arguments.export_format,
export_extras=arguments.export_extras,
export_def_bones=(bpy.app.version >= (3, 3)) and arguments.export_def_bones,
export_current_frame=arguments.export_current_frame,
use_selection=arguments.use_selection,
use_active_scene=arguments.use_active_scene,
export_animations=arguments.export_animations,
export_all_influences=arguments.export_all_influences,
export_lights=arguments.export_lights,
export_apply=arguments.export_apply,
)
if bpy.app.version < (4,):
return bpy.ops.export_scene.gltf(
filepath=arguments.filepath,
check_existing=arguments.check_existing,
export_format=arguments.export_format,
export_extras=arguments.export_extras,
export_def_bones=arguments.export_def_bones,
export_current_frame=arguments.export_current_frame,
use_selection=arguments.use_selection,
use_active_scene=arguments.use_active_scene,
export_animations=arguments.export_animations,
export_rest_position_armature=arguments.export_rest_position_armature,
export_all_influences=arguments.export_all_influences,
export_lights=arguments.export_lights,
export_apply=arguments.export_apply,
)
if bpy.app.version < (4, 2):
return bpy.ops.export_scene.gltf(
filepath=arguments.filepath,
check_existing=arguments.check_existing,
export_format=arguments.export_format,
export_extras=arguments.export_extras,
export_def_bones=arguments.export_def_bones,
export_current_frame=arguments.export_current_frame,
use_selection=arguments.use_selection,
use_active_scene=arguments.use_active_scene,
export_animations=arguments.export_animations,
export_rest_position_armature=arguments.export_rest_position_armature,
export_try_sparse_sk=arguments.export_try_sparse_sk,
export_all_influences=arguments.export_all_influences,
export_lights=arguments.export_lights,
export_apply=arguments.export_apply,
)
return bpy.ops.export_scene.gltf(
filepath=arguments.filepath,
check_existing=arguments.check_existing,
export_format=arguments.export_format,
export_extras=arguments.export_extras,
export_def_bones=arguments.export_def_bones,
export_current_frame=arguments.export_current_frame,
use_selection=arguments.use_selection,
use_active_scene=arguments.use_active_scene,
export_animations=arguments.export_animations,
export_armature_object_remove=arguments.export_armature_object_remove,
export_rest_position_armature=arguments.export_rest_position_armature,
export_vertex_color=arguments.export_vertex_color,
export_try_sparse_sk=arguments.export_try_sparse_sk,
export_all_influences=arguments.export_all_influences,
export_lights=arguments.export_lights,
export_apply=arguments.export_apply,
)
def export_scene_gltf(arguments: ExportSceneGltfArguments) -> set[str]:
last_exception: Optional[Exception] = None
try:
return __invoke_export_scene_gltf(arguments)
except RuntimeError as exception:
last_exception = exception
# export_armature_object_remove is unstable, and the conditions under which it
# fails are very complex, so I cannot fully grasp all of them.
# https://github.com/KhronosGroup/glTF-Blender-IO/issues/2436
# https://github.com/saturday06/VRM-Addon-for-Blender/issues/1033
if arguments.export_armature_object_remove:
arguments.export_armature_object_remove = False
logger.warning("retrying with `export_armature_object_remove = False`")
try:
return __invoke_export_scene_gltf(arguments)
except RuntimeError as exception:
last_exception = exception
# https://github.com/saturday06/VRM-Addon-for-Blender/commit/26f566d43b15c7a403c0f007b0290f9aef569114
if arguments.export_animations:
arguments.export_animations = False
logger.warning("retrying with `export_animations = False`")
try:
return __invoke_export_scene_gltf(arguments)
except RuntimeError as exception:
last_exception = exception
raise last_exception
def gather_gltf2_io_material(
material: Material, export_settings: dict[str, object]
) -> Optional[gltf2_io.Material]:
if bpy.app.version < (3, 2):
from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials import (
gather_material as gather_material_before_3_2,
)
return gather_material_before_3_2(material, export_settings)
if bpy.app.version < (3, 6):
from io_scene_gltf2.blender.exp.gltf2_blender_gather_materials import (
gather_material as gather_material_before_3_6,
)
return gather_material_before_3_6(material, 0, export_settings)
if bpy.app.version < (4, 0):
from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_materials import (
gather_material as gather_material_before_4_0,
)
return gather_material_before_4_0(material, 0, export_settings)
if bpy.app.version < (4, 3):
from io_scene_gltf2.blender.exp.material.gltf2_blender_gather_materials import (
gather_material as gather_material_before_4_3,
)
gltf2_io_material, _ = gather_material_before_4_3(material, export_settings)
return gltf2_io_material
from io_scene_gltf2.blender.exp.material.materials import gather_material
gltf2_io_material, _ = gather_material(material, export_settings)
return gltf2_io_material