Why Virtual Texturing?为什么需要虚拟纹理?

Modern open-world games use terrain and environmental textures at resolutions that far exceed GPU VRAM. A fully detailed 8km×8km landscape might require a 64K×64K texture — roughly 16 GB at 4 bytes/texel. Virtual Texturing (VT) solves this with the same insight as OS virtual memory: you only ever load and keep in VRAM the pages (tiles) that are actually visible on screen right now, streaming others in and out on demand.

现代开放世界游戏使用的地形和环境纹理分辨率远远超过 GPU VRAM 的容量。 一个完全精细的 8km×8km 地形可能需要 64K×64K 的纹理——以 4 字节/纹素计算约 16 GB。 虚拟纹理(VT)用与操作系统虚拟内存相同的思路解决这个问题: 你只需在 VRAM 中加载并保留当前屏幕上实际可见的页面(瓦片), 其他页面按需流入和流出。

Analogy: Just as a CPU maps virtual addresses to physical RAM pages, Virtual Texturing maps virtual texel coordinates to physical tiles in a fixed-size GPU memory pool. Pages not in use are evicted; frequently-accessed pages are kept hot. 类比:就像 CPU 将虚拟地址映射到物理 RAM 页面一样,虚拟纹理将虚拟纹素坐标映射到固定大小 GPU 内存池中的物理瓦片。不用的页面被驱逐;频繁访问的页面保持热状态。
Virtual Texture System — Full Pipeline (per frame): ┌─────────────────────────────────────────────────────┐ │ 1. GPU RENDER │ │ Shaders sample VT → write (vt_space_id, page_id)│ │ into the Feedback Buffer (1/16 resolution) │ └───────────────────┬─────────────────────────────────┘ │ GPU→CPU readback (async, 2 frames latency) ┌───────────────────▼─────────────────────────────────┐ │ 2. CPU ANALYSIS (FVirtualTextureSystem::Update) │ │ Parse feedback → determine needed pages │ │ → request streaming from FVirtualTextureProducer │ └───────────────────┬─────────────────────────────────┘ │ async I/O + decompression ┌───────────────────▼─────────────────────────────────┐ │ 3. CPU UPLOAD │ │ Allocate page in Physical Pool │ │ Upload tile data via staging buffer → GPU │ └───────────────────┬─────────────────────────────────┘ │ GPU update ┌───────────────────▼─────────────────────────────────┐ │ 4. PAGE TABLE UPDATE │ │ Write new virtual→physical mapping into │ │ the Page Table texture (indirection texture) │ └─────────────────────────────────────────────────────┘

Page Table Architecture — The Indirection Texture页表架构 — 间接纹理

The page table is the GPU-side indirection structure. It's implemented as a texture where each texel stores the location of a physical page in the pool. When a shader samples a virtual texture, it first reads the page table to find out where the data actually lives in physical memory.

页表是 GPU 端的间接结构。它被实现为一个纹理, 每个纹素存储物理池中一个物理页面的位置。当 Shader 采样虚拟纹理时, 它首先读取页表以找出数据实际存放在物理内存的哪个位置

In UE5, a FVirtualTextureSpace owns the page table. Each virtual texture gets assigned a region of the space. The page table itself is a small texture (e.g., 256×256 for a 128K virtual texture with 512-texel pages), where each RGBA8 texel encodes:

在 UE5 中,FVirtualTextureSpace 拥有页表。每个虚拟纹理被分配到空间的某个区域。 页表本身是一个小纹理(例如,对于 128K 虚拟纹理配合 512 纹素页面,页表为 256×256), 每个 RGBA8 纹素编码:

UE5 maintains multiple mip levels in the page table. When a page is not yet loaded, the shader falls back to a coarser mip level that is resident. This means you always see some texture data, never a black hole — just potentially lower resolution. This graceful degradation is one of VT's key advantages over a naive streaming approach.

UE5 在页表中维护多个 Mip 级别。当某页尚未加载时, Shader 回退到已驻留的更粗糙 Mip 级别。这意味着你总能看到纹理数据, 不会出现黑洞——只是可能分辨率较低。这种优雅降级是 VT 相对于朴素流式方案的关键优势之一。

// FVirtualTextureSpace — owns the page table and virtual address space
// Engine/Source/Runtime/Renderer/Private/VT/VirtualTextureSpace.h

class FVirtualTextureSpace
{
public:
    // The page table texture (indirection texture on GPU)
    FTextureRHIRef PageTableTexture;

    // Size of the virtual address space (in pages)
    // e.g., 4096x4096 pages × 128-texel page = 512K virtual texture
    uint32 Dimensions;          // Width/height in pages
    uint32 NumPageTableLayers;  // One layer per mip level

    // Allocate a virtual region for a new texture
    FVirtualTextureProducerHandle RegisterProducer(...);

    // Called each frame to flush page table updates to GPU
    void UpdatePageTable(FRDGBuilder& GraphBuilder, ...);
};

Physical Page Pool — Fixed GPU Memory Budget物理页池 — 固定 GPU 内存预算

The physical page pool is the actual GPU texture atlas that stores loaded pages. It's a large 2D texture divided into a grid of page tiles (e.g., 128-texel or 256-texel squares). Every loaded virtual texture page lives somewhere in this atlas. The pool size is fixed, bounding VRAM usage.

物理页池是存储已加载页面的实际 GPU 纹理图集。 它是一个被划分为页面瓦片网格的大型 2D 纹理(例如 128 或 256 纹素的正方形)。 每个已加载的虚拟纹理页面都存储在这个图集的某个位置。池大小是固定的,限制了 VRAM 使用量。

Property属性 Typical Value典型值 Description描述
Page size128 or 256 texelsSquare tile size, including a 4-texel border for bilinear filtering seams正方形瓦片大小,含 4 纹素边框用于双线性过滤接缝
Pool dimensions2048×2048 ~ 4096×4096Atlas texture resolution — configurable via r.VT.PoolSizeScale图集纹理分辨率——通过 r.VT.PoolSizeScale 配置
Layers1 per formatSeparate pools for BC1, BC3, BC7, etc. — different compression formats每种格式单独的池——BC1、BC3、BC7 等不同压缩格式
Eviction policyLRULeast-recently-used pages are evicted when the pool is full池满时驱逐最近最少使用的页面
Border texels4 texelsDuplicate border prevents bilinear filter from sampling adjacent pages重复边框防止双线性过滤采样到相邻页面
// FVirtualTexturePhysicalSpace — manages the physical page pool atlas
// Engine/Source/Runtime/Renderer/Private/VT/VirtualTexturePhysicalSpace.h

class FVirtualTexturePhysicalSpace
{
public:
    // The actual GPU atlas texture
    FTextureRHIRef PooledRenderTarget;

    // Number of pages that fit in the pool
    uint32 NumTiles;

    // Allocate/free individual pages
    uint16 AcquirePage(uint32 vAddress);
    void   ReleasePage(uint16 pAddress);

    // Get UV coordinates within the atlas for a given page
    FVector4f GetPageUVTransform(uint16 pAddress) const;
};

GPU Feedback Buffer — "Which Pages Do I Need?"GPU 反馈缓冲区 — "我需要哪些页面?"

The feedback buffer is the core mechanism that allows the GPU to tell the CPU which virtual texture pages are needed this frame. Without it, the CPU would have no way to know which parts of a 64K texture are actually on screen.

反馈缓冲区是允许 GPU 告知 CPU 本帧需要哪些虚拟纹理页面的核心机制。 没有它,CPU 就没有办法知道 64K 纹理的哪些部分实际上在屏幕上。

The feedback buffer is rendered at 1/16th resolution (e.g., 120×67 for 1920×1080). During the base pass, each pixel writes its required virtual texture page ID into this buffer. The page ID encodes: VT space index, mip level, page X, and page Y — packed into 32 bits.

反馈缓冲区以1/16 分辨率渲染(例如 1920×1080 时为 120×67)。 在 Base Pass 期间,每个像素将其所需的虚拟纹理页面 ID 写入此缓冲区。 页面 ID 编码:VT 空间索引、Mip 级别、页面 X 和页面 Y——打包为 32 位。

// VirtualTextureFeedback.usf — shader side
// Called inside the base pass material shader when sampling a VT:

uint PackFeedback(uint SpaceIndex, uint MipLevel, uint2 PageAddress)
{
    // Pack: [4 bits mip][8 bits space][10 bits pageX][10 bits pageY]
    return (MipLevel << 28) | (SpaceIndex << 20) | (PageAddress.x << 10) | PageAddress.y;
}

// The feedback buffer is an R32_UINT UAV
// Shader writes: FeedbackBuffer[pixel / 4] = PackFeedback(...);
// Only one write per 4x4 pixel block (stochastic sampling)

After rendering, UE5 reads the feedback buffer back to the CPU asynchronously (with 2 frames of latency to avoid GPU stalls). The CPU then parses all the page IDs to build a set of required pages. Pages that are required but not in the pool are queued for streaming.

渲染后,UE5 异步将反馈缓冲区读回 CPU(有 2 帧延迟以避免 GPU 停顿)。 CPU 然后解析所有页面 ID 以建立所需页面集合。所需但不在池中的页面被排队等待流式传输。

Streaming Pipeline — Async I/O to GPU Upload流式传输管线 — 异步 I/O 到 GPU 上传

When the CPU determines a page is needed, the FVirtualTextureProducer is responsible for supplying the data. Producers are abstract: they might read from disk, decompress from a format like BC1/BC7, or even render the data on the GPU (for Runtime VT). The pipeline is:

当 CPU 确定某页面被需要时,FVirtualTextureProducer 负责提供数据。 Producer 是抽象的:它们可能从磁盘读取、从 BC1/BC7 等格式解压, 甚至在 GPU 上渲染数据(用于 Runtime VT)。管线为:

Producer streaming pipeline: [CPU] FVirtualTextureSystem::Update() → Analyze feedback, determine missing pages → FVirtualTextureProducer::RequestPageData(page_id) ├── TextureProducer: async disk I/O → decompress BC tiles ├── RuntimeVT: schedule GPU render to staging buffer └── AdaptiveVT: generate procedurally [CPU] Upload thread: → Allocate physical page in pool (FVirtualTexturePhysicalSpace::AcquirePage) → Copy decompressed data to RHI staging buffer → UploadToTexture() → GPU pool atlas (async) [GPU] Page table update pass: → Write new (virtual → physical) mapping into page table texture → Next frame: shaders can now sample the newly loaded page

UE5 limits how many new pages can be uploaded per frame (r.VT.MaxUploadPerFrame, default 8) to bound the bandwidth spike. Pages that are frequently needed but not yet loaded will show a lower-resolution mip fallback until the full page arrives.

UE5 限制每帧可以上传的新页面数量(r.VT.MaxUploadPerFrame,默认 8)以限制带宽峰值。 经常需要但尚未加载的页面将显示低分辨率 Mip 回退,直到完整页面到达。

Shader Integration — Sampling a Virtual TextureShader 集成 — 采样虚拟纹理

Sampling a virtual texture in HLSL is different from a regular texture2D sample. It requires an extra indirection through the page table. UE5 wraps this in the TextureVirtualSample HLSL function (defined in VirtualTextureCommon.ush), so Material Editor and HLSL both use the same path.

在 HLSL 中采样虚拟纹理与常规 Texture2D 采样不同。它需要通过页表进行额外的间接访问。 UE5 将这个过程封装在 TextureVirtualSample HLSL 函数中(定义于 VirtualTextureCommon.ush), 因此 Material Editor 和 HLSL 都使用相同的路径。

// VirtualTextureCommon.ush — simplified conceptual version

float4 VirtualTextureSample(
    Texture2D PageTable,     // The indirection texture
    Texture2D PhysicalPool,  // The physical page atlas
    SamplerState Sampler,
    float2 UV,               // Virtual UV [0,1]
    float4 VTPackedPageTableUniform)
{
    // Step 1: calculate which mip level we need
    float MipLevel = CalcMipLevel(UV, VirtualResolution);

    // Step 2: calculate the page address (which tile contains this UV?)
    float2 PageUV = UV * VirtualPageCount;  // in page-space
    float2 PageCoord = floor(PageUV);

    // Step 3: look up the page table (indirection)
    // Result: tells us WHERE in the physical atlas this page lives
    float4 PageTableEntry = PageTable.SampleLevel(Sampler, PageCoord / PageTableSize, MipLevel);
    float2 PhysicalPageOffset = DecodePageTableEntry(PageTableEntry);

    // Step 4: compute physical UV within the pool atlas
    float2 IntraPageUV = frac(PageUV);  // position within the page
    float2 PhysicalUV  = (PhysicalPageOffset + IntraPageUV * PageSizeTexels) / PoolAtlasSize;

    // Step 5: sample the actual data from the physical pool
    return PhysicalPool.Sample(Sampler, PhysicalUV);
}

// Also: while sampling, write feedback
// FeedbackBuffer[pixel/4] = PackFeedback(SpaceID, MipLevel, PageCoord);

Runtime Virtual Texture (RVT) — Render to VT运行时虚拟纹理(RVT)— 渲染至 VT

Runtime Virtual Texture is a VT variant where pages are not loaded from disk but rendered on demand by the GPU. This is used primarily for landscape blending: the landscape material can be very complex (blending 8+ layers), but the result is baked into VT pages that terrain meshes can sample cheaply as a single texture lookup.

运行时虚拟纹理(RVT)是一种 VT 变体,页面不从磁盘加载, 而是由 GPU 按需渲染。这主要用于地形混合: 地形材质可以非常复杂(混合 8+ 个层级),但结果被烘焙到 VT 页面中, 地形网格可以作为单次纹理查找廉价采样。

RVT use case example: Landscape with 8 layers (rock, grass, mud, snow...) blended by weight maps. Without RVT: the full blending shader runs for every pixel of the landscape every frame. With RVT: blending happens only when a new page is needed (or on update). Subsequent frames read a pre-blended page — roughly 8× fewer texture samples in the shading pass. RVT 用例示例:包含 8 个图层(岩石、草地、泥土、雪...)按权重图混合的地形。没有 RVT:完整混合 Shader 每帧对地形每个像素运行。有 RVT:混合仅在需要新页面时发生(或按需更新)。后续帧读取预混合页面——着色通道中纹理采样减少约 8 倍。

RVT pages are rendered by FRuntimeVirtualTextureRender. When a page is needed, a scene capture renders the landscape/terrain into a staging render target at the appropriate resolution, then uploads the result into the physical pool. Pages can be marked as static (never re-render) or dynamic (re-render when the scene changes).

RVT 页面由 FRuntimeVirtualTextureRender 渲染。需要页面时, 场景捕获以适当分辨率将地形渲染到暂存渲染目标,然后将结果上传到物理池。 页面可以标记为静态(从不重新渲染)或动态(场景变化时重新渲染)。

// Setting up RVT in a Landscape in Blueprints / C++:

// 1. Place a RuntimeVirtualTextureVolume actor in the level
// 2. Create a VirtualTexture asset (content browser) with type "Runtime"
// 3. Assign the VT asset to the Landscape's RuntimeVirtualTextures array
// 4. In the Landscape material: use "Runtime Virtual Texture Sample" node
//    to read from the VT instead of sampling all layers directly

// C++ setup:
URuntimeVirtualTexture* RVT = NewObject<URuntimeVirtualTexture>();
RVT->SetSize(ETextureSize::TS_4096);       // 4096×4096 virtual resolution
RVT->SetTileSize(ETileSize::TS_128);       // 128-texel physical pages
RVT->SetTileBorderSize(4);               // 4-texel border
RVT->SetMipLevels(9);                    // log2(4096/128) + 1

// Console: stat VirtualTextureMemory — shows pool usage

Adaptive Virtual Texture — Per-Object VT自适应虚拟纹理 — 逐对象 VT

Adaptive Virtual Texture (AVT) extends VT to work on individual static mesh objects, not just landscapes. Each object can have its own VT space, and the page size adapts based on the object's on-screen pixel density. This is useful for hero assets that need more texture detail than standard streaming can provide without consuming disproportionate VRAM.

自适应虚拟纹理(AVT)将 VT 扩展到单个静态网格对象,而不仅仅是地形。 每个对象可以有自己的 VT 空间,页面大小根据对象的屏幕像素密度自适应。 这对于需要比标准流式传输能提供更多纹理细节的英雄资产非常有用,同时不消耗不成比例的 VRAM。

Usage & Setup Guide使用与配置指南

Enabling VT for a Texture为纹理启用 VT

In the Texture Editor (or bulk-edit): check "Virtual Texture Streaming" in the Texture Compression settings. This makes the texture stream via VT. Then enable r.VirtualTextures=1 (project-wide via Project Settings → Rendering → Virtual Textures).

在纹理编辑器(或批量编辑)中:在纹理压缩设置中勾选"Virtual Texture Streaming"。这使纹理通过 VT 流式传输。然后启用 r.VirtualTextures=1(通过"项目设置 → 渲染 → 虚拟纹理"在项目范围内设置)。

Setting Up RVT for Landscape为地形设置 RVT

  1. In Project Settings → Rendering, enable Virtual Texture Support and Enable virtual texture support in materials.
  2. 在"项目设置 → 渲染"中,启用"虚拟纹理支持""在材质中启用虚拟纹理支持"
  3. Place a RuntimeVirtualTextureVolume actor in your level. Set its bounds to cover the landscape.
  4. 在关卡中放置 RuntimeVirtualTextureVolume Actor,设置其边界以覆盖地形。
  5. Create a VirtualTexture asset in Content Browser. Configure: type = Color+Normal, page size = 128–256, mip levels = auto.
  6. 在内容浏览器中创建 VirtualTexture 资产。配置:类型 = 颜色+法线,页面大小 = 128–256,Mip 级别 = 自动。
  7. In the Landscape actor, under Virtual Textures, add the VT asset. Enable Draw in Virtual Textures.
  8. 在地形 Actor 的虚拟纹理下,添加 VT 资产。启用"在虚拟纹理中绘制"
  9. In the Landscape Material: add a Runtime Virtual Texture Sample node. Set it to read BaseColor, Normal, Roughness from the RVT.
  10. 在地形材质中:添加 Runtime Virtual Texture Sample 节点。设置为从 RVT 读取 BaseColor、Normal、Roughness。
  11. Create a second material (the "write" material) with Runtime Virtual Texture Output node — this renders into the VT pages.
  12. 创建第二个材质("写入"材质),包含 Runtime Virtual Texture Output 节点——它渲染到 VT 页面中。

Debugging Virtual Textures调试虚拟纹理

-- Console commands for VT debugging --

r.VT.Flush             -- Evict all VT pages (forces re-stream)
r.VT.Borders=1         -- Visualize page borders in the viewport
r.VT.Verbose=1         -- Log detailed page streaming events
stat VirtualTextureMemory   -- Pool memory usage statistics
stat VirtualTexturePerf     -- Streaming performance counters

-- Viewport Show menu → Virtual Texture → Feedback shows feedback buffer
-- Viewport Show menu → Virtual Texture → Page Table shows page occupancy

r.VT.PoolSizeScale=1.0      -- Scale physical pool size (0.5 = half, saves VRAM)
r.VT.MaxUploadPerFrame=8    -- Max pages uploaded per frame
r.VT.NumFeedbackSamples=4   -- Feedback buffer density (higher = more responsive)

Performance Tuning性能调优

CVar Default默认 Effect效果
r.VirtualTextures0Master enable (must enable in Project Settings UI)主开关(须在项目设置 UI 中启用)
r.VT.PoolSizeScale1.0Physical pool size multiplier. Reduce to 0.5 on low-VRAM devices.物理池大小乘数。在低显存设备上减到 0.5。
r.VT.MaxUploadPerFrame8Limits bandwidth spike from page uploads. Increase for faster streaming.限制页面上传的带宽峰值。增大可加快流式传输速度。
r.VT.FeedbackFactor16Feedback resolution divisor (16 = 1/16 screen res). Lower = more accurate, more overhead.反馈分辨率除数(16 = 1/16 屏幕分辨率)。越小越精确,开销越大。
r.VT.PageTableExtraLODBias0Extra mip bias for page table lookups. Positive values use coarser (cheaper) mips.页表查找的额外 Mip 偏差。正值使用更粗糙(更便宜)的 Mip。
r.RVT.TileCountBias0Increase RVT tile count for more detail on high-end hardware.为高端硬件增加 RVT 瓦片数以获取更多细节。
Common pitfall — VT Flickering: If you see textures popping or flickering, the physical pool is likely full and pages are being evicted too aggressively. Increase r.VT.PoolSizeScale or reduce the number of unique VT textures in view. Check stat VirtualTextureMemory for pool occupancy. 常见问题 — VT 闪烁:如果看到纹理弹出或闪烁,物理池可能已满且页面被过于积极地驱逐。增大 r.VT.PoolSizeScale 或减少视野中唯一 VT 纹理的数量。通过 stat VirtualTextureMemory 检查池占用情况。

Source Navigation源码导航

All paths relative to Engine/Source/Runtime/Renderer/Private/VT/

所有路径相对于 Engine/Source/Runtime/Renderer/Private/VT/

Unreal Engine 5.6 · Engine/Source/Runtime/Renderer/Private/VT/
Source analysis · For learning purposes