Animation¶
Skewer supports keyframe animation for any transformable element in the scene graph. This guide covers the animation system, motion blur, and how to create animated renders.
Overview¶
Animation in Skewer is defined through keyframed transforms on scene graph nodes. Each keyframe specifies a time and a transform (translation, rotation, scale), and Skewer interpolates between them during rendering.
{
"name": "earth_orbit",
"transform": {
"keyframes": [
{ "time": 0, "rotate": [0, 0, 0] },
{ "time": 1, "rotate": [0, 360, 0] }
]
},
"children": [
{
"type": "obj",
"file": "Earth.obj",
"transform": { "translate": [42.5, 0, 0], "scale": 4 }
}
]
}
In this example, a group node rotates 360 degrees around the Y-axis from time 0 to time 1, carrying its child (the Earth model) in an orbital path.
The scene camera can also be keyframed with camera.keyframes. Camera keyframes use the same time
axis and patch-style carry-forward behavior, but animate camera fields instead of TRS:
"camera": {
"look_from": [0, 2, 5],
"look_at": [0, 0, 0],
"vup": [0, 1, 0],
"vfov": 50,
"aperture_radius": 0.02,
"focus_distance": 5,
"keyframes": [
{ "time": 0, "look_from": [0, 2, 5] },
{ "time": 2, "look_from": [3, 2, 5], "focus_distance": 3, "aperture_radius": 0.08 }
]
}
Moving cameras are evaluated at each sampled ray time, so camera motion contributes true motion blur. Camera animation does not expand BVH/TLAS bounds because the scene geometry has not moved, but every render layer becomes frame-varying when the camera has more than one keyframe.
Keyframe Structure¶
Each keyframe in the keyframes array has:
| Field | Type | Required | Description |
|---|---|---|---|
time |
float | Yes | Time value in arbitrary animation units |
translate |
Vec3 | No | Position [x, y, z]. Omitted fields accumulate from previous keyframe |
rotate |
Vec3 | No | Rotation in degrees [rx, ry, rz] (Euler angles). Accumulates from previous |
scale |
float or Vec3 | No | Uniform scale (number) or per-axis scale [sx, sy, sz]. Accumulates from previous |
curve |
string or object | No (default "linear") |
Interpolation curve to the next keyframe |
TRS Accumulation¶
Keyframes use accumulative interpolation: when a field is omitted from a keyframe, it retains the value from the previous keyframe (or the default for the first keyframe — translate [0,0,0], rotate [0,0,0], scale [1,1,1]).
"keyframes": [
{ "time": 0, "translate": [0, 0, 0] },
{ "time": 1, "translate": [5, 0, 0] },
{ "time": 2 }
]
- At time 0: translate =
[0, 0, 0] - At time 1: translate =
[5, 0, 0] - At time 2: translate =
[5, 0, 0](accumulated from time 1, since omitted)
This means the object moves from origin to [5, 0, 0] in the first second, then stays there.
Rotation Accumulation¶
Rotation values accumulate, which enables continuous spinning:
"keyframes": [
{ "time": 0, "rotate": [0, 0, 0] },
{ "time": 0.5, "rotate": [0, 180, 0] },
{ "time": 1, "rotate": [0, 360, 0] },
{ "time": 1.5, "rotate": [0, 540, 0] },
{ "time": 2, "rotate": [0, 720, 0] }
]
The object completes two full rotations over 2 time units. The rotation is interpolated via spherical linear interpolation (SLERP) on quaternions, which avoids gimbal lock and produces smooth rotation.
Clamping¶
When the animation time falls outside the keyframe range, the transform is clamped to the nearest endpoint:
t <= first_keyframe.time→ returns first keyframe's transformt >= last_keyframe.time→ returns last keyframe's transform
There is no automatic looping or ping-ponging — you must design keyframes to cover the full animation duration.
Interpolation Curves¶
The curve field on each keyframe controls how the interpolation eases between that keyframe and the next.
Preset Curves¶
| Preset | Description | Visual |
|---|---|---|
"linear" |
Constant speed (default) | Straight line |
"ease-in" |
Slow start, fast end | Accelerating |
"ease-out" |
Fast start, slow end | Decelerating |
"ease-in-out" |
Slow start and end, fast middle | S-curve |
Custom Bezier Curves¶
For fine-grained control, specify a cubic Bezier curve:
The four values are [p1x, p1y, p2x, p2y] — the control points of a cubic Bezier where P0=(0,0) and P3=(1,1) are fixed. This is the same format as CSS cubic-bezier().
The curve maps a normalized time parameter u in [0, 1] to an eased value in [0, 1]. Skewer uses Newton's method to solve for the Bezier parameter t given u, then evaluates the Y component.
Curve Assignment¶
The curve on a keyframe governs the interpolation from that keyframe to the next. The curve on the last keyframe is ignored (there is no next keyframe to interpolate toward).
Scene Graph Transforms¶
Static Transforms¶
A transform without keyframes is a single static TRS:
Transform Inheritance¶
Transforms compose down the scene graph. A child node's world transform is the composition of all ancestor transforms with its own:
The composition follows standard TRS math:
- Scale: parent_scale × child_scale (component-wise)
- Rotation: parent_rotation × child_rotation (quaternion multiplication)
- Translation: parent_translation + parent_rotation × (parent_scale × child_translation)
Animated Groups¶
When a group node has an animated transform, all children inherit the animated transform at each ray time:
This is the standard pattern for orbital animation: animate the group's rotation, and children orbit around the group's origin.
Motion Blur¶
Motion blur is produced by sampling rays at random times within the camera's shutter interval:
How It Works¶
- Each ray sample gets a random time:
ray_time = shutter_open + random() × (shutter_close - shutter_open) - Animated transforms are evaluated at
ray_timeviaAnimatedTransform::Evaluate(ray_time) - Bounding volumes are expanded to cover the full shutter interval for acceleration structure correctness
- The accumulation of samples at different times produces motion blur
Matching Shutter to Animation¶
The shutter interval determines how much motion is captured:
If animation runs from time 0 to time 1:
shutter [0, 0.1] → captures 10% of total motion (light blur)
shutter [0, 0.5] → captures 50% of total motion (medium blur)
shutter [0, 1.0] → captures full motion (heavy blur)
Shutter Interval
For a specific motion blur amount, set shutter_close - shutter_open to the fraction of the animation you want blurred. A 10% blur on a 1-second animation uses [0, 0.1].
Performance Impact¶
Motion blur significantly increases noise because each sample effectively renders a different frame of the animation. Expect to need 2-4× more samples than a static scene for comparable quality.
Bounding Volume Expansion¶
Animated objects require expanded bounding volumes for the BVH acceleration structure. The bounding box is computed as the union of the object's bounds at shutter_open and shutter_close. This can make the BVH less efficient for fast-moving objects, slightly increasing ray intersection cost.
Acceleration Structure for Animation¶
Skewer uses a two-level BVH:
- Bottom-level BVH: Static mesh geometry, built once per layer load
- Top-level BVH (TLAS): Instance bounds over the shutter window; animated instance transforms are evaluated at ray time during traversal
For animated instances, the TLAS evaluates transforms at the ray's time. For static instances, the transform is precomputed at t=0.
Animated cameras only change primary rays and deep-output camera depth projection; they do not require
different BLAS/TLAS bounds.
Static vs Animated Instances¶
An instance is classified as animated if any node in its transform chain has more than one keyframe. Static instances use a single precomputed transform, which is faster for ray traversal. If only part of a scene is animated, ensure the animated nodes are in separate groups to minimize the number of animated instances.
Common Patterns¶
Orbital Motion¶
{
"name": "orbit_group",
"transform": {
"keyframes": [
{ "time": 0, "rotate": [0, 0, 0] },
{ "time": 1, "rotate": [0, 360, 0] }
]
},
"children": [
{
"type": "obj",
"file": "planet.obj",
"transform": { "translate": [10, 0, 0] }
}
]
}
Linear Translation¶
{
"name": "moving_camera_rig",
"transform": {
"keyframes": [
{ "time": 0, "translate": [-5, 2, 0] },
{ "time": 1, "translate": [5, 2, 0], "curve": "ease-in-out" }
]
},
"children": [ ... ]
}
Pulsing Scale¶
{
"transform": {
"keyframes": [
{ "time": 0, "scale": 1.0 },
{ "time": 0.5, "scale": 1.2, "curve": "ease-out" },
{ "time": 1, "scale": 1.0, "curve": "ease-in" }
]
}
}
See Also¶
- Scene Format — Transform and keyframe syntax reference
- Rendering Tips — Motion blur quality and performance tips
- CLI Reference — Command-line options
- Mathematical Foundations — Interpolation and quaternion math
- Compositing — Layer compositing with loom
- Architecture Overview — System design