Triangle Count vs. File Size: Why the Numbers Do Not Move Together
Calculate how indices, rendered vertices, attributes, textures, animation, and compression contribute to GLB size before choosing decimation.
Triangle count measures surface topology. File size measures encoded bytes. The two often correlate because more triangles can require more indices and vertices, but the relationship is neither fixed nor necessarily dominant. A 50,000-triangle model with four large texture images can be bigger than a 1-million-triangle untextured scan. Cutting both meshes in half will not produce the same byte reduction.
Before decimating, split the GLB into components and estimate each one. This prevents a common failure: damaging a silhouette to save 400 KB while leaving 18 MB of images untouched.
What one indexed triangle costs#
An indexed triangle references three vertex indices. If indices use 16-bit unsigned integers, the raw index cost is 3 × 2 = 6 bytes per triangle. With 32-bit indices, it is 3 × 4 = 12 bytes.
For 100,000 triangles:
- 16-bit indices require approximately 600,000 raw bytes;
- 32-bit indices require approximately 1,200,000 raw bytes.
The mesh also needs vertex attributes. A common uncompressed layout might include:
- position: three 32-bit floats, 12 bytes;
- normal: three 32-bit floats, 12 bytes;
- tangent: four 32-bit floats, 16 bytes;
- primary UV: two 32-bit floats, 8 bytes;
- total: 48 bytes per rendered vertex.
If 100,000 triangles use 70,000 distinct rendered vertices, those attributes occupy about 3,360,000 raw bytes. Adding 32-bit indices gives roughly 4,560,000 bytes before alignment, other attributes, morph targets, compression, or container data.
This example shows why “12 bytes per triangle” is incomplete. Indices reference vertex rows; the attribute rows often cost more.
Triangle count does not determine vertex count#
In a fully disconnected triangle list, 100,000 triangles can have 300,000 vertices. In an indexed manifold surface, neighboring triangles share many positions. A large regular grid with 100,000 triangles may use close to 50,000 geometric positions.
Rendered vertices split whenever any required per-vertex attribute differs. A point on a hard edge needs different normals for its adjacent faces. A UV seam needs different coordinates on each island. Mirrored tangent space, vertex colors, joint weights, and primitive boundaries can cause more duplication.
A cube illustrates the distinction:
- it has 8 geometric corner positions;
- it contains 6 quad faces, normally delivered as 12 triangles;
- flat face normals require separate corner records per face;
- a typical face-by-face UV unwrap also separates those corners;
- the rendered mesh commonly has 24 vertices, not 8.
Reducing triangle count without reducing attribute seams may save fewer vertex bytes than expected. Conversely, cleaning unnecessary splits can reduce vertices while leaving all triangles intact.
Export statistics must come from final primitives. Blender may report source control points before modifiers and export conversion. glTF accessors reveal the delivered vertex and index counts.
Attributes change the bytes per vertex#
The 48-byte static layout above is only one case. A simpler unlit mesh with positions and UVs might use 20 float bytes per vertex. A skinned mesh can add joints and weights. A model with two UV sets, colors, tangents, skinning, and morph targets can be far larger.
Consider 80,000 vertices with:
- base position, normal, tangent, and UV: 48 bytes;
- four 16-bit joints plus four 16-bit normalized weights: 16 bytes;
- secondary UV as two floats: 8 bytes;
- total base row: 72 bytes.
The base attributes use 5,760,000 bytes. Add five morph targets containing float position and normal deltas: 5 × 80,000 × 24 = 9,600,000 more bytes. Morph data is now the larger contributor even though triangle count never changed.
Audit whether attributes are required. Tangents are useful for tangent-space normal mapping, but can be omitted for a model without that feature if the runtime does not regenerate and retain them anyway. A second UV set is waste when nothing references it. Skin weights cannot be stripped from a character simply to meet a byte target.
Component types and quantization#
Not every attribute must use 32-bit floats. KHR_mesh_quantization permits compact component types for appropriate attributes. Normals can use normalized integers; UVs and positions can use reduced precision within a decoded range.
Replacing a 12-byte float position with three 16-bit components uses 6 raw bytes before alignment. Storing a normal in four 8-bit normalized components could use 4 instead of 12 bytes. Exact layouts and decode behavior depend on the extension, exporter, and runtime.
Precision must follow asset scale. If a position axis spans 100 meters and receives 16-bit uniform steps, one step is approximately 100 / 65,535 = 0.001526 meters, or 1.526 millimeters. That may be fine for an environment and poor for a small precision mechanism. If the same quantization spans 1 meter, the step is about 0.015 millimeters.
Quantization can also make later compression more effective because integers with bounded precision contain less entropy than noisy floats. Compare the rendered result at the closest view, including animation and grazing highlights.
Texture bytes can make geometry irrelevant#
Suppose a GLB contains the 4.56 MB raw geometry example plus four embedded images:
- base color JPEG: 5.8 MB;
- normal PNG: 8.1 MB;
- ORM PNG: 4.2 MB;
- emissive PNG: 1.4 MB.
Encoded images total 19.5 MB. Even removing every geometry byte could not reduce the container below 19.5 MB. A 50% triangle and vertex reduction might save roughly 2.28 MB before compression, moving a 24 MB file to about 21.7 MB—only around 9.5% overall.
Resizing each image from 4096 × 4096 to 2048 × 2048 removes 75% of its texels. Encoded byte savings depend on image content and format, but the opportunity is likely much larger. This does not mean textures should always be cut first; it means the component inventory should decide.
Check for images that are embedded but unused. A GLB can carry buffer views that no material references, depending on how it was assembled. A validator identifies structural issues, while a custom reference walk can identify orphaned payloads.
Animation and skin data are independent contributors#
Animation stores time and output accessors. A 60-second capture sampled at 60 Hz contains 3,601 time samples if both endpoints are included. For 100 animated nodes with translation and quaternion rotation stored as floats, raw outputs can grow quickly:
- translation: 3,601 × 3 × 4 ≈ 43,212 bytes per node;
- rotation: 3,601 × 4 × 4 ≈ 57,616 bytes per node;
- combined for 100 nodes: about 10.08 MB, before shared time arrays and compression.
Real exports may optimize constant channels, reduce keyframes, or use other representations. The example shows that triangle count cannot explain animation-heavy file size.
Remove redundant keys against an error tolerance, not by blindly retaining every tenth sample. Fast joint motion may require dense keys; a stationary node needs none. Test the reduced clip at original playback speed and at frame boundaries where contact matters.
Skins also store inverse bind matrices and joint references, though these are usually small compared with dense animation or morph targets. The per-vertex JOINTS and WEIGHTS attributes can be significant.
JSON and scene structure#
glTF JSON describes nodes, primitives, materials, accessors, animations, extensions, and metadata. For normal assets, JSON is rarely the largest contributor. It can become material when an export produces tens of thousands of nodes, verbose metadata, or many animation samplers.
Minifying JSON saves some bytes in a textual glTF file; the GLB JSON chunk generally has no reason to contain pretty-print indentation. Yet removing 200 KB of whitespace from a 30 MB asset is not a meaningful geometry strategy.
Scene structure affects runtime more than storage. One thousand primitives might add a modest amount of JSON while creating substantial draw-submission overhead. Report bytes and object counts as separate concerns.
Container overhead and alignment#
GLB has a 12-byte header. Each chunk has an 8-byte chunk header, and chunks are padded to 4-byte alignment. Buffer views and vertex attributes may also use alignment or stride requirements.
For a model containing megabytes of data, this overhead is tiny. For thousands of extremely small buffer views, padding can accumulate, but reorganizing micro-buffers and reducing scene fragmentation is more relevant than arguing about the main header.
Measure actual chunk lengths from the file. Do not attribute an unexplained 8 MB difference to “GLB overhead”; it is almost certainly payload, duplicated data, or a changed compression profile.
Compression breaks simple raw-byte predictions#
Draco, meshopt compression, image formats, and generic packing can make two geometrically identical assets differ greatly in transferred size. Compression ratio depends on ordering, precision, smoothness, repetition, and encoder settings.
A regular grid has predictable positions and indices. A noisy scan contains high-entropy deviations that are harder to compress. Reordering vertices and triangles can improve locality and compression without changing triangle count. Quantizing values removes precision that would otherwise consume entropy.
When comparing candidates, record:
- raw attribute and index bytes;
- encoded geometry bytes;
- embedded image bytes;
- total GLB bytes;
- decoder payload bytes on a cold client;
- decode time and temporary memory;
- post-decode GPU buffer bytes;
- visual precision at target views.
The smallest encoded file is not automatically the best delivery. A marginal 40 KB saving that adds 300 milliseconds of low-end decode is a poor trade if acquisition was already fast.
Build a component-level size report#
Unpack or inspect the GLB and produce one row per resource. For geometry accessors, include semantic, count, component type, element type, buffer-view length, and compression extension. For images, include dimensions, format, encoded bytes, referenced materials, and alpha use. For animation, include clip, channel, key count, duration, and output bytes.
Summarize by category:
- positions;
- other vertex attributes;
- indices;
- morph targets;
- animation;
- images;
- JSON and miscellaneous binary;
- geometry compression metadata;
- total.
Percentages make prioritization clear. If images are 82% of the GLB and indices are 3%, triangle reduction cannot produce the requested 70% file reduction unless it also changes another category.
Keep both logical bytes and allocated buffer-view bytes. Interleaved attributes may share one buffer view, so naively adding the same view once per accessor double-counts it.
Predicting a decimation result#
A rough model can estimate whether decimation is worth testing.
Assume a file has:
- 3.4 MB geometry after compression;
- 12.0 MB images;
- 0.6 MB animation and structure;
- total 16.0 MB.
If a 50% triangle candidate also reduces compressed geometry by 45%, it saves 1.53 MB. New total is 14.47 MB, a 9.6% overall reduction. If the business target is under 8 MB, geometry alone cannot reach it.
Now suppose texture work cuts images from 12.0 to 5.0 MB, and geometry work cuts 3.4 to 1.87 MB. The new total is about 7.47 MB. Both interventions are required, and quality review should allocate damage where it is least visible.
Predictions need validation because vertex count may not fall with triangles at the same ratio, compression ratios change, and simplification can alter material or seam structure.
When triangle reduction does help size#
Decimation is a strong file-size tool when geometry is the dominant category, especially for dense scans, CAD tessellations, or meshes with many morph attributes. It may also unlock 16-bit indices when each primitive's vertex count falls below 65,536, cutting index bytes in half for that primitive if the runtime and exporter choose the smaller type.
It is less effective when images, audio-like extension payloads, or animation dominate. It can even increase encoded size if the new topology is disordered, attribute splits grow, or an exporter changes compression settings.
Keep a source candidate, simplify, re-export through the exact same profile, and compare component reports. Do not compare an uncompressed original with a compressed reduced version and attribute all savings to triangle count.
Decision checklist#
- Triangle count comes from exported primitives.
- Rendered vertex count includes attribute and primitive splits.
- Every attribute has a documented runtime purpose.
- Index component width is recorded per primitive.
- Raw buffer bytes and encoded geometry bytes are separate metrics.
- Embedded images are inventoried by encoded bytes and dimensions.
- Morph targets and animation have independent totals.
- Interleaved buffer views are not double-counted.
- Compression candidates use equivalent visual precision.
- Decoder cost is included in delivery comparison.
- Predicted savings are calculated against total GLB bytes.
- Visual review determines how far reduction can safely proceed.
Sources and further reading#
- Khronos glTF 2.0 specification: accessors and data alignment
- Khronos glTF 2.0 specification: meshes
- Khronos extension: KHR_mesh_quantization
- Google Draco repository
- meshoptimizer documentation
Triangle count and file size answer different questions. Use triangle count to understand topology and per-frame geometric work; use a component byte report to understand storage. The best optimization begins where the report says the bytes actually are.