WebGPU法线贴图压缩:BC5与ASTC的RG通道精确保留与WGSL重建实践
在 WebGPU 时代,现代图形管线对显存带宽和渲染品质的要求达到了新的高度。法线贴图(Normal Map)作为决定表面细节的核心贴图,其压缩质量直接影响光影表现。
传统的 BC1/BC3(DXT1/DXT5)压缩格式由于其通道间的关联性(Co-relation)以及对绿色通道的非对称偏好,在压缩法线贴图时极易引入明显的块状伪影和交叉干扰(Crosstalk)。为了获得无损般的法线细节,行业标准的做法是丢弃 Z 通道,仅保留 R、G 通道,并采用针对单双通道优化的压缩格式:在桌面端/主机端采用 BC5,在移动端/跨平台则采用 ASTC。
本文将深入探讨在 WebGPU 中,如何通过 BC5 和 ASTC 格式精确保留法线的 RG 通道,以及如何在 WGSL 着色器中高效、精确地重建 Z 通道。
一、 核心数学原理:切线空间法线的 2 通道重建
在切线空间(Tangent Space)中,法线向量 $\vec{N} = (x, y, z)$ 是一个单位向量,满足恒等式:
$$x^2 + y^2 + z^2 = 1$$
由于切线空间的法线方向总是朝向表面外侧(即 $z \ge 0$),我们可以忽略负半轴。因此,只要确保 $x$ 和 $y$(即法线贴图的 R 和 G 通道)的精度,就可以在着色器中安全地推导出 $z$:
$$z = \sqrt{\max(0.0, 1.0 - x^2 - y^2)}$$
通过在压缩阶段丢弃 Z 通道,我们可以把原本分配给 Z 的宝贵码率(Bitrate)全部倾斜给 R 和 G 通道,从而实现画质的倍增。
二、 BC5 压缩方案(桌面端/WebGPU 主机端)
BC5(在 Direct3D 中称为 BC5,在 OpenGL/Vulkan 中常称为 RGTC2)是专门为双通道数据设计的块压缩格式。
1. 存储机制
- 块大小:8x8 像素的图像块被压缩为 16 字节(128 bit),每个通道分配 64 bit。
- 高精度独立编码:BC5 的 R 和 G 通道是完全独立编码的,每个通道使用类似于 BC4(或 DXT5 Alpha 块)的渐变区间插值算法。它在块内定义两个 8 位的基准值,然后生成 6 或 8 个渐变插值点,每个像素用 3 个 bit 来索引这些插值。
- 无通道干扰:由于 R 和 G 物理上完全分立,彻底杜绝了 BC3 中 RGB 共享终点色导致的色彩交叉干扰,法线梯度的平滑度极高。
2. WebGPU 格式选择
在 WebGPU 中使用 BC5 需要开启 "texture-compression-bc" 扩展。推荐使用 bc5-snorm 格式,而不是 bc5-unorm:
// WebGPU 设备初始化时需请求扩展
const device = await adapter.requestDevice({
requiredFeatures: ["texture-compression-bc"]
});
// 创建纹理时使用 bc5-snorm
const normalTexture = device.createTexture({
size: [width, height, 1],
format: "bc5-snorm", // 自动将 [-1.0, 1.0] 的有符号数据映射到内部
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
});
为什么优先选择 bc5-snorm?
bc5-unorm:采样器返回的值域在 $[0.0, 1.0]$。在着色器中,你需要手动进行重映射:$x = x_{raw} \times 2.0 - 1.0$。bc5-snorm:采样器返回的值域直接是 $[-1.0, 1.0]$。WebGPU 驱动层和硬件会自动在采样时完成解包,消除了着色器中的算术开销,性能更优。
三、 ASTC 压缩方案(移动端/跨平台)
在移动端设备(如 iOS、Android)上,BC5 并不被硬件原生支持。我们需要使用 ASTC(Adaptive Scalable Texture Compression)格式(WebGPU 扩展为 "texture-compression-astc")。
ASTC 极其灵活,但若直接将普通 RGB 格式的法线丢给 ASTC 进行块压缩,由于其内部色度/亮度终点分配机制,仍可能在边缘产生伪影。
1. 精确存储:ASTC_RA(双平面模式)
为了在 ASTC 中精确保留 RG 通道,图形学界通用的做法是双平面编码(Dual-Plane Mode),通常称为 ASTC_RA 布局:
- 将法线的 $X$ 写入 Red 通道。
- 将法线的 $Y$ 写入 Alpha 通道(而非 Green)。
- 在 ASTC 编码器中,启用双平面压缩(Dual-Plane),并将第二个平面指定为 Alpha 通道。
技术细节:ASTC 支持将一个块的比特流分配给两个独立的平面(例如,Plane 1 负责 RGB,Plane 2 负责 Alpha)。通过将 $X$ 和 $Y$ 分别放在 Red 和 Alpha 两个通道,并开启双平面模式,这两个通道就获得了完全独立的端点插值。这与 BC5 的独立编码思想异曲同工,能极大限度降低通道间的串扰。
2. 编码器配置示例
使用 ARM 官方的 astcenc 命令行工具进行法线贴图编码时,应指定 -normal 模式(该模式会自动选择双平面,并将 Y 分量映射至 Alpha 通道):
# 使用 6x6 块大小进行高品质法线压缩
astcenc -cl normal_map.png normal_map_6x6.astc 6x6 -normal -thorough
3. WebGPU 格式映射
由于 ASTC 在采样时会将未指定的通道填充为默认值,在 WebGPU 中,我们通常使用通用格式如 astc-4x4-unorm 或 astc-6x6-unorm:
const normalTexture = device.createTexture({
size: [width, height, 1],
format: "astc-6x6-unorm", // 配合 astc_ra 布局
usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST
});
四、 WGSL 精确重建 Z 通道实现
在着色器中,我们需要根据采样的 RG 数据(或 RA 数据),以最高效的方式恢复出完整法线。
下面给出了针对 BC5 (Snorm) 和 ASTC (RA 布局) 的 WGSL 核心采样函数实现:
// 绑定资源
@group(0) @binding(0) var normalSampler: sampler;
@group(0) @binding(1) var bc5NormalTex: texture_2d<f32>; // bc5-snorm
@group(0) @binding(2) var astcNormalTex: texture_2d<f32>; // astc-unorm (RA layout)
// ==========================================
// 1. 从 BC5-Snorm 纹理重建法线
// ==========================================
fn reconstructNormalBC5(uv: vec2<f32>) -> vec3<f32> {
// bc5-snorm 采样后,xy 分量已经自动映射在 [-1.0, 1.0] 区间
let raw = textureSample(bc5NormalTex, normalSampler, uv).xy;
let x = raw.x;
let y = raw.y;
// 浮点数计算误差可能导致 1.0 - x^2 - y^2 略小于 0,必须使用 max 进行保护
let z = sqrt(max(0.0, 1.0 - x * x - y * y));
return vec3<f32>(x, y, z);
}
// ==========================================
// 2. 从 ASTC (RA 布局) 纹理重建法线
// ==========================================
fn reconstructNormalASTC_RA(uv: vec2<f32>) -> vec3<f32> {
// 采样 unorm 纹理,返回 [0.0, 1.0] 区间的值
let raw = textureSample(astcNormalTex, normalSampler, uv);
// 提取 Red (X) 和 Alpha (Y) 通道,并手动映射到 [-1.0, 1.0]
let x = raw.r * 2.0 - 1.0;
let y = raw.a * 2.0 - 1.0;
// 重建 Z 通道
let z = sqrt(max(0.0, 1.0 - x * x - y * y));
return vec3<f32>(x, y, z);
}
关键细节说明:
- 防止 NaN 异常:由于浮点插值与压缩精度损失,若某个像素的法线在横向上被拉扯得非常极限,计算 $1.0 - x^2 - y^2$ 时可能会得到微小的负数(例如
-0.00001)。直接对其求平方根(sqrt)会产生NaN,导致渲染出黑点。在 WGSL 中,使用max(0.0, ...)进行截断是极其必要的一步防错。 - 切线空间转换:重建出来的
vec3<f32>(x, y, z)处于切线空间,后续可通过典型的 TBN(Tangent, Bitangent, Normal)矩阵将其变换至世界空间参与光照计算。
五、 性能与画质对比总结
| 特性 / 格式 | BC5 (snorm) | ASTC (RA layout, 6x6) |
|---|---|---|
| 平台支持 | 桌面端 (Windows, macOS) | 移动端 (iOS, Android), Apple Silicon Mac |
| 显存占用 (BPP) | 8 bpp (每个像素 1 字节) | 3.56 bpp (对于 6x6 块大小) |
| 着色器算术开销 | 极低 (无重映射, 仅 1x max, 1x sqrt) |
较低 (需执行一次 MAD 操作重映射) |
| RG通道隔离度 | 绝对物理隔离(高) | 强隔离(双平面独立端点分配) |
| 重建品质 | 极高,无色差,无宏块效应 | 高,通过调节 Block Size 可弹性微调画质/带宽 |
生产环境最佳实践指南:
- 多平台资产分发:在打包 WebGPU 应用程序时,建议通过检测当前环境支持的 Feature(
device.features.has("texture-compression-bc")还是"texture-compression-astc"),在运行时分发不同的纹理资产。 - 对于精度要求严苛的微表面(如雨水冲刷的金属、高光瓷砖):在 PC 端坚定选择
bc5-snorm;在移动端,若显存和带宽允许,ASTC 推荐使用4x4或5x5块大小,并始终采用RA双平面模式进行编码。