- PODTimeSamples: Add add_typed_array_sample() and get_typed_array_at() to store TypedArray<T> as 8-byte packed pointers instead of full Value objects - Tydra: Redesign animation system to match glTF/Three.js architecture - Replace Animation struct with AnimationClip containing KeyframeSampler and AnimationChannel - Use flat float arrays for keyframe storage (matches Three.js KeyframeTrack) - Remove node_animations from Node struct - animations now reference nodes by index - Add AnimationPath enum (Translation/Rotation/Scale/Weights) matching glTF paths - Add AnimationInterpolation enum (Linear/Step/CubicSpline) matching glTF spec - Documentation: Add THREEJS_ANIMATION.md with Three.js animation system reference - Documentation: Add ANIMATION_SYSTEM_REDESIGN.md with migration guide This makes Tydra animations directly compatible with Three.js AnimationClip and glTF 2.0 animation structure for easier web export. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
9.4 KiB
Three.js Animation System Documentation
Overview
This document describes Three.js's keyframe animation system with a focus on rigid node animations (translation, scale, rotation/quaternion), particularly as it relates to glTF loading and the design considerations for Tydra RenderScene conversion.
Core Animation Architecture
1. Animation System Hierarchy
The Three.js animation system consists of three main components:
Keyframes (raw data)
↓
KeyframeTrack (property animation)
↓
AnimationClip (collection of tracks)
↓
AnimationMixer (playback control)
2. KeyframeTrack Types
Three.js provides specialized track types for different properties:
- VectorKeyframeTrack: For
positionandscale(3D vectors) - QuaternionKeyframeTrack: For
rotation(quaternions) - NumberKeyframeTrack: For single scalar values or individual components
- ColorKeyframeTrack: For color animations
- BooleanKeyframeTrack: For boolean properties
- StringKeyframeTrack: For string properties
Rigid Node Animation Properties
Translation (Position)
Track Type: VectorKeyframeTrack
Property Path: .position
Value Type: 3D vector [x, y, z]
Units: Scene units (typically meters in glTF)
const positionKF = new THREE.VectorKeyframeTrack(
'.position',
[0, 1, 2], // Times in seconds
[0, 0, 0, // Position at t=0: (x=0, y=0, z=0)
30, 0, 0, // Position at t=1: (x=30, y=0, z=0)
0, 0, 0] // Position at t=2: (x=0, y=0, z=0)
);
Scale
Track Type: VectorKeyframeTrack
Property Path: .scale
Value Type: 3D vector [x, y, z]
Units: Scale factors (1.0 = no scaling)
const scaleKF = new THREE.VectorKeyframeTrack(
'.scale',
[0, 1, 2], // Times in seconds
[1, 1, 1, // Scale at t=0
2, 2, 2, // Scale at t=1 (2x in all axes)
1, 1, 1] // Scale at t=2
);
Rotation
Quaternion Rotation (Recommended)
Track Type: QuaternionKeyframeTrack
Property Path: .quaternion
Value Type: Quaternion [x, y, z, w]
Units: Unit quaternion (normalized)
const quaternionKF = new THREE.QuaternionKeyframeTrack(
'.quaternion',
[0, 1, 2], // Times in seconds
[0, 0, 0, 1, // Identity quaternion at t=0
0, 0.707, 0, 0.707, // 90° Y rotation at t=1
0, 0, 0, 1] // Identity at t=2
);
Important: Three.js animations use quaternions for rotations, NOT Euler angles. This avoids gimbal lock and provides smooth interpolation via spherical linear interpolation (slerp).
Euler Angle Rotation (Limited Support)
While Object3D.rotation stores Euler angles, animations cannot directly target Euler angles. You must either:
- Use quaternions (recommended)
- Animate individual rotation components:
// Animate Y-axis rotation only
const rotationYKF = new THREE.NumberKeyframeTrack(
'.rotation[y]', // Note the bracket notation
[0, 1, 2],
[0, Math.PI/2, 0] // Radians
);
Euler Angle Specifications
Units
All angles in Three.js are in RADIANS, not degrees.
Rotation Order
- Default:
'XYZ' - Available orders:
'XYZ','YZX','ZXY','XZY','YXZ','ZYX' - Type: Intrinsic Tait-Bryan angles
- Meaning: Rotations are performed with respect to the local coordinate system
- For 'XYZ': Rotate around local X, then local Y, then local Z
Setting Euler Order
object.rotation.order = 'YXZ'; // Change rotation order
glTF Animation Mapping
glTF to Three.js Translation
When GLTFLoader loads animations, it maps:
| glTF Path | Three.js Track Type | Three.js Property |
|---|---|---|
translation |
VectorKeyframeTrack | .position |
rotation |
QuaternionKeyframeTrack | .quaternion |
scale |
VectorKeyframeTrack | .scale |
weights |
NumberKeyframeTrack | .morphTargetInfluences[i] |
glTF Animation Structure
{
"animations": [{
"channels": [{
"sampler": 0,
"target": {
"node": 0,
"path": "rotation" // or "translation", "scale"
}
}],
"samplers": [{
"input": 0, // Accessor for time values
"output": 1, // Accessor for property values
"interpolation": "LINEAR" // or "STEP", "CUBICSPLINE"
}]
}]
}
Interpolation Modes
-
STEP: Discrete, no interpolation
- Three.js:
THREE.InterpolateDiscrete
- Three.js:
-
LINEAR: Linear interpolation
- Position/Scale: Linear interpolation
- Rotation: Spherical linear interpolation (slerp)
- Three.js:
THREE.InterpolateLinear
-
CUBICSPLINE: Cubic spline with tangents
- Requires in-tangent, value, out-tangent for each keyframe
- Three.js: Custom interpolant in GLTFLoader
- Can cause issues; "Always Sample Animation" in Blender forces LINEAR
Loading and Playing glTF Animations
const loader = new THREE.GLTFLoader();
loader.load('model.gltf', (gltf) => {
scene.add(gltf.scene);
// Create AnimationMixer
const mixer = new THREE.AnimationMixer(gltf.scene);
// Play all animations
gltf.animations.forEach((clip) => {
mixer.clipAction(clip).play();
});
// Update in render loop
function animate(deltaTime) {
mixer.update(deltaTime);
}
});
Property Path Syntax
Three.js supports complex property paths:
'.position' // Object position
'.rotation[x]' // X component of rotation
'.scale' // Object scale
'.material.opacity' // Material opacity
'.bones[R_hand].scale' // Specific bone scale
'.materials[3].diffuse[r]' // Red channel of 4th material
'.morphTargetInfluences[0]' // First morph target
Data Storage Format
Keyframe data is stored in flat arrays:
// For VectorKeyframeTrack (3 values per keyframe)
times = [0, 1, 2];
values = [x0, y0, z0, x1, y1, z1, x2, y2, z2];
// For QuaternionKeyframeTrack (4 values per keyframe)
times = [0, 1, 2];
values = [x0, y0, z0, w0, x1, y1, z1, w1, x2, y2, z2, w2];
Complete Animation Example
// Create keyframe tracks
const positionKF = new THREE.VectorKeyframeTrack(
'.position',
[0, 1, 2],
[0, 0, 0, 10, 5, 0, 0, 0, 0]
);
const quaternionKF = new THREE.QuaternionKeyframeTrack(
'.quaternion',
[0, 1, 2],
[0, 0, 0, 1,
0, 0.707, 0, 0.707,
0, 0, 0, 1]
);
const scaleKF = new THREE.VectorKeyframeTrack(
'.scale',
[0, 1, 2],
[1, 1, 1, 2, 2, 2, 1, 1, 1]
);
// Create animation clip
const clip = new THREE.AnimationClip('Action', 2, [
positionKF,
quaternionKF,
scaleKF
]);
// Create mixer and play
const mixer = new THREE.AnimationMixer(mesh);
const action = mixer.clipAction(clip);
action.play();
// Update in render loop
function animate() {
const delta = clock.getDelta();
mixer.update(delta);
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
Design Considerations for Tydra RenderScene
Key Points for Implementation
-
Use Quaternions for Rotations
- Store rotations as quaternions to match Three.js animation system
- Avoid Euler angles in animation data
-
Radians for All Angles
- Ensure all angle values are in radians
- Convert from degrees if necessary
-
Flat Array Storage
- Store keyframe values in flat arrays
- Values array length = times array length × components per value
-
Support Multiple Interpolation Modes
- Implement STEP, LINEAR, and optionally CUBICSPLINE
- Use slerp for quaternion interpolation
-
Time in Seconds
- Animation times should be in seconds (floating point)
- Support arbitrary time ranges
-
Property Path Mapping
- Map USD properties to Three.js property paths
- Support nested property access
Recommended Data Structure
struct AnimationChannel {
enum PropertyType {
TRANSLATION, // vec3
ROTATION, // quat
SCALE // vec3
};
PropertyType type;
std::vector<float> times; // Keyframe times in seconds
std::vector<float> values; // Flat array of values
InterpolationType interpolation; // STEP, LINEAR, CUBICSPLINE
int nodeIndex; // Target node
};
struct AnimationClip {
std::string name;
float duration;
std::vector<AnimationChannel> channels;
};
Known Issues and Workarounds
-
Cubic Spline Issues: GLTFLoader has had problems with cubic spline interpolation. Consider using LINEAR interpolation or implementing custom handling.
-
Rotation > 360°: When exporting from Blender, rotations over 360° may not export correctly to glTF. Use quaternions to avoid this issue.
-
Gimbal Lock: Using Euler angles can cause gimbal lock. Always prefer quaternions for rotations.
-
Performance: Large numbers of keyframes can impact performance. Consider optimizing by reducing keyframe count where possible.