Foundations

glTF Explained: The Format Behind Real-Time 3D

A detailed map of glTF 2.0 scenes, nodes, meshes, accessors, materials, animation, extensions, and the byte costs they represent.

glTF 2.0 is an open standard for delivering render-ready 3D scenes. It is often called the “JPEG of 3D,” but the analogy is incomplete: a JPEG stores one raster image, while glTF can describe a hierarchy of objects, multiple meshes and materials, cameras, animation clips, skinning, morph targets, and references to binary and image resources.

The useful way to learn glTF is not to memorize its JSON schema. Follow one rendered triangle from a scene root to a node, from the node to a mesh primitive, from the primitive to typed accessors, and from those accessors to byte ranges in a buffer. The same reference pattern is used throughout the format.

What glTF is designed to do#

The Khronos glTF 2.0 specification describes glTF as a runtime-neutral transmission format. Its data model is close to the resources a renderer needs, which reduces format-specific processing after download.

glTF deliberately is not three things:

  • It is not an authoring format. It does not preserve Blender modifiers, sculpt layers, procedural node graphs, construction history, or application-specific editing state.
  • It is not a scene-streaming protocol. Buffer data can be fetched in ranges, but glTF 2.0 does not define a complete streaming or level-of-detail system.
  • It is not a guarantee of efficiency. A valid glTF can contain millions of redundant triangles, oversized textures, hundreds of materials, or extensions that a target viewer cannot decode.

Use .blend, .ma, .max, or another editable project as the source of truth. Treat glTF or GLB as a produced artifact that can be regenerated for a delivery target.

The top-level object graph#

A glTF JSON document contains indexed arrays. Objects refer to one another with integer indices rather than embedded copies. A minimal relationship looks like this:

scene 0
  └─ node 0
      └─ mesh 0
          └─ primitive 0
              ├─ POSITION → accessor 2 → bufferView 1 → buffer 0
              ├─ NORMAL   → accessor 3 → bufferView 1 → buffer 0
              ├─ TEXCOORD → accessor 4 → bufferView 1 → buffer 0
              ├─ indices  → accessor 1 → bufferView 0 → buffer 0
              └─ material → material 0 → texture 0 → image 0

This indirection is not needless bureaucracy. It lets multiple objects share data, lets the format describe raw bytes without rewriting them as JSON numbers, and maps cleanly to GPU buffers and draw calls.

The asset object records the glTF version. The optional scene property chooses the default scene. A file may contain several scenes, although many applications load only the default one.

Scenes and nodes#

A scene is a list of root-node indices. A node can contain:

  • a local transform;
  • child-node indices;
  • one mesh;
  • one camera;
  • one skin reference;
  • extension-defined content such as a punctual light.

Nodes form a directed hierarchy. A child transform is evaluated relative to its parent. glTF permits either a 4 × 4 matrix or separate translation, rotation, and scale properties. When translation/rotation/scale are used, the specification composes them as T × R × S: scale acts first on a vertex, then rotation, then translation.

Rotation is stored as a quaternion with four components, not Euler angles. Translation defaults to [0, 0, 0], rotation to [0, 0, 0, 1], and scale to [1, 1, 1]. Omitting identity transforms saves JSON and avoids implying an unnecessary node operation.

A node hierarchy affects more than organization. Deep or highly fragmented hierarchies add transform updates and traversal work. Negative scale can reverse triangle winding and tangent handedness. Unapplied parent transforms can make exported bounds surprising. Inspect hierarchy and world-space dimensions before blaming mesh data.

Meshes and primitives#

A mesh is an array of mesh primitives. A primitive binds one topology, one set of vertex attributes, optional indices, and one material. The glTF specification notes that primitives correspond to the data required for GPU draw calls.

The normal topology is mode 4, TRIANGLES. For an indexed triangle primitive, the index accessor count is three times the triangle count. A 100,000-triangle mesh therefore uses 300,000 indices. If those indices are 32-bit unsigned integers, the raw index buffer is about 1.2 MB:

300,000 indices × 4 bytes = 1,200,000 bytes

The primitive's attribute object can reference accessors with standardized meanings:

  • POSITION: three-dimensional positions;
  • NORMAL: surface-normal vectors;
  • TANGENT: four components; xyz is direction and w is handedness;
  • TEXCOORD_0, TEXCOORD_1: UV sets;
  • COLOR_0: vertex color;
  • JOINTS_0 and WEIGHTS_0: skinning influences.

Every attribute accessor in one primitive has the same element count. In other words, index 125 selects position 125, normal 125, UV 125, and every other attribute at 125 as one complete rendered vertex. If two adjacent faces share a position but require different UVs or normals, that point must be represented by separate attribute tuples. This is why a Blender mesh with 40,000 control points can export as 55,000 glTF vertices around UV seams and flat-shaded edges. The Blender glTF exporter documentation explicitly calls out this behavior.

Buffers, buffer views, and accessors#

These three layers answer different questions:

  • A buffer answers: where is the block of bytes?
  • A bufferView answers: which byte range is relevant, and is it vertex or index data?
  • An accessor answers: how should that range be interpreted?

An accessor specifies a component type, an element shape, a count, and offsets. For example:

{
  "bufferView": 3,
  "componentType": 5126,
  "count": 50000,
  "type": "VEC3"
}

Component type 5126 means 32-bit floating point. VEC3 means three components per element. This accessor therefore describes 50,000 × 3 × 4 = 600,000 bytes of position values, assuming tightly packed data.

Add common attributes for the same 50,000 rendered vertices:

POSITION: 3 floats × 4 bytes = 12 bytes/vertex
NORMAL:   3 floats × 4 bytes = 12 bytes/vertex
TEXCOORD: 2 floats × 4 bytes =  8 bytes/vertex
Total:                          32 bytes/vertex

50,000 × 32 = 1,600,000 bytes

That is before indices, tangents, colors, skinning attributes, morph targets, alignment, or compression. The calculation is a quick way to test whether a report makes sense. Quantizing attributes to smaller component types can materially reduce these buffers, but precision must be tested against the asset's scale and visual requirements.

The Khronos buffers and accessors tutorial illustrates offsets, byte stride, and alignment. Misaligned or out-of-range accessors are format errors, not artistic imperfections.

Materials, textures, images, and samplers#

glTF core uses a physically based metallic-roughness material model. A primitive points to a material; the material can supply factors and texture references for base color, metallic/roughness, normal mapping, occlusion, and emissive output.

The texture chain has three distinct objects:

  • image identifies encoded pixel data such as PNG or JPEG bytes;
  • sampler controls minification/magnification filtering and wrapping;
  • texture pairs an image source with a sampler;
  • textureInfo inside a material chooses the texture and UV set.

Keeping these roles separate permits one image to be sampled in different ways or one sampler definition to be reused.

Color-space rules are semantic. Base-color and emissive RGB values are encoded with an sRGB transfer function. Metallic, roughness, normal, and occlusion data are linear. Treating a roughness map as sRGB changes the numeric values and therefore the highlights. In the core packed metallic-roughness image, roughness is read from the green channel and metalness from blue; other channels do not contribute to those two values.

The normal texture changes the shading normal but does not move the surface or silhouette. It is useful for fine relief, not for replacing the outline of a handle, spike, or beveled corner.

Animation, skins, and morph targets#

An animation contains samplers and channels. Do not confuse an animation sampler with a texture sampler: the animation sampler connects an input accessor of keyframe times with an output accessor of values and specifies interpolation. A channel targets a node's translation, rotation, scale, or morph-target weights.

A skin supplies joint-node indices and, normally, inverse-bind matrices. Skinned primitives provide JOINTS_0 and WEIGHTS_0 attributes. Each rendered vertex can then be transformed by a weighted combination of joints.

A morph target stores attribute deltas—commonly position, normal, or tangent changes. Its storage grows with the number of affected vertices and targets. Ten dense position morph targets on 50,000 vertices, stored as float VEC3 values, represent up to 6,000,000 raw bytes before compression:

10 targets × 50,000 vertices × 3 components × 4 bytes = 6,000,000 bytes

This is why removing or sparsifying unused shape keys before export can be more valuable than a modest triangle reduction on an animated face.

Extensions: used versus required#

glTF extensions add capabilities without changing the core schema. Examples include geometry compression, GPU texture formats, punctual lights, clearcoat, transmission, and variants.

Two top-level arrays communicate compatibility:

  • extensionsUsed lists extensions present in the asset.
  • extensionsRequired lists the subset a loader must support to render the asset correctly.

If KHR_materials_clearcoat is used but not required, a loader may fall back to the core material and lose the clearcoat layer. If KHR_draco_mesh_compression is required and there is no uncompressed fallback, a loader without Draco cannot recover the mesh. Audit extensions against the specific viewer rather than assuming that “glTF support” includes the whole registry.

Extensions prefixed KHR are Khronos-ratified. EXT extensions have multi-vendor interest. Vendor prefixes such as MSFT identify vendor-defined behavior. Status does not automatically determine quality, but it affects expected portability and where to find authoritative semantics.

.gltf, embedded .gltf, and .glb#

The data model is the same in three common packages:

  1. A .gltf JSON file references external .bin and image files.
  2. A .gltf JSON file embeds resources as base64 data URIs.
  3. A .glb binary container includes JSON and one binary chunk.

Data URIs increase the encoded resource length by roughly one third because 3 source bytes become 4 base64 characters. GLB avoids that expansion and is easier to upload as one artifact. External files allow independent caching and replacement. Pick packaging after considering the delivery system; do not confuse packaging with mesh or texture optimization.

How to trace a rendering bug through glTF#

Use the reference chain instead of changing random exporter settings.

A material is wrong#

  1. Identify the rendered primitive.
  2. Read its material index.
  3. Inspect material factors, alphaMode, doubleSided, and extensions.
  4. Follow each textureInfo to a texture, then image and sampler.
  5. Verify the UV-set index and color-space semantics.
  6. Test the same asset in the Khronos Sample Viewer to separate asset errors from application errors.

Geometry is missing or exploded#

  1. Check the node transform and all parent transforms.
  2. Inspect the primitive mode and index accessor.
  3. Verify accessor count, component type, min/max, and byte offsets.
  4. Confirm indices are within the vertex-accessor count.
  5. Check required compression extensions and decoder configuration.
  6. Run the official validator before attempting repair.

Shading has seams#

  1. Confirm NORMAL data exists and is unit length.
  2. If a normal map is used, inspect TANGENT and TEXCOORD_0.
  3. Check mirrored UV islands and tangent handedness.
  4. Compare hard-edge splits with UV seams.
  5. Determine whether the importer regenerated normals or tangents.

A practical inspection workflow#

For every delivery asset:

  • Run the Khronos glTF Validator and resolve errors.
  • Inspect counts and resources with glTF-Transform or an equivalent structured report.
  • List extensionsUsed and extensionsRequired.
  • Calculate raw attribute and index bytes from accessor types and counts.
  • Record every image's MIME type, dimensions, and encoded byte length.
  • Count primitives and materials, not only meshes.
  • Record scene bounds, units, and important world transforms.
  • Test materials in a neutral reference viewer and the production viewer.
  • Profile decoded memory and frame cost on a target device.
  • Keep the authoring source and export settings with the delivery artifact.

Sources and further reading#

Once the reference graph makes sense, glTF stops looking like a wall of JSON. It becomes a compact set of render resources with explicit relationships—and every size, appearance, or compatibility problem has a path you can trace.

Keep learning

Related guides

Foundations10 min

What Is a GLB File?

A practical guide to the binary glTF container: its byte layout, contents, loading behavior, inspection tools, and limits.

glbgltf3d-formats
Foundations14 min

Skeletal Rigging Basics: How Bones and Skin Weights Drive a 3D Character

Learn how joints, bone hierarchies, and skin weights make a 3D character move, and how a rigged GLB actually deforms in web and game engines.

riggingskeletal animationskin weights
Foundations10 min

Quad Retopology for Beginners: Build a Clean Mesh That Keeps the Shape

Learn what quad retopology is, why artists use it, and how to rebuild a dense sculpt or scan as a practical low-resolution mesh.

quad retopologyretopologytopology