- 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
906 lines
32 KiB
Python
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
|