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:
361
common/preferences.py
Normal file
361
common/preferences.py
Normal file
@@ -0,0 +1,361 @@
|
||||
# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
|
||||
from collections.abc import Sequence
|
||||
from typing import TYPE_CHECKING, Protocol, TypedDict, Union
|
||||
|
||||
from bpy.app.translations import pgettext
|
||||
from bpy.props import BoolProperty, IntVectorProperty
|
||||
from bpy.types import AddonPreferences, Context, Operator, UILayout
|
||||
|
||||
from . import version
|
||||
from .logger import get_logger
|
||||
from .shader import (
|
||||
add_mtoon1_auto_setup_shader_node_group,
|
||||
remove_mtoon1_auto_setup_shader_node_group,
|
||||
)
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
addon_package_name = ".".join(__name__.split(".")[:-2])
|
||||
|
||||
|
||||
class ImportPreferencesProtocol(Protocol):
|
||||
extract_textures_into_folder: bool
|
||||
make_new_texture_folder: bool
|
||||
set_shading_type_to_material_on_import: bool
|
||||
set_view_transform_to_standard_on_import: bool
|
||||
set_armature_display_to_wire: bool
|
||||
set_armature_display_to_show_in_front: bool
|
||||
set_armature_bone_shape_to_default: bool
|
||||
enable_mtoon_outline_preview: bool
|
||||
|
||||
|
||||
class ImportPreferencesDict(TypedDict):
|
||||
extract_textures_into_folder: bool
|
||||
make_new_texture_folder: bool
|
||||
set_shading_type_to_material_on_import: bool
|
||||
set_view_transform_to_standard_on_import: bool
|
||||
set_armature_display_to_wire: bool
|
||||
set_armature_display_to_show_in_front: bool
|
||||
set_armature_bone_shape_to_default: bool
|
||||
enable_mtoon_outline_preview: bool
|
||||
|
||||
|
||||
def create_import_preferences_dict(
|
||||
source: ImportPreferencesProtocol,
|
||||
) -> ImportPreferencesDict:
|
||||
return {
|
||||
"extract_textures_into_folder": source.extract_textures_into_folder,
|
||||
"make_new_texture_folder": source.make_new_texture_folder,
|
||||
"set_shading_type_to_material_on_import": (
|
||||
source.set_shading_type_to_material_on_import
|
||||
),
|
||||
"set_view_transform_to_standard_on_import": (
|
||||
source.set_view_transform_to_standard_on_import
|
||||
),
|
||||
"set_armature_display_to_wire": (source.set_armature_display_to_wire),
|
||||
"set_armature_display_to_show_in_front": (
|
||||
source.set_armature_display_to_show_in_front
|
||||
),
|
||||
"set_armature_bone_shape_to_default": source.set_armature_bone_shape_to_default,
|
||||
"enable_mtoon_outline_preview": source.enable_mtoon_outline_preview,
|
||||
}
|
||||
|
||||
|
||||
def copy_import_preferences(
|
||||
*, source: ImportPreferencesProtocol, destination: ImportPreferencesProtocol
|
||||
) -> None:
|
||||
(
|
||||
destination.extract_textures_into_folder,
|
||||
destination.make_new_texture_folder,
|
||||
destination.set_shading_type_to_material_on_import,
|
||||
destination.set_view_transform_to_standard_on_import,
|
||||
destination.set_armature_display_to_wire,
|
||||
destination.set_armature_display_to_show_in_front,
|
||||
destination.set_armature_bone_shape_to_default,
|
||||
destination.enable_mtoon_outline_preview,
|
||||
) = (
|
||||
source.extract_textures_into_folder,
|
||||
source.make_new_texture_folder,
|
||||
source.set_shading_type_to_material_on_import,
|
||||
source.set_view_transform_to_standard_on_import,
|
||||
source.set_armature_display_to_wire,
|
||||
source.set_armature_display_to_show_in_front,
|
||||
source.set_armature_bone_shape_to_default,
|
||||
source.enable_mtoon_outline_preview,
|
||||
)
|
||||
|
||||
|
||||
def draw_import_preferences_layout(
|
||||
preferences: ImportPreferencesProtocol, layout: UILayout
|
||||
) -> None:
|
||||
if not isinstance(preferences, (AddonPreferences, Operator)):
|
||||
return
|
||||
|
||||
layout.prop(preferences, "extract_textures_into_folder")
|
||||
layout.prop(preferences, "make_new_texture_folder")
|
||||
layout.prop(preferences, "set_shading_type_to_material_on_import")
|
||||
layout.prop(preferences, "set_view_transform_to_standard_on_import")
|
||||
layout.prop(preferences, "set_armature_display_to_wire")
|
||||
layout.prop(preferences, "set_armature_display_to_show_in_front")
|
||||
layout.prop(preferences, "set_armature_bone_shape_to_default")
|
||||
layout.prop(preferences, "enable_mtoon_outline_preview")
|
||||
|
||||
|
||||
class ExportPreferencesProtocol(Protocol):
|
||||
export_invisibles: bool
|
||||
export_only_selections: bool
|
||||
enable_advanced_preferences: bool
|
||||
export_all_influences: bool
|
||||
export_lights: bool
|
||||
export_gltf_animations: bool
|
||||
export_try_sparse_sk: bool
|
||||
|
||||
|
||||
def copy_export_preferences(
|
||||
*, source: ExportPreferencesProtocol, destination: ExportPreferencesProtocol
|
||||
) -> None:
|
||||
(
|
||||
destination.export_invisibles,
|
||||
destination.export_only_selections,
|
||||
destination.enable_advanced_preferences,
|
||||
destination.export_all_influences,
|
||||
destination.export_lights,
|
||||
destination.export_gltf_animations,
|
||||
destination.export_try_sparse_sk,
|
||||
) = (
|
||||
source.export_invisibles,
|
||||
source.export_only_selections,
|
||||
source.enable_advanced_preferences,
|
||||
source.export_all_influences,
|
||||
source.export_lights,
|
||||
source.export_gltf_animations,
|
||||
source.export_try_sparse_sk,
|
||||
)
|
||||
|
||||
|
||||
def draw_advanced_options_description(
|
||||
preferences: Union[AddonPreferences, Operator],
|
||||
property_name: str,
|
||||
layout: UILayout,
|
||||
description: str,
|
||||
) -> None:
|
||||
column = layout.box().column(align=True)
|
||||
column.prop(preferences, property_name)
|
||||
description_column = column.box().column(align=True)
|
||||
for i, line in enumerate(description.splitlines()):
|
||||
icon = "ERROR" if i == 0 else "NONE"
|
||||
description_column.label(text=line, translate=False, icon=icon)
|
||||
|
||||
|
||||
def draw_export_preferences_layout(
|
||||
preferences: ExportPreferencesProtocol,
|
||||
layout: UILayout,
|
||||
*,
|
||||
show_vrm1_options: bool,
|
||||
) -> None:
|
||||
if not isinstance(preferences, (AddonPreferences, Operator)):
|
||||
return
|
||||
|
||||
layout.prop(preferences, "export_invisibles")
|
||||
layout.prop(preferences, "export_only_selections")
|
||||
|
||||
if not show_vrm1_options:
|
||||
return
|
||||
|
||||
layout.prop(preferences, "enable_advanced_preferences")
|
||||
if not preferences.enable_advanced_preferences:
|
||||
return
|
||||
|
||||
advanced_options_column = layout.box().column()
|
||||
|
||||
# UniVRM 0.115.0 doesn't support `export_try_sparse_sk`
|
||||
# https://github.com/saturday06/VRM-Addon-for-Blender/issues/381#issuecomment-1838365762
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_try_sparse_sk",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"The file size will be reduced,\n"
|
||||
+ "but it will no longer be readable by\n"
|
||||
+ "older apps with UniVRM 0.115.0 or\n"
|
||||
+ "earlier."
|
||||
),
|
||||
)
|
||||
|
||||
# The upstream says that Models may appear incorrectly in many viewers.
|
||||
# https://github.com/KhronosGroup/glTF-Blender-IO/blob/356b3dda976303d3ecce8b3bd1591245e576db38/addons/io_scene_gltf2/__init__.py#L760
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_all_influences",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"By default, 4 bone influences\n"
|
||||
+ "are exported for each vertex. Many\n"
|
||||
+ "apps truncate to 4. Increasing it\n"
|
||||
+ "may cause jagged meshes."
|
||||
),
|
||||
)
|
||||
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_lights",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"There is no consensus on how\n"
|
||||
+ "to handle lights in VRM, so it is\n"
|
||||
+ "impossible to predict what the\n"
|
||||
+ "outcome will be."
|
||||
),
|
||||
)
|
||||
|
||||
draw_advanced_options_description(
|
||||
preferences,
|
||||
"export_gltf_animations",
|
||||
advanced_options_column,
|
||||
pgettext(
|
||||
"UniVRM does not export\n"
|
||||
+ "glTF Animations, so it is disabled\n"
|
||||
+ "by default. Please consider using\n"
|
||||
+ "VRM Animation."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class VrmAddonPreferences(AddonPreferences):
|
||||
bl_idname = addon_package_name
|
||||
|
||||
INITIAL_ADDON_VERSION: tuple[int, int, int] = (0, 0, 0)
|
||||
|
||||
addon_version: IntVectorProperty( # type: ignore[valid-type]
|
||||
size=3,
|
||||
default=INITIAL_ADDON_VERSION,
|
||||
)
|
||||
|
||||
def update_add_mtoon_shader_node_group(self, context: Context) -> None:
|
||||
if self.add_mtoon_shader_node_group:
|
||||
add_mtoon1_auto_setup_shader_node_group(context)
|
||||
else:
|
||||
remove_mtoon1_auto_setup_shader_node_group(context)
|
||||
|
||||
add_mtoon_shader_node_group: BoolProperty( # type: ignore[valid-type]
|
||||
name="Add MToon shader node group",
|
||||
default=True,
|
||||
update=update_add_mtoon_shader_node_group,
|
||||
)
|
||||
|
||||
extract_textures_into_folder: BoolProperty( # type: ignore[valid-type]
|
||||
name="Extract texture images into the folder",
|
||||
default=False,
|
||||
)
|
||||
make_new_texture_folder: BoolProperty( # type: ignore[valid-type]
|
||||
name="Don't overwrite existing texture folder",
|
||||
default=True,
|
||||
)
|
||||
set_shading_type_to_material_on_import: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set shading type to "Material"',
|
||||
default=True,
|
||||
)
|
||||
set_view_transform_to_standard_on_import: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set view transform to "Standard"',
|
||||
default=True,
|
||||
)
|
||||
set_armature_display_to_wire: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set an imported armature display to "Wire"',
|
||||
default=True,
|
||||
)
|
||||
set_armature_display_to_show_in_front: BoolProperty( # type: ignore[valid-type]
|
||||
name='Set an imported armature display to show "In-Front"',
|
||||
default=True,
|
||||
)
|
||||
set_armature_bone_shape_to_default: BoolProperty( # type: ignore[valid-type]
|
||||
name="Set an imported bone shape to default",
|
||||
default=True,
|
||||
)
|
||||
|
||||
enable_mtoon_outline_preview: BoolProperty( # type: ignore[valid-type]
|
||||
name="Enable MToon Outline Preview",
|
||||
default=True,
|
||||
)
|
||||
|
||||
export_invisibles: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Invisible Objects",
|
||||
)
|
||||
export_only_selections: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Only Selections",
|
||||
)
|
||||
enable_advanced_preferences: BoolProperty( # type: ignore[valid-type]
|
||||
name="Enable Advanced Options",
|
||||
)
|
||||
export_all_influences: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export All Bone Influences",
|
||||
)
|
||||
export_lights: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export Lights",
|
||||
)
|
||||
export_gltf_animations: BoolProperty( # type: ignore[valid-type]
|
||||
name="Export glTF Animations",
|
||||
)
|
||||
export_try_sparse_sk: BoolProperty( # type: ignore[valid-type]
|
||||
name="Use Sparse Accessors",
|
||||
)
|
||||
|
||||
def draw(self, _context: Context) -> None:
|
||||
layout = self.layout
|
||||
|
||||
warning_message = version.preferences_warning_message()
|
||||
if warning_message:
|
||||
box = 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",
|
||||
)
|
||||
|
||||
layout.prop(self, "add_mtoon_shader_node_group")
|
||||
|
||||
import_box = layout.box()
|
||||
import_box.label(text="Import", icon="IMPORT")
|
||||
draw_import_preferences_layout(self, import_box)
|
||||
|
||||
export_box = layout.box()
|
||||
export_box.label(text="Export", icon="EXPORT")
|
||||
draw_export_preferences_layout(self, export_box, show_vrm1_options=True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# This code is auto generated.
|
||||
# To regenerate, run the `uv run tools/property_typing.py` command.
|
||||
addon_version: Sequence[int] # type: ignore[no-redef]
|
||||
add_mtoon_shader_node_group: bool # type: ignore[no-redef]
|
||||
extract_textures_into_folder: bool # type: ignore[no-redef]
|
||||
make_new_texture_folder: bool # type: ignore[no-redef]
|
||||
set_shading_type_to_material_on_import: bool # type: ignore[no-redef]
|
||||
set_view_transform_to_standard_on_import: bool # type: ignore[no-redef]
|
||||
set_armature_display_to_wire: bool # type: ignore[no-redef]
|
||||
set_armature_display_to_show_in_front: bool # type: ignore[no-redef]
|
||||
set_armature_bone_shape_to_default: bool # type: ignore[no-redef]
|
||||
enable_mtoon_outline_preview: 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]
|
||||
|
||||
|
||||
def get_preferences(context: Context) -> VrmAddonPreferences:
|
||||
addon = context.preferences.addons.get(addon_package_name)
|
||||
if not addon:
|
||||
message = f"No add-on preferences for {addon_package_name}"
|
||||
raise AssertionError(message)
|
||||
|
||||
preferences = addon.preferences
|
||||
if not isinstance(preferences, VrmAddonPreferences):
|
||||
raise TypeError(
|
||||
f"Add-on preferences for {addon_package_name} is not a VrmAddonPreferences"
|
||||
+ f" but {type(preferences)}"
|
||||
)
|
||||
|
||||
return preferences
|
||||
Reference in New Issue
Block a user