# GLB geometry budget audit

An original Goblin3D accounting exercise, checked on 4 October 2026. These files inspect geometry declarations and local attribute data. They do **not** benchmark a generation product, render a scene, test LOD transitions, or predict frame rate.

## Run locally

Requires Python 3.9 or newer, standard library only. The inspector makes no network requests and never uploads the input.

```sh
python3 inspect_glb.py path/to/model.glb > audit.json
python3 inspect_glb.py path/to/model.glb --scene 0 > scene-zero-audit.json
python3 test-budget.py
python3 test-budget.py --write-fixtures synthetic-fixtures
```

Use the optional scene override only when you mean to select that scene. Without it, the inspector uses the asset's declared default scene. If no default exists, `selected_scene` is null. It never silently chooses scene zero.

## What the fields mean

- `stored.assembled_triangles`: sum over every mesh primitive definition once, including meshes unreachable from the selected scene. This is not binary-data deduplication. TRIANGLES uses element count divided by three; TRIANGLE_STRIP and TRIANGLE_FAN use element count minus two. An element is an index when indexed, otherwise an implicit position-row index. Degenerate triangle slots remain included.
- `stored.primitive_position_rows`: sum of the POSITION accessor count for every primitive. Shared accessors can appear more than once in this sum.
- `stored.unique_position_accessor_rows`: POSITION rows counted once per distinct accessor ID. This does not deduplicate matching coordinates or overlapping binary ranges.
- `per_primitive[].distinct_local_position_tuples`: count of exactly equal decoded FLOAT XYZ tuples across all rows of that primitive's POSITION accessor. It is not a topology/weld test, does not apply node transforms, and can include rows not referenced by an index.
- `per_primitive[].normal_evidence`: when FLOAT VEC3 normals are present, counts matching-position rows with different normal values. No shading-quality judgment is made.
- `selected_scene.assembled_triangles`: triangle slots expanded for all mesh copies reachable in that scene. Ordinary mesh-node reuse and EXT_mesh_gpu_instancing are counted; the latter replaces the base copy rather than adding one.
- `selected_scene.primitive_position_rows`: the per-primitive POSITION row sum multiplied by scene copies. It is not allocated memory or actual shader invocations.
- Material references are distinct material IDs used by primitives; absent material is null rather than material zero. Declared-but-unused materials are separately visible.

Primitive counts are not measured draw calls. Visibility, culling, batching, multiple passes and engine decisions change rendering work. This tool does not evaluate textures, skins, morphs, animation cost, surface quality, or correspondence between proposed LODs.

## Deliberately bounded input

Accepts GLB 2.0 with one embedded buffer, FLOAT VEC3 positions/normals, unsigned scalar indices, byte offsets/strides, and core primitive modes. Supported instancing reads attribute counts. Files over 64 MiB or decoded accessors over two million rows are refused.

Sparse decoded accessors, compressed geometry (Draco or meshopt), quantized positions, external buffers, MSFT_lod and unknown required extensions are refused with exit status 2. The script uses core fallback semantics for other optional extensions; it is not a general extension interpreter. Do not remove required extensions merely to bypass a refusal. Export a separate uncompressed inspection copy and retain the original if needed. This is a bounded inspector, not the [Khronos glTF Validator](https://github.com/KhronosGroup/glTF-Validator).

## Actual public-sample observations

Both samples were downloaded at pinned Khronos glTF-Sample-Assets commit `edc7c9e67c639d230715049ee31f9a96a6babbbe`. See `source-provenance.json` for exact source, license, timestamp and SHA-256 records.

| Sample | File bytes | Triangle slots | POSITION rows | Distinct local XYZ tuples | Material references | Mesh copies in default scene |
|---|---:|---:|---:|---:|---:|---:|
| Box | 1,664 | 12 | 24 | 8 | 1 | 1 |
| BoxTextured | 5,956 | 12 | 24 | 8 | 1 | 1 |

Both contain six normal directions and three normal values at each of eight XYZ coordinates: 24 distinct position/normal pairs. Box has no texture-coordinate accessor. Consequently, these files do not establish that adding the texture caused a vertex-count increase. The split is already present in the untextured sample.

[Box](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/edc7c9e67c639d230715049ee31f9a96a6babbbe/Models/Box) and [BoxTextured](https://github.com/KhronosGroup/glTF-Sample-Assets/tree/edc7c9e67c639d230715049ee31f9a96a6babbbe/Models/BoxTextured) are credited to Cesium, copyright 2017, under CC BY 4.0. The BoxTextured license excludes logo/trademark rights. **No third-party GLB, image, texture or documentation is redistributed in this bundle.** Only measurement results and source links are included. All bundled GLBs are original synthetic fixtures.

## Synthetic tests

`test-budget.py` includes 24 tests covering indexed/nonindexed triangles, strips/fans, strided data and accessor offsets, shared positions, material zero versus no material, selected-scene copies, GPU instance counts, absent default scenes, unreachable meshes, normal discontinuities, degenerate slots, and explicit refusal of malformed or unsupported inputs.

The eight generated fixtures are accounting examples, not product outputs. A single indexed square has two stored triangle slots and four POSITION rows. Two scene nodes referencing that mesh produce four scene triangle slots while leaving its stored count at two. Three GPU instances produce six scene triangle slots, not eight. Two material primitives sharing one POSITION accessor produce eight per-primitive rows but four unique-accessor rows.

## Primary technical references

- [glTF 2.0 geometry and accessors](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes)
- [Khronos-maintained Blender exporter documentation, pinned source](https://github.com/KhronosGroup/glTF-Blender-IO/blob/6e6990a22385bc7325c205ad98ab4c404568c7e1/docs/blender_docs/scene_gltf2.rst#meshes): Blender's glTF export triangulates polygons and preserves discontinuous attributes in separate rows
- [EXT_mesh_gpu_instancing](https://github.com/KhronosGroup/glTF/tree/main/extensions/2.0/Vendor/EXT_mesh_gpu_instancing): extension node instance counts replace a non-instanced base rendering
