
低级 API 中的纹理视图支持
在本次 Evergine 发布中,我们将 Texture Views 作为底层图形 API 的一等概念引入。Texture Views 允许你引用纹理内的子范围、重新解释像素格式以及更改视图类型——所有这些都无需创建副本或分配额外的 GPU 内存。此功能使 Evergine 与现代图形 API 保持一致,并消除了以往影响性能的不必要纹理复制。
什么是 Texture View?
GPU 上的纹理可以有多种形式:1D、2D、3D、立方体贴图、数组以及它们的组合。现代图形 API(如 DirectX 12、Vulkan、Metal 和 WebGPU)不允许直接将纹理绑定到渲染管线。相反,它们需要一个 Texture View——一个描述 GPU 应如何解释纹理数据的对象。
Texture View 本身不包含任何数据。它引用一个现有纹理,并提供一个透镜,GPU 通过该透镜读取或写入数据。你可以将其视为纹理的一个窗口:它可以显示整个纹理,也可以只显示其中的一部分,甚至能以不同于原始纹理的格式或结构呈现数据。

核心能力
Evergine 中的 Texture Views 提供四项主要能力:
子范围选择
你可以从纹理中选取 mip 级别和数组图层的子集。这由四个参数控制:baseMip、mipCount、baseLayer 和 layerCount。
例如,假设你有一个包含 18 个图层的 Texture2DArray,代表 3 个点光源的阴影贴图(每个光源 6 个立方体贴图面)。你可以创建单独的 Texture Views,分别引用图层 0–5、6–11 和 12–17,从而为每个光源提供其自己的立方体贴图视图,而无需复制任何数据。
格式重新解释
只要格式兼容,Texture Views 就可以用不同的像素格式呈现相同的数据。例如,以 R8G8B8A8_UNorm 存储的纹理可以被视为 R8G8B8A8_UNorm_SRgb 以应用伽马校正。同样,以 D32_Float 存储的深度缓冲区可以被视为 R32_Float,以便计算着色器将其作为常规浮点值读取。
视图类型更改
Texture View 可以更改 GPU 解释纹理结构的方式。恰好具有 6 个图层的 Texture2DArray 可以被视为 TextureCube,从而允许着色器使用立方体贴图坐标对其进行采样。这对于基于立方体的渲染技术(如点光源阴影贴图或环境探针)特别有用。
双重绑定
借助 Texture Views,你可以从同一纹理创建 Shader Resource View(用于读取)和 Unordered Access View(用于写入)。这意味着计算着色器可以写入纹理,而像素着色器可以从中读取,无需创建中间副本。
引入 Texture Views 之前的问题
在引入 Texture Views 之前,Evergine 创建的每个纹理都具有固定的视图类型。例如,在 DirectX 11 中,纹理在创建时要么绑定为 Shader Resource View,要么绑定为 Unordered Access View——且该选择之后无法更改。
这一限制使得无法将同一纹理同时用作输入和输出。如果计算着色器需要写入纹理,然后像素着色器需要从中读取,引擎就必须维护两个独立的纹理,并在它们之间执行 GPU 复制。这导致内存使用量翻倍,并消耗了每次复制操作的 GPU 带宽。
后处理管线受到的影响尤为严重:链中的每个效果都需要自己的中间纹理和复制操作,在复杂场景中累积起来会造成显著的开销。

Texture Views 在 Evergine 中的使用方式
在此版本中,我们已在引擎的三个关键领域内部应用了 Texture Views:
后处理管线
现在,每个后处理效果都会在其输入纹理上创建一个具有适当格式的 Texture View。通过使用格式重新解释(通过 ToColorFormat()),管线消除了先前处理节点之间所需的中间副本。
色调映射
色调映射通道先前需要完整的纹理复制才能读取 HDR 输入。借助 Texture Views,现在可以直接以兼容格式引用同一纹理,从而完全消除了复制操作。
计算深度范围
此计算着色器计算帧中的最小和最大深度值,以动态调整相机的近远裁剪面,从而保持深度精度。它需要读取深度缓冲区,但深度格式无法直接作为颜色数据采样。通过纹理视图重新解释格式,计算着色器便能读取它。
在底层 API 中使用纹理视图
在 Evergine 中创建纹理视图非常简单。Factory.CreateTextureView 方法接受源纹理和可选参数来自定义视图:
TextureView CreateTextureView(
Texture texture,
PixelFormat pixelFormat = PixelFormat.Unknown,
uint baseMip = 0,
uint mipCount = uint.MaxValue,
uint baseLayer = 0,
uint layerCount = uint.MaxValue,
TextureType? viewType = null);以下是三个实际示例:
从包含 6 个图层的 2D 数组创建立方体贴图视图
// facesTexture is a Texture2DArray with ArraySize = 6
var cubeView = gfxFactory.CreateTextureView(facesTexture, viewType: TextureType.TextureCube);创建无伽马校正的存储视图,用于计算着色器写入
UAV 不支持 sRGB 格式:
var facesTextureDesc = new TextureDescription
{
Type = TextureType.Texture2DArray,
Format = PixelFormat.R8G8B8A8_UNorm_SRgb,
Width = 1024,
Height = 1024,
ArraySize = 6,
Flags = TextureFlags.ShaderResource | TextureFlags.UnorderedAccess,
};
var facesTexture = graphicsContext.Factory.CreateTexture(ref facesTextureDesc, "Faces");
// Remove gamma for UAV compatibility
var storeView = graphicsContext.Factory.CreateTextureView(
facesTexture, pixelFormat: facesTextureDesc.Format.WithoutGamma());重新解释深度缓冲区,供计算着色器读取
var depthView = graphicsContext.Factory.CreateTextureView(
depthTexture, pixelFormat: PixelFormat.R32_Float);纹理视图已在所有渲染后端实现:DirectX 11、DirectX 12、Vulkan、Metal、WebGPU 和 OpenGL。
下一步计划
此版本为 Evergine 中的纹理视图奠定了基础。在后续版本中,我们计划将其应用扩展到引擎的更多部分。
其他渲染管线阶段将采用纹理视图,尽可能消除中间副本。
我们还计划在高层 API 中更广泛地公开纹理视图,使开发者无需直接操作底层即可在自己的项目中更轻松地利用此功能。
结论
纹理视图是现代图形 API 中的基础概念,Evergine 现已完全支持。它们消除了不必要的纹理复制,减少了 GPU 内存占用,并支持在不分配额外资源的情况下灵活地重新解释数据。后处理管线、色调映射和深度范围计算已从此功能中受益,更多引擎系统将在未来版本中跟进。
Daniel Cáceres Maña – 高级研究员
