Three.js From Zero · Article s5-08
S5-08 Virtual Texturing
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
- Split the giant texture into small tiles (128×128 or 256×256).
- Keep a physical texture in VRAM — say 4096×4096 — as a cache.
- An indirection texture maps "virtual tile ID" → "location in physical cache."
- Fragment shader reads indirection, computes physical UV, samples.
- 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.