Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Animation 🎬

GLTF files can carry animation data, and renderling can play it back. There are three related mechanisms:

  1. Animation clips - keyframed translations, rotations and scales for the nodes in a scene, played back with the Animator.
  2. Morph targets - per-vertex “shapes” blended by weight.
  3. Skins - vertices bound to a skeleton of joint transforms.

Playing an animation clip

We’ll start with a GLTF file containing an animated triangle. As usual, load it through the stage, then render the starting frame:

    use renderling::{
        camera,
        context::Context,
        glam::{Vec3, Vec4},
        stage::Stage,
    };

    let ctx = Context::headless(512, 512).await;
    let stage: Stage = ctx
        .new_stage()
        .with_lighting(false)
        .with_bloom(false)
        .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0));

    let projection = camera::perspective(512.0, 512.0);
    let view = camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y);
    let _camera = stage
        .new_camera()
        .with_projection_and_view(projection, view);

    // Load a GLTF file containing an animation.
    let doc = stage
        .load_gltf_document_from_path(workspace_dir().join("gltf/animated_triangle.gltf"))
        .unwrap();

    let frame = ctx.get_next_frame().unwrap();
    stage.render(&frame.view());
    let img_frame_0 = frame.read_image().await.unwrap();
    img_frame_0.save("animation/frame-0.png").unwrap();
    frame.present();

To play the document’s animation, collect the scene’s nodes and hand them to the Animator along with a clip:

    use renderling::gltf::Animator;

    // Collect every node in the scene, and build an `Animator` for the
    // document's first animation clip.
    let nodes = doc
        .recursive_nodes_in_scene(doc.default_scene.unwrap_or_default())
        .collect::<Vec<_>>();
    let mut animator = Animator::new(nodes, doc.animations.first().unwrap().clone());

Then advance the animation and render. In an application this happens every frame, with dt the time since the previous one:

    // Advance the animation and render frames. In an application this
    // would happen every frame, with `dt` the time since the last one.
    let dt = 1.0 / 8.0;
    let mut img_frame_2 = None;
    let mut img_frame_5 = None;
    for i in 1..=8 {
        animator.progress(dt).unwrap();

        if i == 2 || i == 5 {
            let frame = ctx.get_next_frame().unwrap();
            stage.render(&frame.view());
            let img = frame.read_image().await.unwrap();
            img.save(format!("animation/frame-{i}.png")).unwrap();
            if i == 2 {
                img_frame_2 = Some(img);
            } else {
                img_frame_5 = Some(img);
            }
            frame.present();
        }
    }
    let img_frame_2 = img_frame_2.unwrap();
    let img_frame_5 = img_frame_5.unwrap();

a triangle at the start of its animation

the same triangle, further along its animation

the same triangle, further still

The animation wraps around automatically once it runs past its last keyframe.

Morph targets

Morph targets are alternative positions (and normals/tangents) for a mesh’s vertices, blended in by weight. This is how faces emote, how lips sync, and how simple effects like this twisted quad are done:

    use renderling::{
        camera,
        context::Context,
        geometry::{MorphTarget, Vertex},
        glam::{Vec3, Vec4},
        stage::Stage,
    };

    let ctx = Context::headless(512, 512).await;
    let stage: Stage = ctx
        .new_stage()
        .with_lighting(false)
        .with_background_color(Vec4::new(0.25, 0.25, 0.25, 1.0));

    let projection = camera::perspective(512.0, 512.0);
    let view = camera::look_at(Vec3::Z * 3.0, Vec3::ZERO, Vec3::Y);
    let _camera = stage
        .new_camera()
        .with_projection_and_view(projection, view);

    // A quad.
    let quad = stage
        .new_primitive()
        .with_vertices(stage.new_vertices([
            Vertex::default().with_position([-1.0, -1.0, 0.0]),
            Vertex::default().with_position([1.0, -1.0, 0.0]),
            Vertex::default().with_position([1.0, 1.0, 0.0]),
            Vertex::default().with_position([-1.0, 1.0, 0.0]),
        ]))
        .with_material(
            stage
                .new_material()
                .with_albedo_factor(Vec4::new(0.95, 0.85, 0.1, 1.0))
                .with_has_lighting(false),
        );

    // One morph target per vertex, describing where each vertex moves at
    // full weight. Here the quad twists: the left edge comes toward the
    // camera, the right edge moves away.
    let twist = stage.new_morph_targets([vec![
        MorphTarget {
            position: Vec3::new(-1.0, -1.0, 1.0),
            ..Default::default()
        },
        MorphTarget {
            position: Vec3::new(1.0, -1.0, -1.0),
            ..Default::default()
        },
        MorphTarget {
            position: Vec3::new(1.0, 1.0, -1.0),
            ..Default::default()
        },
        MorphTarget {
            position: Vec3::new(-1.0, 1.0, 1.0),
            ..Default::default()
        },
    ]]);
    let weights = stage.new_morph_target_weights([0.0f32]);
    quad.set_morph_targets(twist, weights.clone());

    let frame = ctx.get_next_frame().unwrap();
    stage.render(&frame.view());
    let img_flat = frame.read_image().await.unwrap();
    img_flat.save("animation/morph-0.png").unwrap();
    frame.present();

    // Morph weights can be changed at runtime.
    weights.set_item(0, 1.0);

    let frame = ctx.get_next_frame().unwrap();
    stage.render(&frame.view());
    let img_twisted = frame.read_image().await.unwrap();
    img_twisted.save("animation/morph-1.png").unwrap();
    frame.present();

a flat quad at morph weight zero

the same quad twisted, at morph weight one

Here we built the morph target by hand with Stage::new_morph_targets and Stage::new_morph_target_weights, attached them with Primitive::set_morph_targets, and updated the weight at runtime. For GLTF files, the loader stages morph targets for you, and animation clips can drive the weights automatically through the Animator.

Skins

“Skinned” meshes have their vertices bound to a skeleton: each vertex follows a weighted combination of “joint” transforms, so moving a joint drags the mesh along like skin over bone. Loading a rigged GLTF file stages the skin, and rendering with skinning is enabled with Stage::set_has_vertex_skinning (or with_vertex_skinning on the stage):

stage.set_has_vertex_skinning(true);

Caveat: vertex skinning currently produces no visible effect, even with the skeleton posed - see issue #244. The manual will cover skinning in depth once that is fixed.