Rendering

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

ClassSourceRole
VirtualTextureSystemtexture/virtual/VirtualTextureSystem.jsFacade — owns and coordinates the three sub-systems
VirtualTextureUsageUpdatertexture/virtual/VirtualTextureUsageUpdater.jsGPU usage pass + CPU read-back + tile priority list
VirtualTexturePagetexture/virtual/VirtualTexturePage.jsPage texture + tile loader + LRU eviction
VirtualTextureMemoryMappingtexture/virtual/VirtualTextureMemoryMapping.jsMapping texture (mip pyramid → page slot)
VirtualTextureMaterialtexture/virtual/VirtualTextureMaterial.jsThree.js ShaderMaterial that samples through the mapping
VirtualTextureTileLoadertexture/virtual/VirtualTextureTileLoader.jsAsync tile fetch queue, rate-limited by queue_limit

Uniforms exposed to the material shader

UniformTypeContent
u_pagesampler2DPage texture (resident tiles)
u_page_resolutionvec2Page texture size in tiles
u_tile_resolutionfloatPixels per tile (without margin)
u_tile_paddingfloatMargin pixels per tile side
u_mappingusampler2DMapping texture (mip pyramid)
u_texture_resolutionfloatFull virtual texture size in pixels
u_max_mip_levelfloatlog2(texture_resolution / tile_resolution)
u_mapping_texture_widthfloatWidth of the mapping texture in texels

Limitations and trade-offs

  • One GPU read-back per frame. The usage pass calls renderer.readRenderTargetPixels synchronously. This stalls the pipeline on some drivers. The notes file (NOTES.md in the source) documents an async PBO path (WebGL 2 PIXEL_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 VirtualTextureUsageShader encodes a single texture_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.