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

1076 lines
37 KiB
Python

# SPDX-License-Identifier: MIT OR GPL-3.0-or-later
import itertools
import struct
from collections.abc import Sequence
from os import environ
from pathlib import Path
from typing import Optional
import bpy
from bpy.types import (
Action,
Armature,
Context,
FCurve,
Object,
PoseBone,
)
from mathutils import Euler, Matrix, Quaternion, Vector
from ..common import version
from ..common.convert import Json
from ..common.deep import make_json
from ..common.gl import GL_FLOAT
from ..common.gltf import pack_glb
from ..common.logger import get_logger
from ..common.rotation import (
ROTATION_MODE_AXIS_ANGLE,
ROTATION_MODE_EULER,
ROTATION_MODE_QUATERNION,
get_rotation_as_quaternion,
)
from ..common.vrm1.human_bone import HumanBoneName, HumanBoneSpecification
from ..common.workspace import save_workspace
from ..editor.extension import get_armature_extension
from ..editor.t_pose import setup_humanoid_t_pose
from ..editor.vrm1.property_group import Vrm1PropertyGroup
logger = get_logger(__name__)
class VrmAnimationExporter:
@staticmethod
def execute(context: Context, path: Path, armature: Object) -> set[str]:
armature_data = armature.data
if not isinstance(armature_data, Armature):
return {"CANCELLED"}
with (
setup_humanoid_t_pose(context, armature),
save_workspace(context, armature, mode="POSE"),
):
output_bytes = export_vrm_animation(context, armature)
path.write_bytes(output_bytes)
return {"FINISHED"}
def connect_humanoid_node_dicts(
human_bone_specification: HumanBoneSpecification,
human_bone_name_to_node_dict: dict[HumanBoneName, dict[str, Json]],
parent_node_dict: Optional[dict[str, Json]],
node_dicts: list[dict[str, Json]],
human_bone_name_to_node_index: dict[HumanBoneName, int],
) -> None:
current_node_dict = human_bone_name_to_node_dict.get(human_bone_specification.name)
if isinstance(current_node_dict, dict):
node_index = len(node_dicts)
human_bone_name_to_node_index[human_bone_specification.name] = node_index
node_dicts.append(current_node_dict)
if parent_node_dict is not None:
children = parent_node_dict.get("children")
if not isinstance(children, list):
children = []
parent_node_dict["children"] = children
children.append(node_index)
parent_node_dict = current_node_dict
for child in human_bone_specification.children():
connect_humanoid_node_dicts(
child,
human_bone_name_to_node_dict,
parent_node_dict,
node_dicts,
human_bone_name_to_node_index,
)
def create_node_dicts(
bone: PoseBone,
parent_bone: Optional[PoseBone],
node_dicts: list[dict[str, Json]],
bone_name_to_node_index: dict[str, int],
) -> int:
node_index = len(node_dicts)
node_dict: dict[str, Json] = {"name": bone.name}
node_dicts.append(node_dict)
bone_name_to_node_index[bone.name] = node_index
matrix = parent_bone.matrix.inverted() @ bone.matrix if parent_bone else bone.matrix
translation = matrix.to_translation()
rotation = matrix.to_quaternion()
node_dict["translation"] = [
translation.x,
translation.z,
-translation.y,
]
node_dict["rotation"] = [
rotation.x,
rotation.z,
-rotation.y,
rotation.w,
]
children = [
create_node_dicts(child_bone, bone, node_dicts, bone_name_to_node_index)
for child_bone in bone.children
]
if children:
node_dict["children"] = make_json(children)
return node_index
def export_vrm_animation(context: Context, armature: Object) -> bytes:
armature_data = armature.data
if not isinstance(armature_data, Armature):
message = "Armature data is not an Armature"
raise TypeError(message)
vrm1 = get_armature_extension(armature_data).vrm1
human_bones = vrm1.humanoid.human_bones
node_dicts: list[dict[str, Json]] = []
bone_name_to_node_index: dict[str, int] = {}
bone_name_to_base_quaternion: dict[str, Quaternion] = {}
scene_node_indices: list[int] = [0]
data_path_to_bone_and_property_name: dict[str, tuple[PoseBone, str]] = {}
root_node_translation = armature.matrix_world.to_translation()
root_node_rotation = armature.matrix_world.to_quaternion()
root_node_scale = armature.matrix_world.to_scale()
root_node_dict: dict[str, Json] = {
"name": armature.name,
"translation": [
root_node_translation.x,
root_node_translation.z,
-root_node_translation.y,
],
"rotation": [
root_node_rotation.x,
root_node_rotation.z,
-root_node_rotation.y,
root_node_rotation.w,
],
"scale": [
root_node_scale.x,
root_node_scale.z,
root_node_scale.y,
],
}
root_node_child_indices: list[int] = []
node_dicts.append(root_node_dict)
for bone in armature.pose.bones:
if not bone.parent:
root_node_child_indices.append(
create_node_dicts(
bone,
None,
node_dicts,
bone_name_to_node_index,
)
)
base_quaternion: Optional[Quaternion] = None
if bone_parent := bone.parent:
base_quaternion = (
bone_parent.matrix.inverted_safe() @ bone.matrix
).to_quaternion()
else:
base_quaternion = bone.matrix.to_quaternion()
bone_name_to_base_quaternion[bone.name] = (
base_quaternion @ get_rotation_as_quaternion(bone).inverted()
)
if bone.rotation_mode == ROTATION_MODE_QUATERNION:
data_path_to_bone_and_property_name[
bone.path_from_id("rotation_quaternion")
] = (bone, "rotation_quaternion")
elif bone.rotation_mode == ROTATION_MODE_AXIS_ANGLE:
data_path_to_bone_and_property_name[
bone.path_from_id("rotation_axis_angle")
] = (bone, "rotation_axis_angle")
elif bone.rotation_mode in ROTATION_MODE_EULER:
data_path_to_bone_and_property_name[bone.path_from_id("rotation_euler")] = (
bone,
"rotation_euler",
)
else:
logger.error(
"Unexpected rotation mode for bone %s: %s",
bone.name,
bone.rotation_mode,
)
if human_bones.hips.node.bone_name == bone.name:
data_path_to_bone_and_property_name[bone.path_from_id("location")] = (
bone,
"location",
)
if root_node_child_indices:
root_node_dict["children"] = make_json(root_node_child_indices)
frame_to_timestamp_factor = context.scene.render.fps_base / float(
context.scene.render.fps
)
buffer0_bytearray = bytearray()
accessor_dicts: list[dict[str, Json]] = []
buffer_view_dicts: list[dict[str, Json]] = []
animation_sampler_dicts: list[dict[str, Json]] = []
animation_channel_dicts: list[dict[str, Json]] = []
preset_expression_dict: dict[str, dict[str, Json]] = {}
custom_expression_dict: dict[str, dict[str, Json]] = {}
frame_start = context.scene.frame_start
frame_end = context.scene.frame_end
create_expression_animation(
vrm1,
frame_start=frame_start,
frame_end=frame_end,
frame_to_timestamp_factor=frame_to_timestamp_factor,
armature_data=armature_data,
node_dicts=node_dicts,
accessor_dicts=accessor_dicts,
buffer_view_dicts=buffer_view_dicts,
animation_channel_dicts=animation_channel_dicts,
animation_sampler_dicts=animation_sampler_dicts,
scene_node_indices=scene_node_indices,
buffer0_bytearray=buffer0_bytearray,
preset_expression_dict=preset_expression_dict,
custom_expression_dict=custom_expression_dict,
)
create_node_animation(
vrm1,
frame_start=frame_start,
frame_end=frame_end,
frame_to_timestamp_factor=frame_to_timestamp_factor,
armature=armature,
data_path_to_bone_and_property_name=data_path_to_bone_and_property_name,
bone_name_to_node_index=bone_name_to_node_index,
bone_name_to_base_quaternion=bone_name_to_base_quaternion,
buffer0_bytearray=buffer0_bytearray,
buffer_view_dicts=buffer_view_dicts,
accessor_dicts=accessor_dicts,
animation_channel_dicts=animation_channel_dicts,
animation_sampler_dicts=animation_sampler_dicts,
)
look_at_target_node_index = create_look_at_animation(
vrm1,
frame_start=frame_start,
frame_end=frame_end,
frame_to_timestamp_factor=frame_to_timestamp_factor,
node_dicts=node_dicts,
accessor_dicts=accessor_dicts,
buffer_view_dicts=buffer_view_dicts,
animation_channel_dicts=animation_channel_dicts,
animation_sampler_dicts=animation_sampler_dicts,
buffer0_bytearray=buffer0_bytearray,
)
buffer_dicts: list[dict[str, Json]] = [{"byteLength": len(buffer0_bytearray)}]
human_bones_dict: dict[str, Json] = {}
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
for human_bone_name, human_bone in human_bone_name_to_human_bone.items():
bone_name = human_bone.node.bone_name
node_index = bone_name_to_node_index.get(bone_name)
if not isinstance(node_index, int):
continue
human_bones_dict[human_bone_name.value] = {"node": node_index}
addon_version = version.get_addon_version()
if environ.get("BLENDER_VRM_USE_TEST_EXPORTER_VERSION") == "true":
addon_version = (999, 999, 999)
vrmc_vrm_animation_dict: dict[str, Json] = {
"specVersion": "1.0",
"humanoid": {
"humanBones": human_bones_dict,
},
"expressions": make_json(
{
"preset": preset_expression_dict,
"custom": custom_expression_dict,
}
),
}
if look_at_target_node_index is not None:
vrmc_vrm_animation_dict["lookAt"] = {
"node": look_at_target_node_index,
"offsetFromHeadBone": list(vrm1.look_at.offset_from_head_bone),
}
vrma_dict = make_json(
{
"asset": {
"version": "2.0",
"generator": "VRM Add-on for Blender v"
+ ".".join(map(str, addon_version)),
},
"nodes": node_dicts,
"scenes": [{"nodes": scene_node_indices}],
"buffers": buffer_dicts,
"bufferViews": buffer_view_dicts,
"accessors": accessor_dicts,
"animations": [
{
"channels": animation_channel_dicts,
"samplers": animation_sampler_dicts,
}
],
"extensionsUsed": ["VRMC_vrm_animation"],
"extensions": {
"VRMC_vrm_animation": vrmc_vrm_animation_dict,
},
}
)
if not isinstance(vrma_dict, dict):
message = "vrma_dict is not a dict"
raise TypeError(message)
return pack_glb(vrma_dict, buffer0_bytearray)
def create_look_at_animation(
vrm1: Vrm1PropertyGroup,
*,
frame_start: int,
frame_end: int,
frame_to_timestamp_factor: float,
node_dicts: list[dict[str, Json]],
accessor_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
animation_channel_dicts: list[dict[str, Json]],
animation_sampler_dicts: list[dict[str, Json]],
buffer0_bytearray: bytearray,
) -> Optional[int]:
look_at_target_object = vrm1.look_at.preview_target_bpy_object
if not look_at_target_object:
return None
animation_data = look_at_target_object.animation_data
if not animation_data:
return None
action = animation_data.action
if not action:
return None
look_at_translation_offsets: list[Vector] = []
data_path = look_at_target_object.path_from_id("location")
for fcurve in get_action_fcurves(action):
if fcurve.mute:
continue
if not fcurve.is_valid:
continue
if fcurve.data_path != data_path:
continue
for frame in range(frame_start, frame_end + 1):
offset = frame - frame_start
value = float(fcurve.evaluate(frame))
if offset < len(look_at_translation_offsets):
translation_offset = look_at_translation_offsets[offset]
else:
translation_offset = Vector((0.0, 0.0, 0.0))
look_at_translation_offsets.append(translation_offset)
translation_offset[fcurve.array_index] = value
parent = look_at_target_object.parent
parent_world_matrix = parent.matrix_world if parent else Matrix()
look_at_translations = [
parent_world_matrix @ look_at_translation_offset
for look_at_translation_offset in look_at_translation_offsets
]
if not look_at_translations:
return None
look_at_target_node_index = len(node_dicts)
look_at_default_node_translation = (
look_at_target_object.matrix_world.to_translation()
)
node_dicts.append(
{
"name": look_at_target_object.name,
"translation": [
look_at_default_node_translation.x,
look_at_default_node_translation.z,
-look_at_default_node_translation.y,
],
}
)
input_byte_offset = len(buffer0_bytearray)
input_floats = [
frame * frame_to_timestamp_factor
for frame, _ in enumerate(look_at_translations)
]
input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
buffer0_bytearray.extend(input_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
input_buffer_view_index = len(buffer_view_dicts)
input_buffer_view_dict: dict[str, Json] = {
"buffer": 0,
"byteLength": len(input_bytes),
}
if input_byte_offset > 0:
input_buffer_view_dict["byteOffset"] = input_byte_offset
buffer_view_dicts.append(input_buffer_view_dict)
output_byte_offset = len(buffer0_bytearray)
gltf_translations = [
(
translation.x,
translation.z,
-translation.y,
)
for translation in look_at_translations
]
translation_floats = list(itertools.chain(*gltf_translations))
translation_bytes = struct.pack(
"<" + "f" * len(translation_floats), *translation_floats
)
buffer0_bytearray.extend(translation_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
output_buffer_view_index = len(buffer_view_dicts)
output_buffer_view_dict: dict[str, Json] = {
"buffer": 0,
"byteLength": len(translation_bytes),
}
if output_byte_offset > 0:
output_buffer_view_dict["byteOffset"] = output_byte_offset
buffer_view_dicts.append(output_buffer_view_dict)
input_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": input_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(input_floats),
"type": "SCALAR",
"min": [min(input_floats)],
"max": [max(input_floats)],
}
)
output_accessor_index = len(accessor_dicts)
gltf_translation_x_values = [t[0] for t in gltf_translations]
gltf_translation_y_values = [t[1] for t in gltf_translations]
gltf_translation_z_values = [t[2] for t in gltf_translations]
accessor_dicts.append(
{
"bufferView": output_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(look_at_translations),
"type": "VEC3",
"min": [
min(gltf_translation_x_values),
min(gltf_translation_y_values),
min(gltf_translation_z_values),
],
"max": [
max(gltf_translation_x_values),
max(gltf_translation_y_values),
max(gltf_translation_z_values),
],
}
)
animation_sampler_index = len(animation_sampler_dicts)
animation_sampler_dicts.append(
{
"input": input_accessor_index,
"output": output_accessor_index,
}
)
animation_channel_dicts.append(
{
"sampler": animation_sampler_index,
"target": {
"node": look_at_target_node_index,
"path": "translation",
},
}
)
return look_at_target_node_index
def create_expression_animation(
vrm1: Vrm1PropertyGroup,
*,
frame_start: int,
frame_end: int,
frame_to_timestamp_factor: float,
armature_data: Armature,
node_dicts: list[dict[str, Json]],
accessor_dicts: list[dict[str, Json]],
buffer_view_dicts: list[dict[str, Json]],
animation_channel_dicts: list[dict[str, Json]],
animation_sampler_dicts: list[dict[str, Json]],
scene_node_indices: list[int],
buffer0_bytearray: bytearray,
preset_expression_dict: dict[str, dict[str, Json]],
custom_expression_dict: dict[str, dict[str, Json]],
) -> None:
expression_animation_data = armature_data.animation_data
if not expression_animation_data:
return
action = expression_animation_data.action
if not action:
return
data_path_to_expression_name: dict[str, str] = {}
for (
expression_name,
expression,
) in vrm1.expressions.all_name_to_expression_dict().items():
if expression_name in [
"lookUp",
"lookDown",
"lookLeft",
"lookRight",
]:
continue
data_path_to_expression_name[expression.path_from_id("preview")] = (
expression.name
)
expression_name_to_expression_values: dict[
str, list[tuple[float, float, float]]
] = {}
expression_export_index = 0
for fcurve in get_action_fcurves(action):
if fcurve.mute:
continue
if not fcurve.is_valid:
continue
expression_name = data_path_to_expression_name.get(fcurve.data_path)
if not expression_name:
continue
for frame in range(frame_start, frame_end + 1):
expression_values = expression_name_to_expression_values.get(
expression_name
)
if not expression_values:
expression_values = []
expression_name_to_expression_values[expression_name] = (
expression_values
)
expression_values.append(
(
max(0, min(fcurve.evaluate(frame), 1)),
0,
expression_export_index / 8.0,
)
)
expression_export_index += 1
node_index: Optional[int] = None
for (
expression_name,
expression_translations,
) in expression_name_to_expression_values.items():
node_index = len(node_dicts)
node_dicts.append(
{
"name": expression_name,
}
)
if expression_name in vrm1.expressions.preset.name_to_expression_dict():
preset_expression_dict[expression_name] = {
"node": node_index,
}
else:
custom_expression_dict[expression_name] = {
"node": node_index,
}
scene_node_indices.append(node_index)
input_byte_offset = len(buffer0_bytearray)
input_floats = [
frame * frame_to_timestamp_factor
for frame, _ in enumerate(expression_translations)
]
input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
buffer0_bytearray.extend(input_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
input_buffer_view_index = len(buffer_view_dicts)
input_buffer_view_dict: dict[str, Json] = {
"buffer": 0,
"byteLength": len(input_bytes),
}
if input_byte_offset > 0:
input_buffer_view_dict["byteOffset"] = input_byte_offset
buffer_view_dicts.append(input_buffer_view_dict)
output_byte_offset = len(buffer0_bytearray)
expression_translation_floats: list[float] = list(
itertools.chain(*expression_translations)
)
translation_bytes = struct.pack(
"<" + "f" * len(expression_translation_floats),
*expression_translation_floats,
)
buffer0_bytearray.extend(translation_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
output_buffer_view_index = len(buffer_view_dicts)
output_buffer_view_dict: dict[str, Json] = {
"buffer": 0,
"byteLength": len(translation_bytes),
}
if output_byte_offset > 0:
output_buffer_view_dict["byteOffset"] = output_byte_offset
buffer_view_dicts.append(output_buffer_view_dict)
input_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": input_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(input_floats),
"type": "SCALAR",
"min": [min(input_floats)],
"max": [max(input_floats)],
}
)
output_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": output_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(expression_translations),
"type": "VEC3",
"min": [
min(values)
for values in [
[
gltf_translation[i]
for gltf_translation in expression_translations
]
for i in range(3)
]
],
"max": [
max(values)
for values in [
[
gltf_translation[i]
for gltf_translation in expression_translations
]
for i in range(3)
]
],
}
)
animation_sampler_index = len(animation_sampler_dicts)
animation_sampler_dicts.append(
{
"input": input_accessor_index,
"output": output_accessor_index,
}
)
animation_channel_dicts.append(
{
"sampler": animation_sampler_index,
"target": {"node": node_index, "path": "translation"},
}
)
def create_node_animation(
vrm1: Vrm1PropertyGroup,
*,
frame_start: int,
frame_end: int,
frame_to_timestamp_factor: float,
armature: Object,
data_path_to_bone_and_property_name: dict[str, tuple[PoseBone, str]],
bone_name_to_node_index: dict[str, int],
bone_name_to_base_quaternion: dict[str, Quaternion],
buffer0_bytearray: bytearray,
buffer_view_dicts: list[dict[str, Json]],
accessor_dicts: list[dict[str, Json]],
animation_channel_dicts: list[dict[str, Json]],
animation_sampler_dicts: list[dict[str, Json]],
) -> None:
human_bones = vrm1.humanoid.human_bones
human_bone_name_to_human_bone = human_bones.human_bone_name_to_human_bone()
animation_data = armature.animation_data
if not animation_data:
return
action = animation_data.action
if not action:
return
bone_name_to_quaternion_offsets: dict[str, list[Quaternion]] = {}
bone_name_to_euler_offsets: dict[str, list[Euler]] = {}
bone_name_to_axis_angle_offsets: dict[str, list[list[float]]] = {}
hips_translation_offsets: list[Vector] = []
for fcurve in get_action_fcurves(action):
if fcurve.mute:
continue
if not fcurve.is_valid:
continue
bone_and_property_name = data_path_to_bone_and_property_name.get(
fcurve.data_path
)
if not bone_and_property_name:
continue
bone, property_name = bone_and_property_name
for frame in range(frame_start, frame_end + 1):
offset = frame - frame_start
value = float(fcurve.evaluate(frame))
if property_name == "rotation_quaternion":
quaternion_offsets = bone_name_to_quaternion_offsets.get(bone.name)
if quaternion_offsets is None:
quaternion_offsets = []
bone_name_to_quaternion_offsets[bone.name] = quaternion_offsets
if offset < len(quaternion_offsets):
quaternion_offset = quaternion_offsets[offset]
else:
quaternion_offset = Quaternion()
quaternion_offsets.append(quaternion_offset)
quaternion_offset[fcurve.array_index] = value
elif property_name == "rotation_axis_angle":
axis_angle_offsets = bone_name_to_axis_angle_offsets.get(bone.name)
if axis_angle_offsets is None:
axis_angle_offsets = []
bone_name_to_axis_angle_offsets[bone.name] = axis_angle_offsets
if offset < len(axis_angle_offsets):
axis_angle_offset = axis_angle_offsets[offset]
else:
axis_angle_offset = [0.0, 0.0, 0.0, 0.0]
axis_angle_offsets.append(axis_angle_offset)
axis_angle_offset[fcurve.array_index] = value
elif property_name == "rotation_euler":
euler_offsets = bone_name_to_euler_offsets.get(bone.name)
if euler_offsets is None:
euler_offsets = []
bone_name_to_euler_offsets[bone.name] = euler_offsets
if offset < len(euler_offsets):
euler_offset = euler_offsets[offset]
else:
euler_offset = Euler((0, 0, 0))
euler_offsets.append(euler_offset)
indices = {
"XYZ": [0, 1, 2],
"XZY": [0, 2, 1],
"YXZ": [1, 0, 2],
"YZX": [1, 2, 0],
"ZXY": [2, 0, 1],
"ZYX": [2, 1, 0],
}
index = indices.get(bone.rotation_mode)
if index is None:
continue
euler_offset[index[fcurve.array_index]] = value
elif property_name == "location":
if offset < len(hips_translation_offsets):
translation_offset = hips_translation_offsets[offset]
else:
translation_offset = Vector((0.0, 0.0, 0.0))
hips_translation_offsets.append(translation_offset)
translation_offset[fcurve.array_index] = value
bone_name_to_quaternions: dict[str, list[Quaternion]] = {}
for bone_name, quaternion_offsets in bone_name_to_quaternion_offsets.items():
base_quaternion = bone_name_to_base_quaternion.get(bone_name)
if base_quaternion is None:
continue
bone_name_to_quaternions[bone_name] = [
# Muted items and other factors may cause quaternion values to be
# denormalized, so we normalize them
base_quaternion @ quaternion_offset.normalized()
for quaternion_offset in quaternion_offsets
]
for bone_name, euler_offsets in bone_name_to_euler_offsets.items():
base_quaternion = bone_name_to_base_quaternion.get(bone_name)
if base_quaternion is None:
continue
bone_name_to_quaternions[bone_name] = [
base_quaternion @ euler.to_quaternion() for euler in euler_offsets
]
for bone_name, axis_angle_offsets in bone_name_to_axis_angle_offsets.items():
base_quaternion = bone_name_to_base_quaternion.get(bone_name)
if base_quaternion is None:
continue
bone_name_to_quaternions[bone_name] = [
base_quaternion
@ Quaternion(
(axis_angle_offset[0], axis_angle_offset[1], axis_angle_offset[2]),
axis_angle_offset[3],
).normalized()
for axis_angle_offset in axis_angle_offsets
]
# Export rotation
for bone_name, quaternions in bone_name_to_quaternions.items():
human_bone_name = next(
(
n
for n, human_bone in human_bone_name_to_human_bone.items()
if human_bone.node.bone_name == bone_name
),
None,
)
if human_bone_name is None:
logger.error("Failed to find human bone name for bone %s", bone_name)
continue
if human_bone_name in [HumanBoneName.RIGHT_EYE, HumanBoneName.LEFT_EYE]:
continue
node_index = bone_name_to_node_index.get(bone_name)
if not isinstance(node_index, int):
logger.error("Failed to find node index for bone %s", bone_name)
continue
input_byte_offset = len(buffer0_bytearray)
input_floats = [
frame * frame_to_timestamp_factor for frame, _ in enumerate(quaternions)
]
input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
buffer0_bytearray.extend(input_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
input_buffer_view_index = len(buffer_view_dicts)
input_buffer_view_dict: dict[str, Json] = {
"buffer": 0,
"byteLength": len(input_bytes),
}
if input_byte_offset > 0:
input_buffer_view_dict["byteOffset"] = input_byte_offset
buffer_view_dicts.append(input_buffer_view_dict)
output_byte_offset = len(buffer0_bytearray)
gltf_quaternions = [
(
quaternion.x,
quaternion.z,
-quaternion.y,
quaternion.w,
)
for quaternion in quaternions
]
quaternion_floats: list[float] = list(itertools.chain(*gltf_quaternions))
quaternion_bytes = struct.pack(
"<" + "f" * len(quaternion_floats), *quaternion_floats
)
buffer0_bytearray.extend(quaternion_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
output_buffer_view_index = len(buffer_view_dicts)
output_buffer_view_dict: dict[str, Json] = {
"buffer": 0,
"byteLength": len(quaternion_bytes),
}
if output_byte_offset > 0:
output_buffer_view_dict["byteOffset"] = output_byte_offset
buffer_view_dicts.append(output_buffer_view_dict)
input_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": input_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(input_floats),
"type": "SCALAR",
"min": [min(input_floats)],
"max": [max(input_floats)],
}
)
output_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": output_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(quaternions),
"type": "VEC4",
"min": [
min(values)
for values in [
[gltf_quaternion[i] for gltf_quaternion in gltf_quaternions]
for i in range(4)
]
],
"max": [
max(values)
for values in [
[gltf_quaternion[i] for gltf_quaternion in gltf_quaternions]
for i in range(4)
]
],
}
)
animation_sampler_index = len(animation_sampler_dicts)
animation_sampler_dicts.append(
{
"input": input_accessor_index,
"output": output_accessor_index,
}
)
animation_channel_dicts.append(
{
"sampler": animation_sampler_index,
"target": {"node": node_index, "path": "rotation"},
}
)
# Export hips translation
hips_bone_name = human_bones.hips.node.bone_name
hips_bone = armature.pose.bones.get(hips_bone_name)
if not hips_bone:
return
hips_node_index = bone_name_to_node_index.get(hips_bone_name)
if not isinstance(hips_node_index, int):
return
if hips_bone.parent:
base_matrix = hips_bone.parent.matrix.inverted_safe()
else:
base_matrix = Matrix()
hips_translations = [
# TODO: Find the correct alignment
base_matrix @ hips_bone.matrix @ hips_translation_offset
for hips_translation_offset in hips_translation_offsets
]
if not hips_translations:
return
input_byte_offset = len(buffer0_bytearray)
input_floats = [
frame * frame_to_timestamp_factor for frame, _ in enumerate(hips_translations)
]
input_bytes = struct.pack("<" + "f" * len(input_floats), *input_floats)
buffer0_bytearray.extend(input_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
input_buffer_view_index = len(buffer_view_dicts)
input_buffer_view_dict = {
"buffer": 0,
"byteLength": len(input_bytes),
}
if input_byte_offset > 0:
input_buffer_view_dict["byteOffset"] = input_byte_offset
buffer_view_dicts.append(input_buffer_view_dict)
output_byte_offset = len(buffer0_bytearray)
gltf_translations = [
(
translation.x,
translation.z,
-translation.y,
)
for translation in hips_translations
]
translation_floats: list[float] = list(itertools.chain(*gltf_translations))
translation_bytes = struct.pack(
"<" + "f" * len(translation_floats), *translation_floats
)
buffer0_bytearray.extend(translation_bytes)
while len(buffer0_bytearray) % 32 != 0: # TODO: Find the correct alignment
buffer0_bytearray.append(0)
output_buffer_view_index = len(buffer_view_dicts)
output_buffer_view_dict = {
"buffer": 0,
"byteLength": len(translation_bytes),
}
if output_byte_offset > 0:
output_buffer_view_dict["byteOffset"] = output_byte_offset
buffer_view_dicts.append(output_buffer_view_dict)
input_accessor_index = len(accessor_dicts)
accessor_dicts.append(
{
"bufferView": input_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(input_floats),
"type": "SCALAR",
"min": [min(input_floats)],
"max": [max(input_floats)],
}
)
output_accessor_index = len(accessor_dicts)
gltf_translation_x_values = [t[0] for t in gltf_translations]
gltf_translation_y_values = [t[1] for t in gltf_translations]
gltf_translation_z_values = [t[2] for t in gltf_translations]
accessor_dicts.append(
{
"bufferView": output_buffer_view_index,
"componentType": GL_FLOAT,
"count": len(hips_translations),
"type": "VEC3",
"min": [
min(gltf_translation_x_values),
min(gltf_translation_y_values),
min(gltf_translation_z_values),
],
"max": [
max(gltf_translation_x_values),
max(gltf_translation_y_values),
max(gltf_translation_z_values),
],
}
)
animation_sampler_index = len(animation_sampler_dicts)
animation_sampler_dicts.append(
{
"input": input_accessor_index,
"output": output_accessor_index,
}
)
animation_channel_dicts.append(
{
"sampler": animation_sampler_index,
"target": {"node": hips_node_index, "path": "translation"},
}
)
def get_action_fcurves(action: Action) -> Sequence[FCurve]:
if bpy.app.version < (4, 4):
return list(action.fcurves)
from bpy.types import ActionKeyframeStrip
# https://developer.blender.org/docs/release_notes/4.4/python_api/#deprecated
layers = action.layers
if not layers:
return []
layer = layers[0]
strips = layer.strips
if not strips:
return []
strip = strips[0]
if not isinstance(strip, ActionKeyframeStrip):
return []
slots = action.slots
if not slots:
return []
slot = slots[0]
channelbag = strip.channelbag(slot)
return list(channelbag.fcurves)