- 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
376 lines
17 KiB
Python
376 lines
17 KiB
Python
# 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
|