Files
blender-vrm/exporter/export_scene.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

906 lines
32 KiB
Python

# 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