虚拟纹理(Virtual Texturing)完整技术解析

How Virtual Textures Unlock Huge Worlds

一、问题的起源:大纹理的困境

1.1 经典案例:Crash Bandicoot 的启示

  • 初代 PlayStation 仅有 2MB 系统 RAM + 1MB VRAM
  • 大多数 PS 游戏在关卡开始前将所有资源从 CD 一次性加载到内存,关卡运行期间 CD 仅用于播放音乐
  • Crash Bandicoot 打破了这一范式:将关卡分解为 固定大小的页(Page),按虚拟内存的方式管理
    • 预计算可见性布局,确定每个区域需要哪些页
    • 只要 工作集(Working Set) 能装入内存,流式加载对玩家完全透明
    • 关卡的有效大小不再受限于主存容量,而是受限于 磁盘上分配的空间
  • 核心洞察:在任何时刻,只有关卡的一小部分是可见的,大量被加载的数据从未参与最终画面

1.2 大纹理为什么难

  • 大型开放世界或生物医学成像数据需要 巨大纹理 以保留精细细节
  • 这些纹理往往 无法放入 VRAM
  • 即使 GPU 能容纳,跨总线传输如此大量数据也不现实—— 内存带宽在容量之前就会崩溃
  • 真正的限制因素是屏幕分辨率,而非内存或带宽:
    • 屏幕像素数固定,只有投影到屏幕上的纹素才能影响最终图像
    • 例如:一张 24K 纹理 投影到 4K 显示器 上,纹理是屏幕的 6 倍大,永远不可能同时以全分辨率被看到
    • 拉远看:屏幕做降采样,仅小部分像素参与
    • 拉近看:更多细节可见,但只是一小块区域

1.3 2D 与 3D 的复杂度差异

  • 2D 场景(如 Google Maps、Deep Zoom)中设计运行时系统很简单:
    • 任意时刻只有一个 Mip 级别活跃,加上少量页
    • LOD 选择仅需挑选那个 Mip
    • 每个页可以作为独立 Tile 用自己的 Draw Call 渲染
  • 3D 场景中复杂度大增
    • 必须同时采样多个 Mip 级别——近处表面需要高分辨率页,远处几何体则采样低分辨率页
    • LOD 选择必须 逐像素 进行
    • 传统变通方案(拆分几何体、绑定多张纹理、手动管理 LOD)带来严重问题:
      • 纹理绑定(Texture Binds)激增
      • Draw Call 爆炸
      • 带宽使用飙升
    • 花在"喂数据给 GPU"上的时间比实际渲染还多

二、虚拟纹理的核心思想

2.1 类比:CPU 虚拟内存

  • CPU 几十年前就解决了"数据太多、内存太少"的问题—— 虚拟内存
  • 每个程序相信自己拥有一块 连续地址空间,这是一种 幻觉
  • 程序使用的地址是 虚拟的 而非物理的
  • 物理内存碎片化且不断变化,虚拟地址通过 页表(Page Table) 翻译为物理地址
  • 当 RAM 不足时,OS 将不活跃的页换出到磁盘;程序访问未驻留页时触发 缺页中断(Page Fault),OS 加载缺失页、更新页表、恢复执行

2.2 将虚拟内存思想应用到纹理

  • 虚拟纹理 向应用暴露一张巨大纹理,但只有当前视图所需的页保留在 GPU 内存中(通常仅几 MB)
  • 驻留状态通过 页表显式追踪
  • 同时解决两个问题:
    1. 永远不分配全分辨率纹理
    2. 仅上传屏幕实际采样的纹素,限制带宽
  • 间接寻址(Indirection)解开了 3D 中的 LOD 选择难题
    • 网格从 单一统一虚拟纹理 中采样
    • 纹理绑定 坍缩为一张物理纹理
    • 高分辨率和低分辨率页 共存于同一地址空间

关键点:CPU 和 OS 免费提供虚拟内存;在 GPU 上,现代引擎 仍然自己构建 虚拟纹理系统——即使有硬件稀疏纹理支持,引擎也自行管理页表、驻留逻辑和流式系统。


三、系统架构概览

虚拟纹理 不是单个 Shader,而是一个 系统。多个组件跨 CPU 和 GPU 协同工作。

每帧系统必须回答三个问题:

关注点执行位置职责
寻址(Addressing)GPU确定每个纹素应从哪里采样
反馈(Feedback)GPU → CPU记录哪些虚拟页在什么分辨率下被访问
驻留(Residency)CPU决定哪些页必须存在于 GPU 内存中

这三个关注点 被刻意分离:寻址完全在 GPU 渲染期间完成;反馈观察采样行为但不干扰它;驻留决策在 CPU 上做出,便于显式管理策略、缓存和 I/O。


四、寻址:虚拟坐标到物理坐标的翻译

4.1 三层表示

系统对同一纹理操作三种表示:

  1. 虚拟纹理(Virtual Texture):定义完整地址空间,UV 跨越全分辨率纹理
  2. 页表(Page Table):翻译虚拟页并追踪驻留状态,通常实现为 2D 纹理,每个纹素对应一个虚拟页
  3. 物理纹理图集(Physical Texture Atlas):存储当前驻留在 GPU 内存中的页

4.2 页表的结构

  • 每个条目编码:
    • 页是否 已驻留
    • 页在物理图集中的 位置
  • 通常打包为 单个 32 位整数,保持表小且缓存友好
  • 概念上每个 Mip 级别有一张页表,实践中常将多张页表打包到单一纹理的 Mip 层级中
  • 额外位可用于 LOD 提示、驱逐状态或调试

4.3 完整的采样路径

步骤 1:手动计算 Mip 级别

虚拟纹理 不能依赖 GPU 自动 Mip 选择(因为驻留是显式管理的),Shader 必须使用 屏幕空间导数(Screen-Space Derivatives) 手动计算:

其中 是虚拟纹理 Mip 0 的尺寸。该表达式度量像素在纹素空间中的足迹并转换为 Mip 索引。

步骤 2:计算虚拟页索引和页内偏移

Mip 级别 下的页网格分辨率:

其中 为页大小(Page Size)。基础层包含 个页,每个后续 Mip 分辨率减半。

虚拟坐标先乘以页网格分辨率,产生 整数虚拟页索引页内分数偏移

步骤 3:查页表,获取物理页索引

用虚拟页索引 从页表中 texelFetch 获取条目。若页已驻留,条目编码出 物理页索引 (图集中的槽位坐标)。

步骤 4:计算图集中的最终采样位置

其中 是图集尺寸, 是页大小。将物理页索引加上页内偏移后按图集与页的比例缩放,得到归一化的物理纹理坐标。

步骤 5:从物理图集采样

使用 textureLod(atlas, uv_atlas, 0.0) 直接采样,Mip 级别传 0(因为图集中存的就是正确 Mip 级别的数据)。

4.4 Shader 实现要点

vec4 SampleVirtualTexture(vec2 uv) {
  // 1. Clamp UV
  uv = clamp(uv, 0.0, 1.0 - 1e-7);
 
  // 2. 手动 Mip 计算
  vec2 dx = dFdx(uv) * u_VirtualSize;
  vec2 dy = dFdy(uv) * u_VirtualSize;
  float footprint2 = max(dot(dx, dx), dot(dy, dy));
  int mip = clamp(
    int(floor(0.5 * log2(max(footprint2, 1e-8)))),
    u_MinMaxMipLevel.x, u_MinMaxMipLevel.y
  );
 
  // 3. 页网格分辨率 & 虚拟页索引
  vec2 pages = max((u_VirtualSize / u_PageSize) * exp2(-float(mip)), vec2(1.0));
  vec2 page_coords_f = uv * pages;
  ivec2 virtual_page = ivec2(floor(page_coords_f));
  vec2 page_uv = fract(page_coords_f);
 
  // 4. 查页表
  uint entry = texelFetch(u_PageTable, virtual_page, mip).r;
  if ((entry & 1u) == 0u)
    return vec4(0.0, 0.0, 0.0, 1.0); // fallback
 
  // 5. 解码物理页位置
  ivec2 physical_page = ivec2(
    int((entry >> 1) & 0xFFu),
    int((entry >> 9) & 0xFFu)
  );
 
  // 6. 图集采样
  vec2 atlas_uv = (vec2(physical_page) + page_uv) * u_PageScale;
  return textureLod(u_TextureAtlas, atlas_uv, 0.0);
}
  • 翻译路径保证 Shader 总是产生有效采样
  • 当理想页缺失时,使用 回退(Fallback) 使缺失显式化

五、反馈通道(Feedback Pass)

5.1 为什么需要独立的反馈通道

  • 如果在主渲染通道中记录反馈,每个片元都要写入,破坏了采样的只读行为
  • 成本随 像素数 而非 唯一页数 缩放,完全不划算

5.2 反馈通道的工作方式

  • 降低分辨率 的离屏目标重新渲染场景
  • 产生一个 紧凑摘要:哪些虚拟页在什么 Mip 级别下被需要
  • 用精度换取可扩展性,同时保留驱动驻留决策所需的信息

5.3 反馈条目的编码

每个反馈条目记录:

  • 虚拟页索引(page_x, page_y)
  • Mip 级别

打包为紧凑的二进制表示(例如 21 位:5 位 Mip + 8 位 page_x + 8 位 page_y)

5.4 计算逻辑

  • 与采样 Shader 使用 完全相同的 Mip 计算和虚拟页索引数学
  • 区别:不进行纹理采样——一旦确定虚拟页索引和 Mip 级别,直接写入反馈缓冲区
uint EncodeFeedback(vec2 uv) {
  // 同样的 Mip 计算逻辑...
  // 同样的页网格和虚拟页索引计算...
  
  // 打包结果
  return mip_bits | (page_x << 5) | (page_y << 13);
}
  • 反馈通道完成后,反馈缓冲区包含一组 页请求(Page Requests)
  • 这个缓冲区是 渲染与驻留管理之间的交接点

六、页管理器(Page Manager)

6.1 职责

页管理器 是 CPU 端的编排器,负责将页请求转化为驻留变更:

  1. 解码 反馈缓冲区(使用与反馈 Shader 相同的位布局)
  2. 检查 页表确定页是否已驻留
  3. 分类 请求:
    • 已驻留 → 标记为"当前帧使用"
    • 未驻留 → 发起 异步 I/O 请求 加载页数据
  4. 页数据到达后:
    • 在物理图集中 分配槽位
    • 上传 纹素数据
    • 更新 页表
    • 必要时 驱逐 现有页

6.2 驱逐策略

  • 大多数系统实现 LRU(最近最少使用) 缓存
    • 与典型相机运动的 空间和时间一致性(Spatial & Temporal Coherence) 很好地对齐
  • 缓存管理一组固定的页槽(通过 x/y 坐标标识)
    • 有空槽 → 直接使用
    • 缓存满 → 选择最近最少使用的页驱逐

6.3 页固定(Page Pinning)

  • 某些页可以被 固定(Pin),永不被驱逐
  • 通常固定 最低 LOD 级别的页,保证始终有有效数据可采样
  • 固定防止纹理出现空洞,但必须 谨慎使用
    • 过度固定 → 增加可见的 纹理弹跳(Popping),降低缓存有效性
    • 良好调优的系统中,固定页仅作为 安全网 而非稳态方案

6.4 闭环系统

反馈通道 + 页管理器 形成 闭环

  • 当页交付跟得上需求时,系统 收敛到理想工作集
  • 当跟不上时,系统 优雅降级
    • 采样较低分辨率页
    • 请求堆积
    • 驻留在后续帧中逐渐赶上

七、实践中的虚拟纹理

7.1 id Tech 5 与 MegaTexture

  • 虚拟纹理从论文走向产品的标志性案例是 John Carmackid Tech 5 上的工作,称为 MegaTexture
  • 独特之处:id Tech 5 将虚拟纹理作为 唯一纹理路径(不是可选优化)
    • 静态世界中每个表面都从单一虚拟地址空间采样
    • 消除了每次 Draw Call 绑定离散纹理的需要
  • 视觉效果显著
    • 重复的 Tile 图案消失
    • 美术师可以在大型环境中绘制 唯一细节 而无需担心复用
  • 主要代价不在 GPU 吞吐,而在系统其他部分的延迟
    • 虚拟化将压力转移到 CPU 和 I/O 栈
    • 转码线程争夺 CPU 时间
    • 磁盘寻道延迟影响页交付
    • 工作集超出容量时,延迟的页上传导致 可见的纹理弹出(Texture Pop-in)
  • id Software 在后续引擎中 放弃了完全虚拟化纹理,但底层思想延续了下来

7.2 硬件稀疏纹理(Sparse Textures)

  • 现代 GPU 通过 稀疏纹理 直接在硬件中支持虚拟化纹理寻址
    • 页表翻译在硅片中处理
    • 资源分配与物理驻留解耦
  • 但稀疏纹理只提供机制,不定义策略
    • 不决定加载哪些页
    • 不决定何时驱逐
    • 不生成反馈
    • 跨 GPU 的支持和行为有差异
  • 大多数现代引擎在可用硬件特性之上 实现自己的虚拟纹理系统
    • 优先考虑对驻留、反馈、驱逐的 显式控制跨平台确定性
    • 将硬件支持视为 机制而非完整方案

7.3 适用场景的判断

虚拟纹理仅在狭窄的、数据主导的场景中有效——纹理大小远超 GPU 内存时。

场景推荐方案
大型开放世界,纹理远超 VRAM✅ 虚拟纹理
多通道生物医学成像,体数据远超 GPU 内存✅ 虚拟纹理
大多数实时工作负载,纹理预算适中❌ 传统纹理性能更好、迭代更快

虚拟纹理不是通用升级路径,而是当带宽和内存约束使传统方法不可行时的专用技术。

7.4 引入的新权衡

虚拟纹理将原本的 GPU 内存/带宽问题转化为:

  • CPU 负载:转码、反馈解码、驻留决策
  • I/O 延迟:寻道延迟、上传调度
  • 感知弹跳(Perceptual Popping):页交付不及时时的视觉瑕疵
  • 驱逐策略 的设计复杂性

八、更广泛的模式:虚拟化思想的延伸

8.1 虚拟几何(Virtual Geometry)

  • Nanite 等现代技术遵循相同模式:
    • 只加载和处理对最终图像有贡献的几何体
    • 模糊了实时渲染与离线渲染的界限
  • Carmack 在 id Tech 6 时代曾讨论过虚拟化几何作为 MegaTexture 的后续,但在实现前离开了 id Software

8.2 科学可视化

  • 在生物医学成像中,数据不同但约束相同:巨大的体数据,微小的工作集
  • 虚拟纹理将问题变得可处理:
    • 一旦数据访问被虚拟化,多通道渲染简化为在一致地址空间上的 单次光线步进(Ray Marching)
    • 分辨率自动适应可见内容

8.3 核心原则

性能极限很少由总数据量定义,更多由物理瓶颈定义——屏幕空间、可见性和感知分辨率。

有效系统的构建方式是 结构化数据,使得在任何给定时刻 只有可被观察到的内容需要存在

虚拟纹理是一种更广泛模式的实例: