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

3912 lines
145 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
# SPDX-FileCopyrightText: 2018 iCyP
import itertools
import re
import statistics
import struct
from collections.abc import Iterator, Mapping, MutableSequence, Sequence
from contextlib import contextmanager
from dataclasses import dataclass, field
from os import environ
from sys import float_info
from typing import Final, Optional, Union
import bpy
from bpy.types import (
Armature,
Context,
Curve,
Image,
Key,
Material,
Mesh,
MeshUVLoopLayer,
Node,
Object,
PoseBone,
ShaderNodeGroup,
ShaderNodeTexImage,
ShapeKey,
)
from mathutils import Matrix, Vector
from ..common import convert, gltf, shader
from ..common.convert import Json
from ..common.deep import make_json, make_json_dict
from ..common.gl import (
GL_FLOAT,
GL_LINEAR,
GL_REPEAT,
GL_UNSIGNED_INT,
GL_UNSIGNED_SHORT,
)
from ..common.gltf import (
FLOAT_NEGATIVE_MAX,
FLOAT_POSITIVE_MAX,
)
from ..common.legacy_gltf import TEXTURE_INPUT_NAMES
from ..common.logger import get_logger
from ..common.mtoon_unversioned import MtoonUnversioned
from ..common.progress import Progress, create_progress
from ..common.version import get_addon_version
from ..common.vrm0.human_bone import HumanBoneSpecifications
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 (
Mtoon0TexturePropertyGroup,
Mtoon1KhrTextureTransformPropertyGroup,
Mtoon1SamplerPropertyGroup,
Mtoon1TextureInfoPropertyGroup,
Mtoon1TexturePropertyGroup,
)
from ..editor.search import MESH_CONVERTIBLE_OBJECT_TYPES
from ..editor.t_pose import setup_humanoid_t_pose
from ..editor.vrm0.property_group import Vrm0BlendShapeGroupPropertyGroup
from ..external.io_scene_gltf2_support import (
gather_gltf2_io_material,
image_to_image_bytes,
init_extras_export,
)
from .abstract_base_vrm_exporter import (
AbstractBaseVrmExporter,
assign_dict,
force_apply_modifiers,
)
logger = get_logger(__name__)
class Vrm0Exporter(AbstractBaseVrmExporter):
@dataclass(frozen=True)
class Gltf2IoTextureImage:
name: str
mime_type: str
image_bytes: bytes
export_image_index: int
@dataclass
class PrimitiveTarget:
name: str
position: bytearray = field(default_factory=bytearray)
position_max_x = FLOAT_NEGATIVE_MAX
position_min_x = FLOAT_POSITIVE_MAX
position_max_y = FLOAT_NEGATIVE_MAX
position_min_y = FLOAT_POSITIVE_MAX
position_max_z = FLOAT_NEGATIVE_MAX
position_min_z = FLOAT_POSITIVE_MAX
normal: bytearray = field(default_factory=bytearray)
class VertexAttributesCollector:
POSITION_STRUCT: Final = struct.Struct("<fff")
NORMAL_STRUCT: Final = struct.Struct("<fff")
TEXCOORD_STRUCT: Final = struct.Struct("<ff")
WEIGHTS_STRUCT: Final = struct.Struct("<ffff")
JOINTS_STRUCT: Final = struct.Struct("<HHHH")
IndexSearchKey = tuple[
int, tuple[float, float, float], Optional[tuple[float, float]]
]
def __init__(self) -> None:
self.count = 0
self.position = bytearray()
self.position_max_x = FLOAT_NEGATIVE_MAX
self.position_min_x = FLOAT_POSITIVE_MAX
self.position_max_y = FLOAT_NEGATIVE_MAX
self.position_min_y = FLOAT_POSITIVE_MAX
self.position_max_z = FLOAT_NEGATIVE_MAX
self.position_min_z = FLOAT_POSITIVE_MAX
self.normal = bytearray()
self.texcoord = bytearray()
self.weights = bytearray()
self.joints = bytearray()
self.index_search_dict: dict[
tuple[int, tuple[float, float, float], Optional[tuple[float, float]]],
int,
] = {}
@staticmethod
def create_index_search_key(
*,
blender_vertex_index: int,
normal: tuple[float, float, float],
texcoord: Optional[tuple[float, float]],
) -> IndexSearchKey:
return (
# TODO: Format compatible with old exporter
blender_vertex_index,
normal,
texcoord,
)
def find_added_vertex_index(
self,
blender_vertex_index: int,
normal: tuple[float, float, float],
texcoord: Optional[tuple[float, float]],
) -> Optional[int]:
index_search_key = self.create_index_search_key(
blender_vertex_index=blender_vertex_index,
normal=normal,
texcoord=texcoord,
)
return self.index_search_dict.get(index_search_key)
def add_vertex(
self,
*,
blender_vertex_index: int,
position: tuple[float, float, float],
normal: tuple[float, float, float],
texcoord: Optional[tuple[float, float]],
weights: Optional[tuple[float, float, float, float]],
joints: Optional[tuple[int, int, int, int]],
) -> int:
index = self.count
self.count += 1
index_search_key = self.create_index_search_key(
blender_vertex_index=blender_vertex_index,
normal=normal,
texcoord=texcoord,
)
self.index_search_dict[index_search_key] = index
self.position.extend(self.POSITION_STRUCT.pack(*position))
position_x, position_y, position_z = position
self.position_max_x = max(self.position_max_x, position_x)
self.position_min_x = min(self.position_min_x, position_x)
self.position_max_y = max(self.position_max_y, position_y)
self.position_min_y = min(self.position_min_y, position_y)
self.position_max_z = max(self.position_max_z, position_z)
self.position_min_z = min(self.position_min_z, position_z)
self.normal.extend(self.NORMAL_STRUCT.pack(*normal))
if texcoord is not None:
self.texcoord.extend(self.TEXCOORD_STRUCT.pack(*texcoord))
if weights is not None:
self.weights.extend(self.WEIGHTS_STRUCT.pack(*weights))
if joints is not None:
self.joints.extend(self.JOINTS_STRUCT.pack(*joints))
return index
class VertexMorphTargetCollector:
def __init__(self, count: int) -> None:
self.count = count
self.POSITION_STRUCT = (
Vrm0Exporter.VertexAttributesCollector.POSITION_STRUCT
)
self.position = bytearray(self.POSITION_STRUCT.size * count)
self.position_max_x = FLOAT_NEGATIVE_MAX
self.position_min_x = FLOAT_POSITIVE_MAX
self.position_max_y = FLOAT_NEGATIVE_MAX
self.position_min_y = FLOAT_POSITIVE_MAX
self.position_max_z = FLOAT_NEGATIVE_MAX
self.position_min_z = FLOAT_POSITIVE_MAX
self.NORMAL_STRUCT = Vrm0Exporter.VertexAttributesCollector.NORMAL_STRUCT
self.normal = bytearray(self.NORMAL_STRUCT.size * count)
def add_vertex(
self,
*,
vertex_index: int,
target_position: tuple[float, float, float],
target_normal: tuple[float, float, float],
) -> None:
if not (0 <= vertex_index < self.count):
logger.error(
"invalid vertex_index: %d, count %d", vertex_index, self.count
)
return
target_position_x, target_position_y, target_position_z = target_position
position_start_index = vertex_index * self.POSITION_STRUCT.size
self.position[
position_start_index : position_start_index + self.POSITION_STRUCT.size
] = self.POSITION_STRUCT.pack(
target_position_x, target_position_y, target_position_z
)
self.position_max_x = max(self.position_max_x, target_position_x)
self.position_min_x = min(self.position_min_x, target_position_x)
self.position_max_y = max(self.position_max_y, target_position_y)
self.position_min_y = min(self.position_min_y, target_position_y)
self.position_max_z = max(self.position_max_z, target_position_z)
self.position_min_z = min(self.position_min_z, target_position_z)
normal_start_index = vertex_index * self.NORMAL_STRUCT.size
self.normal[
normal_start_index : normal_start_index + self.NORMAL_STRUCT.size
] = self.NORMAL_STRUCT.pack(*target_normal)
def export_vrm(self) -> Optional[bytes]:
init_extras_export()
with (
save_workspace(self.context),
self.clear_blend_shape_proxy_previews(self.armature_data),
self.enable_deform_for_all_referenced_bones(self.armature_data),
setup_humanoid_t_pose(self.context, self.armature),
self.hide_mtoon1_outline_geometry_nodes(self.context),
create_progress(self.context) as progress,
):
json_dict: dict[str, Json] = {}
buffer0 = bytearray()
self.write_glb_structure(progress, json_dict, buffer0)
return gltf.pack_glb(json_dict, buffer0)
def write_glb_structure(
self, progress: Progress, json_dict: dict[str, Json], buffer0: bytearray
) -> None:
json_dict["asset"] = {"generator": self.get_asset_generator(), "version": "2.0"}
scene_dicts: list[dict[str, Json]] = []
node_dicts: list[dict[str, Json]] = []
mesh_dicts: list[dict[str, Json]] = []
skin_dicts: list[dict[str, Json]] = []
material_dicts: list[dict[str, Json]] = []
texture_dicts: list[dict[str, Json]] = []
sampler_dicts: list[dict[str, Json]] = []
image_dicts: list[dict[str, Json]] = []
accessor_dicts: list[dict[str, Json]] = []
buffer_view_dicts: list[dict[str, Json]] = []
extensions_vrm_material_property_dicts: list[Json] = []
extensions_vrm_dict: dict[str, Json] = {
"materialProperties": extensions_vrm_material_property_dicts,
}
extensions_used: list[str] = []
material_name_to_material_index: dict[str, int] = {}
image_name_to_image_index: dict[str, int] = {}
self.write_materials(
progress,
material_dicts,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_vrm_material_property_dicts,
extensions_used,
buffer0,
material_name_to_material_index,
image_name_to_image_index,
)
bone_name_to_node_index: dict[str, int] = {}
mesh_object_name_to_mesh_index: dict[str, int] = {}
self.write_scene(
progress,
scene_dicts,
node_dicts,
mesh_dicts,
skin_dicts,
material_dicts,
accessor_dicts,
buffer_view_dicts,
extensions_vrm_material_property_dicts,
buffer0,
bone_name_to_node_index,
mesh_object_name_to_mesh_index,
material_name_to_material_index,
)
self.write_extensions_vrm(
progress,
mesh_dicts,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_vrm_dict,
extensions_used,
buffer0,
image_name_to_image_index,
bone_name_to_node_index,
mesh_object_name_to_mesh_index,
)
if scene_dicts:
json_dict["scenes"] = make_json(scene_dicts)
json_dict["scene"] = 0
if node_dicts:
json_dict["nodes"] = make_json(node_dicts)
if mesh_dicts:
json_dict["meshes"] = make_json(mesh_dicts)
if material_dicts:
json_dict["materials"] = make_json(material_dicts)
if skin_dicts:
json_dict["skins"] = make_json(skin_dicts)
if accessor_dicts:
json_dict["accessors"] = make_json(accessor_dicts)
if texture_dicts:
json_dict["textures"] = make_json(texture_dicts)
if image_dicts:
json_dict["images"] = make_json(image_dicts)
if sampler_dicts:
json_dict["samplers"] = make_json(sampler_dicts)
if buffer_view_dicts:
json_dict["bufferViews"] = make_json(buffer_view_dicts)
if extensions_used:
json_dict["extensionsUsed"] = list(dict.fromkeys(extensions_used).keys())
json_dict["extensions"] = make_json({"VRM": extensions_vrm_dict})
json_dict["buffers"] = [
{
"byteLength": len(buffer0),
}
]
def write_extensions_vrm(
self,
progress: Progress,
mesh_dicts: list[dict[str, Json]],
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
vrm_dict: dict[str, Json],
extensions_used: list[str],
buffer0: bytearray,
image_name_to_image_index: dict[str, int],
bone_name_to_node_index: Mapping[str, int],
mesh_object_name_to_mesh_index: Mapping[str, int],
) -> None:
vrm_dict["specVersion"] = "0.0"
vrm_dict["exporterVersion"] = self.get_asset_generator()
self.write_extensions_vrm_first_person(
progress, vrm_dict, bone_name_to_node_index, mesh_object_name_to_mesh_index
)
self.write_extensions_vrm_meta(
progress,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
vrm_dict,
buffer0,
image_name_to_image_index,
)
self.write_extensions_vrm_blend_shape_master(
progress, mesh_dicts, mesh_object_name_to_mesh_index, vrm_dict
)
self.write_extensions_vrm_secondary_animation(
progress, vrm_dict, bone_name_to_node_index
)
self.write_extensions_vrm_humanoid(progress, vrm_dict, bone_name_to_node_index)
extensions_used.append("VRM")
def write_extensions_vrm_humanoid(
self,
_progress: Progress,
vrm_dict: dict[str, Json],
bone_name_to_node_index: Mapping[str, int],
) -> None:
human_bone_dicts: list[Json] = []
humanoid_dict: dict[str, Json] = {"humanBones": human_bone_dicts}
vrm_dict["humanoid"] = humanoid_dict
humanoid = get_armature_extension(self.armature_data).vrm0.humanoid
for human_bone_name in HumanBoneSpecifications.all_names:
for human_bone in humanoid.human_bones:
if not (
human_bone.bone == human_bone_name
and (
node_index := bone_name_to_node_index.get(
human_bone.node.bone_name
)
)
is not None
):
continue
human_bone_dict: dict[str, Json] = {
"bone": human_bone_name,
"node": node_index,
"useDefaultValues": human_bone.use_default_values,
}
human_bone_dicts.append(human_bone_dict)
if not human_bone.use_default_values:
human_bone_dict.update(
{
"min": {
"x": human_bone.min[0],
"y": human_bone.min[1],
"z": human_bone.min[2],
},
"max": {
"x": human_bone.max[0],
"y": human_bone.max[1],
"z": human_bone.max[2],
},
"center": {
"x": human_bone.center[0],
"y": human_bone.center[1],
"z": human_bone.center[2],
},
"axisLength": human_bone.axis_length,
}
)
break
humanoid_dict["armStretch"] = humanoid.arm_stretch
humanoid_dict["legStretch"] = humanoid.leg_stretch
humanoid_dict["upperArmTwist"] = humanoid.upper_arm_twist
humanoid_dict["lowerArmTwist"] = humanoid.lower_arm_twist
humanoid_dict["upperLegTwist"] = humanoid.upper_leg_twist
humanoid_dict["lowerLegTwist"] = humanoid.lower_leg_twist
humanoid_dict["feetSpacing"] = humanoid.feet_spacing
humanoid_dict["hasTranslationDoF"] = humanoid.has_translation_dof
def write_extensions_vrm_blend_shape_master(
self,
_progress: Progress,
mesh_dicts: list[dict[str, Json]],
mesh_object_name_to_mesh_index: Mapping[str, int],
vrm_dict: dict[str, Json],
) -> None:
blend_shape_master = get_armature_extension(
self.armature_data
).vrm0.blend_shape_master
first_person = get_armature_extension(self.armature_data).vrm0.first_person
blend_shape_group_dicts: list[Json] = []
blend_shape_master_dict: dict[str, Json] = {
"blendShapeGroups": blend_shape_group_dicts
}
vrm_dict["blendShapeMaster"] = blend_shape_master_dict
remaining_preset_names = [
preset_name.identifier
for preset_name in Vrm0BlendShapeGroupPropertyGroup.preset_name_enum
if preset_name != Vrm0BlendShapeGroupPropertyGroup.PRESET_NAME_UNKNOWN
]
for blend_shape_group in blend_shape_master.blend_shape_groups:
blend_shape_group_dict: dict[str, Json] = {}
if not blend_shape_group.name:
continue
blend_shape_group_dict["name"] = blend_shape_group.name
if not blend_shape_group.preset_name:
continue
if blend_shape_group.preset_name != "unknown":
if blend_shape_group.preset_name not in remaining_preset_names:
continue
remaining_preset_names.remove(blend_shape_group.preset_name)
blend_shape_group_dict["presetName"] = blend_shape_group.preset_name
bind_dicts: list[Json] = []
blend_shape_group_dict["binds"] = bind_dicts
for bind in blend_shape_group.binds:
bind_dict: dict[str, Json] = {}
mesh_index = mesh_object_name_to_mesh_index.get(
bind.mesh.mesh_object_name
)
if mesh_index is None:
# logger.warning("%s => None", bind.mesh.mesh_object_name)
continue
bind_dict["mesh"] = mesh_index
if not (
0 <= mesh_index < len(mesh_dicts)
and (mesh_dict := mesh_dicts[mesh_index])
and isinstance(mesh_extras_dict := mesh_dict.get("extras"), dict)
and isinstance(
target_names := mesh_extras_dict.get("targetNames"), list
)
):
continue
if bind.index not in target_names:
continue
bind_dict["index"] = target_names.index(bind.index)
bind_dict["weight"] = min(max(bind.weight * 100, 0), 100)
bind_dicts.append(bind_dict)
material_value_dicts: list[Json] = []
blend_shape_group_dict["materialValues"] = material_value_dicts
for material_value in blend_shape_group.material_values:
material_value_material = material_value.material
if not material_value_material or not material_value_material.name:
continue
material_value_dicts.append(
{
"materialName": material_value_material.name,
"propertyName": material_value.property_name,
"targetValue": [v.value for v in material_value.target_value],
}
)
blend_shape_group_dict["isBinary"] = blend_shape_group.is_binary
blend_shape_group_dicts.append(blend_shape_group_dict)
# VirtualMotionCapture requires some blend shape presets
# https://twitter.com/sh_akira/status/1674237253231714305
# Note: the VRM specification does not require them. UniVRM 0.112.0
# can be configured not to output them.
for preset_name in remaining_preset_names:
if first_person.look_at_type_name == "Bone" and preset_name.startswith(
"look"
):
continue
name = next(
(
enum.name.replace(" ", "")
for enum in Vrm0BlendShapeGroupPropertyGroup.preset_name_enum
if enum.identifier == preset_name
),
preset_name.capitalize(),
)
blend_shape_group_dicts.append(
{
"name": name,
"presetName": preset_name,
"binds": [],
"materialValues": [],
"isBinary": False,
}
)
def write_extensions_vrm_secondary_animation(
self,
_progress: Progress,
vrm_dict: dict[str, Json],
bone_name_to_node_index: Mapping[str, int],
) -> None:
secondary_animation = get_armature_extension(
self.armature_data
).vrm0.secondary_animation
secondary_animation_dict: dict[str, Json] = {}
vrm_dict["secondaryAnimation"] = secondary_animation_dict
collider_group_dicts: list[Json] = []
secondary_animation_dict["colliderGroups"] = collider_group_dicts
secondary_animation = get_armature_extension(
self.armature_data
).vrm0.secondary_animation
collider_group_name_to_index: dict[str, int] = {}
for collider_group in secondary_animation.collider_groups:
node_index = bone_name_to_node_index.get(collider_group.node.bone_name)
if node_index is None:
continue
collider_group_dict: dict[str, Json] = {}
collider_dicts: list[Json] = []
collider_group_dict["colliders"] = collider_dicts
collider_group_name_to_index[collider_group.name] = len(
collider_group_dicts
)
collider_group_dicts.append(collider_group_dict)
collider_group_dict["node"] = node_index
for collider in collider_group.colliders:
collider_object = collider.bpy_object
if (
not collider_object
or collider_object.parent_bone not in self.armature.pose.bones
):
continue
collider_dict: dict[str, Json] = {}
offset = [
collider_object.matrix_world.to_translation()[i]
- (
self.armature.matrix_world
@ Matrix.Translation(
self.armature.pose.bones[collider_object.parent_bone].head
)
).to_translation()[i]
for i in range(3)
]
object_mean_scale = statistics.mean(
abs(s) for s in collider_object.matrix_world.to_scale()
)
collider_dict["radius"] = (
collider_object.empty_display_size * object_mean_scale
)
collider_dict["offset"] = {
# https://github.com/vrm-c/UniVRM/issues/65
"x": -offset[0],
"y": offset[2],
"z": -offset[1],
}
collider_dicts.append(collider_dict)
bone_group_dicts: list[Json] = []
secondary_animation_dict["boneGroups"] = bone_group_dicts
for bone_group in secondary_animation.bone_groups:
bone_group_dict: dict[str, Json] = {
"comment": bone_group.comment,
"stiffiness": bone_group.stiffiness,
"gravityPower": bone_group.gravity_power,
"gravityDir": {
# TODO: firstPerson.firstPersonBoneOffset and BoneGroup.gravityDir
# axis conversion is original. Need to document this.
"x": bone_group.gravity_dir[0],
"y": bone_group.gravity_dir[2],
"z": bone_group.gravity_dir[1],
},
"dragForce": bone_group.drag_force,
"center": bone_name_to_node_index.get(bone_group.center.bone_name, -1),
"hitRadius": bone_group.hit_radius,
"bones": [
node_index
for bone in bone_group.bones
if (node_index := bone_name_to_node_index.get(bone.bone_name))
is not None
],
}
collider_group_indices: list[Json] = []
for collider_group_name in bone_group.collider_groups:
collider_group_index = collider_group_name_to_index.get(
collider_group_name.value
)
if collider_group_index is None:
continue
collider_group_indices.append(collider_group_index)
bone_group_dict["colliderGroups"] = collider_group_indices
bone_group_dicts.append(bone_group_dict)
def write_extensions_vrm_meta(
self,
_progress: Progress,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
vrm_dict: dict[str, Json],
buffer0: bytearray,
image_name_to_index_dict: dict[str, int],
) -> None:
meta = get_armature_extension(self.armature_data).vrm0.meta
meta_dict: dict[str, Json] = {
"title": meta.title,
"version": meta.version,
"author": meta.author,
"contactInformation": meta.contact_information,
"reference": meta.reference,
"allowedUserName": meta.allowed_user_name,
"violentUssageName": meta.violent_ussage_name,
"sexualUssageName": meta.sexual_ussage_name,
"commercialUssageName": meta.commercial_ussage_name,
"otherPermissionUrl": meta.other_permission_url,
"licenseName": meta.license_name,
"otherLicenseUrl": meta.other_license_url,
}
if meta.texture:
image_index = self.find_or_create_image(
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_index_dict,
meta.texture,
)
sampler_dict: dict[str, Json] = {
"magFilter": GL_LINEAR,
"minFilter": GL_LINEAR,
"wrapS": GL_REPEAT,
"wrapT": GL_REPEAT,
}
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_index = len(texture_dicts)
texture_dicts.append(
{
"sampler": sampler_index,
"source": image_index,
},
)
meta_dict["texture"] = texture_index
vrm_dict["meta"] = meta_dict
def write_extensions_vrm_first_person(
self,
_progress: Progress,
vrm_dict: dict[str, Json],
bone_name_to_node_index: Mapping[str, int],
mesh_object_name_to_mesh_index: Mapping[str, int],
) -> None:
first_person_dict: dict[str, Json] = {}
vrm_dict["firstPerson"] = first_person_dict
first_person = get_armature_extension(self.armature_data).vrm0.first_person
if first_person.first_person_bone.bone_name:
first_person_bone_name: Optional[str] = (
first_person.first_person_bone.bone_name
)
else:
first_person_bone_name = next(
human_bone.node.bone_name
for human_bone in (
get_armature_extension(self.armature_data).vrm0.humanoid.human_bones
)
if human_bone.bone == "head"
)
if first_person_bone_name:
first_person_bone_index = bone_name_to_node_index.get(
first_person_bone_name
)
if isinstance(first_person_bone_index, int):
first_person_dict["firstPersonBone"] = first_person_bone_index
first_person_dict["firstPersonBoneOffset"] = {
# TODO: firstPerson.firstPersonBoneOffset and BoneGroup.gravityDir
# axis conversion is original. Need to document this.
"x": first_person.first_person_bone_offset[0],
"y": first_person.first_person_bone_offset[2],
"z": first_person.first_person_bone_offset[1],
}
mesh_annotation_dicts: list[Json] = []
first_person_dict["meshAnnotations"] = mesh_annotation_dicts
for mesh_annotation in first_person.mesh_annotations:
mesh_index = mesh_object_name_to_mesh_index.get(
mesh_annotation.mesh.mesh_object_name
)
if mesh_index is None:
mesh_index = -1
mesh_annotation_dicts.append(
{
"mesh": mesh_index,
"firstPersonFlag": mesh_annotation.first_person_flag,
}
)
first_person_dict["lookAtTypeName"] = first_person.look_at_type_name
for look_at, look_at_dict_key in [
(
first_person.look_at_horizontal_inner,
"lookAtHorizontalInner",
),
(
first_person.look_at_horizontal_outer,
"lookAtHorizontalOuter",
),
(
first_person.look_at_vertical_down,
"lookAtVerticalDown",
),
(
first_person.look_at_vertical_up,
"lookAtVerticalUp",
),
]:
first_person_dict[look_at_dict_key] = {
"curve": list(look_at.curve),
"xRange": look_at.x_range,
"yRange": look_at.y_range,
}
def write_scene(
self,
progress: Progress,
scene_dicts: list[dict[str, Json]],
node_dicts: list[dict[str, Json]],
mesh_dicts: list[dict[str, Json]],
skin_dicts: list[dict[str, Json]],
material_dicts: list[dict[str, Json]],
accessor_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_vrm_material_property_dicts: list[Json],
buffer0: bytearray,
bone_name_to_node_index: dict[str, int],
mesh_object_name_to_mesh_index: dict[str, int],
material_name_to_material_index: Mapping[str, int],
) -> None:
scene0_nodes: list[Json] = []
armature_root_node_indices, skin_dict, skin_joints = self.write_armature(
progress,
node_dicts,
accessor_dicts,
buffer_view_dicts,
buffer0,
bone_name_to_node_index,
)
scene0_nodes.extend(armature_root_node_indices)
scene0_nodes.extend(
self.write_mesh_nodes(
progress,
node_dicts,
mesh_dicts,
skin_dicts,
material_dicts,
accessor_dicts,
buffer_view_dicts,
extensions_vrm_material_property_dicts,
buffer0,
bone_name_to_node_index,
mesh_object_name_to_mesh_index,
material_name_to_material_index,
skin_dict,
skin_joints,
)
)
scene_dicts.append({"nodes": scene0_nodes})
def write_mtoon1_downgraded_material(
self,
_progress: Progress,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_used: list[str],
buffer0: bytearray,
image_name_to_image_index: dict[str, int],
material: Material,
material_dict: dict[str, Json],
vrm_material_property_dict: dict[str, Json],
) -> None:
gltf = get_material_extension(material).mtoon1
mtoon = gltf.extensions.vrmc_materials_mtoon
material_dict |= make_json_dict(
{
"alphaMode": gltf.alpha_mode,
"doubleSided": gltf.double_sided,
"extensions": {"KHR_materials_unlit": {}},
}
)
extensions_used.append("KHR_materials_unlit")
pbr_metallic_roughness_dict: dict[str, Json] = {
"metallicFactor": 0,
"roughnessFactor": 0.9,
}
keyword_map: dict[str, bool] = {}
tag_map: dict[str, str] = {}
float_properties: dict[str, float] = {}
vector_properties: dict[str, Sequence[float]] = {}
texture_properties: dict[str, int] = {}
pbr_metallic_roughness_dict["baseColorFactor"] = list(
gltf.pbr_metallic_roughness.base_color_factor
)
extensions = gltf.pbr_metallic_roughness.base_color_texture.extensions
khr_texture_transform = extensions.khr_texture_transform
vector_properties["_Color"] = convert.linear_to_srgb(
gltf.pbr_metallic_roughness.base_color_factor
)
if assign_dict(
pbr_metallic_roughness_dict,
"baseColorTexture",
self.create_mtoon1_downgraded_texture_info(
gltf.pbr_metallic_roughness.base_color_texture,
texture_properties,
"_MainTex",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
khr_texture_transform,
),
):
vector_properties["_MainTex"] = [
khr_texture_transform.offset[0],
khr_texture_transform.offset[1],
khr_texture_transform.scale[0],
khr_texture_transform.scale[1],
]
extensions_used.append("KHR_texture_transform")
vector_properties["_ShadeColor"] = convert.linear_to_srgb(
[*mtoon.shade_color_factor, 1]
)
self.create_mtoon1_downgraded_texture(
mtoon.shade_multiply_texture.index,
texture_properties,
"_ShadeTexture",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
float_properties["_BumpScale"] = gltf.normal_texture.scale
if assign_dict(
material_dict,
"normalTexture",
self.create_mtoon1_downgraded_texture_info(
gltf.normal_texture,
texture_properties,
"_BumpMap",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
khr_texture_transform,
),
):
normal_texture_to_index_dict = material_dict.get("normalTexture")
if isinstance(normal_texture_to_index_dict, dict):
normal_texture_to_index_dict["scale"] = gltf.normal_texture.scale
keyword_map["_NORMALMAP"] = True
self.create_mtoon1_downgraded_texture(
gltf.mtoon0_shading_grade_texture,
texture_properties,
"_ShadingGradeTexture",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
float_properties["_ShadingGradeRate"] = gltf.mtoon0_shading_grade_rate
float_properties["_ShadeShift"] = convert.mtoon_shading_shift_1_to_0(
mtoon.shading_toony_factor, mtoon.shading_shift_factor
)
float_properties["_ShadeToony"] = convert.mtoon_shading_toony_1_to_0(
mtoon.shading_toony_factor, mtoon.shading_shift_factor
)
float_properties["_IndirectLightIntensity"] = (
convert.mtoon_gi_equalization_to_intensity(mtoon.gi_equalization_factor)
)
float_properties["_RimLightingMix"] = gltf.mtoon0_rim_lighting_mix
float_properties["_RimFresnelPower"] = mtoon.parametric_rim_fresnel_power_factor
float_properties["_RimLift"] = mtoon.parametric_rim_lift_factor
emissive_strength = (
gltf.extensions.khr_materials_emissive_strength.emissive_strength
)
emissive_factor = Vector(gltf.emissive_factor)
hdr_emissive_factor = (
Vector(convert.linear_to_srgb(emissive_factor)) * emissive_strength
)
vector_properties["_EmissionColor"] = [*hdr_emissive_factor, 1]
if emissive_factor.length_squared > 0:
material_dict["emissiveFactor"] = list(emissive_factor)
assign_dict(
material_dict,
"emissiveTexture",
self.create_mtoon1_downgraded_texture_info(
gltf.emissive_texture,
texture_properties,
"_EmissionMap",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
khr_texture_transform,
),
)
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
self.create_mtoon1_downgraded_texture(
mtoon.matcap_texture.index,
texture_properties,
"_SphereAdd",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
vector_properties["_RimColor"] = convert.linear_to_srgb(
[*mtoon.parametric_rim_color_factor, 1]
)
self.create_mtoon1_downgraded_texture(
mtoon.rim_multiply_texture.index,
texture_properties,
"_RimTexture",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
vector_properties["_OutlineColor"] = convert.linear_to_srgb(
[*mtoon.outline_color_factor, 1]
)
self.create_mtoon1_downgraded_texture(
mtoon.outline_width_multiply_texture.index,
texture_properties,
"_OutlineWidthTexture",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
float_properties["_UvAnimScrollX"] = mtoon.uv_animation_scroll_x_speed_factor
float_properties["_UvAnimScrollY"] = -mtoon.uv_animation_scroll_y_speed_factor
float_properties["_UvAnimRotation"] = mtoon.uv_animation_rotation_speed_factor
self.create_mtoon1_downgraded_texture(
mtoon.uv_animation_mask_texture.index,
texture_properties,
"_UvAnimMaskTexture",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
float_properties["_OutlineLightingMix"] = mtoon.outline_lighting_mix_factor
outline_color_mode = 1 if mtoon.outline_lighting_mix_factor > 0 else 0
float_properties["_OutlineColorMode"] = outline_color_mode
float_properties["_OutlineWidth"] = 0.0
outline_width_world = False
outline_width_screen = False
outline_color_fixed = False
outline_color_mixed = False
if mtoon.outline_width_mode == mtoon.OUTLINE_WIDTH_MODE_NONE.identifier:
float_properties["_OutlineWidthMode"] = 0
float_properties["_OutlineLightingMix"] = 0
float_properties["_OutlineColorMode"] = 0
elif (
mtoon.outline_width_mode
== mtoon.OUTLINE_WIDTH_MODE_WORLD_COORDINATES.identifier
):
float_properties["_OutlineWidth"] = mtoon.outline_width_factor * 100
float_properties["_OutlineWidthMode"] = 1
outline_width_world = True
if outline_color_mode == 0:
outline_color_fixed = True
else:
outline_color_mixed = True
elif (
mtoon.outline_width_mode
== mtoon.OUTLINE_WIDTH_MODE_SCREEN_COORDINATES.identifier
):
float_properties["_OutlineWidth"] = mtoon.outline_width_factor * 200
float_properties["_OutlineWidthMode"] = 2
outline_width_screen = True
if outline_color_mode == 0:
outline_color_fixed = True
else:
outline_color_mixed = True
if outline_width_world:
keyword_map["MTOON_OUTLINE_WIDTH_WORLD"] = True
elif outline_width_screen:
keyword_map["MTOON_OUTLINE_WIDTH_SCREEN"] = True
if outline_color_fixed:
keyword_map["MTOON_OUTLINE_COLOR_FIXED"] = outline_color_fixed
elif outline_color_mixed:
keyword_map["MTOON_OUTLINE_COLOR_MIXED"] = outline_color_mixed
float_properties["_Cutoff"] = 0.5
if gltf.alpha_mode == gltf.ALPHA_MODE_OPAQUE.identifier:
blend_mode = 0
src_blend = 1
dst_blend = 0
z_write = 1
alphatest_on = False
alphablend_on = False
render_queue = -1
render_type = "Opaque"
elif gltf.alpha_mode == gltf.ALPHA_MODE_MASK.identifier:
blend_mode = 1
src_blend = 1
dst_blend = 0
z_write = 1
alphatest_on = True
alphablend_on = False
render_queue = gltf.mtoon0_render_queue
render_type = "TransparentCutout"
float_properties["_Cutoff"] = gltf.alpha_cutoff
material_dict["alphaCutoff"] = gltf.alpha_cutoff
elif not mtoon.transparent_with_z_write:
blend_mode = 2
src_blend = 5
dst_blend = 10
z_write = 0
alphatest_on = False
alphablend_on = True
render_queue = gltf.mtoon0_render_queue
render_type = "Transparent"
else:
blend_mode = 3
src_blend = 5
dst_blend = 10
z_write = 1
alphatest_on = False
alphablend_on = True
render_queue = gltf.mtoon0_render_queue
render_type = "Transparent"
float_properties["_Cutoff"] = gltf.alpha_cutoff # for compatibility
self.create_mtoon1_downgraded_texture(
gltf.mtoon0_receive_shadow_texture,
texture_properties,
"_ReceiveShadowTexture",
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
float_properties["_ReceiveShadowRate"] = gltf.mtoon0_receive_shadow_rate
keyword_map["_ALPHABLEND_ON"] = alphablend_on
keyword_map["_ALPHAPREMULTIPLY_ON"] = False
float_properties["_BlendMode"] = blend_mode
float_properties["_SrcBlend"] = src_blend
float_properties["_DstBlend"] = dst_blend
float_properties["_ZWrite"] = z_write
if alphatest_on:
keyword_map["_ALPHATEST_ON"] = alphatest_on
tag_map["RenderType"] = render_type
float_properties["_MToonVersion"] = MtoonUnversioned.version
if gltf.mtoon0_front_cull_mode:
float_properties["_CullMode"] = 1
elif material.use_backface_culling:
float_properties["_CullMode"] = 2
else:
float_properties["_CullMode"] = 0
float_properties["_OutlineCullMode"] = 1
float_properties["_DebugMode"] = 0
float_properties["_LightColorAttenuation"] = gltf.mtoon0_light_color_attenuation
float_properties["_OutlineScaledMaxDistance"] = (
gltf.mtoon0_outline_scaled_max_distance
)
keyword_map["MTOON_DEBUG_NORMAL"] = False
keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
vrm_material_property_dict.update(
{
"name": material.name,
"shader": "VRM/MToon",
"keywordMap": make_json(keyword_map),
"tagMap": make_json(tag_map),
"floatProperties": make_json(float_properties),
"vectorProperties": make_json(vector_properties),
"textureProperties": make_json(texture_properties),
"renderQueue": render_queue,
}
)
def find_or_create_image(
self,
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
buffer0: bytearray,
image_name_to_index_dict: dict[str, int],
image: Image,
) -> int:
image_index = image_name_to_index_dict.get(image.name)
if isinstance(image_index, int):
return image_index
image_bytes, mime = image_to_image_bytes(
image, self.gltf2_addon_export_settings
)
image_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": len(buffer0),
"byteLength": len(image_bytes),
}
)
buffer0.extend(image_bytes)
image_index = len(image_dicts)
image_dicts.append(
{
"name": image.name,
"mimeType": mime,
"bufferView": image_buffer_view_index,
}
)
image_name_to_index_dict[image.name] = image_index
return image_index
def create_mtoon0_khr_texture_transform(
self, node: Node, texture_input_name: str
) -> tuple[dict[str, Json], tuple[float, float, float, float]]:
default: tuple[dict[str, Json], tuple[float, float, float, float]] = (
{
"offset": [0, 0],
"scale": [1, 1],
},
(0, 0, 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
location_input = uv_offset_scaling_node.inputs.get("Location")
offset = (0.0, 0.0)
if isinstance(location_input, shader.VECTOR_SOCKET_CLASSES):
offset = (
location_input.default_value[0],
location_input.default_value[1],
)
scale_input = uv_offset_scaling_node.inputs.get("Scale")
scale = (1.0, 1.0)
if isinstance(scale_input, shader.VECTOR_SOCKET_CLASSES):
scale = (
scale_input.default_value[0],
scale_input.default_value[1],
)
return (
{
"offset": list(offset),
"scale": list(scale),
},
(*offset, *scale),
)
def create_mtoon0_texture_info_dict(
self,
context: Context,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_used: list[str],
buffer0: bytearray,
node: Node,
texture_input_name: str,
image_name_to_index_dict: dict[str, int],
*,
use_khr_texture_transform: bool = False,
) -> Optional[tuple[dict[str, Json], int, tuple[float, float, float, float]]]:
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 = self.find_or_create_image(
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_index_dict,
context.blend_data.images[image_name],
)
sampler_dict: dict[str, Json] = {
"magFilter": filter_type,
"minFilter": filter_type,
"wrapS": wrap_type,
"wrapT": wrap_type,
}
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_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)
khr_texture_transform_dict, vector_property = (
self.create_mtoon0_khr_texture_transform(node, texture_input_name)
)
texture_info: dict[str, Json] = {"index": texture_index}
if use_khr_texture_transform:
texture_info["extensions"] = {
"KHR_texture_transform": khr_texture_transform_dict
}
extensions_used.append("KHR_texture_transform")
return (
texture_info,
texture_index,
vector_property,
)
def write_legacy_mtoon_unversioned_material(
self,
_progress: Progress,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_used: list[str],
buffer0: bytearray,
image_name_to_image_index: dict[str, int],
material: Material,
material_dict: dict[str, Json],
vrm_material_property_dict: dict[str, Json],
node: ShaderNodeGroup,
) -> None:
keyword_map: dict[str, bool] = {}
tag_map: dict[str, str] = {}
texture_properties: dict[str, int] = {}
material_dict |= make_json_dict(
{
"name": material.name,
"extensions": {"KHR_materials_unlit": {}},
"doubleSided": not material.use_backface_culling,
}
)
extensions_used.append("KHR_materials_unlit")
pbr_metallic_roughness_dict: dict[str, Json] = {
"metallicFactor": 0,
"roughnessFactor": 0.9,
}
color = shader.get_rgba_value_or(
node,
"DiffuseColor",
0.0,
1.0,
(
1.0,
1.0,
1.0,
1.0,
),
)
emission_color = shader.get_rgba_value(node, "EmissionColor", 0.0, 1.0)
if emission_color is None:
emission_color = (0.0, 0.0, 0.0, 1.0)
else:
material_dict["emissiveFactor"] = make_json(emission_color[:3])
pbr_metallic_roughness_dict["baseColorFactor"] = make_json(color)
vector_properties: dict[str, Sequence[float]] = {
"_Color": color,
"_EmissionColor": emission_color,
"_ShadeColor": shader.get_rgba_value_or(node, "ShadeColor", 0.0, 1.0),
"_OutlineColor": shader.get_rgba_value_or(node, "OutlineColor", 0.0, 1.0),
"_RimColor": shader.get_rgba_value_or(node, "RimColor", 0.0, 1.0),
}
float_properties: dict[str, Union[float, int]] = {
"_MToonVersion": MtoonUnversioned.version,
"_DebugMode": 0,
"_ShadeShift": shader.get_float_value_or(node, "ShadeShift"),
"_ShadeToony": shader.get_float_value_or(
node, "ShadeToony", default_value=0.5
),
"_ShadingGradeRate": shader.get_float_value_or(
node, "ShadingGradeRate", default_value=0.5
),
"_ReceiveShadowRate": shader.get_float_value_or(
node, "ReceiveShadowRate", default_value=0.5
),
"_LightColorAttenuation": shader.get_float_value_or(
node, "LightColorAttenuation", default_value=0.5
),
"_IndirectLightIntensity": shader.get_float_value_or(
node, "IndirectLightIntensity", default_value=0.5
),
"_RimFresnelPower": shader.get_float_value_or(
node, "RimFresnelPower", 0.0, float_info.max, default_value=1.0
),
"_RimLift": shader.get_float_value_or(node, "RimLift"),
"_RimLightingMix": shader.get_float_value_or(node, "RimLightingMix"),
"_OutlineLightingMix": shader.get_float_value_or(
node, "OutlineLightingMix"
),
"_OutlineScaledMaxDistance": shader.get_float_value_or(
node, "OutlineScaleMaxDistance"
),
"_OutlineWidth": shader.get_float_value_or(node, "OutlineWidth"),
"_UvAnimRotation": shader.get_float_value_or(node, "UV_Scroll_Rotation"),
"_UvAnimScrollX": shader.get_float_value_or(node, "UV_Scroll_X"),
"_UvAnimScrollY": shader.get_float_value_or(node, "UV_Scroll_Y"),
}
alpha_cutoff = shader.get_float_value(node, "CutoffRate", 0, float_info.max)
if alpha_cutoff is not None:
float_properties["_Cutoff"] = alpha_cutoff
outline_width_mode = max(
0, min(2, round(shader.get_float_value_or(node, "OutlineWidthMode")))
)
float_properties["_OutlineWidthMode"] = outline_width_mode
if outline_width_mode == 1:
keyword_map["MTOON_OUTLINE_WIDTH_WORLD"] = True
elif outline_width_mode == 2:
keyword_map["MTOON_OUTLINE_WIDTH_SCREEN"] = True
outline_color_mode = max(
0, min(2, round(shader.get_float_value_or(node, "OutlineColorMode")))
)
float_properties["_OutlineColorMode"] = outline_color_mode
if outline_width_mode > 0:
if outline_color_mode == 1:
keyword_map["MTOON_OUTLINE_COLOR_MIXED"] = True
elif outline_color_mode == 2:
keyword_map["MTOON_OUTLINE_COLOR_FIXED"] = True
if material.blend_method == "OPAQUE":
tag_map["RenderType"] = "Opaque"
material_dict["alphaMode"] = "OPAQUE"
float_properties["_BlendMode"] = 0
float_properties["_SrcBlend"] = 1
float_properties["_DstBlend"] = 0
float_properties["_ZWrite"] = 1
float_properties["_OutlineCullMode"] = 1
keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
keyword_map["MTOON_DEBUG_NORMAL"] = False
keyword_map["_ALPHABLEND_ON"] = False
keyword_map["_ALPHAPREMULTIPLY_ON"] = False
render_queue = -1
elif material.blend_method == "CLIP":
tag_map["RenderType"] = "TransparentCutout"
material_dict["alphaMode"] = "MASK"
float_properties["_BlendMode"] = 1
float_properties["_SrcBlend"] = 1
float_properties["_DstBlend"] = 0
float_properties["_ZWrite"] = 1
float_properties["_OutlineCullMode"] = 1
keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
keyword_map["MTOON_DEBUG_NORMAL"] = False
keyword_map["_ALPHABLEND_ON"] = False
keyword_map["_ALPHAPREMULTIPLY_ON"] = False
keyword_map["_ALPHATEST_ON"] = True
if alpha_cutoff is not None:
material_dict["alphaCutoff"] = alpha_cutoff
render_queue = 2450
else:
tag_map["RenderType"] = "Transparent"
material_dict["alphaMode"] = "BLEND"
float_properties["_SrcBlend"] = 5
float_properties["_DstBlend"] = 10
float_properties["_OutlineCullMode"] = 1
keyword_map["MTOON_DEBUG_LITSHADERATE"] = False
keyword_map["MTOON_DEBUG_NORMAL"] = False
keyword_map["_ALPHABLEND_ON"] = True
keyword_map["_ALPHAPREMULTIPLY_ON"] = False
transparent_with_z_write = shader.get_float_value_or(
node, "TransparentWithZWrite", 0
)
if transparent_with_z_write < float_info.epsilon:
float_properties["_BlendMode"] = 2
float_properties["_ZWrite"] = 0
render_queue = 3000
else:
float_properties["_BlendMode"] = 3
float_properties["_ZWrite"] = 1
render_queue = 2501
float_properties["_CullMode"] = 2 if material.use_backface_culling else 0
# Control texture addition order for compatibility with old exporter
main_tex = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"MainTexture",
image_name_to_image_index,
use_khr_texture_transform=True,
)
if main_tex:
(
base_color_texture_dict,
main_tex_texture_property,
main_tex_vector_property,
) = main_tex
pbr_metallic_roughness_dict["baseColorTexture"] = base_color_texture_dict
texture_properties["_MainTex"] = main_tex_texture_property
vector_properties["_MainTex"] = main_tex_vector_property
# TODO: For compatibility. There might be correct configuration values
default_texture_vector_property = [0, 0, 1, 1]
shade_texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"ShadeTexture",
image_name_to_image_index,
)
if shade_texture:
_, shade_texture_texture_property, _ = shade_texture
texture_properties["_ShadeTexture"] = shade_texture_texture_property
vector_properties["_ShadeTexture"] = default_texture_vector_property
bump_scale = shader.get_float_value_or(node, "BumpScale", default_value=0.5)
float_properties["_BumpScale"] = bump_scale
bump_map = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"NormalmapTexture",
image_name_to_image_index,
use_khr_texture_transform=True,
)
if not bump_map:
bump_map = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"NomalmapTexture",
image_name_to_image_index,
use_khr_texture_transform=True,
)
if bump_map:
normal_texture_info, bump_map_texture_property, _ = bump_map
material_dict["normalTexture"] = normal_texture_info
texture_properties["_BumpMap"] = bump_map_texture_property
vector_properties["_BumpMap"] = default_texture_vector_property
keyword_map["_NORMALMAP"] = True
normal_texture_info["scale"] = bump_scale
for socket_name, texture_property_key in {
"ReceiveShadow_Texture": "_ReceiveShadowTexture",
"ShadingGradeTexture": "_ShadingGradeTexture",
}.items():
texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
socket_name,
image_name_to_image_index,
)
if not texture:
continue
_, texture_property, _ = texture
texture_properties[texture_property_key] = texture_property
vector_properties[texture_property_key] = default_texture_vector_property
emission_map = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"Emission_Texture",
image_name_to_image_index,
use_khr_texture_transform=True,
)
if emission_map:
emissive_texture_info, emission_map_texture_property, _ = emission_map
material_dict["emissiveTexture"] = emissive_texture_info
texture_properties["_EmissionMap"] = emission_map_texture_property
vector_properties["_EmissionMap"] = default_texture_vector_property
for socket_name, texture_property_key in {
"SphereAddTexture": "_SphereAdd",
"RimTexture": "_RimTexture",
"OutlineWidthTexture": "_OutlineWidthTexture",
"UV_Animation_Mask_Texture": "_UvAnimMaskTexture",
}.items():
texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
socket_name,
image_name_to_image_index,
)
if not texture:
continue
_, texture_property, _ = texture
texture_properties[texture_property_key] = texture_property
vector_properties[texture_property_key] = default_texture_vector_property
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
vrm_material_property_dict.update(
{
"name": material.name,
"shader": "VRM/MToon",
"renderQueue": render_queue,
"keywordMap": make_json(keyword_map),
"tagMap": make_json(tag_map),
"floatProperties": make_json(float_properties),
"vectorProperties": make_json(vector_properties),
"textureProperties": make_json(texture_properties),
}
)
def write_legacy_gltf_material(
self,
_progress: Progress,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_used: list[str],
buffer0: bytearray,
image_name_to_image_index: dict[str, int],
material: Material,
material_dict: dict[str, Json],
vrm_material_property_dict: dict[str, Json],
node: ShaderNodeGroup,
) -> None:
vrm_material_property_dict |= make_json_dict(
{
"name": material.name,
"shader": "VRM_USE_GLTFSHADER",
"keywordMap": {},
"tagMap": {},
"floatProperties": {},
"vectorProperties": {},
"textureProperties": {},
"extras": {"VRM_Addon_for_Blender_legacy_gltf_material": {}},
}
)
pbr_metallic_roughness_dict: dict[str, Json] = {
"baseColorFactor": list(
shader.get_rgba_value_or(
node, "base_Color", 0.0, 1.0, default_value=(1, 1, 1, 1)
)
),
"metallicFactor": shader.get_float_value_or(node, "metallic", 0.0, 1.0),
"roughnessFactor": shader.get_float_value_or(node, "roughness", 0.0, 1.0),
}
material_dict |= make_json_dict(
{
"name": material.name,
"emissiveFactor": list(
shader.get_rgb_value_or(node, "emissive_color", 0.0, 1.0)
),
"doubleSided": not material.use_backface_culling,
}
)
if material.blend_method == "OPAQUE":
material_dict["alphaMode"] = "OPAQUE"
elif material.blend_method == "CLIP":
material_dict["alphaMode"] = "MASK"
material_dict["alphaCutoff"] = material.alpha_threshold
else:
material_dict["alphaMode"] = "BLEND"
normal_texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"normal",
image_name_to_image_index,
)
if normal_texture:
(normal_texture_dict, _, _) = normal_texture
material_dict["normalTexture"] = normal_texture_dict
emissive_texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"emissive_texture",
image_name_to_image_index,
)
if emissive_texture:
(emissive_texture_dict, _, _) = emissive_texture
material_dict["emissiveTexture"] = emissive_texture_dict
base_color_texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"color_texture",
image_name_to_image_index,
)
if base_color_texture:
(base_color_texture_dict, _, _) = base_color_texture
pbr_metallic_roughness_dict["baseColorTexture"] = base_color_texture_dict
metallic_roughness_texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"metallic_roughness_texture",
image_name_to_image_index,
)
if metallic_roughness_texture:
(metallic_roughness_texture_dict, _, _) = metallic_roughness_texture
pbr_metallic_roughness_dict["metallicRoughnessTexture"] = (
metallic_roughness_texture_dict
)
occlusion_texture = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"occlusion_texture",
image_name_to_image_index,
)
if occlusion_texture:
(occlusion_texture_dict, _, _) = occlusion_texture
pbr_metallic_roughness_dict["occlusionTexture"] = occlusion_texture_dict
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
if shader.get_float_value_or(node, "unlit") > 0.5:
material_dict["extensions"] = {"KHR_materials_unlit": {}}
extensions_used.append("KHR_materials_unlit")
def write_legacy_transparent_zwrite_material(
self,
_progress: Progress,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_used: list[str],
buffer0: bytearray,
image_name_to_image_index: dict[str, int],
material: Material,
material_dict: dict[str, Json],
vrm_material_property_dict: dict[str, Json],
node: ShaderNodeGroup,
) -> None:
vector_properties: dict[str, Sequence[float]] = {}
texture_properties: dict[str, int] = {}
vrm_material_property_dict |= make_json_dict(
{
"name": material.name,
"shader": "VRM/UnlitTransparentZWrite",
"renderQueue": 2600,
"keywordMap": {},
"tagMap": {"RenderType": "Transparent"},
"floatProperties": {},
}
)
pbr_metallic_roughness_dict: dict[str, Json] = {
"baseColorFactor": [1, 1, 1, 1],
"metallicFactor": 0,
"roughnessFactor": 0.9,
}
material_dict |= make_json_dict(
{
"name": material.name,
"alphaMode": "BLEND",
"doubleSided": False,
"extensions": {"KHR_materials_unlit": {}},
}
)
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
main_tex = self.create_mtoon0_texture_info_dict(
self.context,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
node,
"Main_Texture",
image_name_to_image_index,
)
if main_tex:
(
base_color_texture_dict,
main_tex_texture_property,
main_tex_vector_property,
) = main_tex
pbr_metallic_roughness_dict["baseColorTexture"] = base_color_texture_dict
texture_properties["_MainTex"] = main_tex_texture_property
vector_properties["_MainTex"] = main_tex_vector_property
vrm_material_property_dict["vectorProperties"] = make_json(vector_properties)
vrm_material_property_dict["textureProperties"] = make_json(texture_properties)
def write_gltf_material(
self,
_progress: Progress,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_used: list[str],
buffer0: bytearray,
gltf2_io_texture_images: list[Gltf2IoTextureImage],
material: Material,
material_dict: dict[str, Json],
vrm_material_property_dict: dict[str, Json],
) -> None:
vrm_material_property_dict.update(
{
"name": material.name,
"shader": "VRM_USE_GLTFSHADER",
"keywordMap": {},
"tagMap": {},
"floatProperties": {},
"vectorProperties": {},
"textureProperties": {},
}
)
gltf2_io_material = gather_gltf2_io_material(
material, self.gltf2_addon_export_settings
)
if not gltf2_io_material:
return
try:
alpha_cutoff = convert.float_or_none(
getattr(gltf2_io_material, "alpha_cutoff", None)
)
if alpha_cutoff is not None:
material_dict["alphaCutoff"] = alpha_cutoff
alpha_mode = getattr(gltf2_io_material, "alpha_mode", None)
if isinstance(alpha_mode, str):
material_dict["alphaMode"] = alpha_mode
double_sided = getattr(gltf2_io_material, "double_sided", None)
if isinstance(double_sided, bool):
material_dict["doubleSided"] = double_sided
emissive_factor = convert.sequence_or_none(
getattr(gltf2_io_material, "emissive_factor", None)
)
if emissive_factor is not None:
material_dict["emissiveFactor"] = make_json(emissive_factor)
assign_dict(
material_dict,
"emissiveTexture",
self.create_gltf2_io_texture(
getattr(gltf2_io_material, "emissive_texture", None),
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
gltf2_io_texture_images,
),
)
extensions = convert.mapping_or_none(
getattr(gltf2_io_material, "extensions", None)
)
if extensions is not None:
extensions_dict: dict[str, Json] = {}
# https://github.com/KhronosGroup/glTF/tree/19a1d820040239bca1327fc26220ae8cae9f948c/extensions/2.0/Khronos/KHR_materials_unlit
if extensions.get("KHR_materials_unlit") is not None:
extensions_dict["KHR_materials_unlit"] = {}
extensions_used.append("KHR_materials_unlit")
# https://github.com/KhronosGroup/glTF/blob/9c4a3567384b4d9f2706cdd9623bbb5ca7b341ad/extensions/2.0/Khronos/KHR_materials_emissive_strength
khr_materials_emissive_strength = convert.mapping_or_none(
getattr(
extensions.get("KHR_materials_emissive_strength"),
"extension",
None,
)
)
if isinstance(khr_materials_emissive_strength, dict):
emissive_strength = convert.float_or_none(
khr_materials_emissive_strength.get("emissiveStrength")
)
if (
emissive_strength is not None
and emissive_strength >= 0
and emissive_strength != 1.0
):
extensions_dict["KHR_materials_emissive_strength"] = {
"emissiveStrength": emissive_strength,
}
extensions_used.append("KHR_materials_emissive_strength")
if extensions_dict:
material_dict["extensions"] = extensions_dict
assign_dict(
material_dict,
"normalTexture",
self.create_gltf2_io_texture(
getattr(gltf2_io_material, "normal_texture", None),
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
gltf2_io_texture_images,
),
)
assign_dict(
material_dict,
"occlusionTexture",
self.create_gltf2_io_texture(
getattr(gltf2_io_material, "occlusion_texture", None),
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
gltf2_io_texture_images,
),
)
pbr_metallic_roughness = getattr(
gltf2_io_material, "pbr_metallic_roughness", None
)
if pbr_metallic_roughness is not None:
pbr_metallic_roughness_dict: dict[str, Json] = {}
base_color_factor = convert.sequence_or_none(
getattr(pbr_metallic_roughness, "base_color_factor", None)
)
if base_color_factor is not None:
pbr_metallic_roughness_dict["baseColorFactor"] = make_json(
base_color_factor
)
assign_dict(
pbr_metallic_roughness_dict,
"baseColorTexture",
self.create_gltf2_io_texture(
getattr(pbr_metallic_roughness, "base_color_texture", None),
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
gltf2_io_texture_images,
),
)
metallic_factor = convert.float_or_none(
getattr(pbr_metallic_roughness, "metallic_factor", None)
)
if metallic_factor is not None:
pbr_metallic_roughness_dict["metallicFactor"] = metallic_factor
assign_dict(
pbr_metallic_roughness_dict,
"metallicRoughnessTexture",
self.create_gltf2_io_texture(
getattr(
pbr_metallic_roughness, "metallic_roughness_texture", None
),
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
gltf2_io_texture_images,
),
)
roughness_factor = convert.float_or_none(
getattr(pbr_metallic_roughness, "roughness_factor", None)
)
if roughness_factor is not None:
pbr_metallic_roughness_dict["roughnessFactor"] = roughness_factor
if pbr_metallic_roughness_dict:
material_dict["pbrMetallicRoughness"] = pbr_metallic_roughness_dict
except Exception:
logger.exception("Failed to generate glTF Material using glTF 2.0 add-on")
def write_material(
self,
progress: Progress,
material_dicts: list[dict[str, Json]],
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_vrm_material_property_dicts: list[Json],
extensions_used: list[str],
buffer0: bytearray,
material_name_to_material_index: dict[str, int],
image_name_to_image_index: dict[str, int],
gltf2_io_texture_images: list[Gltf2IoTextureImage],
material: Material,
) -> None:
material_dict: dict[str, Json] = {
"name": material.name,
}
vrm_material_property_dict: dict[str, Json] = {
"name": material.name,
"shader": "VRM_USE_GLTFSHADER",
}
material_index = len(material_dicts)
material_name_to_material_index[material.name] = material_index
material_dicts.append(material_dict)
extensions_vrm_material_property_dicts.append(vrm_material_property_dict)
mtoon1 = get_material_extension(material).mtoon1
if mtoon1.enabled:
self.write_mtoon1_downgraded_material(
progress,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
image_name_to_image_index,
material,
material_dict,
vrm_material_property_dict,
)
return
legacy_shader_node_group, legacy_shader_name = search.legacy_shader_node(
material
)
if not legacy_shader_node_group:
pass
elif legacy_shader_name == "MToon_unversioned":
self.write_legacy_mtoon_unversioned_material(
progress,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
image_name_to_image_index,
material,
material_dict,
vrm_material_property_dict,
legacy_shader_node_group,
)
return
elif legacy_shader_name == "GLTF":
self.write_legacy_gltf_material(
progress,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
image_name_to_image_index,
material,
material_dict,
vrm_material_property_dict,
legacy_shader_node_group,
)
return
elif legacy_shader_name == "TRANSPARENT_ZWRITE":
self.write_legacy_transparent_zwrite_material(
progress,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
image_name_to_image_index,
material,
material_dict,
vrm_material_property_dict,
legacy_shader_node_group,
)
return
self.write_gltf_material(
progress,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_used,
buffer0,
gltf2_io_texture_images,
material,
material_dict,
vrm_material_property_dict,
)
def write_materials(
self,
_progress: Progress,
material_dicts: list[dict[str, Json]],
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_vrm_material_property_dicts: list[Json],
extensions_used: list[str],
buffer0: bytearray,
material_name_to_material_index: dict[str, int],
image_name_to_image_index: dict[str, int],
) -> None:
gltf2_io_texture_images: list[Vrm0Exporter.Gltf2IoTextureImage] = []
for material in search.export_materials(self.context, self.export_objects):
self.write_material(
_progress,
material_dicts,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
extensions_vrm_material_property_dicts,
extensions_used,
buffer0,
material_name_to_material_index,
image_name_to_image_index,
gltf2_io_texture_images,
material,
)
def write_armature(
self,
progress: Progress,
node_dicts: list[dict[str, Json]],
accessor_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
buffer0: bytearray,
bone_name_to_node_index: dict[str, int],
) -> tuple[Sequence[int], Mapping[str, Json], Sequence[int]]:
bones = [bone for bone in self.armature.pose.bones if not bone.parent]
root_node_index = len(node_dicts)
if not bones:
logger.error("No bones")
node_dicts.append(
{
"name": self.armature.name,
}
)
return [root_node_index], {}, []
bone_name_to_inverse_bind_matrix: dict[str, Matrix] = {}
humanoid_root_bone_index: Optional[int] = None
humanoid_root_bone: Optional[PoseBone] = None
if len(bones) == 1:
humanoid_root_bone = bones[0]
humanoid_root_bone_index = self.write_armature_bone_nodes(
progress,
node_dicts,
buffer0,
bones[0],
bone_name_to_node_index,
bone_name_to_inverse_bind_matrix,
)
scene_node_indices: list[int] = [humanoid_root_bone_index]
else:
# If there are multiple root bones, expand each to the scene
# This follows the old exporter specification, but I'm not
# confident it's correct
# It might be better to create a parent bone and attach skin to it
scene_node_indices = []
for bone in bones:
root_node_index = self.write_armature_bone_nodes(
progress,
node_dicts,
buffer0,
bone,
bone_name_to_node_index,
bone_name_to_inverse_bind_matrix,
)
scene_node_indices.append(root_node_index)
# Check if there are bones belonging to Humanoid,
# and set them as humanoid_root_bone_index
humanoid = get_armature_extension(self.armature_data).vrm0.humanoid
traversing_bones = [bone]
while traversing_bones:
traversing_bone = traversing_bones.pop()
if any(
human_bone.node.bone_name == traversing_bone.name
for human_bone in humanoid.human_bones
):
humanoid_root_bone = bone
humanoid_root_bone_index = root_node_index
break
traversing_bones.extend(traversing_bone.children)
if humanoid_root_bone is None or humanoid_root_bone_index is None:
logger.error("No human bone")
return [root_node_index], {}, []
while len(buffer0) % 4:
buffer0.append(0)
inverse_bind_matrices_offset = len(buffer0)
skin_joint_node_indices: list[int] = []
inverse_bind_matrix_struct = struct.Struct("<16f")
for bone_name, node_index in bone_name_to_node_index.items():
skin_joint_node_indices.append(node_index)
inverse_bind_matrix = bone_name_to_inverse_bind_matrix.get(bone_name)
if inverse_bind_matrix is None:
message = f"No inverse bind matrix for {bone_name}"
raise AssertionError(message)
buffer0.extend(
inverse_bind_matrix_struct.pack(*itertools.chain(*inverse_bind_matrix))
)
buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": inverse_bind_matrices_offset,
"byteLength": len(buffer0) - inverse_bind_matrices_offset,
}
)
accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": buffer_view_index,
"byteOffset": 0,
"type": "MAT4",
"componentType": GL_FLOAT,
"count": len(skin_joint_node_indices),
}
)
# Hitogata 0.6.0.1 seems to error when sharing skin,
# so give each mesh its own skin with the same content
skin_dict: dict[str, Json] = {
"joints": make_json(skin_joint_node_indices),
"inverseBindMatrices": accessor_index,
"skeleton": humanoid_root_bone_index,
}
return scene_node_indices, skin_dict, skin_joint_node_indices
def write_armature_bone_nodes(
self,
progress: Progress,
node_dicts: list[dict[str, Json]],
buffer0: bytearray,
bone: PoseBone,
bone_name_to_node_index: dict[str, int],
bone_name_to_inverse_bind_matrices: dict[str, Matrix],
) -> int:
node_index = len(node_dicts)
bone_name_to_node_index[bone.name] = node_index
parent_bone = bone.parent
if parent_bone is None:
parent_world_translation = Vector((0, 0, 0))
else:
parent_world_translation = self.armature.matrix_world @ parent_bone.head
world_translation = self.armature.matrix_world @ bone.head
bone_name_to_inverse_bind_matrices[bone.name] = Matrix(
(
(1, 0, 0, 0),
(0, 1, 0, 0),
(0, 0, 1, 0),
(world_translation.x, -world_translation.z, -world_translation.y, 1),
)
)
node_dict: dict[str, Json] = {
"name": bone.name,
"translation": make_json(
convert.axis_blender_to_gltf(
world_translation - parent_world_translation
)
),
}
node_dicts.append(node_dict)
if bone.children:
node_dict["children"] = [
self.write_armature_bone_nodes(
progress,
node_dicts,
buffer0,
child,
bone_name_to_node_index,
bone_name_to_inverse_bind_matrices,
)
for child in bone.children
]
return node_index
def get_or_write_cluster_empty_material(
self,
material_dicts: list[dict[str, Json]],
extensions_vrm_material_property_dicts: list[Json],
) -> int:
# Cluster does not allow primitives without materials,
# so assign empty material.
missing_material_name = "glTF_2_0_default_material"
for i, material_dict in enumerate(material_dicts):
if material_dict.get("name") == missing_material_name:
return i
new_missing_material_index = len(material_dicts)
material_dicts.append({"name": missing_material_name})
extensions_vrm_material_property_dicts.append(
{
"name": missing_material_name,
"shader": "VRM_USE_GLTFSHADER",
"keywordMap": {},
"tagMap": {},
"floatProperties": {},
"vectorProperties": {},
"textureProperties": {},
}
)
return new_missing_material_index
def collect_vertex(
self,
obj: Object,
main_mesh_data: Mesh,
vertex_index: int,
uv_layer: Optional[MeshUVLoopLayer],
loop_index: int,
vertex_group_index_to_joint: Mapping[int, int],
bone_name_to_node_index: Mapping[str, int],
skin_joints: Sequence[int],
vertex_attributes_collector: VertexAttributesCollector,
*,
have_skin: bool,
) -> int:
texcoord = None
if uv_layer:
texcoord_u, texcoord_v = uv_layer.data[loop_index].uv
texcoord = (texcoord_u, 1 - texcoord_v)
# Use loop normals instead of vertex normals. This may lose something,
# but it's judged safer to keep it the same as the glTF 2.0 addon.
# https://github.com/KhronosGroup/glTF-Blender-IO/pull/1127
# TODO: This implementation should really average the three normals
# from the loop
normal = main_mesh_data.loops[loop_index].normal
already_added_vertex_index = (
vertex_attributes_collector.find_added_vertex_index(
blender_vertex_index=vertex_index,
normal=convert.axis_blender_to_gltf(normal),
texcoord=texcoord,
)
)
if isinstance(already_added_vertex_index, int):
return already_added_vertex_index
vertex = main_mesh_data.vertices[vertex_index]
position_x, position_y, position_z = vertex.co
joints: Optional[tuple[int, int, int, int]] = None
weights: Optional[tuple[float, float, float, float]] = None
if have_skin:
weight_and_joint_list: list[tuple[float, int]] = [
(weight, joint)
for vertex_group_element in vertex.groups
if (
(
joint := vertex_group_index_to_joint.get(
vertex_group_element.group
)
)
is not None
)
# Set joint to zero when weight is zero
# https://github.com/KhronosGroup/glTF/tree/f33f90ad9439a228bf90cde8319d851a52a3f470/specification/2.0#skinned-mesh-attributes
and not ((weight := vertex_group_element.weight) < float_info.epsilon)
]
weight_and_joint_list.sort(reverse=True)
while len(weight_and_joint_list) < 4:
weight_and_joint_list.append((0.0, 0))
weights = (
weight_and_joint_list[0][0],
weight_and_joint_list[1][0],
weight_and_joint_list[2][0],
weight_and_joint_list[3][0],
)
joints = (
weight_and_joint_list[0][1],
weight_and_joint_list[1][1],
weight_and_joint_list[2][1],
weight_and_joint_list[3][1],
)
total_weight = sum(weights)
if total_weight < float_info.epsilon:
logger.debug(
"No weight on vertex index=%d mesh=%s",
vertex_index,
main_mesh_data.name,
)
# Attach near bone
joint = None
mesh_parent: Optional[Object] = obj
while mesh_parent:
if mesh_parent.parent_type == "BONE":
if (
mesh_parent.parent == self.armature
and (
bone_index := bone_name_to_node_index.get(
mesh_parent.parent_bone
)
)
is not None
and bone_index in skin_joints
):
joint = skin_joints.index(bone_index)
break
if mesh_parent.parent_type == "OBJECT":
mesh_parent = mesh_parent.parent
else:
break
if joint is None:
# TODO: Probably better to use root bone traced from hips
# rather than hips itself
ext = get_armature_extension(self.armature_data)
for human_bone in ext.vrm0.humanoid.human_bones:
if human_bone.bone != "hips":
continue
if (
bone_index := bone_name_to_node_index.get(
human_bone.node.bone_name
)
) is not None and bone_index in skin_joints:
joint = skin_joints.index(bone_index)
if joint is None:
message = "No fallback bone index found"
if not skin_joints:
raise ValueError(message)
logger.error(message)
joint = skin_joints[0]
weights = (1.0, 0, 0, 0)
joints = (joint, 0, 0, 0)
else:
weights = (
weights[0] / total_weight,
weights[1] / total_weight,
weights[2] / total_weight,
weights[3] / total_weight,
)
return vertex_attributes_collector.add_vertex(
blender_vertex_index=vertex_index,
position=convert.axis_blender_to_gltf(
(
position_x,
position_y,
position_z,
)
),
normal=convert.axis_blender_to_gltf(normal),
texcoord=texcoord,
joints=joints,
weights=weights,
)
def collect_morph_target(
self,
main_mesh_data: Mesh,
shape_key_mesh_data: Mesh,
vertex_index: int,
uv_layer: Optional[MeshUVLoopLayer],
loop_index: int,
vertex_attributes_collector: VertexAttributesCollector,
vertex_morph_target_collector: VertexMorphTargetCollector,
vertex_index_to_morph_normal_diffs: tuple[tuple[float, float, float], ...],
) -> None:
texcoord = None
if uv_layer:
texcoord_u, texcoord_v = uv_layer.data[loop_index].uv
texcoord = (texcoord_u, 1 - texcoord_v)
gltf_vertex_index = vertex_attributes_collector.find_added_vertex_index(
blender_vertex_index=vertex_index,
normal=convert.axis_blender_to_gltf(
main_mesh_data.loops[loop_index].normal
),
texcoord=texcoord,
)
if gltf_vertex_index is None:
return
main_mesh_data_vertices = main_mesh_data.vertices
shape_key_mesh_data_vertices = shape_key_mesh_data.vertices
if vertex_index < min(
len(main_mesh_data_vertices), len(shape_key_mesh_data_vertices)
):
(
position_x,
position_y,
position_z,
) = main_mesh_data_vertices[vertex_index].co
(
shape_key_position_x,
shape_key_position_y,
shape_key_position_z,
) = shape_key_mesh_data_vertices[vertex_index].co
target_position = convert.axis_blender_to_gltf(
(
shape_key_position_x - position_x,
shape_key_position_y - position_y,
shape_key_position_z - position_z,
)
)
else:
target_position = (0, 0, 0)
if vertex_index < len(vertex_index_to_morph_normal_diffs):
morph_normal_diff = vertex_index_to_morph_normal_diffs[vertex_index]
target_normal = convert.axis_blender_to_gltf(morph_normal_diff)
else:
target_normal = (0, 0, 0)
vertex_morph_target_collector.add_vertex(
vertex_index=gltf_vertex_index,
target_position=target_position,
target_normal=target_normal,
)
def write_mesh_node(
self,
_progress: Progress,
node_dicts: list[dict[str, Json]],
mesh_dicts: list[dict[str, Json]],
skin_dicts: list[dict[str, Json]],
material_dicts: list[dict[str, Json]],
accessor_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_vrm_material_property_dicts: list[Json],
buffer0: bytearray,
object_name_to_node_index: dict[str, int],
bone_name_to_node_index: Mapping[str, int],
obj: Object,
mesh_convertible_objects: Sequence[Object],
mesh_object_name_to_mesh_index: dict[str, int],
material_name_to_material_index: Mapping[str, int],
skin_dict: Mapping[str, Json],
skin_joints: Sequence[int],
) -> Optional[int]:
if obj.type not in MESH_CONVERTIBLE_OBJECT_TYPES:
return None
have_skin = bool(skin_joints) and self.have_skin(obj)
node_index = len(node_dicts)
node_dict: dict[str, Json] = {
"name": obj.name,
# TODO: Planned for removal as it's identical to default value
"rotation": [0, 0, 0, 1],
"scale": [1, 1, 1],
}
parent_node_index = None
parent_translation = None
if have_skin:
# Becomes scene root node when there's skin
pass
else:
if (
obj.parent_type in ["ARMATURE", "OBJECT"]
and (
parent := self.get_export_parent_object(
obj, mesh_convertible_objects
)
)
and parent in self.export_objects
):
# TODO: Don't restore nested meshes for compatibility, but will
# restore in the future
# parent_translation = parent.matrix_world.to_translation()
# parent_node_index = object_name_to_node_index.get(parent.name)
pass
if obj.parent_type == "BONE" and (
bone := self.armature.pose.bones.get(obj.parent_bone)
):
parent_translation = (
self.armature.matrix_world @ bone.matrix
).to_translation()
parent_node_index = bone_name_to_node_index.get(obj.parent_bone)
if parent_node_index is not None and 0 <= parent_node_index < len(node_dicts):
parent_node_dict = node_dicts[parent_node_index]
parent_children = parent_node_dict.get("children")
if parent_children is None or not isinstance(
parent_children, MutableSequence
):
parent_children = []
parent_node_dict["children"] = parent_children
parent_children.append(node_index)
if not parent_translation:
parent_translation = Vector((0, 0, 0))
if not have_skin:
node_dict["translation"] = make_json(
convert.axis_blender_to_gltf(
obj.matrix_world.to_translation() - parent_translation
)
)
node_dicts.append(node_dict)
object_name_to_node_index[obj.name] = node_index
scene_node_index = None
if parent_node_index is None:
scene_node_index = node_index
original_mesh_convertible = obj.data
if not isinstance(original_mesh_convertible, (Curve, Mesh)):
logger.error(
"Unexpected mesh convertible object type: %s",
type(original_mesh_convertible),
)
return scene_node_index
mesh_index = len(mesh_dicts)
vertex_morph_target_collectors: dict[
str, Vrm0Exporter.VertexMorphTargetCollector
] = {}
original_shape_keys: Optional[Key] = None
if isinstance(original_mesh_convertible, Mesh):
original_mesh_convertible.calc_loop_triangles()
original_shape_keys = original_mesh_convertible.shape_keys
with save_workspace(self.context):
# TODO: Executing move for compatibility with old addon,
# but seems unnecessary
mesh_data_transform = Matrix.Identity(4)
if not have_skin:
mesh_data_transform @= Matrix.Translation(
-obj.matrix_world.to_translation()
)
mesh_data_transform @= obj.matrix_world
main_mesh_data = force_apply_modifiers(
self.context,
obj,
preserve_shape_keys=False,
transform=mesh_data_transform,
)
if main_mesh_data is None:
return scene_node_index
if bpy.app.version < (4, 1):
main_mesh_data.calc_normals_split()
material_slot_index_to_material_name: Mapping[int, str] = {
material_index: material_ref.name
for material_index, material_slot in enumerate(obj.material_slots)
if (material_ref := material_slot.material) and material_ref.name
}
material_index_to_vertex_indices: dict[int, bytearray] = {}
vertex_indices_struct = struct.Struct("<I")
vertex_group_index_to_joint: Mapping[int, int] = {
vertex_group_index: skin_joints.index(vertex_group_node_index)
for vertex_group_index, vertex_group in enumerate(obj.vertex_groups)
if (
vertex_group_node_index := bone_name_to_node_index.get(
vertex_group.name
)
)
is not None
and vertex_group_node_index in skin_joints
}
vertex_attributes_collector = Vrm0Exporter.VertexAttributesCollector()
uv_layers = main_mesh_data.uv_layers
uv_layer = uv_layers.active
main_mesh_data.calc_loop_triangles()
for loop_triangle in main_mesh_data.loop_triangles:
material_slot_index = loop_triangle.material_index
material_name = material_slot_index_to_material_name.get(
material_slot_index
)
material_index = None
if material_name:
material_index = material_name_to_material_index.get(material_name)
if material_index is None:
material_index = self.get_or_write_cluster_empty_material(
material_dicts, extensions_vrm_material_property_dicts
)
vertex_indices = material_index_to_vertex_indices.get(material_index)
if vertex_indices is None:
vertex_indices = bytearray()
material_index_to_vertex_indices[material_index] = vertex_indices
for loop_index in loop_triangle.loops:
loop = main_mesh_data.loops[loop_index]
original_vertex_index = loop.vertex_index
vertex_index = self.collect_vertex(
obj,
main_mesh_data,
original_vertex_index,
uv_layer,
loop_index,
vertex_group_index_to_joint,
bone_name_to_node_index,
skin_joints,
vertex_attributes_collector,
have_skin=have_skin,
)
vertex_indices.extend(vertex_indices_struct.pack(vertex_index))
if original_shape_keys:
no_morph_normal_export_vertex_indices: set[int] = (
self.create_no_morph_normal_export_vertex_indices(
self.context,
main_mesh_data,
)
)
reference_vertex_normal_vectors = (
Vrm0Exporter.create_export_vertex_normal_vectors(
no_morph_normal_export_vertex_indices,
main_mesh_data,
)
)
# Create mesh with modifiers applied for each shape key.
# This is because for VRM 0.x, glTF Node rotation and scale
# are normalized, but when rotation or scale is applied in
# pose mode, the weight calculation for that normalization is
# not applied to shape keys, so we need to recalculate the
# change amount for each shape key ourselves.
# There might be bad patterns where vertex indices change,
# so there's room for improvement.
for shape_key in original_shape_keys.key_blocks:
if original_shape_keys.reference_key.name == shape_key.name:
continue
shape_key.value = 1.0
self.context.view_layer.update()
shape_mesh_data = force_apply_modifiers(
self.context,
obj,
preserve_shape_keys=False,
transform=mesh_data_transform,
)
shape_key.value = 0.0
self.context.view_layer.update()
if not shape_mesh_data:
continue
if bpy.app.version < (4, 1):
shape_mesh_data.calc_normals_split()
vertex_morph_target_collector = vertex_morph_target_collectors.get(
shape_key.name
)
if vertex_morph_target_collector is None:
vertex_morph_target_collector = (
Vrm0Exporter.VertexMorphTargetCollector(
vertex_attributes_collector.count
)
)
vertex_morph_target_collectors[shape_key.name] = (
vertex_morph_target_collector
)
shape_mesh_data.calc_loop_triangles()
vertex_index_to_morph_normal_diffs = (
Vrm0Exporter.create_vertex_index_to_morph_normal_diffs(
no_morph_normal_export_vertex_indices,
shape_mesh_data,
reference_vertex_normal_vectors,
)
)
for loop_triangle in main_mesh_data.loop_triangles:
for loop_index in loop_triangle.loops:
loop = main_mesh_data.loops[loop_index]
original_vertex_index = loop.vertex_index
self.collect_morph_target(
main_mesh_data,
shape_mesh_data,
original_vertex_index,
uv_layer,
loop_index,
vertex_attributes_collector,
vertex_morph_target_collector,
vertex_index_to_morph_normal_diffs,
)
if shape_mesh_data.users:
logger.warning(
'Failed to remove "%s" with %d users while exporting'
" shape key meshes",
shape_mesh_data.name,
shape_mesh_data.users,
)
else:
self.context.blend_data.meshes.remove(shape_mesh_data)
if main_mesh_data.users:
logger.warning(
'Failed to remove "%s" with %d users while exporting meshes',
main_mesh_data.name,
main_mesh_data.users,
)
else:
self.context.blend_data.meshes.remove(main_mesh_data)
main_mesh_data = None
if not material_index_to_vertex_indices:
return scene_node_index
node_dict["mesh"] = mesh_index
primitive_dicts: list[dict[str, Json]] = []
while len(buffer0) % 4:
buffer0.append(0)
# TODO: Make buffer writing class independent
# Write indices
for (
primitive_material_index,
vertex_indices,
) in material_index_to_vertex_indices.items():
indices_buffer_offset = len(buffer0)
buffer0.extend(vertex_indices)
indices_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": indices_buffer_offset,
"byteLength": len(vertex_indices),
}
)
indices_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": indices_buffer_view_index,
"byteOffset": 0,
"type": "SCALAR",
"componentType": GL_UNSIGNED_INT,
# TODO: Avoid division as it can lead to mistakes
"count": int(len(vertex_indices) / 4),
}
)
primitive_dicts.append(
{
"material": primitive_material_index,
"indices": indices_accessor_index,
}
)
# Made attributes shared by design
primitive_attribute_dict: dict[str, Json] = {}
for primitive_dict in primitive_dicts:
primitive_dict["attributes"] = primitive_attribute_dict
position_buffer_offset = len(buffer0)
buffer0.extend(vertex_attributes_collector.position)
position_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": position_buffer_offset,
"byteLength": len(vertex_attributes_collector.position),
}
)
position_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": position_buffer_view_index,
"byteOffset": 0,
"type": "VEC3",
"componentType": GL_FLOAT,
"count": vertex_attributes_collector.count,
"min": [
vertex_attributes_collector.position_min_x,
vertex_attributes_collector.position_min_y,
vertex_attributes_collector.position_min_z,
],
"max": [
vertex_attributes_collector.position_max_x,
vertex_attributes_collector.position_max_y,
vertex_attributes_collector.position_max_z,
],
}
)
primitive_attribute_dict["POSITION"] = position_accessor_index
normal_buffer_offset = len(buffer0)
buffer0.extend(vertex_attributes_collector.normal)
normal_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": normal_buffer_offset,
"byteLength": len(vertex_attributes_collector.normal),
}
)
normal_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": normal_buffer_view_index,
"byteOffset": 0,
"type": "VEC3",
"componentType": GL_FLOAT,
"count": vertex_attributes_collector.count,
# "normalized": True, # TODO: Needs investigation
}
)
primitive_attribute_dict["NORMAL"] = normal_accessor_index
primitive_texcoord = vertex_attributes_collector.texcoord
if primitive_texcoord:
texcoord_buffer_offset = len(buffer0)
buffer0.extend(primitive_texcoord)
texcoord_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": texcoord_buffer_offset,
"byteLength": len(primitive_texcoord),
}
)
texcoord_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": texcoord_buffer_view_index,
"byteOffset": 0,
"type": "VEC2",
"componentType": GL_FLOAT,
"count": vertex_attributes_collector.count,
}
)
primitive_attribute_dict["TEXCOORD_0"] = texcoord_accessor_index
primitive_joints = vertex_attributes_collector.joints
if primitive_joints:
joints_buffer_offset = len(buffer0)
buffer0.extend(primitive_joints)
joints_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": joints_buffer_offset,
"byteLength": len(primitive_joints),
}
)
joints_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": joints_buffer_view_index,
"byteOffset": 0,
"type": "VEC4",
"componentType": GL_UNSIGNED_SHORT,
"count": vertex_attributes_collector.count,
}
)
primitive_attribute_dict["JOINTS_0"] = joints_accessor_index
primitive_weights = vertex_attributes_collector.weights
if primitive_weights:
while len(buffer0) % 4:
buffer0.append(0)
weights_buffer_offset = len(buffer0)
buffer0.extend(primitive_weights)
weights_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": weights_buffer_offset,
"byteLength": len(primitive_weights),
}
)
weights_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": weights_buffer_view_index,
"byteOffset": 0,
"type": "VEC4",
"componentType": GL_FLOAT,
"count": vertex_attributes_collector.count,
}
)
primitive_attribute_dict["WEIGHTS_0"] = weights_accessor_index
# Made targets shared by design
morph_target_names = make_json(vertex_morph_target_collectors.keys())
if vertex_morph_target_collectors:
while len(buffer0) % 4:
buffer0.append(0)
primitive_target_dicts: list[dict[str, Json]] = []
for target in vertex_morph_target_collectors.values():
target_position_buffer_offset = len(buffer0)
buffer0.extend(target.position)
target_position_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": target_position_buffer_offset,
"byteLength": len(target.position),
}
)
target_position_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": target_position_buffer_view_index,
"byteOffset": 0,
"type": "VEC3",
"componentType": GL_FLOAT,
"count": vertex_attributes_collector.count,
"min": [
target.position_min_x,
target.position_min_y,
target.position_min_z,
],
"max": [
target.position_max_x,
target.position_max_y,
target.position_max_z,
],
}
)
target_normal_buffer_offset = len(buffer0)
buffer0.extend(target.normal)
target_normal_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": target_normal_buffer_offset,
"byteLength": len(target.normal),
}
)
target_normal_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": target_normal_buffer_view_index,
"byteOffset": 0,
"type": "VEC3",
"componentType": GL_FLOAT,
"count": vertex_attributes_collector.count,
}
)
primitive_target_dicts.append(
{
"POSITION": target_position_accessor_index,
"NORMAL": target_normal_accessor_index,
}
)
for primitive_dict in primitive_dicts:
primitive_dict["targets"] = make_json(primitive_target_dicts)
primitive_dict["extras"] = {
# for compatibility with legacy practices
# https://github.com/vrm-c/UniVRM/blob/38ccb92300c9ab41c72eb3d5b8dc8ce664a659d5/Assets/UniGLTF/Runtime/UniGLTF/Format/ExtensionsAndExtras/gltf_mesh_extras_targetNames.cs#L7-L12
"targetNames": morph_target_names,
}
mesh_dict = {
"name": original_mesh_convertible.name,
"primitives": make_json(primitive_dicts),
}
if morph_target_names:
mesh_dict["extras"] = {
# targetNames is not included in glTF specification, but is used
# in many implementations
# https://github.com/KhronosGroup/glTF/blob/0251c5c0cce8daec69bd54f29f891e3d0cdb52c8/specification/2.0/Specification.adoc?plain=1#L1500-L1504
"targetNames": morph_target_names,
}
mesh_dicts.append(mesh_dict)
mesh_object_name_to_mesh_index[obj.name] = mesh_index
if skin_dict and have_skin:
# TODO: Create separate skin for each mesh
node_dict["skin"] = len(skin_dicts)
skin_dicts.append(dict(skin_dict))
return scene_node_index
def get_export_parent_object(
self, obj: Object, mesh_convertible_objects: Sequence[Object]
) -> Optional[Object]:
if obj.parent_type != "OBJECT":
return None
parent = obj.parent
while parent:
if parent in mesh_convertible_objects:
return parent
parent = parent.parent
return None
def write_mesh_nodes(
self,
progress: Progress,
node_dicts: list[dict[str, Json]],
mesh_dicts: list[dict[str, Json]],
skin_dicts: list[dict[str, Json]],
material_dicts: list[dict[str, Json]],
accessor_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
extensions_vrm_material_property_dicts: list[Json],
buffer0: bytearray,
bone_name_to_node_index: Mapping[str, int],
mesh_object_name_to_mesh_index: dict[str, int],
material_name_to_material_index: Mapping[str, int],
skin_dict: Mapping[str, Json],
skin_joints: Sequence[int],
) -> list[int]:
object_name_to_node_index: dict[str, int] = {}
node_indices: list[int] = []
mesh_convertible_objects = [
mesh_object
for mesh_object in self.export_objects
if mesh_object.type in search.MESH_CONVERTIBLE_OBJECT_TYPES
]
# Sort meshes according to parent-child relationships
while True:
swapped = False
for mesh_object in list(mesh_convertible_objects):
parent_mesh_object = self.get_export_parent_object(
mesh_object, mesh_convertible_objects
)
if not parent_mesh_object:
continue
if mesh_convertible_objects.index(
mesh_object
) < mesh_convertible_objects.index(parent_mesh_object):
mesh_convertible_objects.remove(mesh_object)
mesh_convertible_objects.append(mesh_object)
swapped = True
if not swapped:
break
for obj in self.export_objects:
node_index = None
with save_workspace(self.context, obj):
node_index = self.write_mesh_node(
progress,
node_dicts,
mesh_dicts,
skin_dicts,
material_dicts,
accessor_dicts,
buffer_view_dicts,
extensions_vrm_material_property_dicts,
buffer0,
object_name_to_node_index,
bone_name_to_node_index,
obj,
mesh_convertible_objects,
mesh_object_name_to_mesh_index,
material_name_to_material_index,
skin_dict,
skin_joints,
)
if node_index is None:
continue
node_indices.append(node_index)
return node_indices
@property
def armature_data(self) -> Armature:
if not self.armature:
message = "armature is not set"
raise AssertionError(message)
armature_data = self.armature.data
if not isinstance(armature_data, Armature):
message = f"{type(armature_data)} is not an Armature"
raise TypeError(message)
return armature_data
@staticmethod
def enter_clear_shape_key_values(
context: Context, export_objects: Sequence[Object]
) -> Mapping[tuple[str, str], float]:
mesh_name_and_shape_key_name_to_value: dict[tuple[str, str], float] = {}
mesh_objs = [obj for obj in export_objects if obj.type == "MESH"]
for mesh_obj in mesh_objs:
mesh = mesh_obj.data
if not isinstance(mesh, Mesh):
continue
shape_keys = mesh.shape_keys
if not shape_keys:
continue
key_block: Optional[ShapeKey] = None
for key_block in shape_keys.key_blocks:
mesh_name_and_shape_key_name_to_value[(mesh.name, key_block.name)] = (
key_block.value
)
key_block.value = 0
context.view_layer.update()
return mesh_name_and_shape_key_name_to_value
@staticmethod
def leave_clear_shape_key_values(
context: Context,
mesh_name_and_shape_key_name_to_value: Mapping[tuple[str, str], float],
) -> None:
for (
mesh_name,
shape_key_name,
), value in mesh_name_and_shape_key_name_to_value.items():
mesh = context.blend_data.meshes.get(mesh_name)
if not mesh:
continue
shape_keys = mesh.shape_keys
if not shape_keys:
continue
key_block = shape_keys.key_blocks.get(shape_key_name)
if not key_block:
continue
key_block.value = value
@contextmanager
def clear_shape_key_values(self) -> Iterator[Mapping[tuple[str, str], float]]:
mesh_name_and_shape_key_name_to_value = self.enter_clear_shape_key_values(
self.context, self.export_objects
)
try:
yield mesh_name_and_shape_key_name_to_value
# After yield, bpy native objects may be deleted or frames may advance
# making them invalid. Accessing them in this state can cause crashes,
# so be careful not to access potentially invalid native objects after yield
finally:
self.leave_clear_shape_key_values(
self.context, mesh_name_and_shape_key_name_to_value
)
def get_legacy_shader_images(self, material: Material) -> Sequence[Image]:
node, legacy_shader_name = search.legacy_shader_node(material)
if not node or not legacy_shader_name:
return []
images: list[Image] = []
if legacy_shader_name == "MToon_unversioned":
for (
raw_input_socket_name
) in MtoonUnversioned.texture_kind_exchange_dict.values():
# Support models that were loaded by earlier versions
# (1.3.5 or earlier), which had this typo
#
# Those models have node.inputs["NomalmapTexture"] instead of
# "NormalmapTexture". But 'shader_vals' which comes from
# MaterialMtoon.texture_kind_exchange_dict is "NormalmapTexture".
# if script reference node.inputs["NormalmapTexture"] in that
# situation, it will occur error. So change it to "NomalmapTexture"
# which is typo but points to the same thing in those models.
if (
raw_input_socket_name == "NormalmapTexture"
and "NormalmapTexture" not in node.inputs
and "NomalmapTexture" in node.inputs
):
input_socket_name = "NomalmapTexture"
elif raw_input_socket_name == "ReceiveShadow_Texture":
input_socket_name = "ReceiveShadow_Texture_alpha"
else:
input_socket_name = raw_input_socket_name
input_socket = node.inputs.get(input_socket_name)
if not input_socket:
continue
image_node = shader.search_input_node(input_socket, ShaderNodeTexImage)
if not image_node:
continue
image = image_node.image
if not image:
continue
images.append(image)
elif legacy_shader_name == "GLTF":
for input_socket_name in TEXTURE_INPUT_NAMES:
input_socket = node.inputs.get(input_socket_name)
if not input_socket:
continue
image_node = shader.search_input_node(input_socket, ShaderNodeTexImage)
if not image_node:
continue
image = image_node.image
if not image:
continue
images.append(image)
elif legacy_shader_name == "TRANSPARENT_ZWRITE":
input_socket = node.inputs.get("Main_Texture")
if not input_socket:
return []
image_node = shader.search_input_node(input_socket, ShaderNodeTexImage)
if not image_node:
return []
image = image_node.image
if not image:
return []
return [image]
return list(dict.fromkeys(images).keys()) # Remove duplicates
def create_gltf2_io_texture(
self,
gltf2_io_texture_info: object,
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
buffer0: bytearray,
gltf2_io_texture_images: list[Gltf2IoTextureImage],
) -> Json:
source = getattr(getattr(gltf2_io_texture_info, "index", None), "source", None)
if not source:
return None
source_name = getattr(source, "name", None)
if not isinstance(source_name, str):
source_name = ""
source_mime_type = getattr(source, "mime_type", None)
if not isinstance(source_mime_type, str):
source_mime_type = "image/png"
source_buffer_view_data = getattr(
getattr(source, "buffer_view", None), "data", None
)
if not isinstance(source_buffer_view_data, bytes):
return None
export_image_index = None
for gltf2_io_texture_image in gltf2_io_texture_images:
if gltf2_io_texture_image.name != source_name:
continue
if gltf2_io_texture_image.mime_type != source_mime_type:
continue
if gltf2_io_texture_image.image_bytes != source_buffer_view_data:
continue
export_image_index = gltf2_io_texture_image.export_image_index
break
if export_image_index is None:
image_base_name = re.sub(
r"^BlenderVrmAddonImport[0-9]+Image[0-9]+_", "", source_name
)
# Avoid duplicate naming for compatibility with old exporter
image_name = image_base_name
for count in range(100000):
if count:
image_name = image_base_name + "." + str(count)
if not any(
image_name == image_dict.get("name") for image_dict in image_dicts
):
break
image_buffer_view_index = len(buffer_view_dicts)
buffer_view_dicts.append(
{
"buffer": 0,
"byteOffset": len(buffer0),
"byteLength": len(source_buffer_view_data),
}
)
buffer0.extend(source_buffer_view_data)
export_image_index = len(image_dicts)
image_dict: dict[str, Json] = {
"name": image_name,
"mimeType": source_mime_type,
"bufferView": image_buffer_view_index,
}
image_dicts.append(image_dict)
gltf2_io_texture_images.append(
Vrm0Exporter.Gltf2IoTextureImage(
name=source_name,
mime_type=source_mime_type,
image_bytes=source_buffer_view_data,
export_image_index=export_image_index,
)
)
sampler = getattr(
getattr(gltf2_io_texture_info, "index", None), "sampler", None
)
wrap_s = getattr(sampler, "wrap_s", None)
wrap_t = getattr(sampler, "wrap_t", None)
mag_filter = getattr(sampler, "mag_filter", None)
min_filter = getattr(sampler, "min_filter", None)
if not isinstance(wrap_s, int):
wrap_s = GL_REPEAT
if not isinstance(wrap_t, int):
wrap_t = GL_REPEAT
if not isinstance(mag_filter, int):
mag_filter = GL_LINEAR
if not isinstance(min_filter, int):
min_filter = GL_LINEAR
sampler_dict: dict[str, Json] = {
"magFilter": mag_filter,
"minFilter": min_filter,
"wrapS": wrap_s,
"wrapT": wrap_t,
}
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_dict: dict[str, Json] = {
"sampler": sampler_index,
"source": export_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)
texture_info: dict[str, Json] = {"index": texture_index}
texture_info_scale = convert.float_or_none(
getattr(gltf2_io_texture_info, "scale", None)
)
if texture_info_scale is not None:
texture_info["scale"] = texture_info_scale
texture_info_strength = convert.float_or_none(
getattr(gltf2_io_texture_info, "strength", None)
)
if texture_info_strength is not None:
texture_info["strength"] = texture_info_strength
return make_json(texture_info)
def create_mtoon1_downgraded_texture(
self,
texture: Union[Mtoon0TexturePropertyGroup, Mtoon1TexturePropertyGroup],
texture_properties: dict[str, int],
texture_properties_key: str,
vector_properties: dict[str, Sequence[float]],
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
buffer0: bytearray,
image_name_to_image_index: dict[str, int],
) -> Optional[dict[str, Json]]:
source = texture.get_connected_node_image()
if not source:
return None
wrap_s = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
texture.sampler.wrap_s,
Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
)
wrap_t = Mtoon1SamplerPropertyGroup.wrap_enum.identifier_to_value(
texture.sampler.wrap_t,
Mtoon1SamplerPropertyGroup.WRAP_DEFAULT.value,
)
mag_filter = Mtoon1SamplerPropertyGroup.mag_filter_enum.identifier_to_value(
texture.sampler.mag_filter,
Mtoon1SamplerPropertyGroup.MAG_FILTER_DEFAULT.value,
)
min_filter = Mtoon1SamplerPropertyGroup.min_filter_enum.identifier_to_value(
texture.sampler.min_filter,
Mtoon1SamplerPropertyGroup.MIN_FILTER_DEFAULT.value,
)
sampler_dict: dict[str, Json] = {
"magFilter": mag_filter,
"minFilter": min_filter,
"wrapS": wrap_s,
"wrapT": wrap_t,
}
if sampler_dict in sampler_dicts:
sampler_index = sampler_dicts.index(sampler_dict)
else:
sampler_index = len(sampler_dicts)
sampler_dicts.append(sampler_dict)
image_index = self.find_or_create_image(
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
source,
)
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)
texture_properties[texture_properties_key] = texture_index
vector_properties[texture_properties_key] = [0, 0, 1, 1]
texture_info: dict[str, Json] = {"index": texture_index}
return texture_info
def create_mtoon1_downgraded_texture_info(
self,
texture_info: Mtoon1TextureInfoPropertyGroup,
texture_properties: dict[str, int],
texture_properties_key: str,
vector_properties: dict[str, Sequence[float]],
texture_dicts: list[dict[str, Json]],
sampler_dicts: list[dict[str, Json]],
image_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
buffer0: bytearray,
image_name_to_image_index: dict[str, int],
khr_texture_transform: Optional[Mtoon1KhrTextureTransformPropertyGroup],
) -> Optional[dict[str, Json]]:
texture_info_dict = self.create_mtoon1_downgraded_texture(
texture_info.index,
texture_properties,
texture_properties_key,
vector_properties,
texture_dicts,
sampler_dicts,
image_dicts,
buffer_view_dicts,
buffer0,
image_name_to_image_index,
)
if texture_info_dict is None:
return None
if khr_texture_transform is not None:
texture_info_dict["extensions"] = {
"KHR_texture_transform": {
"offset": list(khr_texture_transform.offset),
"scale": list(khr_texture_transform.scale),
}
}
return texture_info_dict
@staticmethod
def create_no_morph_normal_export_vertex_indices(
context: Context,
mesh_data: Mesh,
) -> set[int]:
# logger.error("CREATE UNIQ:")
# Collect vertex indices where normal difference is forced to zero
# setting is enabled
exclusion_vertex_indices: set[int] = set()
material_refs = mesh_data.materials
for polygon in mesh_data.polygons:
material_ref_index = polygon.material_index
if not (0 <= material_ref_index < len(material_refs)):
continue
material_ref = material_refs[material_ref_index]
if material_ref is None:
continue
# Use non-evaluated material
material = context.blend_data.materials.get(material_ref.name)
if material is None:
continue
material_extension = get_material_extension(material)
if material_extension.mtoon1.export_shape_key_normals:
continue
if material_extension.mtoon1.enabled:
exclusion_vertex_indices.update(polygon.vertices)
continue
node, legacy_shader_name = search.legacy_shader_node(material)
if not node:
continue
if legacy_shader_name == "MToon_unversioned":
exclusion_vertex_indices.update(polygon.vertices)
return exclusion_vertex_indices
@staticmethod
def create_export_vertex_normal_vectors(
no_morph_normal_export_vertex_indices: set[int],
mesh_data: Mesh,
) -> list[Vector]:
# Collect normal values for each shape key
# logger.error(" refkey=%s key=%s", reference_key_name, shape_key_name)
# Use split (loop) normals instead of vertex normals
# https://github.com/KhronosGroup/glTF-Blender-IO/pull/1129
vertex_normal_sum_vectors = [Vector([0.0, 0.0, 0.0])] * len(mesh_data.vertices)
for loop_triangle in mesh_data.loop_triangles:
# logger.error(" loop_triangle mat=%s", loop_triangle.material_index)
for vertex_index, normal in zip(
loop_triangle.vertices, loop_triangle.split_normals
):
# logger.error(
# " vindex=%s normal=%s",
# vertex_index,
# normal,
# )
if vertex_index in no_morph_normal_export_vertex_indices:
# logger.error(" EXCLUDE")
continue
if not (0 <= vertex_index < len(vertex_normal_sum_vectors)):
# logger.error(" OUT OF RANGE")
continue
vertex_normal_sum_vectors[vertex_index] = (
# Normally we would use the += operator, but for some
# reason the result changes so we don't use it
vertex_normal_sum_vectors[vertex_index] + Vector(normal)
)
# logger.error(
# " => %s:%s",
# vertex_normal_sum_vectors[vertex_index],
# list(vertex_normal_sum_vectors[vertex_index]),
# )
return [
Vector((0, 0, 0))
if vector.length_squared < float_info.epsilon
else vector.normalized()
for vector in vertex_normal_sum_vectors
]
@staticmethod
def create_vertex_index_to_morph_normal_diffs(
no_morph_normal_export_vertex_indices: set[int],
shape_key_mesh_data: Mesh,
reference_vertex_normal_vectors: list[Vector],
) -> tuple[tuple[float, float, float], ...]:
vertex_normal_vectors = Vrm0Exporter.create_export_vertex_normal_vectors(
no_morph_normal_export_vertex_indices,
shape_key_mesh_data,
)
# Collect normal differences from reference key for each shape key
# logger.error("REFERENCE_KEY: %s", mesh_data.shape_keys.reference_key.name)
# for v in reference_vertex_normal_vectors:
# logger.error(" %s", v)
# for n, vv in shape_key_name_to_vertex_normal_vectors.items():
# logger.error("KEY: %s", n)
# for v in vv:
# logger.error(" %s", v)
# logger.error("RESULT:")
vertex_index_to_morph_normal_diffs: tuple[tuple[float, float, float], ...] = (
tuple(
(
vertex_normal_vector.x - reference_vertex_normal_vector.x,
vertex_normal_vector.y - reference_vertex_normal_vector.y,
vertex_normal_vector.z - reference_vertex_normal_vector.z,
)
for vertex_normal_vector, reference_vertex_normal_vector in zip(
vertex_normal_vectors,
reference_vertex_normal_vectors,
)
)
)
# logger.error(
# " %s:%s",
# shape_key_name,
# vertex_index_to_morph_normal_diffs,
# )
return vertex_index_to_morph_normal_diffs
def have_skin(self, mesh: Object) -> bool:
# TODO: This method has false positives but is kept as-is for compatibility.
# In the future, this will be replaced with correct implementation
while mesh:
if any(
modifier.show_viewport and modifier.type == "ARMATURE"
for modifier in mesh.modifiers
):
return True
parent_mesh = mesh.parent
if not parent_mesh:
return True
if (
mesh.parent_type == "BONE"
and parent_mesh.type == "ARMATURE"
and mesh.parent_bone
):
return False
if (
mesh.parent_type != "OBJECT"
or parent_mesh.type not in search.MESH_CONVERTIBLE_OBJECT_TYPES
):
return True
mesh = parent_mesh
return True
def get_asset_generator(self) -> str:
addon_version = get_addon_version()
if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true":
addon_version = (999, 999, 999)
return "saturday06_blender_vrm_exporter_experimental_" + ".".join(
map(str, addon_version)
)