Virtual texturing
Sparse virtual texturing streams high-resolution texture tiles on demand using per-frame GPU feedback, keeping memory use proportional to what is visible.
Virtual texturing (VT) lets you author an arbitrarily large texture — a terrain that covers an entire world, for example — and have the engine stream only the tiles that are actually visible in the current frame, at the correct mip level. Memory cost stays proportional to the screen, not to the total texture size.
Meep’s implementation is based on id Software’s Megatexture / sparse virtual texture technique and the Adaptive Virtual Texture Rendering in Far Cry 4 presentation by Ka Chen (Ubisoft, GDC 2015).
How it works
The system has three GPU data structures and one CPU read-back per frame.
Usage buffer — the scene is rendered at 1/32 of viewport resolution using a special override material (VirtualTextureUsageShader). The fragment shader encodes the UV coordinates and mip level that each visible pixel needs into an RGBA8UI render target. The CPU reads this buffer back and sorts the result into a priority-ordered list of (mip, tile_x, tile_y) addresses.
Page texture — a fixed-size RGBA8 DataTexture (default 4096×4096 px, set by VT_DEFAULT_PAGE_RESOLUTION) that holds the currently resident tiles. Each tile slot is tile_resolution + 2*tile_margin pixels square; the margin (default 4 px) enables bilinear filtering across tile edges. Up to 4 new tiles are written into the page texture per frame (#cycle_assignment_limit) to keep the per-frame upload cost predictable.
Mapping texture — a R32UI DataTexture that stores the entire mip pyramid of tile addresses. Each entry encodes a 22-bit page slot index and a 10-bit mip level. When a tile is not resident, the entry falls back to its parent mip’s slot, so the shader always finds some data even during streaming.
A second-level CPU Cache (default 256 MB) sits between the tile loader and the page texture. Tiles evicted from the page remain in the CPU cache and can be re-made resident without a network fetch.
API
All three data structures are managed by VirtualTextureSystem (src/engine/graphics/texture/virtual/VirtualTextureSystem.js).
import { VirtualTextureSystem } from "@woosh/meep-engine/src/engine/graphics/texture/virtual/VirtualTextureSystem.js";
const vt = new VirtualTextureSystem();
vt.initialize({
page_resolution: 4096, // page texture size in pixels (default 4096)
asset_manager: engine.assetManager
});
vt.setTexture({
path: "data/textures/terrain/mega/",
texture_resolution: 65536, // full virtual texture size in pixels
tile_resolution: 256, // pixels per tile (must divide texture_resolution as a power-of-two)
tile_margin: 4 // border pixels for filtering
});
// Create a material that samples the virtual texture
const mat = vt.makeMaterial();
scene.overrideMaterial = mat; // or assign to specific meshes
// Every frame, after the scene is set up:
graphics.on.preRender.add(() => {
vt.update(renderer, scene, camera);
});
makeMaterial() returns a VirtualTextureMaterial with all shared uniforms pre-bound. The uniforms are updated inside vt.update(...) each frame.
Key classes
| Class | Source | Role |
|---|---|---|
VirtualTextureSystem | texture/virtual/VirtualTextureSystem.js | Facade — owns and coordinates the three sub-systems |
VirtualTextureUsageUpdater | texture/virtual/VirtualTextureUsageUpdater.js | GPU usage pass + CPU read-back + tile priority list |
VirtualTexturePage | texture/virtual/VirtualTexturePage.js | Page texture + tile loader + LRU eviction |
VirtualTextureMemoryMapping | texture/virtual/VirtualTextureMemoryMapping.js | Mapping texture (mip pyramid → page slot) |
VirtualTextureMaterial | texture/virtual/VirtualTextureMaterial.js | Three.js ShaderMaterial that samples through the mapping |
VirtualTextureTileLoader | texture/virtual/VirtualTextureTileLoader.js | Async tile fetch queue, rate-limited by queue_limit |
Uniforms exposed to the material shader
| Uniform | Type | Content |
|---|---|---|
u_page | sampler2D | Page texture (resident tiles) |
u_page_resolution | vec2 | Page texture size in tiles |
u_tile_resolution | float | Pixels per tile (without margin) |
u_tile_padding | float | Margin pixels per tile side |
u_mapping | usampler2D | Mapping texture (mip pyramid) |
u_texture_resolution | float | Full virtual texture size in pixels |
u_max_mip_level | float | log2(texture_resolution / tile_resolution) |
u_mapping_texture_width | float | Width of the mapping texture in texels |
Limitations and trade-offs
- One GPU read-back per frame. The usage pass calls
renderer.readRenderTargetPixelssynchronously. This stalls the pipeline on some drivers. The notes file (NOTES.mdin the source) documents an async PBO path (WebGL 2PIXEL_PACK_BUFFER) as a possible future improvement. - Tile data is pre-authored offline. The current API expects tiles to already exist on disk as individual image assets. There is no built-in tile slicer.
- One virtual texture at a time. The
VirtualTextureUsageShaderencodes a singletexture_id(hardcoded to 3 in the current prototype). Using multiple independent virtual textures simultaneously is not yet supported in the public API. - No anisotropic filtering in the shader. The tile margin enables basic bilinear filtering. The notes file includes a GLSL anisotropic-filtering snippet for future integration.