Three.js From Zero · Article s5-08

S5-08 Virtual Texturing

Season 5 · Article 08

Virtual Texturing — streaming 100 GB of texels

Unique texturing for every square meter of a game world = terabytes of texture data. VT solves it: only the tiles you're looking at live in VRAM; the rest stream in on demand.

1. The problem

You want 4K detail every square meter of a 10km² map. That's a 40960×40960 texture. 5.5 GB uncompressed. Doesn't fit in any consumer GPU.

2. The core idea

  1. Split the giant texture into small tiles (128×128 or 256×256).
  2. Keep a physical texture in VRAM — say 4096×4096 — as a cache.
  3. An indirection texture maps "virtual tile ID" → "location in physical cache."
  4. Fragment shader reads indirection, computes physical UV, samples.
  5. On cache miss, request tile to be streamed in. Next frame it's there.

3. The shader dance

// Fragment
vec2 virtUv = vUv;
vec2 tileCoord = floor(virtUv * vec2(VIRT_TILES));
vec4 indirect = texture2D(uIndirectTex, tileCoord / vec2(VIRT_TILES));
// indirect.rg = physical tile origin (0..1)
// indirect.b = mip level present
vec2 localUv = fract(virtUv * vec2(VIRT_TILES)); // within-tile UV
vec2 physUv = indirect.rg + localUv / vec2(PHYS_TILES);
vec4 col = texture2D(uPhysicalTex, physUv);

4. Feedback pass

How does the system know which tiles are needed?

Before shading, render a small "page ID" buffer — each pixel writes the virtual tile it WANTS. Scan it CPU-side, enqueue missing tiles for stream.

// Page-ID pass (simple version)
void main() {
  vec2 uv = ...;
  uint tileId = uint(floor(uv.x * VIRT_TILES)) + uint(floor(uv.y * VIRT_TILES)) * VIRT_TILES;
  uint mipLevel = computeMipFromDerivatives();
  gl_FragColor = encodeTileId(tileId, mipLevel);
}

5. Sparse Virtual Textures (SVT, Rage)

id Software's Megatextures (Rage, 2011): entire game world → single 128K × 128K texture. VT paged from disk. Every surface unique. No tiling visible.

6. Live demo — VT simulation

A large virtual texture split into an 8×8 grid of tiles. Only 16 tiles ever live in "VRAM." Move the camera — watch tiles stream in as cache misses happen. Missing tiles show a placeholder color.

7. What ships in real engines

  • idTech 5/6: megatextures (terrain + unique per-m² detail).
  • Far Cry, Kingdom Come: terrain VT.
  • Unreal: VT is now stock — Runtime Virtual Textures (RVT) for decals, blending, detail.
  • Unity HDRP: streaming virtual textures (SVT) experimental.

8. In Three.js

Not stock. WebGPU makes it feasible: sparse textures (via indirect indexing), compute for feedback scan.

Cheaper approximation: tile atlas + manual LOD switching, good enough for procedural terrains.

9. Gotchas

  • Anisotropic filtering: hard across tile borders. Border padding (2-4 px) helps.
  • Mipmap levels: need separate page tables per level.
  • Tile budget: 2048×2048 physical / 128×128 tiles = 256 tiles cached. Plan visibility accordingly.
  • Disk speed: SSD or NVMe assumed. Cold cache = disk-bound.

10. Takeaways

  • VT = only load the texture tiles you can see.
  • Physical cache + indirection texture + feedback pass.
  • Enables terabyte-class texturing.
  • Stock in Unreal, gaining ground in Unity.
  • Browser: possible via WebGPU; not yet easy.