移动端 shader 也能跑的 NTC(Fast Neural Texture Compression)

性能数据

压缩率

原 1k 纹理包含:Albedo(rgb)、Normal(xyz)、Roughness/AO/Metallic(xyz)

压缩方式 总存储 压缩比 备注
TGA 9 M 1 每个 RGB 纹理 3M
ASTC 4x4 3 M 3.00 每个 RGB 纹理 1M
ASTC 6x6 1.336 M 6.73 每个 RGB 纹理 456KB
FNTC 6x6(Quality) 0.891 M 10.10 两特征纹理分别 456KB 和 456KB,还有少量网络参数(1.5 KB,忽略不计)
FNTC 6x6(Performance) 0.557 M 16.16 两特征纹理分别 456KB 和 114KB,还有少量网络参数(1.5 KB,忽略不计)

纹理质量 [TODO]

Reference:1k PBR纹理材质

PSNR SSIM LPIPS
FNTC(Quality) 44.3085 0.9785 0.0049
FNTC(Performance) 40.5549 0.9717 0.0097

此处质量为训练时质量,还未经过ASTC压缩,因此仅供参考。

实际场景:

Reference(1k PBR纹理材质)

Ref

NTC(Performance)

NTCLQ

NTC(Quality)

NTCHQ

还有一些截图(没来得及整理,之后再补更好看的比较图吧):

左边的材质为传统材质,右边的材质均为NTC材质(performance)。可以看到NTC材质甚至结构更清晰一些。

ScreenShot00001

ScreenShot00000

Feature texture 存储什么值虽然是黑盒,但是我们可以假定(猜测)大概率存的是某种表示纹理结构的值。因此如果直接对 feature texture 使用 ASTC/BC 等传统块压缩方法,可能造成一定的色偏(但不明显),但纹理结构仍然保持清晰,看起来像是更锐化一样。

推理性能

Sponza 场景全屏材质测试(FNTC 6x6 Performance):

ASTC 材质 NTC 材质
FPS 55.0 55.0
GPU clocks/sec 631.58 mega 633.71 mega
ALU / Fragment 166.27 184.95
EFU / Fragment 6.38 6.23
Textures / Fragment 8.69 7.94
Read Total(Bytes/sec) 9.11 giga 8.6 giga

Nvidia NTC 网络结构

Nvidia Neural Texture Compression (NTC) 是一种专为材质纹理设计的新型神经压缩技术。它旨在以极低的比特率实现高保真的纹理压缩,同时支持GPU上实时、随机访问的解码,使其能够替代传统的块压缩(如BC系列)用于磁盘和内存存储。

其核心思想在于联合压缩同一材质的多个纹理通道及其整个mipmap链,充分利用了纹理数据中的多种冗余:

  • 空间相关性:同一纹理内相邻像素间的相关性。
  • 跨mip冗余:高分辨率mip与低分辨率mip之间的强相关性。
  • 跨通道冗余:例如粗糙度(Roughness)与基础色(BaseColor)的细节、法线(Normal)与高度(Height)纹理的细节在局部空间上往往高度相关。

通过一个为每个材质集(Texture Set)专门优化的小型神经网络(MLP)解码器,NTC能够从高度压缩的潜在特征(Latent Feature)中重建出所有通道和所有mip级别的纹理数据。

img

图:NTC解码流程概览。从压缩的特征金字塔中采样,结合位置编码,通过小型MLP解码出最终的纹理值(所有通道)。*

位置编码(Positional Encoding)

为了帮助MLP理解纹理坐标的局部相位信息,从而重建高频细节,NTC引入了位置编码。但它并未使用NeRF等工作中常见的正弦/余弦编码,而是采用了计算开销更低的三角波位置编码(Triangular-wave Positional Encoding),在几乎不损失质量的前提下提升了效率。

Tiled 三角波编码

NTC的位置编码并非针对全局UV坐标,而是针对局部Tile内的相对像素坐标。具体而言:

  1. 纹理在概念上被划分为8×8的Tile。
  2. 对于目标纹素,计算其在所属Tile内的局部坐标 \((u_{local}, v_{local}) \in [0, 1)^2\)
  3. 对每个局部坐标方向(水平u和垂直v),分别生成一组多频段(Octave)的三角波标量。通常,每个方向使用6维编码(包含一个常数项作为偏置基底),因此总共产生12维的位置编码向量。

这种Tiled Encoding模式的设计动机在于:

  • 周期性复用:编码模式在8×8的Tile上周期性重复,使得MLP只需学习局部相位模式,而无需关心纹理的绝对位置。这极大地提高了网络权重的复用性和解码时的随机访问效率。
  • 匹配上采样需求:由于特征网格(Feature Grid)的最大上采样倍率为8(从最低分辨率Grid到LOD 0),因此只需要支持 \(\log_2 8 = 3\) 个octave的高频信息。8×8的Tile周期恰好能满足这一需求,让MLP利用“相位信息 + 邻域潜在特征”的组合来恢复亚像素级别的高频细节(如锐利边缘、条纹),同时避免因量化导致的严重条带(Banding)伪影。

在解码时,这12维的位置编码向量将与从特征网格中采样的潜在特征向量拼接,共同作为MLP的输入。

特征网格(Feature Grid)

NTC的压缩表示是一个多级特征金字塔(Feature Pyramid),它替代了传统的逐mip、逐通道存储的纹理链。

多级特征网格结构

每个特征级别 \(F^j\)

包含一对2D潜在特征网格:

  • 高分辨率网格 \(G_0^j\):分辨率较高,用于捕捉和保留高频纹理细节。
  • 低分辨率网格 \(G_1^j\):分辨率较低,用于稳定地重建低频内容和平滑渐变。

一个特征级别 \(F^j\) 通常负责预测2到3个连续的mip级别。例如,对于一个1024×1024的纹理集,特征级别的配置可能如下所示:

特征级别 \(F^{j}\) \(G^{j}_{0}\) 网格分辨率 \(G^{j}_{1}\) 网格分辨率 预测的mip级别
0 256×256 128×128 0, 1, 2, 3
1 64×64 32×32 4, 5
2 16×16 8×8 6, 7
3 4×4 2×2 8, 9, 10

通过这种设计,整个mip链的存储开销从传统方式的额外33%降低到约6.7%或更少。

量化与采样

  • 量化:特征网格中的值在训练时通过添加均匀噪声来模拟量化,最终被量化为固定的低比特数(例如4-bit)。这大幅减少了存储占用。
  • 采样与插值:解码一个纹素时,系统根据其LOD选择合适的特征级别 \(F^j\),然后从两个网格中采样特征:
    1. 高分辨率网格 \(G_0^j\):采用学习式插值(Learned Interpolation)。取目标纹素周围2×2的四个邻域特征向量,将它们与位置编码向量拼接后输入MLP。MLP学习如何最佳地组合这些特征来重建高频细节,这比标准的双线性插值能保留更锐利的信息。
    2. 低分辨率网格 \(G_1^j\):采用标准的双线性插值。其平滑的输出有助于抑制因高分辨率网格特征重度量化可能产生的条带伪影。

多通道与多mip的共享表达

关键优势在于,所有材质通道(如Albedo, Normal, Roughness, Metallic等)和所有mip级别共享同一套特征网格和同一个MLP解码器。在训练时,MLP和特征网格被联合优化,以最小化所有通道、所有mip级别的重建损失(如L2损失)。这使得模型能够自动发现并利用跨通道和跨尺度的相关性,实现极高的压缩比。

激活函数

NTC在MLP的隐藏层中使用了Hard GELU激活函数,这是GELU(Gaussian Error Linear Unit)的一个快速、分段线性近似版本,定义如下:

\(\operatorname{hardGELU}(x)= \begin{cases}0, & \text { if } x<-\frac{3}{2}, \\ x, & \text { if } x>\frac{3}{2}, \\ \frac{x}{3}\left(x+\frac{3}{2}\right), & \text { otherwise } .\end{cases}\)

该函数在保持GELU非线性特性的同时,计算效率更高,适合实时渲染。需要注意的是,NTC仅在隐藏层使用激活函数,最终的输出层是线性的。

FNTC 网络结构

image-20251128221851086

Forward 过程:

  1. 通过 UV 采样两张经过 ASTC 解压后的 R8G8B8A8 特征纹理,从而获得了 8 个插值后的特征值并 concat 成一个特征向量。
  2. 该特征向量(8 长度向量)进行一层矩阵向量乘法(8x9 矩阵)后并添加 bias(9 长度向量),最终得到各个纹理值的输出(9 长度向量,即 Albedo/Normal/ORM)。

位置编码(Positional Encoding)

一开始,我们也和 Nvidia NTC 那样采用了三角波(Triangle Waves)编码,引入了 3+3(分别为 uv 两轴) 个 positional encoding features,但是 shader 推理会为此引入了很多额外的指令数,这给移动端会造成不小的性能负担。

最后我们决定把 positional encoding 给砍掉。但是对于 PC 端而言,这部分性能负担并不大,完全可以保留下来以进一步提高纹理质量。

特征网格(Feature Grid)

注:2D Grid 实际上就是 2D Texture,后面如果提到了 feature texture 的话其实意思和 featrue grid 是一样的。

特征量化

我们采用了两张 R8G8B8A8 格式的纹理来作为 feature texture,每次硬件采样都可以采样出 4 个量化为 8 bits 的特征(分别对两张纹理采样就可以获得 8 个特征)。

一般来说,同样的特征向量存储量,特征数量越多越好(特征量化的位数越低越好)。

微调网络时发现当特征数量少于 8 时,图像会有一定程度的偏色问题(或者说色相偏移),虽然玩家不会觉得违和,但是对美术来讲可能是个问题。因此必须得使用至少两张 RGBA 特征纹理。

我们也尝试了量化为 4 bits(一次采样 8 个特征)和 2 bits(一次采样 16 个特征)的方案,效果都比 8 bits 进一步提升,但只能使用图形库 API 中的 R32_UINT 纹理,但最终没有采用该方案,其缺点如下:

  • mipmap level 需要手动计算像素的 uv 梯度。
  • 无法实现各向异性过滤特性。
  • 需要采样相邻两级 mipmap 的相邻 2x2 个 texel(共 8 次采样),哪怕有 GatherRed 指令可以一次采样获得同层 mipmap 相邻 2x2 个 texel 值,也得需要 2 次采样。采样后的每个 texel 值还得解码成 8 个特征值,并且手动进行三线性插值,引入了大量的指令计算。

特征纹理再压缩

FNTC 先训练出 feature texture 的原始数据后,再用传统块压缩方法(例如 ASTC)进行二次压缩 feature texture 来得到 compressed feature texture,从而进一步提高压缩率。

最后实验发现,使用 ASTC6x6 压缩的 1k 分辨率 compressed feature texture 通过推理得到的物体纹理结果和使用 ASTC6x6 压缩的 1k 分辨率物体纹理几乎没有差异。甚至在一些远景中前者能更好保留结构信息,从而看起来更加清晰和锐化些。

多级特征网格

为了更高的压缩率,我们可以为特征纹理采用不同的低分辨率,从而让特征纹理学习到高频特征和低频特征,进一步减少冗余信息。

例如,两张 R8G8B8A8 纹理分别采用原始分辨率(纹理组分辨率)和半分辨率。这样既保证采样出来仍然是 8 个特征,同时压缩比再次提升约60%,并且效果上并没有太大损失,高频细节和低频结构都基本保留了。

激活函数

在神经纹理压缩(NTC/FNTC)这类实时场景中,激活函数的选择本质上是精度-速度-稳定性的三方权衡:在目标画质接近的前提下,通常可以用“更强的非线性 + 更小的网络”替代“更弱的非线性 + 更大的网络”,从而降低总体推理成本。但非线性也不是越复杂越好:一旦引入大量 exp、tanh、三角函数等高代价操作(依赖 SFU),其额外开销往往会超过收益,出现明显的边际递减。

因此我们选取了比 ReLU 更有表达力,但又避免重型超越函数的一些候选函数:

  • Leaky ReLU(线性分段,计算最轻)

  • hard-GELU(NVIDIA NTC 使用的近似 GELU 方案)

  • hard-Swish([2024] MobileNetV3 提出,兼顾效果与效率)

在多组纹理上的替换实验表明:不同纹理分布下最优激活并不完全一致,这与激活曲线和纹理信号形态的匹配度有关。但从跨数据集表现看,hard-Swish 平均表现结果最好和发挥平稳(大部分情形都基本只差 hard-GELU 一点点,极端情形发挥第一),因此最终采用 hard-Swish 作为激活函数:

\[\operatorname{HardSwish}(x)=\left\{\begin{array}{cc} 0 & \text { if } x \leq-3, \\ x & \text { if } x \geq+3, \\ x \cdot(x+3) / 6 & \text { otherwise } \end{array}\right. \]

hard-Swish 平均表现更好的原因也具有可解释性:

  1. 比 Leaky ReLU 更有表达力:Leaky ReLU 仅由两段线性函数构成,非线性容量有限;hard-Swish 在中间区间引入平滑门控,对连续变化纹理(渐变、细节过渡)拟合更充分。

  2. 比 hard-GELU 过渡区更宽:hard-GELU 的非线性主要集中在较窄区间,而 hard-Swish 的有效过渡区更大([-3, 3]),对大幅值波动和极端纹理的适应性更好,鲁棒性更强。

Pixel Shader 推理优化

硬件采样

前面我们将特征量化为了 8 bits,那么就刚好适合装进 R8G8B8A8 纹理里,并通过 Anisotropic Sampler(各向异性采样器)或者 Bilinear Sampler(双线性采样器)进行采样,这样就能通过一次 TextureSample2D 采样来获得相邻两级 mipmap 的相邻 2x2 个 texel 插值后(三线性插值)的 4 个特征值。这样做的好处如下:

  • 硬件插值采样会自动计算像素的 uv 梯度。
  • 使用各向异性采样器时可以支持传统纹理的各向异性过滤模式。
  • 通过硬件完成了各向异性的三线性插值,避免引入了手动线性插值的大量指令。
  • 可以进一步简化网络:mipmap level 不再需要作为 feature 编码进网络。

网络参数存储

网络参数我们将存储在 uniform buffer object(UBO)上。当 UBO 的内容极少时(实时上我们的performance网络也是极小的),实际上会放在寄存器上(除非寄存器溢出),从而避免我们获取网络参数的行为变成了访存行为。

通用矩阵向量乘法(GEMV)

注意几个点:

  • FP16 运算:由于 shader 目前还不能支持 FP8/INT8/INT4 等常见的量化运算,而只支持 float(FP32) 和 half(FP16) 运算。为此我们尽可能在 shader 使用 half 变量及运算,从而减少运算量和寄存器占用。
  • 循环展开:shader 里需要进行矩阵向量乘法,该实现是通过一个循环来进行向量点乘。记得加上 [unroll] 展开一下循环就好。
  • 使用 mul(vec4, mat4x4)。

Hard-Swish 优化

由于激活函数会被多次调用,因此激活函数的指令优化也非常重要。

hard-Swish函数原始形式如下:

hardSwish(x) = x * clamp((x + 3.0f) / 6.0f, 0.0f, 1.0f)

可利用MAD指令进一步优化为以下形式(每次调用减少 1 个 ALU 指令):

hardSwish(x) = x * saturate(x * (1.0f / 6.0f) + 0.5f)

基本 shader 代码

// Feature Texture
half4 SampledFeatures0 = Texture2DSample(FeatureTexture0, Sampler, UVs).rgba - 0.5f;
half4 SampledFeatures1 = Texture2DSample(FeatureTexture1, Sampler, UVs).rgba - 0.5f;

half Output[9];
// GEMV : Vec8 * Mat8x9 = Vec9
[unroll]
for (uint i = 0; i < 9; ++i)
{
	uint BaseIndex = i * PackedInputLen;

    half x = 0.0f;
	x += dot(SampledFeatures1, half4(Network[BaseIndex + 0]));
	x += dot(SampledFeatures2, half4(Network[BaseIndex + 1]));
	x += Network[BaseIndex + 2]; // Bias

	// Leaky ReLU
    Output[i] = x - 0.99f * min(x, 0.0f);
}

// Output
BaseColor = half3(Output[0], Output[1], Output[2]);
Normal = half3(Output[3], Output[4], Output[5]) * 2.0f - 1.0f;
Roughness = Output[6];
AmbientOcclusion = Output[7];
Metallic = Output[8];

未来展望

块压缩感知训练

需要支持可微块压缩,通过反向传播来直接训练块压缩格式下的数据(即直接训练 compressed feature texture 数据),从而进一步提高 FNTC 的质量。

目前已有论文实现了类似的工作了,用 近似 的可微块压缩算法来让网络训练感知:[2024] Real-Time Neural Materials using Block-Compressed Features

ps:我们也正在实现中了。

更好的位置编码 [TODO]

RoPE

支持 NTC 格式的游戏引擎材质系统

物体的纹理组一般包含 BaseColor、Normal、ORM 三个纹理,当然也可以根据项目实际需要扩展更多纹理。而一个理想的资产工作流应当是:艺术家和以前一样仍然只关注传统纹理和传统材质,但是材质系统可以提供一个生成按钮:

  1. 该材质自动生成一个新的 permutation(NTC 变体)
  2. 该材质将所用到的传统纹理合成一个纹理组,并开启一个异步任务训练它,并输出 NTC 的资产文件(n 个 texture 文件和 1 个 npz 文件)。
  3. 该材质的 NTC 变体可以自动绑定 NTC 资产文件。

这样,当我们需要打开 NTC 时,就可以运行时将材质切换成 NTC 变体。

当然,这样的话包体就会同时包含传统资产和 NTC 资产,shader 变体也会变多。因此激进点的项目可以考虑彻底砍掉材质的传统变体,只保留 NTC 变体进包,就能同时享受到包体和显存的收益了。

训练加速

目前 FNTC 训练是基于 tiny-cuda-nn 的框架去训练的,一组纹理组训练到基本收敛大约需几分钟,这会严重拖慢 NTC 材质资产的生成速度(一个游戏项目具有数千上万纹理组)。因此要落地游戏工业,还需要更高效更迅速的 CUDA 融合算子来加速训练。

其它扩展

除了材质纹理组外,FNTC 的算法原理也比较适用于一些烘焙类纹理资源的压缩,例如:

  • lightmap/probe TOD 资源:举个例子,假如我们需要烘焙24张lightmap/probe(代表24小时),这24张纹理在结构上都是一致的,只是因为时间点不同而映射出不同的颜色,完全也可以用 FNTC 来做压缩(只不过输出通道不是9个而是24个)。

总结 [TODO]

项目已经开源,FNTC分支地址在这(要注意不是master分支,是FNTC分支):

posted @ 2025-11-28 22:21  KillerAery  阅读(1714)  评论(0)    收藏  举报