Field notesRendering

Ship compressed 3D assets without breaking colour or coordinates

A small model file can still produce a broken page: the decoder may be missing, a texture may be interpreted in the wrong colour space, or the exporter may rotate an asset that was already authored in the target coordinates. The useful unit of delivery is the complete loading path, not just the compressed file.

This repository has a separate scripts/build-3d.mjs pipeline. It runs Blender authoring scripts, compresses geometry with glTF Transform, encodes matcaps with toktx, and copies runtime decoders beside the assets. The ordinary site build consumes those files; it does not run Blender.

Keep asset generation separate from page bundling

The 3D builder names the authoring script, temporary output directory, GLB, and any matcaps for each bundle. That makes it possible to regenerate one bundle without rebuilding every model. Machine dependencies still matter: running the same script is not a promise of byte-identical output across different Blender, encoder, or platform versions.

# Requires this project's Blender and toktx setup.
node scripts/build-3d.mjs about

# Ordinary site build consumes the existing model files.
npm run build

Record tool versions and hashes when comparing builds. If you change geometry and encoder settings at the same time, you cannot attribute the size difference to compression alone.

Report the files that actually exist

These are filesystem byte lengths from the repository on 28 October 2026. KiB means 1,024 bytes. The table includes representative models, textures, and the decoder WASM files so the geometry number is not mistaken for the whole delivery cost.

Encoded assets and decoder files; no raw-to-compressed ratio inferred
FileBytesKiB
lab-crystals.glb36,42035.57
lab-matcap.ktx2443,394433.00
lab-matcap-int.ktx2439,188428.89
about-instrument.glb35,39234.56
draco/draco_decoder.wasm192,420187.91
basis/basis_transcoder.wasm499,935488.22

Download the size record and SHA-256 hashes. These numbers exclude decoder JavaScript, other models, page code, HTTP headers, and any transport compression. A returning visitor may already have decoder files cached. A first-time visitor may not.

A matching raw export was not used for this comparison, so there is no substantiated “10:1” ratio here. File length also does not measure decode time, upload time, GPU memory, or time to the first useful frame.

Quantization trades precision for size

Draco compresses mesh data. Its quantization settings limit attribute precision: more quantization bits generally retain more detail, with a potential size cost. Fewer bits reduce the available levels. Saying “higher quantization means a smaller file” without distinguishing bit depth from quantization strength is ambiguous.

The repository invokes gltf-transform draco without explicit position, normal, or colour bit settings. Therefore this article does not attribute hand-selected 14/10/8-bit settings to that command. If those settings matter to a result, specify them and record the tool version.

Judge normals as well as silhouettes. A small position error may be acceptable on a rough rock while a quantized normal changes a glossy highlight noticeably. Use the intended material and camera distance for the comparison.

Colour metadata must match the pixels

The checked-in command for the lab matcaps includes the following options:

toktx --t2 --encode uastc --uastc_quality 3 \
  --uastc_rdo_l 1.5 --uastc_rdo_d 8192 \
  --zcmp 19 --genmipmap --assign_oetf srgb \
  output.ktx2 input.png

The actual flag is srgb, not linear. Matcap describes how a texture is sampled; it does not, by itself, establish whether its encoded pixel values are linear or sRGB. Match the metadata to how the source image was created, and keep the renderer’s output conversion consistent with that choice.

--assign_oetf assigns transfer-function metadata; it is not a repair for incorrectly encoded pixels. Normal, roughness, and other data textures also need their own interpretation. Marking every texture as a colour image causes a different class of error.

UASTC is a format carried in KTX2. The loader can transcode it to a supported GPU format; the selected target depends on the implementation and device. Zstandard compression reduces the stored UASTC payload, and rate-distortion settings trade fidelity for compressibility. Do not replace a comparison image with a claim that the loss is universally invisible.

Doubling a square texture’s width and height quadruples its texel count. A 2048-by-2048 texture has four times the texels of a 1024-by-1024 texture, before considering mipmaps and format. It does not merely double the texture cost.

Treat decoders as deployment assets

const draco = new DRACOLoader();
draco.setDecoderPath("/assets/3d/draco/");

const ktx2 = new KTX2Loader();
ktx2.setTranscoderPath("/assets/3d/basis/");
ktx2.detectSupport(renderer);

const gltf = new GLTFLoader();
gltf.setDRACOLoader(draco);
gltf.setKTX2Loader(ktx2);

This setup assumes the Three.js loaders have been imported and the renderer created. detectSupport must run before KTX2 loading. Reuse loaders while loading a group of assets, and dispose of decoder workers and owned resources when their job is finished.

Serving decoder files from the same origin simplifies delivery and policy configuration. It is not a universal API requirement: cross-origin loading can work with the necessary CORS and content-security settings. Test the deployed paths, MIME types, and failure behavior rather than relying on where the files lived during development.

Name the coordinate convention

glTF uses Y-up coordinates. Blender normally uses Z-up and its exporter can convert them. The crystal generator uses export_yup=True. Other generators may author their positions in Y-up deliberately and avoid that conversion. The right flag follows the source convention; there is no correct global “always false” rule.

Include an asymmetric reference object or recognizable up direction in a local export check. A sphere can look correct even after an accidental rotation. Verify node names too: if runtime code expects a named mesh, changing its exported name is an interface change.

Finally, block a decoder or texture request in the browser and verify that the page becomes readable. Successful compression is only useful when the asset loads correctly or fails into a usable page.

Sources & revision

Read the dated source excerpt. It includes original file paths, line numbers, and SHA-256 hashes for the version used in this guide. The public repository may contain a different revision.

28 Oct 2026: Corrected the sRGB encoding flag, texture-area arithmetic, quantization explanation, and decoder-origin claim. Added actual encoded file sizes and hashes.

These notes use AI assistance. Read the editorial method and corrections policy. Found an error? Send a correction with this article’s title.