feat: Add VRM Blender addon with complete import/export functionality

- Add core VRM addon infrastructure with manifest and registration
- Add common utilities module with file system, logging, and conversion helpers
- Add human bone mapper with support for multiple rigging standards (Mixamo, MMD, Unreal, Rigify, etc.)
- Add VRM 0.x and 1.x format support with property groups and handlers
- Add editor UI panels for VRM metadata, spring bones, and MToon materials
- Add exporter with glTF2 extension support for VRM format serialization
- Add importer with scene reconstruction and armature generation
- Add MToon shader support with auto-setup and material migration
- Add spring bone physics simulation with constraint handling
- Add node constraint editor for advanced rigging control
- Add comprehensive validation and error handling with user dialogs
- Add scene watcher for real-time property synchronization
- Add workspace management and preference system
- Include Python cache files and Blender manifest configuration
- This is the initial commit establishing the complete VRM addon ecosystem for Blender
This commit is contained in:
2026-01-01 14:21:56 +08:00
commit 091ad6a49a
243 changed files with 60636 additions and 0 deletions

287
editor/panel.py Normal file
View File

@@ -0,0 +1,287 @@
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
from collections.abc import Set as AbstractSet
from typing import Callable, Optional, Protocol, TypeVar, Union, runtime_checkable
from bpy.app.translations import pgettext
from bpy.types import (
AnyType,
Armature,
Context,
Menu,
Operator,
Panel,
UILayout,
UIList,
)
from ..common import version
from ..common.preferences import get_preferences
from . import make_armature, search, validation
from .extension import get_armature_extension
from .ops import layout_operator
__AddOperator = TypeVar("__AddOperator", bound=Operator)
__RemoveOperator = TypeVar("__RemoveOperator", bound=Operator)
__MoveUpOperator = TypeVar("__MoveUpOperator", bound=Operator)
__MoveDownOperator = TypeVar("__MoveDownOperator", bound=Operator)
@runtime_checkable
class TemplateListCollectionProtocol(Protocol):
def __len__(self) -> int: ...
def __getitem__(self, index: int) -> object: ...
def draw_template_list(
layout: UILayout,
template_list: type[UIList],
base_object: AnyType,
collection_attribue_name: str,
active_index_attribute_name: str,
add_operator_type: type[__AddOperator],
remove_operator_type: type[__RemoveOperator],
move_up_operator_type: type[__MoveUpOperator],
move_down_operator_type: type[__MoveDownOperator],
*,
can_remove: Callable[[int], bool] = lambda _: True,
can_move: Callable[[int], bool] = lambda _: True,
compact: bool = False,
menu: Optional[type[Menu]] = None,
) -> tuple[
list[Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator]],
list[Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator]],
int,
object,
tuple[
__AddOperator,
__RemoveOperator,
Optional[__MoveUpOperator],
Optional[__MoveDownOperator],
],
]:
collection = getattr(base_object, collection_attribue_name, None)
if not isinstance(collection, TemplateListCollectionProtocol):
message = (
f"{collection}.{collection_attribue_name}"
+ " is not a Template List Collection Protocol."
)
raise TypeError(message)
active_index = getattr(base_object, active_index_attribute_name, None)
if not isinstance(active_index, int):
message = f"{base_object}.{active_index_attribute_name} is not an int."
raise TypeError(message)
if 0 <= active_index < len(collection):
active_object = collection[active_index]
else:
active_object = None
length = len(collection)
list_row_len = 4 if length > int(compact) else 2
list_row = layout.row()
list_row.template_list(
template_list.bl_idname,
"",
base_object,
collection_attribue_name,
base_object,
active_index_attribute_name,
rows=list_row_len,
)
list_side_column = list_row.column(align=True)
add_operator = layout_operator(
list_side_column, add_operator_type, icon="ADD", text="", translate=False
)
if length >= 1 and 0 <= active_index < length and can_remove(active_index):
remove_operator_parent = list_side_column
else:
remove_operator_parent = list_side_column.column(align=True)
remove_operator_parent.enabled = False
remove_operator = layout_operator(
remove_operator_parent,
remove_operator_type,
icon="REMOVE",
text="",
translate=False,
)
if menu is not None:
list_side_column.separator()
list_side_column.menu(menu=menu.bl_idname, text="", icon="DOWNARROW_HLT")
move_operator_parent = list_side_column.column(align=True)
# Without separator, the button width changes mysteriously when list elements are 0
move_operator_parent.separator()
if length <= 1 or not (0 <= active_index < length) or not can_move(active_index):
move_operator_parent.enabled = False
collection_ops: list[
Union[__AddOperator, __RemoveOperator, __MoveUpOperator, __MoveDownOperator]
] = [add_operator]
collection_item_ops: list[
Union[__RemoveOperator, __MoveUpOperator, __MoveDownOperator]
] = [remove_operator]
if length > int(compact):
move_up_operator = layout_operator(
move_operator_parent,
move_up_operator_type,
icon="TRIA_UP",
text="",
translate=False,
)
collection_item_ops.append(move_up_operator)
move_down_operator = layout_operator(
move_operator_parent,
move_down_operator_type,
icon="TRIA_DOWN",
text="",
translate=False,
)
collection_item_ops.append(move_down_operator)
else:
move_up_operator = None
move_down_operator = None
collection_ops.extend(collection_item_ops)
return (
collection_ops,
collection_item_ops,
active_index,
active_object,
(add_operator, remove_operator, move_up_operator, move_down_operator),
)
class VRM_PT_vrm_armature_object_property(Panel):
bl_idname = "VRM_PT_vrm_armature_object_property"
bl_label = "VRM"
bl_space_type = "PROPERTIES"
bl_region_type = "WINDOW"
bl_context = "object"
@classmethod
def poll(cls, context: Context) -> bool:
active_object = context.active_object
if not active_object:
return False
armature_data = active_object.data
return isinstance(armature_data, Armature)
def draw(self, _context: Context) -> None:
warning_message = version.panel_warning_message()
if not warning_message:
return
box = self.layout.box()
warning_column = box.column()
for index, warning_line in enumerate(warning_message.splitlines()):
warning_column.label(
text=warning_line,
translate=False,
icon="NONE" if index else "ERROR",
)
def add_armature(add_armature_op: Operator, _context: Context) -> None:
layout_operator(
add_armature_op.layout,
make_armature.ICYP_OT_make_armature,
text="VRM Humanoid",
icon="OUTLINER_OB_ARMATURE",
).skip_heavy_armature_setup = True
class VRM_PT_current_selected_armature(Panel):
bl_idname = "VRM_PT_current_selected_armature"
bl_label = "Current selected armature"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "VRM"
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
@classmethod
def poll(cls, context: Context) -> bool:
return search.multiple_armatures_exist(context)
def draw(self, context: Context) -> None:
armature = search.current_armature(context)
if not armature:
return
layout = self.layout
layout.label(text=armature.name, icon="ARMATURE_DATA", translate=False)
class VRM_PT_controller(Panel):
bl_idname = "ICYP_PT_ui_controller"
bl_label = "Operator"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "VRM"
def draw_header(self, _context: Context) -> None:
self.layout.label(icon="TOOL_SETTINGS")
def draw(self, context: Context) -> None:
layout = self.layout
preferences = get_preferences(context)
# draw_main
layout_operator(
layout,
make_armature.ICYP_OT_make_armature,
text=pgettext("Create VRM Model"),
icon="OUTLINER_OB_ARMATURE",
).skip_heavy_armature_setup = True
vrm_validator_op = layout_operator(
layout,
validation.WM_OT_vrm_validator,
text=pgettext("Check VRM Model"),
icon="VIEWZOOM",
)
vrm_validator_op.show_successful_message = True
layout.prop(preferences, "export_invisibles")
layout.prop(preferences, "export_only_selections")
armature = search.current_armature(context)
if armature:
vrm_validator_op.armature_object_name = armature.name
armature_data = armature.data
if isinstance(armature_data, Armature):
layout.prop(
get_armature_extension(armature_data),
"spec_version",
text="",
translate=False,
)
class VRM_PT_controller_unsupported_blender_version_warning(Panel):
bl_idname = "VRM_PT_controller_unsupported_blender_version_warning"
bl_label = "Unsupported Blender Version Warning"
bl_space_type = "VIEW_3D"
bl_region_type = "UI"
bl_category = "VRM"
bl_options: AbstractSet[str] = {"HIDE_HEADER"}
@classmethod
def poll(cls, _context: Context) -> bool:
return bool(version.panel_warning_message())
def draw(self, _context: Context) -> None:
warning_message = version.panel_warning_message()
if warning_message is None:
return
box = self.layout.box()
warning_column = box.column()
for index, warning_line in enumerate(warning_message.splitlines()):
warning_column.label(
text=warning_line,
translate=False,
icon="NONE" if index else "ERROR",
)