虚拟纹理(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)
- 驻留状态通过 页表显式追踪
- 同时解决两个问题:
- 永远不分配全分辨率纹理
- 仅上传屏幕实际采样的纹素,限制带宽
- 间接寻址(Indirection)解开了 3D 中的 LOD 选择难题:
- 网格从 单一统一虚拟纹理 中采样
- 纹理绑定 坍缩为一张物理纹理
- 高分辨率和低分辨率页 共存于同一地址空间
关键点:CPU 和 OS 免费提供虚拟内存;在 GPU 上,现代引擎 仍然自己构建 虚拟纹理系统——即使有硬件稀疏纹理支持,引擎也自行管理页表、驻留逻辑和流式系统。
三、系统架构概览
虚拟纹理 不是单个 Shader,而是一个 系统。多个组件跨 CPU 和 GPU 协同工作。
每帧系统必须回答三个问题:
| 关注点 | 执行位置 | 职责 |
|---|---|---|
| 寻址(Addressing) | GPU | 确定每个纹素应从哪里采样 |
| 反馈(Feedback) | GPU → CPU | 记录哪些虚拟页在什么分辨率下被访问 |
| 驻留(Residency) | CPU | 决定哪些页必须存在于 GPU 内存中 |
这三个关注点 被刻意分离:寻址完全在 GPU 渲染期间完成;反馈观察采样行为但不干扰它;驻留决策在 CPU 上做出,便于显式管理策略、缓存和 I/O。
四、寻址:虚拟坐标到物理坐标的翻译
4.1 三层表示
系统对同一纹理操作三种表示:
- 虚拟纹理(Virtual Texture):定义完整地址空间,UV 跨越全分辨率纹理
- 页表(Page Table):翻译虚拟页并追踪驻留状态,通常实现为 2D 纹理,每个纹素对应一个虚拟页
- 物理纹理图集(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 端的编排器,负责将页请求转化为驻留变更:
- 解码 反馈缓冲区(使用与反馈 Shader 相同的位布局)
- 检查 页表确定页是否已驻留
- 分类 请求:
- 已驻留 → 标记为"当前帧使用"
- 未驻留 → 发起 异步 I/O 请求 加载页数据
- 页数据到达后:
- 在物理图集中 分配槽位
- 上传 纹素数据
- 更新 页表
- 必要时 驱逐 现有页
6.2 驱逐策略
- 大多数系统实现 LRU(最近最少使用) 缓存
- 与典型相机运动的 空间和时间一致性(Spatial & Temporal Coherence) 很好地对齐
- 缓存管理一组固定的页槽(通过 x/y 坐标标识)
- 有空槽 → 直接使用
- 缓存满 → 选择最近最少使用的页驱逐
6.3 页固定(Page Pinning)
- 某些页可以被 固定(Pin),永不被驱逐
- 通常固定 最低 LOD 级别的页,保证始终有有效数据可采样
- 固定防止纹理出现空洞,但必须 谨慎使用:
- 过度固定 → 增加可见的 纹理弹跳(Popping),降低缓存有效性
- 良好调优的系统中,固定页仅作为 安全网 而非稳态方案
6.4 闭环系统
反馈通道 + 页管理器 形成 闭环:
- 当页交付跟得上需求时,系统 收敛到理想工作集
- 当跟不上时,系统 优雅降级:
- 采样较低分辨率页
- 请求堆积
- 驻留在后续帧中逐渐赶上
七、实践中的虚拟纹理
7.1 id Tech 5 与 MegaTexture
- 虚拟纹理从论文走向产品的标志性案例是 John Carmack 在 id 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 核心原则
性能极限很少由总数据量定义,更多由物理瓶颈定义——屏幕空间、可见性和感知分辨率。
有效系统的构建方式是 结构化数据,使得在任何给定时刻 只有可被观察到的内容需要存在。
虚拟纹理是一种更广泛模式的实例: