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

3314 lines
126 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import math
import tempfile
from collections.abc import Iterator, Mapping, Sequence
from contextlib import contextmanager
from copy import deepcopy
from os import environ
from pathlib import Path
from sys import float_info
from typing import Optional, Union
from uuid import uuid4
import bpy
from bpy.types import (
ID,
Armature,
ArmatureModifier,
Bone,
Constraint,
Context,
CopyRotationConstraint,
Curve,
DampedTrackConstraint,
Image,
Material,
Mesh,
Node,
Object,
PoseBone,
VectorFont,
)
from mathutils import Matrix, Quaternion, Vector
from ..common import convert, safe_removal, shader
from ..common.char import INTERNAL_NAME_PREFIX
from ..common.convert import Json
from ..common.deep import make_json
from ..common.gltf import pack_glb, parse_glb
from ..common.logger import get_logger
from ..common.preferences import ExportPreferencesProtocol
from ..common.rotation import (
ROTATION_MODE_EULER,
get_rotation_as_quaternion,
set_rotation_without_mode_change,
)
from ..common.version import get_addon_version
from ..common.vrm1.human_bone import HumanBoneName
from ..common.workspace import save_workspace
from ..editor import search
from ..editor.extension import get_armature_extension, get_material_extension
from ..editor.mtoon1.property_group import (
Mtoon1SamplerPropertyGroup,
Mtoon1TextureInfoPropertyGroup,
link_or_unlink_gltf_material_nodes,
)
from ..editor.spring_bone1.handler import sort_spring_bone_joints
from ..editor.spring_bone1.property_group import (
SpringBone1JointPropertyGroup,
SpringBone1SpringBonePropertyGroup,
SpringBone1SpringPropertyGroup,
)
from ..editor.t_pose import setup_humanoid_t_pose
from ..editor.vrm1.property_group import (
Vrm1ExpressionPropertyGroup,
Vrm1ExpressionsPropertyGroup,
Vrm1FirstPersonPropertyGroup,
Vrm1HumanBonesPropertyGroup,
Vrm1HumanoidPropertyGroup,
Vrm1LookAtPropertyGroup,
Vrm1MetaPropertyGroup,
)
from ..external.io_scene_gltf2_support import (
ExportSceneGltfArguments,
export_scene_gltf,
image_to_image_bytes,
init_extras_export,
)
from .abstract_base_vrm_exporter import (
AbstractBaseVrmExporter,
assign_dict,
force_apply_modifiers,
)
logger = get_logger(__name__)
class Vrm1Exporter(AbstractBaseVrmExporter):
def __init__(
self,
context: Context,
export_objects: Sequence[Object],
armature: Object,
export_preferences: ExportPreferencesProtocol,
) -> None:
super().__init__(context, export_objects, armature)
self.export_all_influences = (
export_preferences.enable_advanced_preferences
and export_preferences.export_all_influences
)
self.export_lights = (
export_preferences.enable_advanced_preferences
and export_preferences.export_lights
)
self.export_gltf_animations = (
export_preferences.enable_advanced_preferences
and export_preferences.export_gltf_animations
)
self.export_try_sparse_sk = (
export_preferences.enable_advanced_preferences
and export_preferences.export_try_sparse_sk
)
self.extras_main_armature_key = (
INTERNAL_NAME_PREFIX + self.export_id + "MainArmature"
)
self.extras_bone_name_key = INTERNAL_NAME_PREFIX + self.export_id + "BoneName"
self.extras_object_name_key = (
INTERNAL_NAME_PREFIX + self.export_id + "ObjectName"
)
self.extras_material_name_key = (
INTERNAL_NAME_PREFIX + self.export_id + "MaterialName"
)
@staticmethod
def enter_overwrite_object_visibility_and_selection(
context: Context, export_objects: Sequence[Object]
) -> Mapping[str, tuple[bool, bool]]:
object_visibility_and_selection: dict[str, tuple[bool, bool]] = {}
# https://projects.blender.org/blender/blender/issues/113378
context.view_layer.update()
for obj in context.view_layer.objects:
object_visibility_and_selection[obj.name] = (
obj.hide_get(),
obj.select_get(),
)
enabled = obj in export_objects
obj.hide_set(not enabled)
obj.select_set(enabled)
return object_visibility_and_selection
@staticmethod
def leave_overwrite_object_visibility_and_selection(
context: Context,
object_visibility_and_selection: Mapping[str, tuple[bool, bool]],
) -> None:
for object_name, (
hidden,
selection,
) in object_visibility_and_selection.items():
restore_obj = context.blend_data.objects.get(object_name)
if restore_obj:
restore_obj.hide_set(hidden)
restore_obj.select_set(selection)
@contextmanager
def overwrite_object_visibility_and_selection(self) -> Iterator[None]:
object_visibility_and_selection = (
self.enter_overwrite_object_visibility_and_selection(
self.context, self.export_objects
)
)
try:
yield
# After yield, bpy's native objects may be deleted or frames may advance
# and become invalid. Accessing them in that state can cause crashes, so
# be careful not to access such potentially invalid native objects
# after yield
finally:
self.leave_overwrite_object_visibility_and_selection(
self.context, object_visibility_and_selection
)
@staticmethod
def enter_save_selected_mesh_compat_objects(
context: Context, armature: Object, original_export_objects: Sequence[Object]
) -> tuple[Mapping[str, str], Mapping[str, str]]:
backup_obj_name_to_original_obj_name: dict[str, str] = {}
backup_data_name_to_original_data_name: dict[str, str] = {}
# https://projects.blender.org/blender/blender/issues/113378
context.view_layer.update()
export_objects: list[Object] = []
# Duplicate objects.
# New objects are used for export
# Old objects are given backup names and restored after export
# Also, object data is given backup names
for obj in context.view_layer.objects:
if obj not in original_export_objects:
continue
if obj.type not in search.MESH_CONVERTIBLE_OBJECT_TYPES:
continue
if not obj.select_get():
continue
backup_obj_name = "Backup-Object-" + uuid4().hex
original_obj_name = obj.name
backup_obj_name_to_original_obj_name[backup_obj_name] = original_obj_name
export_obj = obj.copy()
obj.name = backup_obj_name
export_obj.name = original_obj_name
export_objects.append(export_obj)
obj.select_set(False)
scene_collection_objects = context.scene.collection.objects
scene_collection_objects.link(export_obj)
export_obj.select_set(True)
obj_data = obj.data
if not obj_data:
continue
if obj_data.name in backup_data_name_to_original_data_name:
continue
backup_data_name = "Backup-Data-" + uuid4().hex
backup_data_name_to_original_data_name[backup_data_name] = obj_data.name
obj_data.name = backup_data_name
# Duplicate object data
# However, if active modifiers other than Armature modifiers are attached,
# share the object data
data_to_non_sharing_objects: dict[ID, list[Object]] = {}
data_to_sharing_objects: dict[ID, list[Object]] = {}
for export_object in export_objects:
data = export_object.data
if not data:
continue
sharing = True
for modifier in export_object.modifiers:
if not modifier.show_viewport:
continue
if (
isinstance(modifier, ArmatureModifier)
and modifier.object == armature
):
continue
sharing = False
break
mapping = (
data_to_sharing_objects if sharing else data_to_non_sharing_objects
)
objs = mapping.get(data)
if objs is None:
objs = list[Object]()
mapping[data] = objs
objs.append(export_object)
for data, sharing_export_objects in data_to_sharing_objects.items():
original_data_name = backup_data_name_to_original_data_name.get(data.name)
if not original_data_name:
continue # This should not happen
export_data = data.copy()
export_data.name = original_data_name
for sharing_export_object in sharing_export_objects:
sharing_export_object.data = export_data
for data, non_sharing_export_objects in data_to_non_sharing_objects.items():
original_data_name = backup_data_name_to_original_data_name.get(data.name)
if not original_data_name:
continue # This should not happen
for non_sharing_export_object in non_sharing_export_objects:
export_data = data.copy()
export_data.name = original_data_name
non_sharing_export_object.data = export_data
return (
backup_obj_name_to_original_obj_name,
backup_data_name_to_original_data_name,
)
@staticmethod
def leave_save_selected_mesh_compat_objects(
context: Context,
backup_obj_name_to_original_obj_name: Mapping[str, str],
backup_data_name_to_original_data_name: Mapping[str, str],
) -> None:
removing_object_datum: list[ID] = []
# Delete export objects
for original_obj_name in backup_obj_name_to_original_obj_name.values():
restored_export_obj = context.blend_data.objects.get(original_obj_name)
if not restored_export_obj:
continue
# Data may be referenced by multiple objects, so
# collect them here and delete them all at once later
restored_export_obj_data = restored_export_obj.data
if (
restored_export_obj_data
and restored_export_obj_data not in removing_object_datum
):
removing_object_datum.append(restored_export_obj_data)
# Even if deletion fails, randomize the name to avoid
# confusion with the original object
restored_export_obj.name = "Export-" + uuid4().hex
if not safe_removal.remove_object(context, restored_export_obj):
logger.warning(
'Failed to remove "%s" with %d users (temp object for "%s")',
restored_export_obj.name,
restored_export_obj.users,
original_obj_name,
)
# Delete export object data
for removing_object_data in removing_object_datum:
# Even if deletion fails, randomize the name to avoid
# confusion with the original object data
removing_object_data.name = "Export-Data-" + uuid4().hex
if removing_object_data.users:
logger.warning(
'Failed to remove "%s" with %d users',
removing_object_data.name,
removing_object_data.users,
)
elif isinstance(removing_object_data, Mesh):
context.blend_data.meshes.remove(removing_object_data)
elif isinstance(removing_object_data, Curve):
context.blend_data.curves.remove(removing_object_data)
elif isinstance(removing_object_data, VectorFont):
context.blend_data.fonts.remove(removing_object_data)
else:
logger.warning(
'Failed to remove "%s" with %d users. Not implemented.',
removing_object_data.name,
removing_object_data.users,
)
# Restore the names of backed up objects
for (
backup_obj_name,
original_obj_name,
) in backup_obj_name_to_original_obj_name.items():
restored_original_data_name = None
restored_obj = context.blend_data.objects.get(backup_obj_name)
if not restored_obj:
continue
restored_obj.name = original_obj_name
restored_obj.select_set(True)
# Restore the names of backed up object data
restored_obj_data = restored_obj.data
if not restored_obj_data:
continue
restored_original_data_name = backup_data_name_to_original_data_name.get(
restored_obj_data.name
)
if not restored_original_data_name:
continue
restored_obj_data.name = restored_original_data_name
@contextmanager
def save_selected_mesh_compat_objects(self) -> Iterator[Sequence[str]]:
(
backup_obj_name_to_original_obj_name,
backup_data_name_to_original_data_name,
) = self.enter_save_selected_mesh_compat_objects(
self.context, self.armature, self.export_objects
)
try:
yield list(backup_obj_name_to_original_obj_name.values())
# After yield, bpy's native objects may be deleted or frames may advance
# and become invalid. Accessing them in that state can cause crashes, so
# be careful not to access such potentially invalid native objects
# after yield
finally:
self.leave_save_selected_mesh_compat_objects(
self.context,
backup_obj_name_to_original_obj_name,
backup_data_name_to_original_data_name,
)
def enter_mount_skinned_mesh_parent(self, armature: Object) -> Sequence[str]:
mounted_object_names: list[str] = []
for obj in self.export_objects:
if obj.type != "MESH" or not [
True
for m in obj.modifiers
if isinstance(m, ArmatureModifier) and m.object == armature
]:
continue
search_obj = obj
while search_obj != armature:
if search_obj.parent:
search_obj = search_obj.parent
continue
mounted_object_names.append(search_obj.name)
matrix_world = search_obj.matrix_world.copy()
search_obj.parent = armature
search_obj.matrix_world = matrix_world
break
return mounted_object_names
def leave_mount_skinned_mesh_parent(
self, mounted_object_names: Sequence[str]
) -> None:
for mounted_object_name in mounted_object_names:
restore_obj = self.context.blend_data.objects.get(mounted_object_name)
if not restore_obj:
continue
matrix_world = restore_obj.matrix_world.copy()
restore_obj.parent = None
restore_obj.matrix_world = matrix_world
@contextmanager
def mount_skinned_mesh_parent(self) -> Iterator[None]:
# The glTF 2.0 export processing of the addon included with Blender 3.1.2
# outputs invalid glb with incorrect inverseBindMatrices when all of the
# following conditions are met:
# - A mesh that is not a descendant of the armature is skinned to bones
# of that armature
# - Another mesh exists as a child of the skinned bone
# Therefore, make the ancestor parent of meshes that are not descendants
# of the armature as the armature, and restore it later
armature = self.armature
if not armature:
yield
return
if bpy.app.version >= (3, 2, 1):
yield
return
mounted_object_names = self.enter_mount_skinned_mesh_parent(armature)
try:
yield
# After yield, bpy's native objects may be deleted or frames may advance
# and become invalid. Accessing them in that state can cause crashes, so
# be careful not to access such potentially invalid native objects
# after yield
finally:
self.leave_mount_skinned_mesh_parent(mounted_object_names)
@classmethod
def create_meta_dict(
cls,
meta: Vrm1MetaPropertyGroup,
json_dict: dict[str, Json],
buffer0: bytearray,
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> dict[str, Json]:
meta_dict: dict[str, Json] = {
"licenseUrl": "https://vrm.dev/licenses/1.0/",
"name": meta.vrm_name if meta.vrm_name else "undefined",
"version": meta.version if meta.version else "undefined",
"avatarPermission": meta.avatar_permission,
"allowExcessivelyViolentUsage": meta.allow_excessively_violent_usage,
"allowExcessivelySexualUsage": meta.allow_excessively_sexual_usage,
"commercialUsage": meta.commercial_usage,
"allowPoliticalOrReligiousUsage": meta.allow_political_or_religious_usage,
"allowAntisocialOrHateUsage": meta.allow_antisocial_or_hate_usage,
"creditNotation": meta.credit_notation,
"allowRedistribution": meta.allow_redistribution,
"modification": meta.modification,
}
authors = [author.value for author in meta.authors if author.value]
if not authors:
authors = ["undefined"]
meta_dict["authors"] = make_json(authors)
if meta.copyright_information:
meta_dict["copyrightInformation"] = meta.copyright_information
references: list[Json] = [
reference.value for reference in meta.references if reference.value
]
if references:
meta_dict["references"] = references
if meta.third_party_licenses:
meta_dict["thirdPartyLicenses"] = meta.third_party_licenses
if meta.other_license_url:
meta_dict["otherLicenseUrl"] = meta.other_license_url
if meta.thumbnail_image:
meta_dict["thumbnailImage"] = cls.find_or_create_image(
json_dict,
buffer0,
image_name_to_index_dict,
meta.thumbnail_image,
gltf2_addon_export_settings,
)
return meta_dict
@classmethod
def create_humanoid_dict(
cls,
humanoid: Vrm1HumanoidPropertyGroup,
bone_name_to_index_dict: Mapping[str, int],
) -> dict[str, Json]:
human_bones_dict: dict[str, Json] = {}
for (
human_bone_name,
human_bone,
) in humanoid.human_bones.human_bone_name_to_human_bone().items():
index = bone_name_to_index_dict.get(human_bone.node.bone_name)
if isinstance(index, int):
human_bones_dict[human_bone_name.value] = {"node": index}
return {
"humanBones": human_bones_dict,
}
@classmethod
def create_first_person_dict(
cls,
first_person: Vrm1FirstPersonPropertyGroup,
mesh_object_name_to_node_index_dict: Mapping[str, int],
) -> dict[str, Json]:
mesh_annotation_dicts: list[Json] = []
for mesh_annotation in first_person.mesh_annotations:
if not mesh_annotation.node or not mesh_annotation.node.mesh_object_name:
continue
node_index = mesh_object_name_to_node_index_dict.get(
mesh_annotation.node.mesh_object_name
)
if not isinstance(node_index, int):
continue
mesh_annotation_dicts.append(
{
"node": node_index,
"type": mesh_annotation.type,
}
)
if not mesh_annotation_dicts:
return {}
return {"meshAnnotations": mesh_annotation_dicts}
@classmethod
def create_look_at_dict(
cls,
look_at: Vrm1LookAtPropertyGroup,
) -> dict[str, Json]:
return {
"offsetFromHeadBone": list(look_at.offset_from_head_bone),
"type": look_at.type,
"rangeMapHorizontalInner": {
"inputMaxValue": look_at.range_map_horizontal_inner.input_max_value,
"outputScale": look_at.range_map_horizontal_inner.output_scale,
},
"rangeMapHorizontalOuter": {
"inputMaxValue": look_at.range_map_horizontal_outer.input_max_value,
"outputScale": look_at.range_map_horizontal_outer.output_scale,
},
"rangeMapVerticalDown": {
"inputMaxValue": look_at.range_map_vertical_down.input_max_value,
"outputScale": look_at.range_map_vertical_down.output_scale,
},
"rangeMapVerticalUp": {
"inputMaxValue": look_at.range_map_vertical_up.input_max_value,
"outputScale": look_at.range_map_vertical_up.output_scale,
},
}
@classmethod
def create_expression_dict(
cls,
expression: Vrm1ExpressionPropertyGroup,
mesh_object_name_to_node_index_dict: Mapping[str, int],
mesh_object_name_to_morph_target_names_dict: Mapping[str, list[str]],
material_name_to_index_dict: Mapping[str, int],
) -> dict[str, Json]:
expression_dict: dict[str, Json] = {
"isBinary": expression.is_binary,
"overrideBlink": expression.override_blink,
"overrideLookAt": expression.override_look_at,
"overrideMouth": expression.override_mouth,
}
morph_target_bind_dicts: list[Json] = []
for morph_target_bind in expression.morph_target_binds:
if (
not morph_target_bind.node
or not morph_target_bind.node.mesh_object_name
):
continue
node_index = mesh_object_name_to_node_index_dict.get(
morph_target_bind.node.mesh_object_name
)
if not isinstance(node_index, int):
continue
morph_targets = mesh_object_name_to_morph_target_names_dict.get(
morph_target_bind.node.mesh_object_name
)
if not isinstance(morph_targets, list):
continue
if morph_target_bind.index not in morph_targets:
continue
morph_target_bind_dicts.append(
{
"node": node_index,
"index": morph_targets.index(morph_target_bind.index),
"weight": morph_target_bind.weight,
}
)
if morph_target_bind_dicts:
expression_dict["morphTargetBinds"] = morph_target_bind_dicts
material_color_bind_dicts: list[Json] = []
for material_color_bind in expression.material_color_binds:
material_color_bind_material = material_color_bind.material
if (
not material_color_bind_material
or not material_color_bind_material.name
):
continue
material_index = material_name_to_index_dict.get(
material_color_bind_material.name
)
if not isinstance(material_index, int):
continue
if material_color_bind.type == "color":
target_value: Json = list(material_color_bind.target_value)
else:
rgb = material_color_bind.target_value_as_rgb
target_value = [rgb[0], rgb[1], rgb[2], 1.0]
material_color_bind_dicts.append(
{
"material": material_index,
"type": material_color_bind.type,
"targetValue": target_value,
}
)
if material_color_bind_dicts:
expression_dict["materialColorBinds"] = material_color_bind_dicts
texture_transform_binds: list[Json] = []
for texture_transform_bind in expression.texture_transform_binds:
texture_transform_bind_material = texture_transform_bind.material
if (
not texture_transform_bind_material
or not texture_transform_bind_material.name
):
continue
material_index = material_name_to_index_dict.get(
texture_transform_bind_material.name
)
if not isinstance(material_index, int):
continue
texture_transform_binds.append(
{
"material": material_index,
"scale": list(texture_transform_bind.scale),
"offset": list(texture_transform_bind.offset),
}
)
if texture_transform_binds:
expression_dict["textureTransformBinds"] = texture_transform_binds
return expression_dict
@classmethod
def create_expressions_dict(
cls,
expressions: Vrm1ExpressionsPropertyGroup,
mesh_object_name_to_node_index_dict: Mapping[str, int],
mesh_object_name_to_morph_target_names_dict: Mapping[str, list[str]],
material_name_to_index_dict: dict[str, int],
) -> dict[str, Json]:
preset_dict: dict[str, Json] = {}
for (
preset_name,
expression,
) in expressions.preset.name_to_expression_dict().items():
preset_dict[preset_name] = cls.create_expression_dict(
expression,
mesh_object_name_to_node_index_dict,
mesh_object_name_to_morph_target_names_dict,
material_name_to_index_dict,
)
custom_dict: dict[str, Json] = {}
for custom_expression in expressions.custom:
custom_dict[custom_expression.custom_name] = cls.create_expression_dict(
custom_expression,
mesh_object_name_to_node_index_dict,
mesh_object_name_to_morph_target_names_dict,
material_name_to_index_dict,
)
return {
"preset": preset_dict,
"custom": custom_dict,
}
@classmethod
def create_spring_bone_collider_dicts(
cls,
extensions_used: list[Json],
spring_bone: SpringBone1SpringBonePropertyGroup,
bone_name_to_index_dict: Mapping[str, int],
) -> tuple[list[Json], dict[str, int]]:
collider_dicts: list[Json] = []
collider_uuid_to_index_dict: dict[str, int] = {}
for collider in spring_bone.colliders:
collider_dict: dict[str, Json] = {}
node_index = bone_name_to_index_dict.get(collider.node.bone_name)
if not isinstance(node_index, int):
continue
collider_dict["node"] = node_index
extended_collider = collider.extensions.vrmc_spring_bone_extended_collider
if collider.shape_type == collider.SHAPE_TYPE_SPHERE.identifier:
if not extended_collider.enabled:
sphere_offset: Json = list(collider.shape.sphere.offset)
sphere_radius = collider.shape.sphere.radius
elif not extended_collider.automatic_fallback_generation:
sphere_offset = list(collider.shape.sphere.fallback_offset)
sphere_radius = collider.shape.sphere.fallback_radius
elif (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
and not extended_collider.shape.sphere.inside
):
sphere_offset = list(extended_collider.shape.sphere.offset)
sphere_radius = extended_collider.shape.sphere.radius
else:
sphere_offset = [0, -10000, 0]
sphere_radius = 0.00001
sphere_dict: dict[str, Json] = {
"offset": sphere_offset,
"radius": sphere_radius,
}
shape_dict: dict[str, Json] = {"sphere": sphere_dict}
elif collider.shape_type == collider.SHAPE_TYPE_CAPSULE.identifier:
if not extended_collider.enabled:
capsule_offset: Json = list(collider.shape.capsule.offset)
capsule_radius = collider.shape.capsule.radius
capsule_tail: Json = list(collider.shape.capsule.tail)
elif not extended_collider.automatic_fallback_generation:
capsule_offset = list(collider.shape.capsule.fallback_offset)
capsule_radius = collider.shape.capsule.fallback_radius
capsule_tail = list(collider.shape.capsule.fallback_tail)
elif (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
and not extended_collider.shape.capsule.inside
):
capsule_offset = list(extended_collider.shape.capsule.offset)
capsule_radius = extended_collider.shape.capsule.radius
capsule_tail = list(extended_collider.shape.capsule.tail)
else:
capsule_offset = [0, -10000, 0]
capsule_radius = 0.00001
capsule_tail = [0, -10001, 0]
capsule_dict: dict[str, Json] = {
"offset": capsule_offset,
"radius": capsule_radius,
"tail": capsule_tail,
}
shape_dict = {"capsule": capsule_dict}
else:
continue
collider_dict["shape"] = shape_dict
collider_uuid_to_index_dict[collider.uuid] = len(collider_dicts)
collider_dicts.append(collider_dict)
if not extended_collider.enabled:
continue
if (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_SPHERE.identifier
):
extended_shape_dict: dict[str, Json] = {
"sphere": {
"offset": list(extended_collider.shape.sphere.offset),
"radius": extended_collider.shape.sphere.radius,
"inside": extended_collider.shape.sphere.inside,
}
}
elif (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_CAPSULE.identifier
):
extended_shape_dict = {
"capsule": {
"offset": list(extended_collider.shape.capsule.offset),
"radius": extended_collider.shape.capsule.radius,
"tail": list(extended_collider.shape.capsule.tail),
"inside": extended_collider.shape.capsule.inside,
}
}
elif (
extended_collider.shape_type
== extended_collider.SHAPE_TYPE_EXTENDED_PLANE.identifier
):
extended_shape_dict = {
"plane": {
"offset": list(extended_collider.shape.plane.offset),
"normal": list(extended_collider.shape.plane.normal),
}
}
else:
continue
collider_dict["extensions"] = {
"VRMC_springBone_extended_collider": {
"specVersion": "1.0",
"shape": extended_shape_dict,
}
}
if "VRMC_springBone_extended_collider" not in extensions_used:
extensions_used.append("VRMC_springBone_extended_collider")
return collider_dicts, collider_uuid_to_index_dict
@classmethod
def create_spring_bone_collider_group_dicts(
cls,
spring_bone: SpringBone1SpringBonePropertyGroup,
collider_uuid_to_index_dict: Mapping[str, int],
) -> tuple[list[Json], dict[str, int]]:
collider_group_dicts: list[Json] = []
collider_group_uuid_to_index_dict: dict[str, int] = {}
for collider_group in spring_bone.collider_groups:
collider_group_dict: dict[str, Json] = {"name": collider_group.vrm_name}
collider_indices: list[Json] = []
for collider_reference in collider_group.colliders:
collider_index = collider_uuid_to_index_dict.get(
collider_reference.collider_uuid
)
if isinstance(collider_index, int):
collider_indices.append(collider_index)
if not collider_indices:
continue
collider_group_dict["colliders"] = collider_indices
collider_group_uuid_to_index_dict[collider_group.uuid] = len(
collider_group_dicts
)
collider_group_dicts.append(collider_group_dict)
return collider_group_dicts, collider_group_uuid_to_index_dict
@classmethod
def create_spring_bone_spring_dicts(
cls,
spring_bone: SpringBone1SpringBonePropertyGroup,
bone_name_to_index_dict: Mapping[str, int],
collider_group_uuid_to_index_dict: Mapping[str, int],
armature: Object,
) -> list[Json]:
spring_dicts: list[Json] = []
armature_data = armature.data
if not isinstance(armature_data, Armature):
logger.error("%s is not an Armature", type(armature_data))
return []
sorted_spring_and_joints: list[
tuple[
SpringBone1SpringPropertyGroup, Sequence[SpringBone1JointPropertyGroup]
]
] = [
(spring, [joint for joint, _ in joint_and_bones])
for spring in spring_bone.springs
for joint_and_bones in sort_spring_bone_joints(armature, spring.joints)
]
for spring, joints in sorted_spring_and_joints:
spring_dict: dict[str, Json] = {"name": spring.vrm_name}
first_bone: Optional[Bone] = None
joint_dicts: list[Json] = []
for joint in joints:
bone = armature_data.bones.get(joint.node.bone_name)
if not bone:
continue
if first_bone is None:
first_bone = bone
node_index = bone_name_to_index_dict.get(joint.node.bone_name)
if not isinstance(node_index, int):
continue
joint_dicts.append(
{
"node": node_index,
"hitRadius": joint.hit_radius,
"stiffness": joint.stiffness,
"gravityPower": joint.gravity_power,
"gravityDir": [
joint.gravity_dir[0],
joint.gravity_dir[2],
-joint.gravity_dir[1],
],
"dragForce": joint.drag_force,
}
)
if not joint_dicts:
continue
spring_dict["joints"] = joint_dicts
center_bone = armature_data.bones.get(spring.center.bone_name)
if center_bone:
center_bone_is_ancestor_of_first_bone = False
ancestor_of_first_bone = first_bone
while ancestor_of_first_bone:
if center_bone == ancestor_of_first_bone:
center_bone_is_ancestor_of_first_bone = True
break
ancestor_of_first_bone = ancestor_of_first_bone.parent
if center_bone_is_ancestor_of_first_bone:
center_index = bone_name_to_index_dict.get(spring.center.bone_name)
if isinstance(center_index, int):
spring_dict["center"] = center_index
collider_group_indices: list[Json] = []
for collider_group_reference in spring.collider_groups:
collider_group_index = collider_group_uuid_to_index_dict.get(
collider_group_reference.collider_group_uuid
)
if isinstance(collider_group_index, int):
collider_group_indices.append(collider_group_index)
if collider_group_indices:
spring_dict["colliderGroups"] = collider_group_indices
spring_dicts.append(spring_dict)
return spring_dicts
@classmethod
def search_constraint_target_index(
cls,
constraint: Union[CopyRotationConstraint, DampedTrackConstraint],
object_name_to_index_dict: Mapping[str, int],
bone_name_to_index_dict: Mapping[str, int],
) -> Optional[int]:
target = constraint.target
if not target:
return None
if target.type == "ARMATURE" and constraint.subtarget:
return bone_name_to_index_dict.get(constraint.subtarget)
return object_name_to_index_dict.get(target.name)
@classmethod
def create_constraint_dict(
cls,
name: str,
constraints: search.ExportConstraint,
object_name_to_index_dict: Mapping[str, int],
bone_name_to_index_dict: Mapping[str, int],
) -> Optional[dict[str, Json]]:
roll_constraint = constraints.roll_constraints.get(name)
aim_constraint = constraints.aim_constraints.get(name)
rotation_constraint = constraints.rotation_constraints.get(name)
if roll_constraint:
source_index = cls.search_constraint_target_index(
roll_constraint,
object_name_to_index_dict,
bone_name_to_index_dict,
)
if isinstance(source_index, int):
if roll_constraint.use_x:
roll_axis = "X"
elif roll_constraint.use_y:
roll_axis = "Y"
elif roll_constraint.use_z:
roll_axis = "Z"
else:
message = "Unsupported roll axis"
raise ValueError(message)
return {
"roll": {
"source": source_index,
"rollAxis": roll_axis,
"weight": max(0.0, min(1.0, roll_constraint.influence)),
}
}
elif aim_constraint:
source_index = cls.search_constraint_target_index(
aim_constraint,
object_name_to_index_dict,
bone_name_to_index_dict,
)
if isinstance(source_index, int):
return {
"aim": {
"source": source_index,
"aimAxis": convert.BPY_TRACK_AXIS_TO_VRM_AIM_AXIS[
aim_constraint.track_axis
],
"weight": max(0.0, min(1.0, aim_constraint.influence)),
}
}
elif rotation_constraint:
source_index = cls.search_constraint_target_index(
rotation_constraint,
object_name_to_index_dict,
bone_name_to_index_dict,
)
if isinstance(source_index, int):
return {
"rotation": {
"source": source_index,
"weight": max(0.0, min(1.0, rotation_constraint.influence)),
}
}
return None
@classmethod
def create_mtoon0_khr_texture_transform(
cls, node: Node, texture_input_name: str
) -> dict[str, Json]:
default: dict[str, Json] = {
"offset": [0, 0],
"scale": [1, 1],
}
texture_input = node.inputs.get(texture_input_name)
if not texture_input:
return default
texture_input_links = texture_input.links
if not texture_input_links:
return default
x_from_node = texture_input_links[0].from_node
if not x_from_node:
return default
x_inputs = x_from_node.inputs
if not x_inputs:
return default
x_links = x_inputs[0].links
if not x_links:
return default
uv_offset_scaling_node = x_links[0].from_node
if not uv_offset_scaling_node or uv_offset_scaling_node.type != "MAPPING'":
return default
result = default.copy()
location_input = uv_offset_scaling_node.inputs.get("Location")
if isinstance(location_input, shader.VECTOR_SOCKET_CLASSES):
result["offset"] = [
location_input.default_value[0],
location_input.default_value[1],
]
scale_input = uv_offset_scaling_node.inputs.get("Scale")
if isinstance(scale_input, shader.VECTOR_SOCKET_CLASSES):
result["scale"] = [
scale_input.default_value[0],
scale_input.default_value[1],
]
return result
@classmethod
def find_or_create_image(
cls,
json_dict: dict[str, Json],
buffer0: bytearray,
image_name_to_index_dict: dict[str, int],
image: Image,
gltf2_addon_export_settings: dict[str, object],
) -> int:
# TODO: Verify alignment requirement and optimize
while len(buffer0) % 32 == 0:
buffer0.append(0)
image_bytes, mime = image_to_image_bytes(image, gltf2_addon_export_settings)
buffer_view_dicts = json_dict.get("bufferViews")
if not isinstance(buffer_view_dicts, list):
buffer_view_dicts = []
json_dict["bufferViews"] = buffer_view_dicts
image_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": len(buffer0),
"byteLength": len(image_bytes),
}
)
image_index = image_name_to_index_dict.get(image.name)
image_dicts = json_dict.get("images")
if not isinstance(image_dicts, list):
image_dicts = []
json_dict["images"] = image_dicts
if isinstance(image_index, int) and not 0 <= image_index < len(image_dicts):
logger.error(
"Bug: not 0 <= %d < len(images)) for %s", image_index, image.name
)
image_index = None
if not isinstance(image_index, int):
image_index = len(image_dicts)
image_dicts.append(
{
"name": image.name,
"bufferView": image_buffer_view_index,
"mimeType": mime,
}
)
image_name_to_index_dict[image.name] = image_index
buffer0.extend(image_bytes)
# TODO: Verify alignment requirement and optimize
while len(buffer0) % 32 == 0:
buffer0.append(0)
return image_index
@classmethod
def create_mtoon1_texture_info_dict(
cls,
json_dict: dict[str, Json],
buffer0: bytearray,
texture_info: Mtoon1TextureInfoPropertyGroup,
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> Optional[dict[str, Json]]:
image = texture_info.index.get_connected_node_image()
if not image:
return None
image_index = cls.find_or_create_image(
json_dict,
buffer0,
image_name_to_index_dict,
image,
gltf2_addon_export_settings,
)
mag_filter = Mtoon1SamplerPropertyGroup.mag_filter_enum.identifier_to_value(
texture_info.index.sampler.mag_filter,
Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.value,
)
min_filter = Mtoon1SamplerPropertyGroup.min_filter_enum.identifier_to_value(
texture_info.index.sampler.min_filter,
Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.value,
)
wrap_s = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
texture_info.index.sampler.wrap_s,
Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
)
wrap_t = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
texture_info.index.sampler.wrap_t,
Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
)
sampler_dict: dict[str, Json] = {
"magFilter": mag_filter,
"minFilter": min_filter,
"wrapS": wrap_s,
"wrapT": wrap_t,
}
sampler_dicts = json_dict.get("samplers")
if not isinstance(sampler_dicts, list):
sampler_dicts = []
json_dict["samplers"] = sampler_dicts
if sampler_dict in sampler_dicts:
sampler_index = sampler_dicts.index(sampler_dict)
else:
sampler_index = len(sampler_dicts)
sampler_dicts.append(sampler_dict)
texture_dicts = json_dict.get("textures")
if not isinstance(texture_dicts, list):
texture_dicts = []
json_dict["textures"] = texture_dicts
texture_dict: dict[str, Json] = {
"sampler": sampler_index,
"source": image_index,
}
if texture_dict in texture_dicts:
texture_index = texture_dicts.index(texture_dict)
else:
texture_index = len(texture_dicts)
texture_dicts.append(texture_dict)
extensions_used = json_dict.get("extensionsUsed")
if not isinstance(extensions_used, list):
extensions_used = []
json_dict["extensionsUsed"] = extensions_used
if "KHR_texture_transform" not in extensions_used:
extensions_used.append("KHR_texture_transform")
json_dict["extensionsUsed"] = extensions_used
khr_texture_transform = texture_info.extensions.khr_texture_transform
khr_texture_transform_dict: dict[str, Json] = {
"offset": list(khr_texture_transform.offset),
"scale": list(khr_texture_transform.scale),
}
return {
"index": texture_index,
"extensions": {"KHR_texture_transform": khr_texture_transform_dict},
}
@classmethod
def create_mtoon0_texture_info_dict(
cls,
context: Context,
json_dict: dict[str, Json],
buffer0: bytearray,
node: Node,
texture_input_name: str,
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> Optional[dict[str, Json]]:
image_name_and_sampler_type = shader.get_image_name_and_sampler_type(
node, texture_input_name
)
if image_name_and_sampler_type is None:
return None
image_name, wrap_type, filter_type = image_name_and_sampler_type
image_index = cls.find_or_create_image(
json_dict,
buffer0,
image_name_to_index_dict,
context.blend_data.images[image_name],
gltf2_addon_export_settings,
)
sampler_dict: dict[str, Json] = {
"magFilter": filter_type,
"minFilter": filter_type,
"wrapS": wrap_type,
"wrapT": wrap_type,
}
sampler_dicts = json_dict.get("samplers")
if not isinstance(sampler_dicts, list):
sampler_dicts = []
json_dict["samplers"] = sampler_dicts
if sampler_dict in sampler_dicts:
sampler_index = sampler_dicts.index(sampler_dict)
else:
sampler_index = len(sampler_dicts)
sampler_dicts.append(sampler_dict)
texture_dicts = json_dict.get("textures")
if not isinstance(texture_dicts, list):
texture_dicts = []
json_dict["textures"] = texture_dicts
texture_dict: dict[str, Json] = {
"sampler": sampler_index,
"source": image_index,
}
if texture_dict in texture_dicts:
texture_index = texture_dicts.index(texture_dict)
else:
texture_index = len(texture_dicts)
texture_dicts.append(texture_dict)
extensions_used = json_dict.get("extensionsUsed")
if not isinstance(extensions_used, list):
extensions_used = []
json_dict["extensionsUsed"] = extensions_used
if "KHR_texture_transform" not in extensions_used:
extensions_used.append("KHR_texture_transform")
json_dict["extensionsUsed"] = extensions_used
khr_texture_transform_dict = cls.create_mtoon0_khr_texture_transform(
node, texture_input_name
)
return {
"index": texture_index,
"extensions": {"KHR_texture_transform": khr_texture_transform_dict},
}
@classmethod
def create_mtoon1_material_dict(
cls,
json_dict: dict[str, Json],
buffer0: bytearray,
material: Material,
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> dict[str, Json]:
extensions_used = json_dict.get("extensionsUsed")
if not isinstance(extensions_used, list):
extensions_used = []
json_dict["extensionsUsed"] = extensions_used
if "KHR_materials_unlit" not in extensions_used:
extensions_used.append("KHR_materials_unlit")
if "KHR_materials_emissive_strength" not in extensions_used:
extensions_used.append("KHR_materials_emissive_strength")
if "VRMC_materials_mtoon" not in extensions_used:
extensions_used.append("VRMC_materials_mtoon")
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs
mtoon_dict: dict[str, Json] = {
"specVersion": "1.0",
}
gltf = get_material_extension(material).mtoon1
mtoon = gltf.extensions.vrmc_materials_mtoon
extensions_dict: dict[str, Json] = {
"KHR_materials_unlit": {},
"KHR_materials_emissive_strength": {
"emissiveStrength": (
gltf.extensions.khr_materials_emissive_strength.emissive_strength
),
},
"VRMC_materials_mtoon": mtoon_dict,
}
material_dict: dict[str, Json] = {"name": material.name}
pbr_metallic_roughness_dict: dict[str, Json] = {}
material_dict["alphaMode"] = gltf.alpha_mode
mtoon_dict["transparentWithZWrite"] = mtoon.transparent_with_z_write
mtoon_dict["renderQueueOffsetNumber"] = mtoon.render_queue_offset_number
if gltf.alpha_mode == "MASK":
material_dict["alphaCutoff"] = gltf.alpha_cutoff
material_dict["doubleSided"] = gltf.double_sided
pbr_metallic_roughness_dict["baseColorFactor"] = list(
gltf.pbr_metallic_roughness.base_color_factor
)
assign_dict(
pbr_metallic_roughness_dict,
"baseColorTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
gltf.pbr_metallic_roughness.base_color_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
mtoon_dict["shadeColorFactor"] = list(mtoon.shade_color_factor)
assign_dict(
mtoon_dict,
"shadeMultiplyTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
mtoon.shade_multiply_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
assign_dict(
material_dict,
"normalTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
gltf.normal_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
normal_texture_dict = material_dict.get("normalTexture")
if isinstance(normal_texture_dict, dict):
normal_texture_dict["scale"] = gltf.normal_texture.scale
mtoon_dict["shadingShiftFactor"] = mtoon.shading_shift_factor
assign_dict(
mtoon_dict,
"shadingShiftTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
mtoon.shading_shift_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
shading_shift_texture_dict = mtoon_dict.get("shadingShiftTexture")
if isinstance(shading_shift_texture_dict, dict):
shading_shift_texture_dict["scale"] = mtoon.shading_shift_texture.scale
mtoon_dict["shadingToonyFactor"] = mtoon.shading_toony_factor
mtoon_dict["giEqualizationFactor"] = mtoon.gi_equalization_factor
material_dict["emissiveFactor"] = list(gltf.emissive_factor)
assign_dict(
material_dict,
"emissiveTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
gltf.emissive_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
if assign_dict(
mtoon_dict,
"matcapTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
mtoon.matcap_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
):
mtoon_dict["matcapFactor"] = list(mtoon.matcap_factor)
mtoon_dict["parametricRimColorFactor"] = list(mtoon.parametric_rim_color_factor)
mtoon_dict["parametricRimFresnelPowerFactor"] = (
mtoon.parametric_rim_fresnel_power_factor
)
mtoon_dict["parametricRimLiftFactor"] = mtoon.parametric_rim_lift_factor
assign_dict(
mtoon_dict,
"rimMultiplyTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
mtoon.rim_multiply_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
mtoon_dict["rimLightingMixFactor"] = mtoon.rim_lighting_mix_factor
mtoon_dict["outlineWidthMode"] = mtoon.outline_width_mode
mtoon_dict["outlineWidthFactor"] = mtoon.outline_width_factor
assign_dict(
mtoon_dict,
"outlineWidthMultiplyTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
mtoon.outline_width_multiply_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
mtoon_dict["outlineColorFactor"] = list(mtoon.outline_color_factor)
mtoon_dict["outlineLightingMixFactor"] = mtoon.outline_lighting_mix_factor
assign_dict(
mtoon_dict,
"uvAnimationMaskTexture",
cls.create_mtoon1_texture_info_dict(
json_dict,
buffer0,
mtoon.uv_animation_mask_texture,
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
mtoon_dict["uvAnimationRotationSpeedFactor"] = (
mtoon.uv_animation_rotation_speed_factor
)
mtoon_dict["uvAnimationScrollXSpeedFactor"] = (
mtoon.uv_animation_scroll_x_speed_factor
)
mtoon_dict["uvAnimationScrollYSpeedFactor"] = (
mtoon.uv_animation_scroll_y_speed_factor
)
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
extensions_dict["VRMC_materials_mtoon"] = mtoon_dict
material_dict["extensions"] = extensions_dict
return material_dict
@classmethod
def create_legacy_gltf_material_dict(
cls,
context: Context,
json_dict: dict[str, Json],
buffer0: bytearray,
material: Material,
node: Node,
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> dict[str, Json]:
material_dict: dict[str, Json] = {"name": material.name}
pbr_metallic_roughness_dict: dict[str, Json] = {}
if material.blend_method == "OPAQUE":
material_dict["alphaMode"] = "OPAQUE"
elif material.blend_method == "CLIP":
material_dict["alphaCutoff"] = max(0, min(1, material.alpha_threshold))
material_dict["alphaMode"] = "MASK"
else:
material_dict["alphaMode"] = "BLEND"
assign_dict(
material_dict, "doubleSided", not material.use_backface_culling, False
)
assign_dict(
pbr_metallic_roughness_dict,
"baseColorFactor",
shader.get_rgba_value(node, "base_Color", 0.0, 1.0),
)
base_color_texture_dict = cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"color_texture",
image_name_to_index_dict,
gltf2_addon_export_settings,
)
assign_dict(
pbr_metallic_roughness_dict, "baseColorTexture", base_color_texture_dict
)
assign_dict(
pbr_metallic_roughness_dict,
"metallicFactor",
shader.get_float_value(node, "metallic", 0.0, 1.0),
default_value=1.0,
)
assign_dict(
pbr_metallic_roughness_dict,
"roughnessFactor",
shader.get_float_value(node, "roughness", 0.0, 1.0),
default_value=1.0,
)
assign_dict(
pbr_metallic_roughness_dict,
"metallicRoughnessTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"emissive_texture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
normal_texture_dict = cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"normal",
image_name_to_index_dict,
gltf2_addon_export_settings,
)
assign_dict(material_dict, "normalTexture", normal_texture_dict)
assign_dict(
material_dict,
"emissiveFactor",
shader.get_rgb_value(node, "emissive_color", 0.0, 1.0),
)
assign_dict(
material_dict,
"emissiveTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"emissive_texture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
assign_dict(
material_dict,
"occlusionTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"occlusion_texture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
unlit_value = shader.get_float_value(node, "unlit")
if unlit_value is None or unlit_value > 0.5:
extensions_used = json_dict.get("extensionsUsed")
if not isinstance(extensions_used, list):
extensions_used = []
json_dict["extensionsUsed"] = extensions_used
if "KHR_materials_unlit" not in extensions_used:
extensions_used.append("KHR_materials_unlit")
material_dict["extensions"] = {"KHR_materials_unlit": {}}
return material_dict
@classmethod
def create_legacy_transparent_zwrite_material_dict(
cls,
context: Context,
json_dict: dict[str, Json],
buffer0: bytearray,
material: Material,
node: Node,
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> dict[str, Json]:
# https://vrm-c.github.io/UniVRM/en/implementation/transparent_zwrite.html#mtoon-unlit
extensions_used = json_dict.get("extensionsUsed")
if not isinstance(extensions_used, list):
extensions_used = []
json_dict["extensionsUsed"] = extensions_used
if "KHR_materials_unlit" not in extensions_used:
extensions_used.append("KHR_materials_unlit")
if "VRMC_materials_mtoon" not in extensions_used:
extensions_used.append("VRMC_materials_mtoon")
mtoon_dict: dict[str, Json] = {
"specVersion": "1.0",
}
extensions_dict: dict[str, Json] = {
"KHR_materials_unlit": {},
"VRMC_materials_mtoon": mtoon_dict,
}
material_dict: dict[str, Json] = {
"name": material.name,
"emissiveFactor": [1, 1, 1],
}
pbr_metallic_roughness_dict: dict[str, Json] = {
"baseColorFactor": [0, 0, 0, 1],
}
material_dict["alphaMode"] = "BLEND"
mtoon_dict["transparentWithZWrite"] = True
mtoon_dict["renderQueueOffsetNumber"] = 0
assign_dict(
material_dict, "doubleSided", not material.use_backface_culling, False
)
base_color_texture_dict = cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"Main_Texture",
image_name_to_index_dict,
gltf2_addon_export_settings,
)
assign_dict(
pbr_metallic_roughness_dict, "baseColorTexture", base_color_texture_dict
)
if base_color_texture_dict is not None:
mtoon_dict["shadeMultiplyTexture"] = base_color_texture_dict
material_dict["emissiveTexture"] = base_color_texture_dict
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
extensions_dict["VRMC_materials_mtoon"] = mtoon_dict
material_dict["extensions"] = extensions_dict
return material_dict
@classmethod
def create_mtoon_unversioned_material_dict(
cls,
context: Context,
json_dict: dict[str, Json],
buffer0: bytearray,
material: Material,
node: Node,
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> dict[str, Json]:
extensions_used = json_dict.get("extensionsUsed")
if not isinstance(extensions_used, list):
extensions_used = []
json_dict["extensionsUsed"] = extensions_used
if "KHR_materials_unlit" not in extensions_used:
extensions_used.append("KHR_materials_unlit")
if "VRMC_materials_mtoon" not in extensions_used:
extensions_used.append("VRMC_materials_mtoon")
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs
mtoon_dict: dict[str, Json] = {
"specVersion": "1.0",
}
extensions_dict: dict[str, Json] = {
"KHR_materials_unlit": {},
"VRMC_materials_mtoon": mtoon_dict,
}
material_dict: dict[str, Json] = {"name": material.name}
pbr_metallic_roughness_dict: dict[str, Json] = {}
if material.blend_method == "OPAQUE":
material_dict["alphaMode"] = "OPAQUE"
mtoon_dict["transparentWithZWrite"] = False
mtoon_dict["renderQueueOffsetNumber"] = 0
elif material.blend_method == "CLIP":
alpha_cutoff = shader.get_float_value(node, "CutoffRate", 0, float_info.max)
if alpha_cutoff is not None:
material_dict["alphaCutoff"] = alpha_cutoff
else:
material_dict["alphaCutoff"] = 0.5
material_dict["alphaMode"] = "MASK"
mtoon_dict["transparentWithZWrite"] = False
mtoon_dict["renderQueueOffsetNumber"] = 0
else:
material_dict["alphaMode"] = "BLEND"
transparent_with_z_write = shader.get_float_value(
node, "TransparentWithZWrite"
)
if (
transparent_with_z_write is None
or math.fabs(transparent_with_z_write) < float_info.epsilon
):
mtoon_dict["transparentWithZWrite"] = False
else:
mtoon_dict["transparentWithZWrite"] = True
mtoon_dict["renderQueueOffsetNumber"] = 0
assign_dict(
material_dict, "doubleSided", not material.use_backface_culling, False
)
assign_dict(
pbr_metallic_roughness_dict,
"baseColorFactor",
shader.get_rgba_value(node, "DiffuseColor", 0.0, 1.0),
)
base_color_texture_dict = cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"MainTexture",
image_name_to_index_dict,
gltf2_addon_export_settings,
)
assign_dict(
pbr_metallic_roughness_dict, "baseColorTexture", base_color_texture_dict
)
assign_dict(
mtoon_dict,
"shadeColorFactor",
shader.get_rgb_value(node, "ShadeColor", 0.0, 1.0),
)
shade_multiply_texture_dict = cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"ShadeTexture",
image_name_to_index_dict,
gltf2_addon_export_settings,
)
if shade_multiply_texture_dict is not None:
mtoon_dict["shadeMultiplyTexture"] = shade_multiply_texture_dict
elif base_color_texture_dict is not None:
# https://github.com/vrm-c/UniVRM/blob/f3479190c330ec6ecd2b40be919285aa93a53aff/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs#L185-L204
mtoon_dict["shadeMultiplyTexture"] = base_color_texture_dict
normal_texture_dict = cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"NormalmapTexture",
image_name_to_index_dict,
gltf2_addon_export_settings,
)
if not normal_texture_dict:
normal_texture_dict = cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"NomalmapTexture",
image_name_to_index_dict,
gltf2_addon_export_settings,
)
if assign_dict(
material_dict, "normalTexture", normal_texture_dict
) and isinstance(normal_texture_dict, dict):
assign_dict(
normal_texture_dict, "scale", shader.get_float_value(node, "BumpScale")
)
shading_shift_0x = shader.get_float_value(node, "ShadeShift")
if shading_shift_0x is None:
shading_shift_0x = 0.0
shading_toony_0x = shader.get_float_value(node, "ShadeToony")
if shading_toony_0x is None:
shading_toony_0x = 0.0
mtoon_dict["shadingShiftFactor"] = convert.mtoon_shading_shift_0_to_1(
shading_toony_0x, shading_shift_0x
)
mtoon_dict["shadingToonyFactor"] = convert.mtoon_shading_toony_0_to_1(
shading_toony_0x, shading_shift_0x
)
gi_equalization_0x = shader.get_float_value(node, "IndirectLightIntensity")
if gi_equalization_0x is not None:
mtoon_dict["giEqualizationFactor"] = (
convert.mtoon_intensity_to_gi_equalization(gi_equalization_0x)
)
assign_dict(
material_dict,
"emissiveFactor",
shader.get_rgb_value(node, "EmissionColor", 0.0, 1.0),
)
assign_dict(
material_dict,
"emissiveTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"Emission_Texture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
if assign_dict(
mtoon_dict,
"matcapTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"SphereAddTexture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
):
mtoon_dict["matcapFactor"] = [1, 1, 1]
assign_dict(
mtoon_dict,
"parametricRimColorFactor",
shader.get_rgb_value(node, "RimColor", 0.0, 1.0),
)
assign_dict(
mtoon_dict,
"parametricRimFresnelPowerFactor",
shader.get_float_value(node, "RimFresnelPower", 0.0, float_info.max),
)
assign_dict(
mtoon_dict,
"parametricRimLiftFactor",
shader.get_float_value(node, "RimLift"),
)
assign_dict(
mtoon_dict,
"rimMultiplyTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"RimTexture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
# https://github.com/vrm-c/UniVRM/blob/7c9919ef47a25c04100a2dcbe6a75dff49ef4857/Assets/VRM10/Runtime/Migration/Materials/MigrationMToonMaterial.cs#L287-L290
mtoon_dict["rimLightingMixFactor"] = 1.0
centimeter_to_meter = 0.01
one_hundredth = 0.01
outline_width_mode = shader.get_float_value(node, "OutlineWidthMode")
if outline_width_mode is not None:
outline_width_mode = round(outline_width_mode)
else:
outline_width_mode = 0
outline_width = shader.get_float_value(node, "OutlineWidth")
if outline_width is None:
outline_width = 0.0
if outline_width_mode == 1:
mtoon_dict["outlineWidthMode"] = "worldCoordinates"
mtoon_dict["outlineWidthFactor"] = max(
0.0, outline_width * centimeter_to_meter
)
elif outline_width_mode == 2:
mtoon_dict["outlineWidthMode"] = "screenCoordinates"
mtoon_dict["outlineWidthFactor"] = max(
0.0, outline_width * one_hundredth * 0.5
)
else:
mtoon_dict["outlineWidthMode"] = "none"
assign_dict(
mtoon_dict,
"outlineWidthMultiplyTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"OutlineWidthTexture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
assign_dict(
mtoon_dict,
"outlineColorFactor",
shader.get_rgb_value(node, "OutlineColor", 0.0, 1.0),
)
outline_color_mode = shader.get_float_value(node, "OutlineColorMode")
if outline_color_mode is not None:
outline_color_mode = round(outline_color_mode)
else:
outline_color_mode = 0
mtoon_dict["outlineLightingMixFactor"] = 0.0
if outline_color_mode == 1:
assign_dict(
mtoon_dict,
"outlineLightingMixFactor",
shader.get_float_value(node, "OutlineLightingMix"),
)
assign_dict(
mtoon_dict,
"uvAnimationMaskTexture",
cls.create_mtoon0_texture_info_dict(
context,
json_dict,
buffer0,
node,
"UV_Animation_Mask_Texture",
image_name_to_index_dict,
gltf2_addon_export_settings,
),
)
assign_dict(
mtoon_dict,
"uvAnimationRotationSpeedFactor",
shader.get_float_value(node, "UV_Scroll_Rotation"),
)
assign_dict(
mtoon_dict,
"uvAnimationScrollXSpeedFactor",
shader.get_float_value(node, "UV_Scroll_X"),
)
invert_y = -1
uv_animation_scroll_y_speed_factor = shader.get_float_value(node, "UV_Scroll_Y")
if uv_animation_scroll_y_speed_factor is not None:
mtoon_dict["uvAnimationScrollYSpeedFactor"] = (
uv_animation_scroll_y_speed_factor * invert_y
)
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
extensions_dict["VRMC_materials_mtoon"] = mtoon_dict
material_dict["extensions"] = extensions_dict
return material_dict
@classmethod
def save_vrm_materials(
cls,
context: Context,
json_dict: dict[str, Json],
buffer0: bytearray,
material_name_to_index_dict: Mapping[str, int],
image_name_to_index_dict: dict[str, int],
gltf2_addon_export_settings: dict[str, object],
) -> None:
material_dicts = json_dict.get("materials")
if not isinstance(material_dicts, list):
material_dicts = []
json_dict["materials"] = material_dicts
for material_name, index in material_name_to_index_dict.items():
material = context.blend_data.materials.get(material_name)
if not isinstance(material, Material) or not (
0 <= index < len(material_dicts)
):
continue
if get_material_extension(material).mtoon1.enabled:
material_dicts[index] = cls.create_mtoon1_material_dict(
json_dict,
buffer0,
material,
image_name_to_index_dict,
gltf2_addon_export_settings,
)
continue
# MToon_unversioned (MToon for VRM 0.0)
node, legacy_shader_name = search.legacy_shader_node(material)
if not isinstance(node, Node):
continue
if legacy_shader_name == "MToon_unversioned":
material_dicts[index] = cls.create_mtoon_unversioned_material_dict(
context,
json_dict,
buffer0,
material,
node,
image_name_to_index_dict,
gltf2_addon_export_settings,
)
elif legacy_shader_name == "GLTF":
material_dicts[index] = cls.create_legacy_gltf_material_dict(
context,
json_dict,
buffer0,
material,
node,
image_name_to_index_dict,
gltf2_addon_export_settings,
)
elif legacy_shader_name == "TRANSPARENT_ZWRITE":
material_dicts[index] = (
cls.create_legacy_transparent_zwrite_material_dict(
context,
json_dict,
buffer0,
material,
node,
image_name_to_index_dict,
gltf2_addon_export_settings,
)
)
if material_dicts:
json_dict["materials"] = material_dicts
@staticmethod
def enter_disable_mtoon1_material_nodes(context: Context) -> Sequence[str]:
disabled_material_names: list[str] = []
for material in context.blend_data.materials:
if not material:
continue
if not get_material_extension(material).mtoon1.enabled:
continue
link_or_unlink_gltf_material_nodes(context, material, link=False)
disabled_material_names.append(material.name)
return disabled_material_names
@staticmethod
def leave_disable_mtoon1_material_nodes(
context: Context, disabled_material_names: Sequence[str]
) -> None:
for disabled_material_name in disabled_material_names:
disabled_material = context.blend_data.materials.get(disabled_material_name)
if not disabled_material:
continue
link_or_unlink_gltf_material_nodes(context, disabled_material, link=True)
@classmethod
@contextmanager
def disable_mtoon1_material_nodes(cls, context: Context) -> Iterator[None]:
disabled_material_names = cls.enter_disable_mtoon1_material_nodes(context)
try:
yield
# After yield, bpy's native objects may be deleted or frames may advance
# and become invalid. Accessing them in that state can cause crashes, so
# be careful not to access such potentially invalid native objects
# after yield
finally:
cls.leave_disable_mtoon1_material_nodes(context, disabled_material_names)
@classmethod
def unassign_normal_from_mtoon_primitive_morph_target(
cls,
context: Context,
json_dict: dict[str, Json],
material_name_to_index_dict: Mapping[str, int],
) -> None:
mesh_dicts = json_dict.get("meshes")
if not isinstance(mesh_dicts, list):
return
for mesh_dict in mesh_dicts:
if not isinstance(mesh_dict, dict):
continue
primitive_dicts = mesh_dict.get("primitives")
if not isinstance(primitive_dicts, list):
continue
for primitive_dict in primitive_dicts:
if not isinstance(primitive_dict, dict):
continue
material_index = primitive_dict.get("material")
if not isinstance(material_index, int):
continue
skip = True
for (
search_material_name,
search_material_index,
) in material_name_to_index_dict.items():
if material_index != search_material_index:
continue
material = context.blend_data.materials.get(search_material_name)
if not material:
continue
if get_material_extension(material).mtoon1.export_shape_key_normals:
continue
if get_material_extension(material).mtoon1.enabled:
skip = False
break
node, legacy_shader_name = search.legacy_shader_node(material)
if not node:
continue
if legacy_shader_name == "MToon_unversioned":
skip = False
break
if skip:
continue
target_dicts = primitive_dict.get("targets")
if not isinstance(target_dicts, list):
continue
for target_dict in target_dicts:
if not isinstance(target_dict, dict):
continue
target_dict.pop("NORMAL", None)
@staticmethod
def enter_setup_dummy_human_bones(
context: Context, armature_object: Object
) -> Optional[Mapping[HumanBoneName, str]]:
armature_data = armature_object.data
if not isinstance(armature_data, Armature):
return None
ext = get_armature_extension(armature_data)
human_bones = ext.vrm1.humanoid.human_bones
if human_bones.all_required_bones_are_assigned():
return None
human_bone_name_to_bone_name: dict[HumanBoneName, str] = {}
for (
human_bone_name,
human_bone,
) in human_bones.human_bone_name_to_human_bone().items():
if not human_bone.node.bone_name:
continue
human_bone_name_to_bone_name[human_bone_name] = human_bone.node.bone_name
human_bone.node.bone_name = ""
with save_workspace(context, armature_object, mode="EDIT"):
hips_bone = armature_data.edit_bones.new(HumanBoneName.HIPS.value)
hips_bone.head = Vector((0, 0, 0.5))
hips_bone.tail = Vector((0, 1, 0.5))
hips_bone_name = deepcopy(hips_bone.name)
right_upper_leg_bone = armature_data.edit_bones.new(
HumanBoneName.RIGHT_UPPER_LEG.value
)
right_upper_leg_bone.head = Vector((-0.125, 0, 0.5))
right_upper_leg_bone.tail = Vector((-0.125, 1, 0.5))
right_upper_leg_bone.parent = hips_bone
right_upper_leg_bone_name = deepcopy(right_upper_leg_bone.name)
right_lower_leg_bone = armature_data.edit_bones.new(
HumanBoneName.RIGHT_LOWER_LEG.value
)
right_lower_leg_bone.head = Vector((-0.125, 0, 0.25))
right_lower_leg_bone.tail = Vector((-0.125, 1, 0.25))
right_lower_leg_bone.parent = right_upper_leg_bone
right_lower_leg_bone_name = deepcopy(right_lower_leg_bone.name)
right_foot_bone = armature_data.edit_bones.new(
HumanBoneName.RIGHT_FOOT.value
)
right_foot_bone.head = Vector((-0.125, 0, 0))
right_foot_bone.tail = Vector((-0.125, 1, 0))
right_foot_bone.parent = right_lower_leg_bone
right_foot_bone_name = deepcopy(right_foot_bone.name)
left_upper_leg_bone = armature_data.edit_bones.new(
HumanBoneName.LEFT_UPPER_LEG.value
)
left_upper_leg_bone.head = Vector((0.125, 0, 0.5))
left_upper_leg_bone.tail = Vector((0.125, 1, 0.5))
left_upper_leg_bone.parent = hips_bone
left_upper_leg_bone_name = deepcopy(left_upper_leg_bone.name)
left_lower_leg_bone = armature_data.edit_bones.new(
HumanBoneName.LEFT_LOWER_LEG.value
)
left_lower_leg_bone.head = Vector((0.125, 0, 0.25))
left_lower_leg_bone.tail = Vector((0.125, 1, 0.25))
left_lower_leg_bone.parent = left_upper_leg_bone
left_lower_leg_bone_name = deepcopy(left_lower_leg_bone.name)
left_foot_bone = armature_data.edit_bones.new(HumanBoneName.LEFT_FOOT.value)
left_foot_bone.head = Vector((0.125, 0, 0))
left_foot_bone.tail = Vector((0.125, 1, 0))
left_foot_bone.parent = left_lower_leg_bone
left_foot_bone_name = deepcopy(left_foot_bone.name)
spine_bone = armature_data.edit_bones.new(HumanBoneName.SPINE.value)
spine_bone.head = Vector((0, 0, 0.625))
spine_bone.tail = Vector((0, 1, 0.625))
spine_bone.parent = hips_bone
spine_bone_name = deepcopy(spine_bone.name)
right_upper_arm_bone = armature_data.edit_bones.new(
HumanBoneName.RIGHT_UPPER_ARM.value
)
right_upper_arm_bone.head = Vector((-0.125, 0, 0.75))
right_upper_arm_bone.tail = Vector((-0.125, 1, 0.75))
right_upper_arm_bone.parent = spine_bone
right_upper_arm_bone_name = deepcopy(right_upper_arm_bone.name)
right_lower_arm_bone = armature_data.edit_bones.new(
HumanBoneName.RIGHT_LOWER_ARM.value
)
right_lower_arm_bone.head = Vector((-0.25, 0, 0.75))
right_lower_arm_bone.tail = Vector((-0.25, 1, 0.75))
right_lower_arm_bone.parent = right_upper_arm_bone
right_lower_arm_bone_name = deepcopy(right_lower_arm_bone.name)
right_hand_bone = armature_data.edit_bones.new(
HumanBoneName.RIGHT_HAND.value
)
right_hand_bone.head = Vector((-0.375, 0, 0.75))
right_hand_bone.tail = Vector((-0.375, 1, 0.75))
right_hand_bone.parent = right_lower_arm_bone
right_hand_bone_name = deepcopy(right_hand_bone.name)
left_upper_arm_bone = armature_data.edit_bones.new(
HumanBoneName.LEFT_UPPER_ARM.value
)
left_upper_arm_bone.head = Vector((0.125, 0, 0.75))
left_upper_arm_bone.tail = Vector((0.125, 1, 0.75))
left_upper_arm_bone.parent = spine_bone
left_upper_arm_bone_name = deepcopy(left_upper_arm_bone.name)
left_lower_arm_bone = armature_data.edit_bones.new(
HumanBoneName.LEFT_LOWER_ARM.value
)
left_lower_arm_bone.head = Vector((0.25, 0, 0.75))
left_lower_arm_bone.tail = Vector((0.25, 1, 0.75))
left_lower_arm_bone.parent = left_upper_arm_bone
left_lower_arm_bone_name = deepcopy(left_lower_arm_bone.name)
left_hand_bone = armature_data.edit_bones.new(HumanBoneName.LEFT_HAND.value)
left_hand_bone.head = Vector((0.375, 0, 0.75))
left_hand_bone.tail = Vector((0.375, 1, 0.75))
left_hand_bone.parent = left_lower_arm_bone
left_hand_bone_name = deepcopy(left_hand_bone.name)
head_bone = armature_data.edit_bones.new(HumanBoneName.HEAD.value)
head_bone.head = Vector((0, 0, 0.75))
head_bone.tail = Vector((0, 1, 0.75))
head_bone.parent = spine_bone
head_bone_name = deepcopy(head_bone.name)
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
context, armature_data.name
)
human_bones.head.node.bone_name = head_bone_name
human_bones.spine.node.bone_name = spine_bone_name
human_bones.hips.node.bone_name = hips_bone_name
human_bones.right_upper_arm.node.bone_name = right_upper_arm_bone_name
human_bones.right_lower_arm.node.bone_name = right_lower_arm_bone_name
human_bones.right_hand.node.bone_name = right_hand_bone_name
human_bones.left_upper_arm.node.bone_name = left_upper_arm_bone_name
human_bones.left_lower_arm.node.bone_name = left_lower_arm_bone_name
human_bones.left_hand.node.bone_name = left_hand_bone_name
human_bones.right_upper_leg.node.bone_name = right_upper_leg_bone_name
human_bones.right_lower_leg.node.bone_name = right_lower_leg_bone_name
human_bones.right_foot.node.bone_name = right_foot_bone_name
human_bones.left_upper_leg.node.bone_name = left_upper_leg_bone_name
human_bones.left_lower_leg.node.bone_name = left_lower_leg_bone_name
human_bones.left_foot.node.bone_name = left_foot_bone_name
return human_bone_name_to_bone_name
@staticmethod
def leave_setup_dummy_human_bones(
context: Context,
armature_object: Object,
human_bone_name_to_bone_name: Mapping[HumanBoneName, str],
) -> None:
armature_data = armature_object.data
if not isinstance(armature_data, Armature):
return
human_bones = get_armature_extension(armature_data).vrm1.humanoid.human_bones
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
dummy_bone_names = deepcopy(
[
human_bone.node.bone_name
for human_bone in human_bone_name_to_human_bone.values()
if human_bone.node.bone_name
]
)
for human_bone_name, human_bone in human_bone_name_to_human_bone.items():
bone_name = human_bone_name_to_bone_name.get(human_bone_name)
if bone_name:
human_bone.node.bone_name = bone_name
else:
human_bone.node.bone_name = ""
Vrm1HumanBonesPropertyGroup.update_all_node_candidates(
context, armature_data.name
)
with save_workspace(context, armature_object, mode="EDIT"):
for dummy_bone_name in dummy_bone_names:
dummy_edit_bone = armature_data.edit_bones.get(dummy_bone_name)
if dummy_edit_bone:
armature_data.edit_bones.remove(dummy_edit_bone)
@classmethod
@contextmanager
def setup_dummy_human_bones(
cls,
context: Context,
armature: Object,
) -> Iterator[None]:
human_bone_name_to_bone_name = cls.enter_setup_dummy_human_bones(
context,
armature,
)
try:
yield
# After yield, bpy's native objects may be deleted or frames may advance
# and become invalid. Accessing them in that state can cause crashes, so
# be careful not to access such potentially invalid native objects
# after yield
finally:
if human_bone_name_to_bone_name is not None:
cls.leave_setup_dummy_human_bones(
context,
armature,
human_bone_name_to_bone_name,
)
@staticmethod
def clear_constrainted_rotation(
constraint: CopyRotationConstraint, object_or_bone: Union[Object, PoseBone]
) -> None:
euler_order = constraint.euler_order
if euler_order == "AUTO":
if object_or_bone.rotation_mode in ROTATION_MODE_EULER:
euler_order = object_or_bone.rotation_mode
else:
euler_order = "XYZ"
euler = get_rotation_as_quaternion(object_or_bone).to_euler(euler_order)
if constraint.use_x:
euler.x = 0
if constraint.use_y:
euler.y = 0
if constraint.use_z:
euler.z = 0
set_rotation_without_mode_change(object_or_bone, euler.to_quaternion())
@staticmethod
def enter_disable_constraints(
context: Context, armature: Object, export_objects: Sequence[Object]
) -> tuple[Sequence[tuple[str, str]], Sequence[tuple[str, str]]]:
constraint: Optional[Constraint] = None
object_constraints, bone_constraints, _ = search.export_constraints(
export_objects, armature
)
object_name_and_constraint_name: list[tuple[str, str]] = []
for object_name, constraint in object_constraints.all_constraints:
obj = context.blend_data.objects.get(object_name)
if not obj:
continue
if isinstance(constraint, CopyRotationConstraint):
Vrm1Exporter.clear_constrainted_rotation(constraint, obj)
else:
set_rotation_without_mode_change(obj, Quaternion())
constraint.mute = True
object_name_and_constraint_name.append((object_name, constraint.name))
bone_name_and_constraint_name: list[tuple[str, str]] = []
for bone_name, constraint in bone_constraints.all_constraints:
bone = armature.pose.bones.get(bone_name)
if not bone:
continue
if isinstance(constraint, CopyRotationConstraint):
Vrm1Exporter.clear_constrainted_rotation(constraint, bone)
else:
set_rotation_without_mode_change(bone, Quaternion())
constraint.mute = True
bone_name_and_constraint_name.append((bone_name, constraint.name))
return (object_name_and_constraint_name, bone_name_and_constraint_name)
@staticmethod
def leave_disable_constraints(
context: Context,
armature: Object,
object_name_and_constraint_name: Sequence[tuple[str, str]],
bone_name_and_constraint_name: Sequence[tuple[str, str]],
) -> None:
# Consider the possibility that Object or Constraint may have been
# deleted, and retrieve them again
for object_name, constraint_name in object_name_and_constraint_name:
obj = context.blend_data.objects.get(object_name)
if not obj:
continue
constraint = obj.constraints.get(constraint_name)
if not constraint:
continue
constraint.mute = False
for bone_name, constraint_name in bone_name_and_constraint_name:
bone = armature.pose.bones.get(bone_name)
if not bone:
continue
constraint = bone.constraints.get(constraint_name)
if not constraint:
continue
constraint.mute = False
@contextmanager
def disable_constraints(self, context: Context) -> Iterator[None]:
object_name_and_constraint_name, bone_name_and_constraint_name = (
self.enter_disable_constraints(context, self.armature, self.export_objects)
)
try:
yield
# After yield, bpy's native objects may be deleted or frames may advance
# and become invalid. Accessing them in that state can cause crashes, so
# be careful not to access such potentially invalid native objects
# after yield
finally:
self.leave_disable_constraints(
context,
self.armature,
object_name_and_constraint_name,
bone_name_and_constraint_name,
)
def enter_assign_export_custom_properties(self) -> None:
self.armature[self.extras_main_armature_key] = True
armature_data = self.armature.data
if not isinstance(armature_data, Armature):
return
# To minimize the impact of other glTF2ExportUserExtensions,
# use custom properties that seem to have less impact
# to establish correspondence between Blender objects and indices.
for obj in self.context.blend_data.objects:
obj[self.extras_object_name_key] = obj.name
for material in self.context.blend_data.materials:
material[self.extras_material_name_key] = material.name
# The glTF 2.0 addon comment states that it saves custom properties with
# PoseBone, but it actually references custom properties of Bone.
# Therefore, write to both just in case
for pose_bone in self.armature.pose.bones:
pose_bone[self.extras_bone_name_key] = pose_bone.name
for bone in armature_data.bones:
bone[self.extras_bone_name_key] = bone.name
def leave_assign_export_custom_properties(self) -> None:
armature_data = self.armature.data
if not isinstance(armature_data, Armature):
return
for pose_bone in self.armature.pose.bones:
pose_bone.pop(self.extras_bone_name_key, None)
for bone in armature_data.bones:
bone.pop(self.extras_bone_name_key, None)
for material in self.context.blend_data.materials:
material.pop(self.extras_material_name_key, None)
for obj in self.context.blend_data.objects:
obj.pop(self.extras_object_name_key, None)
self.armature.pop(self.extras_main_armature_key, None)
@contextmanager
def assign_export_custom_properties(self) -> Iterator[None]:
self.enter_assign_export_custom_properties()
try:
yield
# After yield, bpy's native objects may be deleted or frames may advance
# and become invalid. Accessing them in that state can cause crashes, so
# be careful not to access such potentially invalid native objects
# after yield
finally:
self.leave_assign_export_custom_properties()
@staticmethod
def gltf_export_armature_object_remove(
context: Context, mesh_object_names: Sequence[str]
) -> bool:
"""Return whether to enable export_armature_object_remove.
export_armature_object_remove is very useful and we'd like to use it,
but it has many bugs, so enable it only when not affected by those bugs.
"""
if bpy.app.version < (4, 2):
return False
if bpy.app.version < (4, 4):
# Root bone with non-deform bones returns False
# https://github.com/KhronosGroup/glTF-Blender-IO/issues/2394
for selected_object in context.selected_objects:
if selected_object.type != "ARMATURE":
continue
armature = selected_object.data
if not isinstance(armature, Armature):
continue
for bone in armature.bones:
if bone.parent:
continue
if not bone.use_deform:
return False
# Return False if there are vertices with Armature modifier but no weights
# https://github.com/KhronosGroup/glTF-Blender-IO/issues/2436
armature_data_name_to_deform_bone_names: dict[str, set[str]] = {}
for mesh_object_name in mesh_object_names:
mesh_object = context.blend_data.objects.get(mesh_object_name)
if not mesh_object:
continue
if mesh_object.type != "MESH":
continue
mesh = mesh_object.data
if not isinstance(mesh, Mesh):
continue
vertex_group_names_sequence: Sequence[set[str]] = [
{
mesh_object.vertex_groups[group.group].name
for group in vertex.groups
if group.weight > 0
and 0 <= group.group < len(mesh_object.vertex_groups)
}
for vertex in mesh.vertices
]
for modifier in mesh_object.modifiers:
if modifier.type != "ARMATURE":
continue
if not isinstance(modifier, ArmatureModifier):
continue
if not modifier.show_viewport:
continue
armature_object = modifier.object
if not armature_object:
continue
armature_data = armature_object.data
if not isinstance(armature_data, Armature):
continue
deform_bone_names = armature_data_name_to_deform_bone_names.get(
armature_data.name
)
if deform_bone_names is None:
deform_bone_names = {
bone.name for bone in armature_data.bones if bone.use_deform
}
armature_data_name_to_deform_bone_names[armature_data.name] = (
deform_bone_names
)
for vertex_group_names in vertex_group_names_sequence:
if all(
vertex_group_name not in deform_bone_names
for vertex_group_name in vertex_group_names
):
return False
return True
def export_vrm(self) -> Optional[bytes]:
init_extras_export()
armature_data = self.armature.data
if not isinstance(armature_data, Armature):
message = f"{type(armature_data)} is not an Armature"
raise TypeError(message)
self.setup_mtoon_gltf_fallback_nodes(self.context, is_vrm0=False)
with (
save_workspace(self.context),
self.setup_dummy_human_bones(self.context, self.armature),
self.clear_blend_shape_proxy_previews(armature_data),
self.enable_deform_for_all_referenced_bones(armature_data),
setup_humanoid_t_pose(self.context, self.armature),
self.overwrite_object_visibility_and_selection(),
):
with (
self.disable_constraints(self.context),
self.hide_mtoon1_outline_geometry_nodes(self.context),
self.disable_mtoon1_material_nodes(self.context),
self.mount_skinned_mesh_parent(),
self.save_selected_mesh_compat_objects() as mesh_compat_object_names,
self.assign_export_custom_properties(),
tempfile.TemporaryDirectory() as temp_dir,
):
force_apply_modifiers_to_objects(
self.context, self.armature, mesh_compat_object_names
)
filepath = Path(temp_dir, "out.glb")
export_scene_gltf_result = export_scene_gltf(
ExportSceneGltfArguments(
filepath=str(filepath),
check_existing=False,
export_format="GLB",
export_extras=True,
export_def_bones=True,
export_current_frame=True,
use_selection=True,
use_active_scene=True,
export_animations=self.export_gltf_animations,
export_armature_object_remove=(
self.gltf_export_armature_object_remove(
self.context, mesh_compat_object_names
)
),
export_rest_position_armature=False,
export_apply=False,
# Models may appear incorrectly in many viewers
export_all_influences=self.export_all_influences,
# TODO: Expose UI Option, Unity allows light export
export_lights=self.export_lights,
export_try_sparse_sk=self.export_try_sparse_sk,
export_vertex_color="MATERIAL",
)
)
if export_scene_gltf_result == {"CANCELLED"}:
return None
if export_scene_gltf_result != {"FINISHED"}:
message = (
'The glTF2 Exporter has not been {"FINISHED"}'
f" but {export_scene_gltf_result}"
)
raise AssertionError(message)
extra_name_assigned_glb = filepath.read_bytes()
vrm_bytes = self.add_vrm_extension_to_glb(extra_name_assigned_glb)
if vrm_bytes is None:
return None
logger.info("Generated VRM size: %s bytes", len(vrm_bytes))
return vrm_bytes
def remove_exported_armature_object_before_4_2(
self,
json_dict: dict[str, Json],
node_dicts: list[Json],
armature_node_index: int,
) -> None:
"""Delete scene armature object if it can be deleted in Blender < 4.2.
This is to prevent the Armature object from being treated as a bone
when re-importing in Blender. TODO: Should be handled more intelligently
using skin.skeleton etc.
When the main armature has transforms, the current method doesn't work
well with Blender 4.2.1, UniVRM 0.126.0, etc., so we don't do it.
When there are multiple root bones with Skin Joints registered,
skin.joints requires a common root bone constraint, so we don't delete it.
"""
if bpy.app.version >= (4, 2):
return
armature_world_matrix = (
find_node_world_matrix(node_dicts, armature_node_index, None) or Matrix()
)
if not (0 <= armature_node_index < len(node_dicts)):
return
armature_node_dict = node_dicts[armature_node_index]
if not isinstance(armature_node_dict, dict):
return
if not is_identity_matrix(armature_world_matrix):
return
scene_dicts = json_dict.get("scenes")
if not isinstance(scene_dicts, list):
return
all_joint_node_indices: list[Sequence[int]] = []
skin_dicts = json_dict.get("skins")
if isinstance(skin_dicts, list):
for skin_dict in skin_dicts:
if not isinstance(skin_dict, dict):
continue
joints = skin_dict.get("joints")
if not isinstance(joints, list):
continue
all_joint_node_indices.append(
[
joint_node_index
for joint_node_index in joints
if isinstance(joint_node_index, int)
]
)
armature_child_indices = (
[child for child in children if isinstance(child, int)]
if isinstance(children := armature_node_dict.get("children"), Sequence)
else []
)
for joint_node_indices in all_joint_node_indices:
if len(set(joint_node_indices).intersection(armature_child_indices)) >= 2:
return
armature_replaced = False
for scene_dict in scene_dicts:
if not isinstance(scene_dict, dict):
continue
scene_node_indices = scene_dict.get("nodes")
if not isinstance(scene_node_indices, list):
continue
# Remove ancestor nodes of that armature among nodes belonging to the scene
for scene_node_index in list(scene_node_indices):
if not isinstance(scene_node_index, int):
continue
search_scene_node_indices = [scene_node_index]
while search_scene_node_indices:
search_scene_node_index = search_scene_node_indices.pop()
if search_scene_node_index == armature_node_index:
scene_node_indices.remove(scene_node_index)
break
if not 0 <= search_scene_node_index < len(node_dicts):
continue
search_scene_node_dict = node_dicts[search_scene_node_index]
if not isinstance(search_scene_node_dict, dict):
continue
child_indices = search_scene_node_dict.get("children")
if not isinstance(child_indices, list):
continue
for child_index in child_indices:
if not isinstance(child_index, int):
continue
search_scene_node_indices.append(child_index)
for armature_child_index in armature_child_indices:
if (
not 0 <= armature_child_index < len(node_dicts)
or armature_child_index in scene_node_indices
):
continue
scene_node_indices.append(armature_child_index)
if armature_replaced:
continue
# Apply world matrix up to main armature to its children
child_node_dict = node_dicts[armature_child_index]
if not isinstance(child_node_dict, dict):
continue
child_matrix = get_node_matrix(child_node_dict)
set_node_matrix(
child_node_dict,
armature_world_matrix @ child_matrix,
)
armature_replaced = True
if armature_replaced:
armature_node_dict.pop("children", None)
armature_node_dict["name"] = "secondary" # Assign dummy name
def add_vrm_extension_to_glb(
self, extra_name_assigned_glb: bytes
) -> Optional[bytes]:
armature_data = self.armature.data
if not isinstance(armature_data, Armature):
message = f"{type(armature_data)} is not an Armature"
raise TypeError(message)
vrm = get_armature_extension(armature_data).vrm1
json_dict, buffer0_bytes = parse_glb(extra_name_assigned_glb)
buffer0 = bytearray(buffer0_bytes)
bone_name_to_index_dict: dict[str, int] = {}
object_name_to_index_dict: dict[str, int] = {}
image_name_to_index_dict: dict[str, int] = {}
mesh_object_name_to_node_index_dict: dict[str, int] = {}
mesh_object_name_to_morph_target_names_dict: dict[str, list[str]] = {}
node_dicts = json_dict.get("nodes")
if not isinstance(node_dicts, list):
node_dicts = []
json_dict["nodes"] = node_dicts
for node_index, node_dict in enumerate(node_dicts):
if not isinstance(node_dict, dict):
continue
extras_dict = node_dict.get("extras")
if not isinstance(extras_dict, dict):
continue
bone_name = extras_dict.pop(self.extras_bone_name_key, None)
if isinstance(bone_name, str):
bone_name_to_index_dict[bone_name] = node_index
is_main_armature = extras_dict.pop(self.extras_main_armature_key, None)
object_name = extras_dict.pop(self.extras_object_name_key, None)
if isinstance(object_name, str):
object_name_to_index_dict[object_name] = node_index
if bpy.app.version < (3, 3):
is_main_armature = object_name == self.armature.name
if is_main_armature:
if not extras_dict:
node_dict.pop("extras", None)
self.remove_exported_armature_object_before_4_2(
json_dict, node_dicts, node_index
)
mesh_index = node_dict.get("mesh")
mesh_dicts = json_dict.get("meshes")
if (
isinstance(object_name, str)
and isinstance(mesh_index, int)
and isinstance(mesh_dicts, list)
and 0 <= mesh_index < len(mesh_dicts)
):
mesh_object_name_to_node_index_dict[object_name] = node_index
mesh_dict = mesh_dicts[mesh_index]
if isinstance(mesh_dict, dict):
mesh_extras_dict = mesh_dict.get("extras")
if isinstance(mesh_extras_dict, dict):
target_names = mesh_extras_dict.get("targetNames")
if isinstance(target_names, list):
mesh_object_name_to_morph_target_names_dict[object_name] = [
str(target_name) for target_name in target_names
]
if isinstance(object_name, str) and (
object_name.startswith(INTERNAL_NAME_PREFIX + "VrmAddonLinkTo")
# or object_name == dummy_skinned_mesh_object_name
):
node_dict.clear()
for child_removing_node_dict in node_dicts:
if not isinstance(child_removing_node_dict, dict):
continue
children = child_removing_node_dict.get("children")
if not isinstance(children, list):
continue
children = [child for child in children if child != node_index]
if children:
child_removing_node_dict["children"] = children
else:
child_removing_node_dict.pop("children", None)
# TODO: remove from scenes, skin joints ...
if not extras_dict and "extras" in node_dict:
node_dict.pop("extras", None)
node_constraint_spec_version = "1.0"
use_node_constraint = False
object_constraints, bone_constraints, _ = search.export_constraints(
self.export_objects, self.armature
)
for object_name, node_index in object_name_to_index_dict.items():
if not 0 <= node_index < len(node_dicts):
continue
node_dict = node_dicts[node_index]
if not isinstance(node_dict, dict):
logger.error(
"gltf.nodes[%d] is invalid value: %s", node_index, node_dict
)
node_dict = {}
node_dicts[node_index] = node_dict
constraint_dict = self.create_constraint_dict(
object_name,
object_constraints,
object_name_to_index_dict,
bone_name_to_index_dict,
)
if constraint_dict:
extensions = node_dict.get("extensions")
if not isinstance(extensions, dict):
node_dict["extensions"] = extensions = {}
extensions["VRMC_node_constraint"] = {
"specVersion": node_constraint_spec_version,
"constraint": constraint_dict,
}
use_node_constraint = True
for bone_name, node_index in bone_name_to_index_dict.items():
if not 0 <= node_index < len(node_dicts):
continue
node_dict = node_dicts[node_index]
if not isinstance(node_dict, dict):
logger.error(
"gltf.nodes[%d] is invalid value: %s", node_index, node_dict
)
node_dict = {}
node_dicts[node_index] = node_dicts
constraint_dict = self.create_constraint_dict(
bone_name,
bone_constraints,
object_name_to_index_dict,
bone_name_to_index_dict,
)
if constraint_dict:
extensions = node_dict.get("extensions")
if not isinstance(extensions, dict):
node_dict["extensions"] = extensions = {}
extensions["VRMC_node_constraint"] = {
"specVersion": node_constraint_spec_version,
"constraint": constraint_dict,
}
use_node_constraint = True
material_name_to_index_dict: dict[str, int] = {}
material_dicts = json_dict.get("materials")
if not isinstance(material_dicts, list):
material_dicts = []
json_dict["materials"] = material_dicts
for material_index, material_dict in enumerate(material_dicts):
if not isinstance(material_dict, dict):
continue
extras_dict = material_dict.get("extras")
if not isinstance(extras_dict, dict):
continue
material_name = extras_dict.pop(self.extras_material_name_key, None)
if not isinstance(material_name, str):
continue
material_name_to_index_dict[material_name] = material_index
if not extras_dict:
material_dict.pop("extras", None)
self.save_vrm_materials(
self.context,
json_dict,
buffer0,
material_name_to_index_dict,
image_name_to_index_dict,
self.gltf2_addon_export_settings,
)
self.unassign_normal_from_mtoon_primitive_morph_target(
self.context, json_dict, material_name_to_index_dict
)
extensions_used = json_dict.get("extensionsUsed")
if not isinstance(extensions_used, list):
extensions_used = []
json_dict["extensionsUsed"] = extensions_used
if use_node_constraint:
extensions_used.append("VRMC_node_constraint")
extensions = json_dict.get("extensions")
if not isinstance(extensions, dict):
json_dict["extensions"] = extensions = {}
extensions_used.append("VRMC_vrm")
extensions["VRMC_vrm"] = {
"specVersion": get_armature_extension(armature_data).spec_version,
"meta": self.create_meta_dict(
vrm.meta,
json_dict,
buffer0,
image_name_to_index_dict,
self.gltf2_addon_export_settings,
),
"humanoid": self.create_humanoid_dict(
vrm.humanoid, bone_name_to_index_dict
),
"firstPerson": self.create_first_person_dict(
vrm.first_person, mesh_object_name_to_node_index_dict
),
"lookAt": self.create_look_at_dict(vrm.look_at),
"expressions": self.create_expressions_dict(
vrm.expressions,
mesh_object_name_to_node_index_dict,
mesh_object_name_to_morph_target_names_dict,
material_name_to_index_dict,
),
}
spring_bone = get_armature_extension(armature_data).spring_bone1
spring_bone_dict: dict[str, Json] = {}
(
spring_bone_collider_dicts,
collider_uuid_to_index_dict,
) = self.create_spring_bone_collider_dicts(
extensions_used, spring_bone, bone_name_to_index_dict
)
if spring_bone_collider_dicts:
spring_bone_dict["colliders"] = spring_bone_collider_dicts
(
spring_bone_collider_group_dicts,
collider_group_uuid_to_index_dict,
) = self.create_spring_bone_collider_group_dicts(
spring_bone, collider_uuid_to_index_dict
)
if spring_bone_collider_group_dicts:
spring_bone_dict["colliderGroups"] = spring_bone_collider_group_dicts
spring_bone_spring_dicts = self.create_spring_bone_spring_dicts(
spring_bone,
bone_name_to_index_dict,
collider_group_uuid_to_index_dict,
self.armature,
)
if spring_bone_spring_dicts:
spring_bone_dict["springs"] = spring_bone_spring_dicts
if spring_bone_dict:
extensions_used.append("VRMC_springBone")
spring_bone_dict["specVersion"] = "1.0"
extensions["VRMC_springBone"] = spring_bone_dict
json_dict["extensions"] = extensions
json_dict["extensionsUsed"] = extensions_used
v = get_addon_version()
if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true":
v = (999, 999, 999)
generator = "VRM Add-on for Blender v" + ".".join(map(str, v))
asset_dict = json_dict.get("asset")
if not isinstance(asset_dict, dict):
asset_dict = {}
json_dict["asset"] = asset_dict
base_generator = asset_dict.get("generator")
if isinstance(base_generator, str):
generator += " with " + base_generator
asset_dict["generator"] = generator
if buffer0:
buffer_dicts = json_dict.get("buffers")
if not isinstance(buffer_dicts, list) or not buffer_dicts:
buffer_dicts = []
json_dict["buffers"] = buffer_dicts
if not buffer_dicts:
buffer_dicts.append({})
buffer_dict = buffer_dicts[0]
if not isinstance(buffer_dict, dict):
buffer_dict = {}
buffer_dicts[0] = buffer_dict
buffer_dict["byteLength"] = len(buffer0)
# https://github.com/KhronosGroup/glTF/blob/b6e0fcc6d8e9f83347aa8b2e3df085b81590a65c/specification/2.0/schema/glTF.schema.json
gltf_root_non_empty_array_keys = [
"extensionsUsed",
"extensionsRequired",
"accessors",
"animations",
"buffers",
"bufferViews",
"cameras",
"images",
"materials",
"meshes",
"nodes",
"samplers",
"scenes",
"skins",
"textures",
]
for key in gltf_root_non_empty_array_keys:
if not json_dict.get(key):
json_dict.pop(key, None)
return pack_glb(json_dict, buffer0)
def find_node_world_matrix(
node_dicts: list[Json],
target_node_index: int,
parent_node_index: Optional[int],
) -> Optional[Matrix]:
if parent_node_index is None:
all_child_indices: list[int] = []
for node_dict in node_dicts:
if not isinstance(node_dict, dict):
continue
child_node_indices = node_dict.get("children")
if isinstance(child_node_indices, list):
all_child_indices.extend(
child_node_index
for child_node_index in child_node_indices
if isinstance(child_node_index, int)
)
for node_index in range(len(node_dicts)):
if node_index in all_child_indices:
continue
matrix = find_node_world_matrix(node_dicts, target_node_index, node_index)
if matrix is not None:
return matrix
return Matrix()
if not 0 <= parent_node_index < len(node_dicts):
return None
node_dict = node_dicts[parent_node_index]
if not isinstance(node_dict, dict):
return None
parent_node_matrix = get_node_matrix(node_dict)
if parent_node_index == target_node_index:
return parent_node_matrix
child_node_indices = node_dict.get("children")
if not isinstance(child_node_indices, list):
return None
for child_node_index in child_node_indices:
if not isinstance(child_node_index, int):
continue
child_node_matrix = find_node_world_matrix(
node_dicts, target_node_index, child_node_index
)
if child_node_matrix is not None:
return parent_node_matrix @ child_node_matrix
return None
def get_node_matrix(node_dict: dict[str, Json]) -> Matrix:
matrix = node_dict.get("matrix")
if isinstance(matrix, list):
if len(matrix) != 16:
return Matrix()
row0 = convert.float4_or_none((matrix[0], matrix[4], matrix[8], matrix[12]))
row1 = convert.float4_or_none((matrix[1], matrix[5], matrix[9], matrix[13]))
row2 = convert.float4_or_none((matrix[2], matrix[6], matrix[10], matrix[14]))
row3 = convert.float4_or_none((matrix[3], matrix[7], matrix[11], matrix[15]))
if not (row0 and row1 and row2 and row3):
return Matrix()
return Matrix((row0, row1, row2, row3))
location_matrix = Matrix()
location = convert.float3_or_none(node_dict.get("translation"))
if location:
location_matrix = Matrix.Translation(location)
rotation_matrix = Matrix()
rotation = convert.float4_or_none(node_dict.get("rotation"))
if rotation:
x, y, z, w = rotation
quaternion = Quaternion((w, x, y, z))
rotation_matrix = quaternion.to_matrix().to_4x4()
scale_matrix = Matrix()
scale = convert.float3_or_none(node_dict.get("scale"))
if scale:
scale_matrix = Matrix.Diagonal(scale).to_4x4()
return location_matrix @ rotation_matrix @ scale_matrix
def set_node_matrix(node_dict: dict[str, Json], matrix: Matrix) -> None:
node_dict.pop("matrix", None)
location, rotation, scale = matrix.decompose()
node_dict["translation"] = list(location)
node_dict["rotation"] = [
rotation.x,
rotation.y,
rotation.z,
rotation.w,
]
node_dict["scale"] = list(scale)
def force_apply_modifiers_to_objects(
context: Context,
armature_object: Object,
mesh_compatible_object_names: Sequence[str],
) -> None:
selected_object_names: list[str] = [
obj.name for obj in context.selectable_objects if obj.select_get()
]
for mesh_compatible_object_name in mesh_compatible_object_names:
force_apply_modifiers_to_object(
context, armature_object, mesh_compatible_object_name
)
for obj in context.selectable_objects:
obj.select_set(obj.name in selected_object_names)
def force_apply_modifiers_to_object(
context: Context,
armature_object: Object,
mesh_compatible_object_name: str,
) -> None:
mesh_compatible_object = context.blend_data.objects.get(mesh_compatible_object_name)
if not mesh_compatible_object:
return
if mesh_compatible_object.type == "MESH":
mesh_object: Optional[Object] = mesh_compatible_object
elif mesh_compatible_object.type in search.MESH_CONVERTIBLE_OBJECT_TYPES:
with save_workspace(context, mesh_compatible_object):
convert_result = bpy.ops.object.convert(target="MESH")
if convert_result != {"FINISHED"}:
logger.warning(
"Failed to convert %s to MESH: %s",
mesh_compatible_object.name,
convert_result,
)
return
mesh_object = context.blend_data.objects.get(mesh_compatible_object_name)
else:
return
if not mesh_object:
return
original_mesh_data = mesh_object.data
if not isinstance(original_mesh_data, Mesh):
return
armature_modifier_name_to_show_render_and_show_viewport: dict[
str, tuple[bool, bool]
] = {}
for modifier in list(mesh_object.modifiers):
if (
not isinstance(modifier, ArmatureModifier)
or modifier.object != armature_object
):
continue
armature_modifier_name_to_show_render_and_show_viewport[modifier.name] = (
modifier.show_render,
modifier.show_viewport,
)
modifier.show_render = False
modifier.show_viewport = False
try:
mesh_data_name = original_mesh_data.name
original_mesh_data.name = "Backup-Apply-Data-" + uuid4().hex
mesh_data: Optional[ID] = force_apply_modifiers(
context, mesh_object, preserve_shape_keys=True
)
if not mesh_data:
return
original_mesh_data.user_remap(mesh_data)
if original_mesh_data.users:
logger.warning(
'Failed to remove "%s" with %d users while applying modifiers',
original_mesh_data.name,
original_mesh_data.users,
)
else:
context.blend_data.meshes.remove(original_mesh_data)
mesh_data.name = mesh_data_name
finally:
for modifier in list(mesh_object.modifiers):
if (
not isinstance(modifier, ArmatureModifier)
or modifier.object != armature_object
):
mesh_object.modifiers.remove(modifier)
continue
show_render_and_show_viewport = (
armature_modifier_name_to_show_render_and_show_viewport.get(
modifier.name
)
)
if show_render_and_show_viewport is None:
continue
modifier.show_render, modifier.show_viewport = show_render_and_show_viewport
def is_identity_matrix(matrix: Matrix) -> bool:
for row_index, row in enumerate(matrix):
for column_index, value in enumerate(row):
if row_index == column_index:
if abs(value - 1) < float_info.epsilon:
continue
elif abs(value) < float_info.epsilon:
continue
return False
return True