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) | Attribute | Size |
|---|---|---|
0..2 | position | 3 |
3..5 | normal | 3 |
6..7 | uv | 2 |
8..10 | colour (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:
| Offset | Attribute |
|---|---|
0..10 | the 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..15 | world matrix (mat4, column-major) |
16..18 | base colour RGB |
19 | opacity |
20 | metallic |
21 | roughness |
22 | hasTexture flag (0/1) |
23 | picking 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:
| Attribute | Runtime | Shipped |
|---|---|---|
| Position | f32×3 | u16×3 over the mesh AABB |
| Normal | f32×3 | oct-encoded int8×2 |
| UV | f32×2 | u16×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.htmlor hosting folder.
See also
- Static Batching - how the 12-float chunk is built
- Export game.html - where quantization and compression run
- Performance Systems - culling and batching over these buffers
- Data Formats & Versions - the scene/project/ship version numbers
- Materials & Hook Shaders - the material fields packed into the Model record