Skip to content

Mesh Format

Awaken uses one interleaved runtime vertex layout, a slightly wider variant for merged static chunks, a separate per-instance record, lossless vertex welding at import, and lossy geometry quantization only in the shipped file.

The runtime vertex - 11 floats

Every mesh the renderer draws is interleaved into an 11-float vertex (packages/render/src/vertexPack.ts, VERTEX_FLOATS = 11), a 44-byte stride:

Offset (floats)AttributeSize
0..2position3
3..5normal3
6..7uv2
8..10colour (per-vertex)3

interleaveMesh() packs the separate attribute arrays into this layout and computes the mesh's local bounding sphere in the same pass (used for culling). Missing UVs default to 0; missing colours default to white. The same function is used for full uploads and for CPU skinned-mesh updates, so both pack identically. Per-vertex colour is only applied to untextured meshes - textured meshes ignore it (imported FBX often stores wind/AO data there).

Static-merge chunks - 12 floats

Static batching merges many local meshes into one chunk drawn in a single call. Merged vertices need to know which object they belong to, so a chunk vertex adds a u32 objIndex after the colour, giving a 12-float (48-byte) stride:

OffsetAttribute
0..10the 11-float runtime vertex
44 (byte)objIndex (u32) → static-model slot

The objIndex selects that object's transform from a stable per-object storage buffer in the vertex shader, which is why moving a merged object is a one-slot write and never a re-merge.

The per-instance record - 24 floats

Transforms and material parameters are not in the vertex buffer - they live in a separate per-instance Model record (packages/render/src/instanceLayout.ts, MODEL_FLOATS = 24), one 96-byte slot per object, indexed by instance in the vertex shader:

Offset (floats)Field
0..15world matrix (mat4, column-major)
16..18base colour RGB
19opacity
20metallic
21roughness
22hasTexture flag (0/1)
23picking id (cache slot + 1)

Keeping the matrix and material in a persistent instance buffer - separate from geometry - means a moved object only rewrites its 24 floats, and instances of the same mesh collapse into one instanced draw. This layout is unit-tested so it can't drift from the WGSL Model struct.

Welding at import - lossless

Imported geometry is frequently fully de-indexed - three vertices per triangle with zero sharing. weldMesh() (packages/assets/src/weld.ts) merges vertices that are bit-identical across every attribute the renderer uses (position, normal, uv, colour) and rebuilds the index buffer to reference the shared vertices.

Because only exactly-equal vertices collapse, the expanded geometry is unchanged - a flat-shaded hard edge keeps its split (its verts have different normals), so the render is pixel-identical. Welding typically cuts vertex count ~2×, which shrinks mesh memory and the exported game, and speeds rendering and shadow/VRAM cost. Skinned meshes are skipped (their parallel joint/weight arrays would also need remapping).

Quantization - shipped file only

Welding is lossless and always applied; quantization is lossy and applied only to the shipped game file (packages/assets/src/quantize.ts). It cuts per-vertex bytes from ~32 to ~12 before compression, using standard glTF KHR_mesh_quantization-style encoding:

AttributeRuntimeShipped
Positionf32×3u16×3 over the mesh AABB
Normalf32×3oct-encoded int8×2
UVf32×2u16×2 over the UV bbox

The player dequantizes each attribute back to Float32 at load, so the renderer's vertex layout and shaders are completely untouched - quantization is purely a smaller-on-disk representation, invisible at runtime. Because the error is sub-visible and gzip/Zstd already captures a lot of redundancy, quantization is a modest but free win layered under the real compression. This is why the shipped scene format has its own version (SCENE_FILE_VERSION).

📸 Screenshot - save as img/render-mesh-weld-stats.png

The Asset Browser or import summary showing a mesh's vertex count before and after welding, illustrating the ~2× reduction.

Where each format is used

  • 11-float vertex - all in-editor and in-Play rendering.
  • 12-float chunk - merged static scenery only.
  • 24-float Model - the per-object instance buffer, every frame.
  • Weld - at import, in memory and in the saved project.
  • Quantize - only when exporting a game.html or hosting folder.

See also

Awaken — browser-native WebGPU game engine.