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:
1
exporter/__init__.py
Normal file
1
exporter/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
BIN
exporter/__pycache__/__init__.cpython-311.pyc
Normal file
BIN
exporter/__pycache__/__init__.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporter/__pycache__/abstract_base_vrm_exporter.cpython-311.pyc
Normal file
BIN
exporter/__pycache__/abstract_base_vrm_exporter.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporter/__pycache__/export_scene.cpython-311.pyc
Normal file
BIN
exporter/__pycache__/export_scene.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporter/__pycache__/gltf2_export_user_extension.cpython-311.pyc
Normal file
BIN
exporter/__pycache__/gltf2_export_user_extension.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
exporter/__pycache__/vrm0_exporter.cpython-311.pyc
Normal file
BIN
exporter/__pycache__/vrm0_exporter.cpython-311.pyc
Normal file
Binary file not shown.
BIN
exporter/__pycache__/vrm1_exporter.cpython-311.pyc
Normal file
BIN
exporter/__pycache__/vrm1_exporter.cpython-311.pyc
Normal file
Binary file not shown.
384
exporter/abstract_base_vrm_exporter.py
Normal file
384
exporter/abstract_base_vrm_exporter.py
Normal file
@@ -0,0 +1,384 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import secrets
|
||||
import string
|
||||
from abc import ABC, abstractmethod
|
||||
from collections.abc import Iterator, Mapping, Sequence
|
||||
from contextlib import contextmanager
|
||||
from typing import Optional, Union
|
||||
|
||||
import bmesh
|
||||
from bpy.types import Armature, Context, Mesh, NodesModifier, Object
|
||||
from mathutils import Matrix, Vector
|
||||
|
||||
from ..common import shader
|
||||
from ..common.convert import Json
|
||||
from ..common.deep import make_json
|
||||
from ..common.logger import get_logger
|
||||
from ..editor.extension import get_armature_extension, get_material_extension
|
||||
from ..editor.property_group import BonePropertyGroup, BonePropertyGroupType
|
||||
from ..editor.search import MESH_CONVERTIBLE_OBJECT_TYPES
|
||||
from ..external import io_scene_gltf2_support
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AbstractBaseVrmExporter(ABC):
|
||||
def __init__(
|
||||
self,
|
||||
context: Context,
|
||||
export_objects: Sequence[Object],
|
||||
armature: Object,
|
||||
) -> None:
|
||||
self.context = context
|
||||
self.export_objects = export_objects
|
||||
self.armature = armature
|
||||
self.export_id = "BlenderVrmAddonExport" + (
|
||||
"".join(secrets.choice(string.digits) for _ in range(10))
|
||||
)
|
||||
self.gltf2_addon_export_settings = (
|
||||
io_scene_gltf2_support.create_export_settings()
|
||||
)
|
||||
|
||||
armature_data = self.armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
message = f"{type(armature_data)} is not an Armature"
|
||||
raise TypeError(message)
|
||||
|
||||
@abstractmethod
|
||||
def export_vrm(self) -> Optional[bytes]:
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def enter_clear_blend_shape_proxy_previews(
|
||||
armature_data: Armature,
|
||||
) -> tuple[Sequence[float], Mapping[str, float]]:
|
||||
ext = get_armature_extension(armature_data)
|
||||
|
||||
saved_vrm0_previews: list[float] = []
|
||||
for blend_shape_group in ext.vrm0.blend_shape_master.blend_shape_groups:
|
||||
saved_vrm0_previews.append(blend_shape_group.preview)
|
||||
blend_shape_group.preview = 0
|
||||
|
||||
saved_vrm1_previews: dict[str, float] = {}
|
||||
for (
|
||||
name,
|
||||
expression,
|
||||
) in ext.vrm1.expressions.all_name_to_expression_dict().items():
|
||||
saved_vrm1_previews[name] = expression.preview
|
||||
expression.preview = 0
|
||||
|
||||
return saved_vrm0_previews, saved_vrm1_previews
|
||||
|
||||
@staticmethod
|
||||
def leave_clear_blend_shape_proxy_previews(
|
||||
armature_data: Armature,
|
||||
saved_vrm0_previews: Sequence[float],
|
||||
saved_vrm1_previews: Mapping[str, float],
|
||||
) -> None:
|
||||
ext = get_armature_extension(armature_data)
|
||||
|
||||
for blend_shape_group, blend_shape_preview in zip(
|
||||
ext.vrm0.blend_shape_master.blend_shape_groups, saved_vrm0_previews
|
||||
):
|
||||
blend_shape_group.preview = blend_shape_preview
|
||||
|
||||
for (
|
||||
name,
|
||||
expression,
|
||||
) in ext.vrm1.expressions.all_name_to_expression_dict().items():
|
||||
expression_preview = saved_vrm1_previews.get(name)
|
||||
if expression_preview is not None:
|
||||
expression.preview = expression_preview
|
||||
|
||||
@contextmanager
|
||||
def clear_blend_shape_proxy_previews(
|
||||
self, armature_data: Armature
|
||||
) -> Iterator[None]:
|
||||
saved_vrm0_previews, saved_vrm1_previews = (
|
||||
self.enter_clear_blend_shape_proxy_previews(armature_data)
|
||||
)
|
||||
try:
|
||||
yield
|
||||
# After yield, native bpy objects may be deleted or frames may advance,
|
||||
# becoming invalid. Accessing them in this state causes crashes, so
|
||||
# be careful not to access potentially invalid native objects after yield
|
||||
finally:
|
||||
self.leave_clear_blend_shape_proxy_previews(
|
||||
armature_data, saved_vrm0_previews, saved_vrm1_previews
|
||||
)
|
||||
|
||||
def enter_enable_deform_for_all_referenced_bones(
|
||||
self, armature_data: Armature
|
||||
) -> list[str]:
|
||||
ext = get_armature_extension(armature_data)
|
||||
modified_non_deform_bone_names = list[str]()
|
||||
for (
|
||||
bone_property_group,
|
||||
bone_property_group_type,
|
||||
) in BonePropertyGroup.get_all_bone_property_groups(armature_data):
|
||||
bone = armature_data.bones.get(bone_property_group.bone_name)
|
||||
if not bone or bone.use_deform:
|
||||
continue
|
||||
if (
|
||||
ext.is_vrm0()
|
||||
and BonePropertyGroupType.is_vrm0(bone_property_group_type)
|
||||
) or (
|
||||
ext.is_vrm1()
|
||||
and BonePropertyGroupType.is_vrm1(bone_property_group_type)
|
||||
):
|
||||
bone.use_deform = True
|
||||
modified_non_deform_bone_names.append(bone.name)
|
||||
return modified_non_deform_bone_names
|
||||
|
||||
def leave_enable_deform_for_all_referenced_bones(
|
||||
self, armature_data: Armature, modified_non_deform_bone_names: list[str]
|
||||
) -> None:
|
||||
for modified_non_deform_bone_name in modified_non_deform_bone_names:
|
||||
bone = armature_data.bones.get(modified_non_deform_bone_name)
|
||||
if bone and bone.use_deform:
|
||||
bone.use_deform = False
|
||||
|
||||
@contextmanager
|
||||
def enable_deform_for_all_referenced_bones(
|
||||
self, armature_data: Armature
|
||||
) -> Iterator[None]:
|
||||
modified_non_deform_bone_names = (
|
||||
self.enter_enable_deform_for_all_referenced_bones(armature_data)
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
self.leave_enable_deform_for_all_referenced_bones(
|
||||
armature_data, modified_non_deform_bone_names
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def enter_hide_mtoon1_outline_geometry_nodes(
|
||||
context: Context,
|
||||
) -> dict[str, list[tuple[str, bool, bool]]]:
|
||||
object_name_to_modifiers: dict[str, list[tuple[str, bool, bool]]] = {}
|
||||
for obj in context.blend_data.objects:
|
||||
for modifier in obj.modifiers:
|
||||
if not modifier.show_viewport:
|
||||
continue
|
||||
if modifier.type != "NODES":
|
||||
continue
|
||||
if not isinstance(modifier, NodesModifier):
|
||||
continue
|
||||
node_group = modifier.node_group
|
||||
if (
|
||||
not node_group
|
||||
or node_group.name != shader.OUTLINE_GEOMETRY_GROUP_NAME
|
||||
):
|
||||
continue
|
||||
modifiers = object_name_to_modifiers.get(obj.name)
|
||||
if modifiers is None:
|
||||
modifiers = []
|
||||
object_name_to_modifiers[obj.name] = modifiers
|
||||
modifiers = object_name_to_modifiers[obj.name]
|
||||
modifiers.append(
|
||||
(
|
||||
modifier.name,
|
||||
modifier.show_render,
|
||||
modifier.show_viewport,
|
||||
)
|
||||
)
|
||||
if modifier.show_render:
|
||||
modifier.show_render = False
|
||||
if modifier.show_viewport:
|
||||
modifier.show_viewport = False
|
||||
return object_name_to_modifiers
|
||||
|
||||
@staticmethod
|
||||
def exit_hide_mtoon1_outline_geometry_nodes(
|
||||
context: Context,
|
||||
object_name_to_modifiers: dict[str, list[tuple[str, bool, bool]]],
|
||||
) -> None:
|
||||
for object_name, modifiers in object_name_to_modifiers.items():
|
||||
for modifier_name, render, viewport in modifiers:
|
||||
obj = context.blend_data.objects.get(object_name)
|
||||
if not obj:
|
||||
continue
|
||||
modifier = obj.modifiers.get(modifier_name)
|
||||
if (
|
||||
not modifier
|
||||
or modifier.type != "NODES"
|
||||
or not isinstance(modifier, NodesModifier)
|
||||
):
|
||||
continue
|
||||
node_group = modifier.node_group
|
||||
if (
|
||||
not node_group
|
||||
or node_group.name != shader.OUTLINE_GEOMETRY_GROUP_NAME
|
||||
):
|
||||
continue
|
||||
if modifier.show_render != render:
|
||||
modifier.show_render = render
|
||||
if modifier.show_viewport != viewport:
|
||||
modifier.show_viewport = viewport
|
||||
|
||||
@staticmethod
|
||||
@contextmanager
|
||||
def hide_mtoon1_outline_geometry_nodes(context: Context) -> Iterator[None]:
|
||||
object_name_to_modifier_names = (
|
||||
AbstractBaseVrmExporter.enter_hide_mtoon1_outline_geometry_nodes(context)
|
||||
)
|
||||
try:
|
||||
yield
|
||||
# After yield, native bpy objects may be deleted or frames may advance,
|
||||
# becoming invalid. Accessing them in this state causes crashes, so
|
||||
# be careful not to access potentially invalid native objects after yield
|
||||
finally:
|
||||
AbstractBaseVrmExporter.exit_hide_mtoon1_outline_geometry_nodes(
|
||||
context, object_name_to_modifier_names
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def setup_mtoon_gltf_fallback_nodes(context: Context, *, is_vrm0: bool) -> None:
|
||||
"""Reflect MToon node values to nodes used for glTF fallback values.
|
||||
|
||||
When MToon nodes are directly edited, glTF fallback values are not
|
||||
automatically set. Therefore, we explicitly set values during export.
|
||||
"""
|
||||
for material in context.blend_data.materials:
|
||||
mtoon1 = get_material_extension(material).mtoon1
|
||||
if not mtoon1.enabled:
|
||||
continue
|
||||
mtoon1.pbr_metallic_roughness.base_color_factor = (
|
||||
mtoon1.pbr_metallic_roughness.base_color_factor
|
||||
)
|
||||
mtoon1.emissive_factor = mtoon1.emissive_factor
|
||||
mtoon1.normal_texture.scale = mtoon1.normal_texture.scale
|
||||
mtoon1.extensions.khr_materials_emissive_strength.emissive_strength = (
|
||||
mtoon1.extensions.khr_materials_emissive_strength.emissive_strength
|
||||
)
|
||||
for texture in mtoon1.all_textures(downgrade_to_mtoon0=is_vrm0):
|
||||
texture.source = texture.get_connected_node_image()
|
||||
|
||||
|
||||
def assign_dict(
|
||||
target: dict[str, Json],
|
||||
key: str,
|
||||
value: Union[Json, tuple[float, float, float], tuple[float, float, float, float]],
|
||||
default_value: Json = None,
|
||||
) -> bool:
|
||||
if value is None or value == default_value:
|
||||
return False
|
||||
target[key] = make_json(value)
|
||||
return True
|
||||
|
||||
|
||||
def generate_evaluated_mesh(context: Context, obj: Object) -> Optional[Mesh]:
|
||||
obj_data = obj.data
|
||||
if obj_data is None:
|
||||
return None
|
||||
|
||||
# https://docs.blender.org/api/2.80/Depsgraph.html
|
||||
# TODO: Shape keys may sometimes break
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
evaluated_obj = obj.evaluated_get(depsgraph)
|
||||
evaluated_temporary_mesh = evaluated_obj.to_mesh(
|
||||
preserve_all_data_layers=True, depsgraph=depsgraph
|
||||
)
|
||||
if not evaluated_temporary_mesh:
|
||||
return None
|
||||
|
||||
# The documentation says to use BlendDataMeshes.new_from_object(), but
|
||||
# that doesn't preserve shape keys.
|
||||
if isinstance(obj_data, Mesh):
|
||||
evaluated_mesh = obj_data.copy()
|
||||
else:
|
||||
logger.error(
|
||||
"Unexpected object type: %s name=%s",
|
||||
type(obj_data),
|
||||
obj_data.name,
|
||||
)
|
||||
evaluated_mesh = context.blend_data.meshes.new(name=obj_data.name)
|
||||
|
||||
bm = bmesh.new()
|
||||
bm.from_mesh(evaluated_temporary_mesh)
|
||||
bm.to_mesh(evaluated_mesh)
|
||||
bm.free()
|
||||
|
||||
evaluated_obj.to_mesh_clear()
|
||||
return evaluated_mesh
|
||||
|
||||
|
||||
def force_apply_modifiers(
|
||||
context: Context,
|
||||
obj: Object,
|
||||
*,
|
||||
preserve_shape_keys: bool,
|
||||
transform: Optional[Matrix] = None,
|
||||
) -> Optional[Mesh]:
|
||||
if obj.type not in MESH_CONVERTIBLE_OBJECT_TYPES:
|
||||
return None
|
||||
|
||||
evaluated_mesh = generate_evaluated_mesh(context, obj)
|
||||
if evaluated_mesh is None:
|
||||
return None
|
||||
|
||||
if transform:
|
||||
evaluated_mesh.transform(transform)
|
||||
|
||||
if not preserve_shape_keys:
|
||||
return evaluated_mesh
|
||||
|
||||
obj_data = obj.data
|
||||
if obj_data is None:
|
||||
return evaluated_mesh
|
||||
|
||||
if not isinstance(obj_data, Mesh):
|
||||
return evaluated_mesh
|
||||
|
||||
shape_keys = obj_data.shape_keys
|
||||
if not shape_keys:
|
||||
return evaluated_mesh
|
||||
|
||||
evaluated_mesh_shape_keys = evaluated_mesh.shape_keys
|
||||
if not evaluated_mesh_shape_keys:
|
||||
return evaluated_mesh
|
||||
|
||||
# If the mesh has shape keys, reproduce them as much as possible
|
||||
for shape_key in shape_keys.key_blocks:
|
||||
evaluated_mesh_shape_key = evaluated_mesh_shape_keys.key_blocks.get(
|
||||
shape_key.name
|
||||
)
|
||||
if not evaluated_mesh_shape_key:
|
||||
continue
|
||||
|
||||
if shape_key.name == shape_keys.reference_key.name:
|
||||
continue
|
||||
|
||||
shape_key.value = 1.0
|
||||
context.view_layer.update()
|
||||
|
||||
depsgraph = context.evaluated_depsgraph_get()
|
||||
baked_shape_key_obj = obj.evaluated_get(depsgraph)
|
||||
baked_shape_key_mesh = baked_shape_key_obj.to_mesh(
|
||||
preserve_all_data_layers=True, depsgraph=depsgraph
|
||||
)
|
||||
if baked_shape_key_mesh:
|
||||
if transform:
|
||||
baked_shape_key_mesh.transform(transform)
|
||||
|
||||
evaluated_mesh_shape_key_data = evaluated_mesh_shape_key.data
|
||||
baked_shape_key_mesh_vertices = baked_shape_key_mesh.vertices
|
||||
|
||||
# TODO: If the number of vertices is different, we should use advanced
|
||||
# graph matching algorithm.
|
||||
for i in range(
|
||||
min(
|
||||
len(evaluated_mesh_shape_key_data),
|
||||
len(baked_shape_key_mesh_vertices),
|
||||
)
|
||||
):
|
||||
evaluated_mesh_shape_key_data[i].co = Vector(
|
||||
baked_shape_key_mesh_vertices[i].co
|
||||
)
|
||||
|
||||
baked_shape_key_obj.to_mesh_clear()
|
||||
shape_key.value = 0.0
|
||||
|
||||
context.view_layer.update()
|
||||
return evaluated_mesh
|
||||
905
exporter/export_scene.py
Normal file
905
exporter/export_scene.py
Normal file
@@ -0,0 +1,905 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import traceback
|
||||
from collections.abc import Set as AbstractSet
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import bpy
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.props import BoolProperty, CollectionProperty, StringProperty
|
||||
from bpy.types import (
|
||||
Armature,
|
||||
Context,
|
||||
Event,
|
||||
Object,
|
||||
Operator,
|
||||
Panel,
|
||||
SpaceFileBrowser,
|
||||
UILayout,
|
||||
)
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
|
||||
from ..common import ops, safe_removal, version
|
||||
from ..common.error_dialog import show_error_dialog
|
||||
from ..common.logger import get_logger
|
||||
from ..common.preferences import (
|
||||
ExportPreferencesProtocol,
|
||||
copy_export_preferences,
|
||||
draw_export_preferences_layout,
|
||||
get_preferences,
|
||||
)
|
||||
from ..common.workspace import save_workspace
|
||||
from ..editor import migration, search, validation
|
||||
from ..editor.extension import get_armature_extension
|
||||
from ..editor.ops import VRM_OT_open_url_in_web_browser, layout_operator
|
||||
from ..editor.property_group import CollectionPropertyProtocol, StringPropertyGroup
|
||||
from ..editor.validation import VrmValidationError
|
||||
from ..editor.vrm0.panel import (
|
||||
draw_vrm0_humanoid_operators_layout,
|
||||
draw_vrm0_humanoid_optional_bones_layout,
|
||||
draw_vrm0_humanoid_required_bones_layout,
|
||||
)
|
||||
from ..editor.vrm0.property_group import Vrm0HumanoidPropertyGroup
|
||||
from ..editor.vrm1.ops import VRM_OT_assign_vrm1_humanoid_human_bones_automatically
|
||||
from ..editor.vrm1.panel import (
|
||||
draw_vrm1_humanoid_optional_bones_layout,
|
||||
draw_vrm1_humanoid_required_bones_layout,
|
||||
)
|
||||
from ..editor.vrm1.property_group import Vrm1HumanBonesPropertyGroup
|
||||
from .abstract_base_vrm_exporter import AbstractBaseVrmExporter
|
||||
from .uni_vrm_vrm_animation_exporter import UniVrmVrmAnimationExporter
|
||||
from .vrm0_exporter import Vrm0Exporter
|
||||
from .vrm1_exporter import Vrm1Exporter
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def export_vrm_update_addon_preferences(
|
||||
export_op: "EXPORT_SCENE_OT_vrm", context: Context
|
||||
) -> None:
|
||||
if export_op.use_addon_preferences:
|
||||
copy_export_preferences(source=export_op, destination=get_preferences(context))
|
||||
|
||||
validation.WM_OT_vrm_validator.detect_errors(
|
||||
context,
|
||||
export_op.errors,
|
||||
export_op.armature_object_name,
|
||||
)
|
||||
|
||||
|
||||
class EXPORT_SCENE_OT_vrm(Operator, ExportHelper):
|
||||
bl_idname = "export_scene.vrm"
|
||||
bl_label = "Save"
|
||||
bl_description = "export VRM"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
filename_ext = ".vrm"
|
||||
filter_glob: StringProperty( # type: ignore[valid-type]
|
||||
default="*.vrm",
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
use_addon_preferences: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export using add-on preferences",
|
||||
description="Export using add-on preferences instead of operator arguments",
|
||||
)
|
||||
export_invisibles: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Invisible Objects",
|
||||
update=export_vrm_update_addon_preferences,
|
||||
)
|
||||
export_only_selections: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Only Selections",
|
||||
update=export_vrm_update_addon_preferences,
|
||||
)
|
||||
enable_advanced_preferences: BoolProperty( # type: ignore[valid-type]
|
||||
name="Enable Advanced Options",
|
||||
update=export_vrm_update_addon_preferences,
|
||||
)
|
||||
export_all_influences: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export All Bone Influences",
|
||||
update=export_vrm_update_addon_preferences,
|
||||
)
|
||||
export_lights: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Lights",
|
||||
update=export_vrm_update_addon_preferences,
|
||||
)
|
||||
export_gltf_animations: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export glTF Animations",
|
||||
update=export_vrm_update_addon_preferences,
|
||||
)
|
||||
export_try_sparse_sk: BoolProperty( # type: ignore[valid-type]
|
||||
name="Use Sparse Accessors",
|
||||
update=export_vrm_update_addon_preferences,
|
||||
)
|
||||
|
||||
errors: CollectionProperty( # type: ignore[valid-type]
|
||||
type=validation.VrmValidationError,
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
ignore_warning: BoolProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
try:
|
||||
if not self.filepath:
|
||||
return {"CANCELLED"}
|
||||
|
||||
if self.use_addon_preferences:
|
||||
copy_export_preferences(
|
||||
source=get_preferences(context), destination=self
|
||||
)
|
||||
|
||||
return export_vrm(
|
||||
Path(self.filepath),
|
||||
self,
|
||||
context,
|
||||
armature_object_name=self.armature_object_name,
|
||||
)
|
||||
except Exception:
|
||||
show_error_dialog(
|
||||
pgettext("Failed to export VRM."),
|
||||
traceback.format_exc(),
|
||||
)
|
||||
raise
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
self.use_addon_preferences = True
|
||||
copy_export_preferences(source=get_preferences(context), destination=self)
|
||||
|
||||
if "gltf" not in dir(bpy.ops.export_scene):
|
||||
return ops.wm.vrm_gltf2_addon_disabled_warning(
|
||||
"INVOKE_DEFAULT",
|
||||
)
|
||||
|
||||
armatures, _ = collect_export_objects(
|
||||
context,
|
||||
self.armature_object_name,
|
||||
self,
|
||||
)
|
||||
if len(armatures) > 1:
|
||||
return ops.wm.vrm_export_armature_selection("INVOKE_DEFAULT")
|
||||
if len(armatures) == 1:
|
||||
armature = armatures[0]
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
pass
|
||||
elif get_armature_extension(armature_data).is_vrm0():
|
||||
Vrm0HumanoidPropertyGroup.fixup_human_bones(armature)
|
||||
Vrm0HumanoidPropertyGroup.update_all_node_candidates(
|
||||
context, armature_data.name
|
||||
)
|
||||
humanoid = get_armature_extension(armature_data).vrm0.humanoid
|
||||
if all(
|
||||
b.node.bone_name not in b.node_candidates
|
||||
for b in humanoid.human_bones
|
||||
):
|
||||
ops.vrm.assign_vrm0_humanoid_human_bones_automatically(
|
||||
armature_object_name=armature.name
|
||||
)
|
||||
if not humanoid.all_required_bones_are_assigned():
|
||||
return ops.wm.vrm_export_human_bones_assignment(
|
||||
"INVOKE_DEFAULT",
|
||||
armature_object_name=self.armature_object_name,
|
||||
)
|
||||
elif get_armature_extension(armature_data).is_vrm1():
|
||||
Vrm1HumanBonesPropertyGroup.fixup_human_bones(armature)
|
||||
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
|
||||
context, armature_data.name
|
||||
)
|
||||
human_bones = get_armature_extension(
|
||||
armature_data
|
||||
).vrm1.humanoid.human_bones
|
||||
if all(
|
||||
human_bone.node.bone_name not in human_bone.node_candidates
|
||||
for human_bone in (
|
||||
human_bones.human_bone_name_to_human_bone().values()
|
||||
)
|
||||
):
|
||||
ops.vrm.assign_vrm1_humanoid_human_bones_automatically(
|
||||
armature_object_name=armature.name
|
||||
)
|
||||
if (
|
||||
not human_bones.all_required_bones_are_assigned()
|
||||
and not human_bones.allow_non_humanoid_rig
|
||||
):
|
||||
return ops.wm.vrm_export_human_bones_assignment(
|
||||
"INVOKE_DEFAULT",
|
||||
armature_object_name=self.armature_object_name,
|
||||
)
|
||||
|
||||
if ops.vrm.model_validate(
|
||||
"INVOKE_DEFAULT",
|
||||
show_successful_message=False,
|
||||
armature_object_name=self.armature_object_name,
|
||||
) != {"FINISHED"}:
|
||||
return {"CANCELLED"}
|
||||
|
||||
validation.WM_OT_vrm_validator.detect_errors(
|
||||
context,
|
||||
self.errors,
|
||||
self.armature_object_name,
|
||||
)
|
||||
if not self.ignore_warning and any(
|
||||
error.severity <= 1 for error in self.errors
|
||||
):
|
||||
return ops.wm.vrm_export_confirmation(
|
||||
"INVOKE_DEFAULT", armature_object_name=self.armature_object_name
|
||||
)
|
||||
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
pass # Is needed to get panels available
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
filter_glob: str # type: ignore[no-redef]
|
||||
use_addon_preferences: bool # type: ignore[no-redef]
|
||||
export_invisibles: bool # type: ignore[no-redef]
|
||||
export_only_selections: bool # type: ignore[no-redef]
|
||||
enable_advanced_preferences: bool # type: ignore[no-redef]
|
||||
export_all_influences: bool # type: ignore[no-redef]
|
||||
export_lights: bool # type: ignore[no-redef]
|
||||
export_gltf_animations: bool # type: ignore[no-redef]
|
||||
export_try_sparse_sk: bool # type: ignore[no-redef]
|
||||
errors: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
VrmValidationError
|
||||
]
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
ignore_warning: bool # type: ignore[no-redef]
|
||||
|
||||
|
||||
def export_vrm(
|
||||
filepath: Path,
|
||||
export_preferences: ExportPreferencesProtocol,
|
||||
context: Context,
|
||||
*,
|
||||
armature_object_name: str,
|
||||
) -> set[str]:
|
||||
if ops.vrm.model_validate(
|
||||
"INVOKE_DEFAULT",
|
||||
show_successful_message=False,
|
||||
armature_object_name=armature_object_name,
|
||||
) != {"FINISHED"}:
|
||||
return {"CANCELLED"}
|
||||
|
||||
armature_objects, export_objects = collect_export_objects(
|
||||
context,
|
||||
armature_object_name,
|
||||
export_preferences,
|
||||
)
|
||||
|
||||
armature_object: Optional[Object] = next(iter(armature_objects), None)
|
||||
is_vrm1 = False
|
||||
|
||||
with save_workspace(
|
||||
context,
|
||||
# Allow restoring after changing active object
|
||||
armature_object,
|
||||
):
|
||||
if armature_object:
|
||||
armature_object_is_temporary = False
|
||||
armature_data = armature_object.data
|
||||
if isinstance(armature_data, Armature):
|
||||
is_vrm1 = get_armature_extension(armature_data).is_vrm1()
|
||||
else:
|
||||
armature_object_is_temporary = True
|
||||
ops.icyp.make_basic_armature("EXEC_DEFAULT")
|
||||
armature_object = context.view_layer.objects.active
|
||||
if not armature_object or armature_object.type != "ARMATURE":
|
||||
message = "Failed to generate temporary armature"
|
||||
raise RuntimeError(message)
|
||||
|
||||
migration.migrate(context, armature_object.name)
|
||||
|
||||
if is_vrm1:
|
||||
vrm_exporter: AbstractBaseVrmExporter = Vrm1Exporter(
|
||||
context,
|
||||
export_objects,
|
||||
armature_object,
|
||||
export_preferences,
|
||||
)
|
||||
else:
|
||||
vrm_exporter = Vrm0Exporter(
|
||||
context,
|
||||
export_objects,
|
||||
armature_object,
|
||||
)
|
||||
|
||||
vrm_bytes = vrm_exporter.export_vrm()
|
||||
if vrm_bytes is None:
|
||||
return {"CANCELLED"}
|
||||
|
||||
Path(filepath).write_bytes(vrm_bytes)
|
||||
|
||||
if armature_object_is_temporary and not safe_removal.remove_object(
|
||||
context, armature_object
|
||||
):
|
||||
logger.warning("Failed to remove temporary armature")
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
class VRM_PT_export_file_browser_tool_props(Panel):
|
||||
bl_idname = "VRM_IMPORTER_PT_export_error_messages"
|
||||
bl_space_type = "FILE_BROWSER"
|
||||
bl_region_type = "TOOL_PROPS"
|
||||
bl_parent_id = "FILE_PT_operator"
|
||||
bl_label = ""
|
||||
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
space_data = context.space_data
|
||||
if not isinstance(space_data, SpaceFileBrowser):
|
||||
return False
|
||||
return space_data.active_operator.bl_idname == "EXPORT_SCENE_OT_vrm"
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
space_data = context.space_data
|
||||
if not isinstance(space_data, SpaceFileBrowser):
|
||||
return
|
||||
|
||||
operator = space_data.active_operator
|
||||
if not isinstance(operator, EXPORT_SCENE_OT_vrm):
|
||||
return
|
||||
|
||||
layout = self.layout
|
||||
|
||||
warning_message = version.panel_warning_message()
|
||||
if warning_message:
|
||||
box = layout.box()
|
||||
warning_column = box.column(align=True)
|
||||
for index, warning_line in enumerate(warning_message.splitlines()):
|
||||
warning_column.label(
|
||||
text=warning_line,
|
||||
translate=False,
|
||||
icon="NONE" if index else "ERROR",
|
||||
)
|
||||
|
||||
show_vrm1_options = False
|
||||
armature_objects, _ = collect_export_objects(
|
||||
context, operator.armature_object_name, operator
|
||||
)
|
||||
if (
|
||||
armature_objects
|
||||
and (armature_object := armature_objects[0])
|
||||
and (armature_data := armature_object.data)
|
||||
and isinstance(armature_data, Armature)
|
||||
):
|
||||
show_vrm1_options = get_armature_extension(armature_data).is_vrm1()
|
||||
|
||||
draw_export_preferences_layout(
|
||||
operator,
|
||||
layout,
|
||||
show_vrm1_options=show_vrm1_options,
|
||||
)
|
||||
|
||||
if operator.errors:
|
||||
validation.WM_OT_vrm_validator.draw_errors(
|
||||
layout.box(),
|
||||
operator.errors,
|
||||
show_successful_message=False,
|
||||
)
|
||||
|
||||
|
||||
class VRM_PT_export_vrma_help(Panel):
|
||||
bl_idname = "VRM_PT_export_vrma_help"
|
||||
bl_space_type = "FILE_BROWSER"
|
||||
bl_region_type = "TOOL_PROPS"
|
||||
bl_parent_id = "FILE_PT_operator"
|
||||
bl_label = ""
|
||||
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
|
||||
|
||||
@classmethod
|
||||
def poll(cls, context: Context) -> bool:
|
||||
space_data = context.space_data
|
||||
if not isinstance(space_data, SpaceFileBrowser):
|
||||
return False
|
||||
return space_data.active_operator.bl_idname == "EXPORT_SCENE_OT_vrma"
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
draw_help_message(self.layout)
|
||||
|
||||
|
||||
def menu_export(menu_op: Operator, _context: Context) -> None:
|
||||
vrm_export_op = layout_operator(
|
||||
menu_op.layout, EXPORT_SCENE_OT_vrm, text="VRM (.vrm)"
|
||||
)
|
||||
vrm_export_op.use_addon_preferences = True
|
||||
vrm_export_op.armature_object_name = ""
|
||||
vrm_export_op.ignore_warning = False
|
||||
|
||||
vrma_export_op = layout_operator(
|
||||
menu_op.layout, EXPORT_SCENE_OT_vrma, text="VRM Animation (.vrma)"
|
||||
)
|
||||
vrma_export_op.armature_object_name = ""
|
||||
|
||||
|
||||
class EXPORT_SCENE_OT_vrma(Operator, ExportHelper):
|
||||
bl_idname = "export_scene.vrma"
|
||||
bl_label = "Save"
|
||||
bl_description = "Export VRM Animation"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
filename_ext = ".vrma"
|
||||
filter_glob: StringProperty( # type: ignore[valid-type]
|
||||
default="*.vrma",
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
try:
|
||||
if WM_OT_vrma_export_prerequisite.detect_errors(
|
||||
context, self.armature_object_name
|
||||
):
|
||||
return {"CANCELLED"}
|
||||
if not self.filepath:
|
||||
return {"CANCELLED"}
|
||||
if not self.armature_object_name:
|
||||
armature = search.current_armature(context)
|
||||
else:
|
||||
armature = context.blend_data.objects.get(self.armature_object_name)
|
||||
if not armature:
|
||||
return {"CANCELLED"}
|
||||
return UniVrmVrmAnimationExporter.execute(
|
||||
context, Path(self.filepath), armature
|
||||
)
|
||||
except Exception:
|
||||
show_error_dialog(
|
||||
pgettext("Failed to export VRM Animation."),
|
||||
traceback.format_exc(),
|
||||
)
|
||||
raise
|
||||
|
||||
def invoke(self, context: Context, event: Event) -> set[str]:
|
||||
if WM_OT_vrma_export_prerequisite.detect_errors(
|
||||
context, self.armature_object_name
|
||||
):
|
||||
return ops.wm.vrma_export_prerequisite(
|
||||
"INVOKE_DEFAULT",
|
||||
armature_object_name=self.armature_object_name,
|
||||
)
|
||||
return ExportHelper.invoke(self, context, event)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
pass # Is needed to get panels available
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
filter_glob: str # type: ignore[no-redef]
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class WM_OT_vrm_export_human_bones_assignment(Operator):
|
||||
bl_label = "Required VRM Human Bones Assignment"
|
||||
bl_idname = "wm.vrm_export_human_bones_assignment"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
preferences = get_preferences(context)
|
||||
armatures, _ = collect_export_objects(
|
||||
context,
|
||||
self.armature_object_name,
|
||||
preferences,
|
||||
)
|
||||
if len(armatures) != 1:
|
||||
return {"CANCELLED"}
|
||||
armature = armatures[0]
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return {"CANCELLED"}
|
||||
if get_armature_extension(armature_data).is_vrm0():
|
||||
Vrm0HumanoidPropertyGroup.fixup_human_bones(armature)
|
||||
Vrm0HumanoidPropertyGroup.update_all_node_candidates(
|
||||
context, armature_data.name
|
||||
)
|
||||
humanoid = get_armature_extension(armature_data).vrm0.humanoid
|
||||
if not humanoid.all_required_bones_are_assigned():
|
||||
return {"CANCELLED"}
|
||||
elif get_armature_extension(armature_data).is_vrm1():
|
||||
Vrm1HumanBonesPropertyGroup.fixup_human_bones(armature)
|
||||
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
|
||||
context, armature_data.name
|
||||
)
|
||||
human_bones = get_armature_extension(
|
||||
armature_data
|
||||
).vrm1.humanoid.human_bones
|
||||
if (
|
||||
not human_bones.all_required_bones_are_assigned()
|
||||
and not human_bones.allow_non_humanoid_rig
|
||||
):
|
||||
return {"CANCELLED"}
|
||||
else:
|
||||
return {"CANCELLED"}
|
||||
return ops.export_scene.vrm(
|
||||
"INVOKE_DEFAULT", armature_object_name=self.armature_object_name
|
||||
)
|
||||
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
preferences = get_preferences(context)
|
||||
armatures, _ = collect_export_objects(
|
||||
context, self.armature_object_name, preferences
|
||||
)
|
||||
if not armatures:
|
||||
return
|
||||
armature = armatures[0]
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
|
||||
if get_armature_extension(armature_data).is_vrm0():
|
||||
WM_OT_vrm_export_human_bones_assignment.draw_vrm0(self.layout, armature)
|
||||
elif get_armature_extension(armature_data).is_vrm1():
|
||||
WM_OT_vrm_export_human_bones_assignment.draw_vrm1(self.layout, armature)
|
||||
|
||||
@staticmethod
|
||||
def draw_vrm0(layout: UILayout, armature: Object) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
|
||||
humanoid = get_armature_extension(armature_data).vrm0.humanoid
|
||||
if humanoid.all_required_bones_are_assigned():
|
||||
alert_box = layout.box()
|
||||
alert_box.label(
|
||||
text="All Required VRM Human Bones have been assigned.",
|
||||
icon="CHECKMARK",
|
||||
)
|
||||
else:
|
||||
alert_box = layout.box()
|
||||
alert_box.alert = True
|
||||
alert_box.label(
|
||||
text="There are unassigned Required VRM Human Bones."
|
||||
+ " Please assign all.",
|
||||
icon="ERROR",
|
||||
)
|
||||
draw_vrm0_humanoid_operators_layout(armature, layout)
|
||||
row = layout.split(factor=0.5)
|
||||
draw_vrm0_humanoid_required_bones_layout(armature, row.column())
|
||||
draw_vrm0_humanoid_optional_bones_layout(armature, row.column())
|
||||
|
||||
@staticmethod
|
||||
def draw_vrm1(layout: UILayout, armature: Object) -> None:
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
return
|
||||
|
||||
human_bones = get_armature_extension(armature_data).vrm1.humanoid.human_bones
|
||||
if human_bones.all_required_bones_are_assigned():
|
||||
alert_box = layout.box()
|
||||
alert_box.label(
|
||||
text="All Required VRM Human Bones have been assigned.",
|
||||
icon="CHECKMARK",
|
||||
)
|
||||
elif human_bones.allow_non_humanoid_rig:
|
||||
alert_box = layout.box()
|
||||
alert_box.label(
|
||||
text="This armature will be exported but not as a humanoid."
|
||||
+ " It cannot have animations applied"
|
||||
+ " for humanoid avatars.",
|
||||
icon="CHECKMARK",
|
||||
)
|
||||
else:
|
||||
alert_box = layout.box()
|
||||
alert_box.alert = True
|
||||
alert_column = alert_box.column(align=True)
|
||||
for error_message in human_bones.error_messages():
|
||||
alert_column.label(text=error_message, translate=False, icon="ERROR")
|
||||
|
||||
layout_operator(
|
||||
layout,
|
||||
VRM_OT_assign_vrm1_humanoid_human_bones_automatically,
|
||||
icon="ARMATURE_DATA",
|
||||
).armature_object_name = armature.name
|
||||
|
||||
row = layout.split(factor=0.5)
|
||||
draw_vrm1_humanoid_required_bones_layout(armature, row.column())
|
||||
draw_vrm1_humanoid_optional_bones_layout(armature, row.column())
|
||||
|
||||
non_humanoid_export_column = layout.column()
|
||||
non_humanoid_export_column.prop(human_bones, "allow_non_humanoid_rig")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
|
||||
|
||||
class WM_OT_vrm_export_confirmation(Operator):
|
||||
bl_label = "VRM Export Confirmation"
|
||||
bl_idname = "wm.vrm_export_confirmation"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
errors: CollectionProperty(type=validation.VrmValidationError) # type: ignore[valid-type]
|
||||
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
export_anyway: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Anyway",
|
||||
)
|
||||
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
if not self.export_anyway:
|
||||
return {"CANCELLED"}
|
||||
ops.export_scene.vrm(
|
||||
"INVOKE_DEFAULT",
|
||||
ignore_warning=True,
|
||||
armature_object_name=self.armature_object_name,
|
||||
)
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
validation.WM_OT_vrm_validator.detect_errors(
|
||||
context,
|
||||
self.errors,
|
||||
self.armature_object_name,
|
||||
)
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
layout = self.layout
|
||||
layout.label(
|
||||
text="There is a high-impact warning. VRM may not export as intended.",
|
||||
icon="ERROR",
|
||||
)
|
||||
|
||||
column = layout.column()
|
||||
for error in self.errors:
|
||||
if error.severity != 1:
|
||||
continue
|
||||
column.prop(
|
||||
error,
|
||||
"message",
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
layout.prop(self, "export_anyway")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
errors: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
VrmValidationError
|
||||
]
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
export_anyway: bool # type: ignore[no-redef]
|
||||
|
||||
|
||||
class WM_OT_vrm_export_armature_selection(Operator):
|
||||
bl_label = "VRM Export Armature Selection"
|
||||
bl_idname = "wm.vrm_export_armature_selection"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
armature_object_name_candidates: CollectionProperty( # type: ignore[valid-type]
|
||||
type=StringPropertyGroup,
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
def execute(self, context: Context) -> set[str]:
|
||||
if not self.armature_object_name:
|
||||
return {"CANCELLED"}
|
||||
armature_object = context.blend_data.objects.get(self.armature_object_name)
|
||||
if not armature_object or armature_object.type != "ARMATURE":
|
||||
return {"CANCELLED"}
|
||||
ops.export_scene.vrm(
|
||||
"INVOKE_DEFAULT", armature_object_name=self.armature_object_name
|
||||
)
|
||||
|
||||
return {"FINISHED"}
|
||||
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
if not self.armature_object_name:
|
||||
armature_object = search.current_armature(context)
|
||||
if armature_object:
|
||||
self.armature_object_name = armature_object.name
|
||||
self.armature_object_name_candidates.clear()
|
||||
for obj in context.blend_data.objects:
|
||||
if obj.type != "ARMATURE":
|
||||
continue
|
||||
candidate = self.armature_object_name_candidates.add()
|
||||
candidate.value = obj.name
|
||||
|
||||
return context.window_manager.invoke_props_dialog(self, width=600)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
layout = self.layout
|
||||
layout.label(
|
||||
text="Multiple armatures were found; please select one to export as VRM.",
|
||||
icon="ERROR",
|
||||
)
|
||||
|
||||
layout.prop_search(
|
||||
self,
|
||||
"armature_object_name",
|
||||
self,
|
||||
"armature_object_name_candidates",
|
||||
icon="OUTLINER_OB_ARMATURE",
|
||||
text="",
|
||||
translate=False,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
armature_object_name_candidates: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
StringPropertyGroup
|
||||
]
|
||||
|
||||
|
||||
class WM_OT_vrma_export_prerequisite(Operator):
|
||||
bl_label = "VRM Animation Export Prerequisite"
|
||||
bl_idname = "wm.vrma_export_prerequisite"
|
||||
bl_options: AbstractSet[str] = {"REGISTER"}
|
||||
|
||||
armature_object_name: StringProperty( # type: ignore[valid-type]
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
armature_object_name_candidates: CollectionProperty( # type: ignore[valid-type]
|
||||
type=StringPropertyGroup,
|
||||
options={"HIDDEN"},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def detect_errors(context: Context, armature_object_name: str) -> list[str]:
|
||||
error_messages: list[str] = []
|
||||
|
||||
if not armature_object_name:
|
||||
armature = search.current_armature(context)
|
||||
else:
|
||||
armature = context.blend_data.objects.get(armature_object_name)
|
||||
|
||||
if not armature:
|
||||
error_messages.append(pgettext("Armature not found"))
|
||||
return error_messages
|
||||
|
||||
armature_data = armature.data
|
||||
if not isinstance(armature_data, Armature):
|
||||
error_messages.append(pgettext("Armature not found"))
|
||||
return error_messages
|
||||
|
||||
ext = get_armature_extension(armature_data)
|
||||
if get_armature_extension(armature_data).is_vrm1():
|
||||
humanoid = ext.vrm1.humanoid
|
||||
if not humanoid.human_bones.all_required_bones_are_assigned():
|
||||
error_messages.append(pgettext("Please assign required human bones"))
|
||||
else:
|
||||
error_messages.append(pgettext("Please set the version of VRM to 1.0"))
|
||||
|
||||
return error_messages
|
||||
|
||||
def execute(self, _context: Context) -> set[str]:
|
||||
return ops.export_scene.vrma(
|
||||
"INVOKE_DEFAULT", armature_object_name=self.armature_object_name
|
||||
)
|
||||
|
||||
def invoke(self, context: Context, _event: Event) -> set[str]:
|
||||
if not self.armature_object_name:
|
||||
armature_object = search.current_armature(context)
|
||||
if armature_object:
|
||||
self.armature_object_name = armature_object.name
|
||||
self.armature_object_name_candidates.clear()
|
||||
for obj in context.blend_data.objects:
|
||||
if obj.type != "ARMATURE":
|
||||
continue
|
||||
candidate = self.armature_object_name_candidates.add()
|
||||
candidate.value = obj.name
|
||||
return context.window_manager.invoke_props_dialog(self, width=800)
|
||||
|
||||
def draw(self, context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
layout.label(
|
||||
text="VRM Animation export requires a VRM 1.0 armature",
|
||||
icon="INFO",
|
||||
)
|
||||
|
||||
error_messages = WM_OT_vrma_export_prerequisite.detect_errors(
|
||||
context, self.armature_object_name
|
||||
)
|
||||
|
||||
layout.prop_search(
|
||||
self,
|
||||
"armature_object_name",
|
||||
self,
|
||||
"armature_object_name_candidates",
|
||||
icon="OUTLINER_OB_ARMATURE",
|
||||
text="Armature to be exported",
|
||||
)
|
||||
|
||||
if error_messages:
|
||||
error_column = layout.box().column(align=True)
|
||||
for error_message in error_messages:
|
||||
error_column.label(text=error_message, icon="ERROR", translate=False)
|
||||
|
||||
if not self.armature_object_name:
|
||||
armature = search.current_armature(context)
|
||||
else:
|
||||
armature = context.blend_data.objects.get(self.armature_object_name)
|
||||
if armature:
|
||||
armature_data = armature.data
|
||||
if isinstance(armature_data, Armature):
|
||||
ext = get_armature_extension(armature_data)
|
||||
if get_armature_extension(armature_data).is_vrm1():
|
||||
humanoid = ext.vrm1.humanoid
|
||||
if not humanoid.human_bones.all_required_bones_are_assigned():
|
||||
WM_OT_vrm_export_human_bones_assignment.draw_vrm1(
|
||||
self.layout, armature
|
||||
)
|
||||
|
||||
draw_help_message(layout)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
armature_object_name: str # type: ignore[no-redef]
|
||||
armature_object_name_candidates: CollectionPropertyProtocol[ # type: ignore[no-redef]
|
||||
StringPropertyGroup
|
||||
]
|
||||
|
||||
|
||||
def draw_help_message(layout: UILayout) -> None:
|
||||
help_message = pgettext(
|
||||
"Animations to be exported\n"
|
||||
+ "- Humanoid bone rotations\n"
|
||||
+ "- Humanoid hips bone translations\n"
|
||||
+ "- Expression preview value\n"
|
||||
+ "- Look At preview target translation\n"
|
||||
)
|
||||
help_box = layout.box()
|
||||
help_column = help_box.column(align=True)
|
||||
for index, help_line in enumerate(help_message.splitlines()):
|
||||
help_column.label(
|
||||
text=help_line,
|
||||
translate=False,
|
||||
icon="NONE" if index else "INFO",
|
||||
)
|
||||
|
||||
open_op = layout_operator(
|
||||
help_column,
|
||||
VRM_OT_open_url_in_web_browser,
|
||||
icon="URL",
|
||||
text="Open help in a Web Browser",
|
||||
)
|
||||
open_op.url = pgettext("https://vrm-addon-for-blender.info/en-us/animation/")
|
||||
|
||||
|
||||
def collect_export_objects(
|
||||
context: Context,
|
||||
armature_object_name: str,
|
||||
export_preferences: ExportPreferencesProtocol,
|
||||
) -> tuple[list[Object], list[Object]]:
|
||||
export_objects = search.export_objects(
|
||||
context,
|
||||
armature_object_name,
|
||||
export_invisibles=export_preferences.export_invisibles,
|
||||
export_only_selections=export_preferences.export_only_selections,
|
||||
export_lights=export_preferences.enable_advanced_preferences
|
||||
and export_preferences.export_lights,
|
||||
)
|
||||
armature_objects = [obj for obj in export_objects if obj.type == "ARMATURE"]
|
||||
return armature_objects, export_objects
|
||||
33
exporter/gltf2_export_user_extension.py
Normal file
33
exporter/gltf2_export_user_extension.py
Normal file
@@ -0,0 +1,33 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
import bpy
|
||||
|
||||
from .abstract_base_vrm_exporter import AbstractBaseVrmExporter
|
||||
|
||||
|
||||
class glTF2ExportUserExtension:
|
||||
def __init__(self) -> None:
|
||||
context = bpy.context
|
||||
|
||||
self.object_name_to_modifier_names = (
|
||||
AbstractBaseVrmExporter.enter_hide_mtoon1_outline_geometry_nodes(context)
|
||||
)
|
||||
|
||||
# 3 arguments in Blender 2.93.0
|
||||
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/709630548cdc184af6ea50b2ff3ddc5450bc0af3/addons/io_scene_gltf2/blender/exp/gltf2_blender_export.py#L68
|
||||
# 5 arguments in Blender 3.3.0
|
||||
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/92061fa5b8058c8dff964c9bf177e079926b9671/addons/io_scene_gltf2/blender/exp/gltf2_blender_export.py#L85
|
||||
def gather_gltf_hook(
|
||||
self,
|
||||
# The number of arguments and specifications vary widely from version to version
|
||||
# of the glTF 2.0 add-on.
|
||||
_arg1: object,
|
||||
_arg2: object,
|
||||
_arg3: object = None,
|
||||
_arg4: object = None,
|
||||
) -> None:
|
||||
context = bpy.context
|
||||
|
||||
AbstractBaseVrmExporter.exit_hide_mtoon1_outline_geometry_nodes(
|
||||
context, self.object_name_to_modifier_names
|
||||
)
|
||||
self.object_name_to_modifier_names.clear()
|
||||
1034
exporter/uni_vrm_vrm_animation_exporter.py
Normal file
1034
exporter/uni_vrm_vrm_animation_exporter.py
Normal file
File diff suppressed because it is too large
Load Diff
3911
exporter/vrm0_exporter.py
Normal file
3911
exporter/vrm0_exporter.py
Normal file
File diff suppressed because it is too large
Load Diff
3313
exporter/vrm1_exporter.py
Normal file
3313
exporter/vrm1_exporter.py
Normal file
File diff suppressed because it is too large
Load Diff
1075
exporter/vrm_animation_exporter.py
Normal file
1075
exporter/vrm_animation_exporter.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user