- 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
1076 lines
37 KiB
Python
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)
|