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:
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
|
||||
Reference in New Issue
Block a user