Skinned Instancing

  Edit on GitHub

These docs are for 0.12.0-SNAPSHOT, an unreleased SNAPSHOT. See the latest release.

Draws large crowds of animated, skinned models. A BakedAnimations bakes every animation of a skinned Model into joint matrices on the GPU once. Each BakedAnimationInstance is a Node3D with its own clip, time, and speed.

Baking

val fox =
    resourcesVfs["models/Fox.glb"].readGltfModel(
        GltfLoaderUnlitConfig(GltfModelConfig(skinned = true))
    )
val baked = BakedAnimations(device, fox, fps = 30f)

baked.clips // every baked clip
baked.clip("Run") // a clip by name, or null

Release it when done. The source model must stay loaded since its meshes and materials are shared.

onRelease {
    baked.release()
    fox.release()
}

Instances

Animations advance when update(dt) is called, e.g. on a parent node.

val horde = Node3D()
repeat(1000) {
    horde.addChild(
        baked.createInstance().apply {
            play("Run") // or play(clip, loop = true, restart = true)
            speed = 1.2f
            tint = Color.RED
            position(it % 40 * 2.5f, 0f, it / 40 * 2.5f)
        }
    )
}

onUpdate { dt ->
    horde.update(dt)
    // ...
    modelBatch.render(horde, camera, environment)
}

Use stop(), time, loop, and isFinished to control playback. Call remove() to stop drawing an instance for good.

Performance

All instances of one BakedAnimations are drawn in one instanced draw call per mesh primitive, are frustum culled, and have their frames interpolated on the GPU. 3000 animated foxes run at the 120 FPS vsync cap on the JVM.

There is no per-instance blending between clips and no procedural bones. Use a regular Model with a live skin for hero characters.

Skinned Model Copies

Copies of a skinned Model draw wherever they are placed. To pose an animation at a time without advancing it, use Animation.sample(time).

fox.animations[0].sample(0.5f)