Skip to content

glTF models

Load .gltf and .glb models and draw them by path: the geometry, its textures, and for skinned models, the skeleton and clips.

component: mesh_renderer .gltf · .glb Vulkan WebGPU Desktop · Android · iOS · Web

There is no separate model component. A mesh_renderer whose mesh is a path ending in .gltf or .glb draws that model, once the app's glTF resolver has loaded it. Two modules do the work: awake:asset:gltf parses the file (GltfParser), and awake:scene:gltf turns it into meshes and materials for a scene (GltfAssetResolver).

Place a model

model.scene.json
{
  "version": 1,
  "nodes": [
    {
      "name": "statue",
      "components": [
        {
          "component": "mesh_renderer",
          "mesh": "assets/models/statue.gltf",
          "material": "lit-shadow"
        }
      ]
    }
  ]
}
Kotlin
onReady {
    world.scene {
        entity("statue") {
            meshRenderer(
                requireMesh("assets/models/statue.gltf"),
                requireMaterial(models.materialName("assets/models/statue.gltf")),
            )
        }
    }
}
  1. Choose File > Import Asset... and pick a .glb or .gltf. Studio copies it into the project's assets/models/ folder.
  2. Open the model from the Files tab.
  3. Click Place in Scene. Studio adds an entity with a mesh_renderer naming the model's path.

Import a .glb to bring one self-contained file. A .gltf that refers to separate .bin or image files needs those copied too.

Both forms build the same MeshRenderer. An untextured model draws with lit-shadow; a model with a base colour texture gets a material named gltf-material:<path>. materialName(path) returns the right one. A textured model draws through the render plan's PipelineKey.Format(VertexFormat.PositionNormalColorUv) pipeline (PackShaderSets.Textured, with GroupBindings.StandardMaterial), so the plan needs one.

Load models in your app

The resolver must load a model before a scene names it. Register it next to the other assets:

Kotlin
val models = GltfAssetResolver().apply { setAssetSource(files) }
models.preload("assets/models/statue.gltf")
val game = app {
    scene("gallery") {
        assets {
            builtInSceneAssets() // "lit-shadow" and the built-in meshes
            resolver(models) // any mesh name ending in .gltf or .glb
        }
        scene(document)
    }
}

setAssetSource tells the resolver where to read files, including a .gltf's separate buffers and images. builtInSceneAssets() registers lit-shadow, which the resolver does not provide.

To play a whole AwakeKt Studio project, loadProject reads the project, loads every model its entry scene names, and runProject wires the resolver and the systems for you:

Kotlin
val project = loadProject(files)
val game = app { scene("play") { runProject(project) } }

runProject also starts every skinned model's first clip on a loop; see Animation.

What is supported

Feature Support
Buffers Embedded data: URIs, the GLB binary chunk, and separate .bin files read through the asset source.
Geometry Positions, normals, COLOR_0 (VEC3), TEXCOORD_0, 8-, 16- and 32-bit indices. Normalised integer attributes, as KHR_mesh_quantization writes them.
Scenes Every mesh and primitive in the default scene, with node matrices or TRS baked in.
Materials Base colour, metallic-roughness, normal, occlusion and emissive textures; OPAQUE and MASK alpha. baseColorFactor is multiplied into the vertex colour.
Skins JOINTS_0 (8- or 16-bit) and WEIGHTS_0, inverse bind matrices, up to 64 joints.
Animation Translation, rotation and scale channels, sampled linearly.

Not supported: Draco and meshopt compression, sparse accessors, morph targets, other texture coordinate sets, texture samplers, and BLEND alpha (drawn as opaque).

How it works

preload(path) parses the file once and keeps the result. For a .glb, the resolver keeps static geometry. For a .gltf, it also keeps the skeleton, skins and clips, and getLoadedScene(path) returns them. When a scene asks for the mesh, a skinned .gltf becomes a PositionNormalColorSkin mesh (use the material skinned-material); anything else becomes static geometry.

A skinned .gltf is also split into parts, gltf-primitive:<path>#<index>, and materialSlots(path) lists each part's mesh and material. Every primitive of a skinned mesh is a part with its own material, and so is every primitive of a mesh node with no skin. A rigid node parented to a joint is bound to that joint and follows it as the clip plays; one under no joint draws in place as static geometry. The skinned nodes' first primitives are numbered first, then their other primitives, then the rigid nodes', so the parts a saved scene names keep their meaning.

Static geometry is rebuilt, not copied. Node transforms are baked into the vertices, normals are recomputed from the triangles, and baseColorFactor is multiplied into the vertex colour. A model with several primitives and materials is split into one mesh per primitive, named gltf-primitive:<path>#<index>; materialSlots(path) lists each primitive's mesh and material.

Preload before the scene loads

A scene that names a model the resolver has not loaded fails with "glTF asset '…' has not been preloaded".

A .glb does not animate

The resolver reads skins and clips only from .gltf files. A skinned .glb draws as static geometry.

Material factors are not applied for you

Apart from baseColorFactor, a model's factors (metallicFactor, roughnessFactor, emissiveFactor) are read, and materialParameters(path) returns them, but nothing puts them on the entity. Add a pbr_material with the values you want.

Missing textures do not stop the model

If a model's textures fail to load, it draws untextured with lit-shadow, and the scene.gltf logger warns "Drawing '…' untextured".

See also