Neural Shading Course

Neural Shading 导论

1. 背景与动机:为何需要 Neural Shading?

图形学在过去40年里一直致力于在极短时间(16ms/33ms)内创造高保真的3D世界。虽然现有的实时渲染技术(如光线追踪)已经取得了巨大进步(比10年前快了约10,000倍),但我们仍面临严峻挑战:

  • 复杂度的规模化难题(The "Strawberry" Problem)
    • 我们目前可以渲染极其复杂的单个物体(如一颗具有精细毛发、次表面散射的草莓)。
    • 核心矛盾:难以将这种级别的细节扩展到整个游戏世界。这面临着存储、传输海量数据以及实时渲染的巨大压力。
  • 硬件红利的消退
    • 摩尔定律(Moore's Law)实际上已经失效。硬件不再像过去那样按部就班地每年大幅提升速度。
    • 单纯依靠硬件性能的自然增长来实全场景高保真渲染已经不再可行。

2. 核心概念:什么是 Neural Shading?

Neural Shading 并不是指单纯地在渲染中加入神经网络,它的定义更为宽泛和实用。

  • 核心定义
    • Neural Shader(神经着色器):指渲染管线中任何可训练(Trainable)的部分。即使它本质上不是严格的“神经网络”,只要能通过优化算法来解决问题,都归入此范畴。
  • 运行位置
    • 不是渲染完成后才运行的独立后处理通道(不同于 DLSS 这样的纯后处理 AI)。
    • 它直接运行在渲染管线内部(Inside the rendering pipeline),可以是计算着色器(Compute Pass)的一部分,也可以集成在光线追踪(Ray Tracing)流程中。

3. 技术优势与价值

Neural Shading 旨在榨干现有硬件的最后一滴性能,并用新的思路解决老问题。

  • 激活闲置硬件
    • 现代消费级 GPU 都搭载了神经网络加速器(Neural Network Accelerators,如 Tensor Cores)。
    • 在传统渲染过程中,这些加速单元通常处于 0% 利用率 的闲置状态(除非在帧末开启 DLSS)。Neural Shading 可以利用这些“沉睡”的算力池。
  • 高效计算
    • 神经网络相关的运算在专用硬件上非常高效。在通用计算性能增长放缓的今天,这是一种提高总计算吞吐量的有效手段。
  • 从“精确解”转向“优化解”
    • 传统图形学倾向于寻找问题的精确数学解。
    • Trainable Shaders 允许我们将问题转化为优化(Optimization)问题。这种近似逼近的方法在处理复杂问题(如压缩、材质表达、几何细节、辐射度缓存、路径引导等)时,往往能以更低的计算成本获得更高质量的结果。

4. 总结与展望

  • 实用工具:Neural Shading 不是为了蹭 AI 热度的噱头,而是解决现有渲染瓶颈的务实工具。
  • 能力要求:它不会自动解决所有问题。引入它需要新的编程模型、算法,以及工程师掌握优化(Optimization)相关的技能。
  • 未来应用:现已可以通过 Vulkan 或 DX(通过 Cooperative Shaders)跨平台访问相关硬件功能,未来有望在渲染管线的各个环节(从几何到光照)发挥作用。

Neural Shading 基础

1. 从“求解”到“优化”:思维方式的转变

传统的图形学工程通常遵循“理解问题 寻找精确解 编码实现”的流程。然而,面对某些复杂问题时,我们可能找不到精确解,或者精确解的实时计算成本过高。此时,我们需要引入优化(Optimization)的思维方式。

  • 核心思想:不再试图直接找到完美的数学公式,而是通过不断调整参数来逼近理想结果。
  • 优化流程
    1. 构建一个包含大量可调参数的数学模型。
    2. 准备大量输入数据和对应的理想输出(Desired Outputs)
    3. 将输入喂给模型,得到实际输出。
    4. 计算实际输出与理想输出之间的差异,即误差(Error)或损失(Loss)
    5. 根据误差反向调整模型参数,旨在减小下一次的误差。
    6. 重复上述过程,直到模型足够好用。

2. 工具介绍:Slang & SlangPy

为了实现上述优化流程,课程使用了以下工具:

  • Slang:一种现代化的、跨平台的着色器语言。
    • 关键特性:支持自动微分(Autodiff)。这意味着它可以自动计算导数,无需手动推导复杂的梯度公式,是进行优化的基础。
    • 由 Khronos Group 管理,支持 D3D, Vulkan, Metal, CUDA 等多种后端。
  • SlangPy:基于 Python 的 Slang 封装库。
    • 提供类似 PyTorch 的功能性 API(Functional API),可以直接在 Python 中调用 Slang 函数。
    • 自动处理底层的 GPU 资源分配、计算核心生成等繁琐工作。

3. 实战案例:Mipmap 生成的优化视角

课程以 Mipmap 生成为例,展示了传统方法的局限性以及优化方法的潜力。

3.1. 问题背景:传统 Mipmap 的缺陷

  • Mipmap 的作用:减少远处物体的纹理混叠,提高显存访问效率。
  • 传统方法:使用简单的盒式滤波器(Box Filter)进行下采样(如取相邻4个像素的平均值)。
  • 局限性
    • 对于颜色贴图(Albedo),简单平均通常效果尚可。
    • 对于几何相关贴图(Normal, Roughness, Displacement),简单平均会导致严重错误。例如,将法线贴图中的一个“峰”和一个“谷”平均,会得到一个平坦的表面,丢失了原有的光照细节,甚至引入错误的镜面高光(Specular Artifacts)。

3.2. 寻找“理想解”(Ground Truth)

为了进行优化,首先需要定义什么是“好”的结果。

  • 低质量参考:直接对 2K 纹理进行下采样得到的 512x512 纹理,渲染结果充满噪点和错误的亮斑。
  • 高质量参考(Ideal Output)
    • 方法超采样(Super Sampling)。在 2K 分辨率下进行完整的 PBR 渲染计算,然后将渲染结果图像下采样到 512x512。
    • 效果:图像稳定,噪点极少,保留了正确的材质光感。
    • 代价:计算量巨大(例如 16 倍的着色计算),无法实时运行,但可以作为离线训练的“金标准”

3.3. 定义“损失函数”(Loss Function)

为了量化当前 Mipmap 的好坏,我们需要一个损失函数来计算它与“金标准”之间的差距。

  • Loss 计算方式
    • 输入:低分辨率材质参数(当前优化的对象)、光照方向、视角方向、高分辨率超采样得到的参考颜色。
    • 过程:用低分辨率材质进行一次实时渲染,得到当前颜色。
    • 输出:(当前颜色 - 参考颜色)^2,即均方误差(MSE)
  • Loss Texture:将每个像素的误差可视化为一张纹理。越亮的地方表示误差越大,也就是需要重点优化的地方。
  • 目标:通过调整低分辨率材质的纹理像素值(不仅仅是简单的平均),使得最终渲染出的 Loss Texture 尽可能变黑。

Neural Shading 核心概念

1. 闭环优化:从推理到反向传播

在上一节中,我们建立了推理阶段(Inference Phase):输入参数(Mipmaps) 渲染函数 计算损失(Loss)。现在我们需要“闭环”,即找出如何调整输入以减小损失。

  • 核心任务:确定输入()需要如何变化,才能使输出的损失()下降。
  • 数学工具导数(Derivative)。它量化了输入的变化如何影响输出。
  • 反向阶段(Backwards Phase):代码需要反向运行,利用导数从损失函数回溯到输入参数,计算出为了降低误差,输入纹理的每个像素应该如何调整。

2. 自动微分(Autodiff):解放生产力

手动推导和编写复杂渲染管线的反向传播代码极其繁琐且容易出错。一旦前向渲染代码(Inference Code)发生任何变动(如修改材质模型),反向传播代码必须重写。

  • Slang 的解决方案:利用编译器的自动微分(Autodiff)功能。
    • 你只需编写前向渲染代码。
    • Slang 编译器在编译时自动分析代码逻辑,生成优化的反向传播导数代码。
    • 优势:极大地提高了迭代速度,保证了前向和反向代码始终同步。
  • 处理非数学操作
    • 对于常规数学运算,编译器知道如何求导。
    • 对于纹理读取(Texture Reads)等内存操作,并没有显而易见的数学导数。
    • 自定义导数:Slang 允许我们为特定函数定义自定义的导数行为。在本例中,我们需要告诉编译器,当读取纹理时,我们关心的是纹素存储值(Texel Values)本身的导数,从而将其存入专门的“导数纹理”中。

3. 优化器:应对噪声的艺术

有了导数,我们就可以更新输入参数了。最简单的方法是梯度下降(Gradient Descent)

  • 学习率(Learning Rate, :一个需要手动调整的自由参数,控制每次更新步长的大小。太小收敛慢,太大可能导致震荡发散。

3.1. 挑战:噪声梯度(Noisy Gradients)

  • 理想情况:为了得到完美的导数,我们需要遍历所有可能的光照方向和视角方向,计算总损失后求导。这在计算上是不可能的。
  • 实际做法:每次迭代随机选择一对光照和视角方向计算损失。
  • 后果:由此得到的梯度充满了噪声(Stochastic Noise),类似于蒙特卡洛渲染中的噪点。朴素的梯度下降法难以处理这种剧烈波动的梯度,导致优化失败。

3.2. 解决方案:Adam 优化器

  • 为了应对噪声梯度,业界通用的标准是使用更高级的优化器,如 Adam
  • 原理:它不仅考虑当前的梯度,还维护了历史梯度的移动平均(Moving Average),从而平滑了噪声,并能自适应地调整每个参数的更新步长。
  • 结果:使用 Adam 后,即使输入梯度有噪声,优化过程也能稳定收敛,生成高质量的 Mipmap。

4. 范式转变:Neural Shading 的真正价值

这一节揭示了 Neural Shading 最激动人心的地方:通用性与可维护性

  • 传统方法(Classical Approach)
    • 需要针对每种材质(如特殊的各向异性高光)发明专门的、复杂的下采样算法(如 Michael Toxvig 的工作)。
    • 工作量巨大,且难以扩展到新材质。
  • Neural Shading 方法
    • 解耦:将“我们想要什么”(通过损失函数定义)与“如何实现”(通过通用优化器实现)分离开来。
    • 通用流程:无论材质多么古怪,只要你能写出它的渲染代码(前向传播),Slang 就能自动生成反向代码。你无需为了新材质重新发明下采样算法,只需重新运行优化循环即可。
    • 这使得解决复杂图形学问题变得更加容易和自动化。

Shader 中的第一个 MLP

1. 从“学习像素”到“生成像素”

上一节我们学习了如何优化现有的纹理像素(Mipmap)。这一节我们将转向神经网络,探讨能否训练一个网络来直接生成纹理像素。

  • 目标:构建一个神经网络,输入纹理坐标 ,输出对应的颜色

2. 神经网络基础:神经元(Neuron)

神经元是神经网络的基本构建块。

  • 数学模型
    • 输入:向量
    • 权重(Weights):向量 ,每个输入对应一个权重。
    • 偏置(Bias):标量
    • 线性计算(点积加偏置)。
    • 非线性激活(Activation Function)。激活函数引入了非线性,使网络能解决复杂问题。
  • 代码实现:本质上就是一个 for 循环,计算加权和,加上偏置,再通过激活函数。

3. 第一次尝试:最简单的网络

我们首先尝试用最简单的网络来拟合一张纹理。

  • 网络结构
    • 输入层:2个节点
    • 输出层:3个节点
    • 无隐藏层。
  • 参数数量 个权重 + 个偏置 = 9 个参数
  • 结果:只能生成一个简单的颜色渐变(Gradient Fill)。
  • 原因:我们试图用区区 9 个参数去拟合包含约 200,000 个像素信息的 256x256 纹理,网络容量严重不足。

4. 改进:引入隐藏层(MLP)

为了提高网络的表达能力,我们需要增加参数,即引入隐藏层(Hidden Layers),构建多层感知机(Multilayer Perceptron, MLP)。

  • 改进后的网络结构
    • 输入
    • 隐藏层 1:32 个神经元
    • 隐藏层 2:32 个神经元
    • 输出
  • 参数数量:增加到 1251 个浮点数参数。虽然比 9 个多得多,但仍远少于原始纹理的 20万个像素数据。
  • 代码变更
    • 在 Slang 中定义多层结构,每层都有自己的权重和偏置。
    • 前向传播时,数据依次通过每一层:Input -> Layer0 -> Activation -> Layer1 -> Activation -> Layer2 -> Activation -> Output
  • 结果:网络开始能大致拟合出纹理的轮廓(如中间的两个大圆盘和周围的星星),虽然仍显模糊(Smudges),但已经从单纯的渐变进化到了能表达形状。

5. 总结

  • 极简网络容量太小,只能拟合最简单的线性变化。
  • 通过增加隐藏层神经元数量,网络(MLP)的表达能力显著增强,能够开始学习和近似复杂的纹理图案。
  • 即使是改进后的 MLP,其参数量也远小于原始纹理数据量,体现了神经网络潜在的数据压缩能力。下一节将继续探讨如何提高拟合精度,从模糊的色块变为清晰的图像。

学习一个 Neural Shader

1. 现实挑战:实时性 vs. 质量

初步训练的 MLP 虽然能生成纹理,但质量堪忧(模糊、缺乏细节)。

  • 增加容量:增加神经元数量和隐藏层数确实能提高质量,最终能近乎完美地拟合纹理。
  • 硬性限制:实时渲染对性能有严格要求,我们不能无限增加网络规模。
  • 核心矛盾:如何在有限的实时预算内,榨干小网络的每一滴性能?这类似于实时光线追踪——需要用聪明的采样策略(如 DLSS, Restir)来弥补光线数量的不足。

2. 工程优化技巧:榨干小网络性能

为了让小网络表现更好,需要一系列工程技巧。

2.1. 训练加速:随机采样

  • 问题:遍历纹理所有像素计算 Loss 太慢。
  • 解决方案
    • 随机采样:每次迭代只随机选取一小部分像素计算 Loss。
    • 抖动网格(Jittered Grid):更优的采样策略,确保样本在空间上分布更均匀。
  • 效果:虽然梯度噪声增加,但相同时间内能进行更多次迭代,最终收敛更快、效果更好。

2.2. 避免“死神经元”:Leaky ReLU

  • 问题:使用标准 ReLU 激活函数时,如果神经元输出为负,梯度变为 0,该神经元从此停止更新,导致训练陷入糟糕的初始状态无法自拔。
  • 解决方案:使用 Leaky ReLU。当输入为负时,给予一个很小的非零斜率,保证梯度始终能回传,避免网络“坏死”。

2.3. 提升平滑度:输出层激活函数

  • 问题:ReLU 导致网络输出是分段线性的,画面看起来像是由无数小三角形拼接而成,不够自然。
  • 解决方案:在输出层使用更平滑的激活函数,如指数函数(Exponential)。它不仅平滑,而且天然“Leaky”(梯度永远非零)。

3. 输入编码(Input Encoding):注入先验知识

仅仅输入 坐标,让小网络去学习复杂的图像结构是非常低效的。

  • 频率编码(Frequency Encoding)
    • 灵感:来源于 JPEG 等图像压缩算法,利用正弦和余弦函数将图像分解为不同频率的分量。
    • 方法:不直接输入 ,而是输入它们的正弦、余弦变换值(不同频率)。
    • 效果:极大提升了小网络捕捉高频细节的能力,质量飞跃。
  • 可学习编码(Learnable Encoding) / 隐空间纹理(Latent Texture)
    • 更进一步:用一个小的、可训练的低分辨率纹理来代替固定的频率编码。
    • 原理:网络学习如何将这个低分辨率的“隐编码”放大、解码成高分辨率纹理。
    • 结果:效果极佳,网络实际上变成了一个神经纹理解压器。

4. 总结与工作流建议

  • 小模型的独特性:大模型(数十亿参数)可以“力大砖飞”,自动学会很多东西;但实时渲染用的小模型需要精心设计输入和结构,注入人类的先验知识(如频率编码)。
  • 快速迭代是关键
    • Neural Shading 的开发需要大量的试错和调参。
    • 工具链:Slang + SlangPy 提供了从 Python 快速原型到 C++ 高性能部署的无缝衔接。避免了用 PyTorch 训练完还得重写 C++ 推理代码的痛苦,保证了着色器代码的单一真实来源(Single Source of Truth)。

自动微分 (Autodiff) 详解

1. 核心概念:前向 vs. 后向微分

Slang 支持两种自动微分模式,它们在数学上等价,但在工程实现和性能特性上各有侧重。

1.1. 前向微分 (Forward Differentiation)

  • 工作原理:在计算函数值的同时,顺便计算输入变量的导数。
    • 类似于高中数学中的求导法则(链式法则)。
    • 对于函数 ,前向微分函数会同时计算:
      • 原始值(Primal Value):
      • 导数值(Differential Value):
  • Slang 中的使用
    • 使用 fwd_diff(func) 获取前向微分函数。
    • 使用 diffPair(val, diff) 创建包含原始值和导数值的特殊数据对。
  • 适用场景:当输入参数很少时(例如计算 SDF 的法线,输入只有 x, y, z 三个坐标)。此时效率较高。

1.2. 后向微分 (Backward Differentiation)

  • 工作原理:先完整运行一次原函数(前向传播),然后从输出端开始,逆向回溯计算每个输入参数对最终输出的影响(梯度)。
    • 对于 ,如果已知 对某个最终损失 的梯度 ,后向微分会计算:
  • Slang 中的使用
    • 使用 bwd_diff(func) 获取后向微分函数。
    • 调用时需要传入输出端的初始梯度(通常为 1.0)。
    • 函数运行后,输入参数的 diffPair 中会被填入计算好的梯度值。
  • 适用场景:当输入参数极多时(例如神经网络训练,可能有数百万个权重参数)。它可以一次性计算出所有输入的梯度,是深度学习训练的核心。

2. 类型系统:可微分 vs. 不可微分

Slang 编译器需要知道哪些变量需要求导,哪些不需要。

  • 内置类型
    • 可微分(Differentiable)float, vector, matrix 等连续值类型。
    • 不可微分(Non-differentiable)int, bool 等离散值类型。求导没有意义。
  • 自定义结构体
    • 默认情况下,自定义 struct 被视为不可微分。
    • 声明方式:让结构体继承 IDifferentiable 接口(例如 struct Point : IDifferentiable { ... }),编译器就会自动为其生成求导代码。

3. 高级特性:自定义导数 (Custom Derivatives)

有时候,编译器的默认求导行为无法满足需求,或者我们需要执行一些特殊操作(如写回显存、调试)。

3.1. 应用场景 1:梯度写回缓冲区 (Buffer Writes)

  • 问题:当函数从全局缓冲区(Buffer)中读取数据时(例如 inputBuffer[index]),index 是整数,不可导。默认的自动微分无法自动将梯度写回原缓冲区。
  • 解决方案
    1. 将缓冲区读取操作封装成一个独立函数,如 getVal(index)
    2. 为这个函数编写一个自定义的后向微分函数,如 getVal_bwd(index, gradient)
    3. getVal_bwd 中,手动将传入的 gradient 值写入到一个专门存储梯度的全局缓冲区中。
    4. 使用 [BackwardDerivative(getVal_bwd)] 属性告诉编译器使用这个自定义函数。

3.2. 应用场景 2:调试梯度 (Debugging Gradients)

  • 问题:自动生成的后向代码是个黑盒,难以查看中间变量的梯度值。
  • 解决方案
    1. 定义一个透传函数 debugGrad(val),它什么都不做,只是返回 val
    2. 为其定义自定义后向函数,在其中插入 printf 打印当前的梯度值。
    3. 在需要调试的地方包裹该函数:y = debugGrad(x * x)。这样就能在不影响计算的前提下“窃听”梯度流。

硬件加速 (Hardware Acceleration)

1. 神经网络层的数学本质

  • 全连接层(Fully Connected Layer):其核心计算可以归结为一次矩阵-向量乘法(Matrix-Vector Multiplication),再加上一个偏置向量,最后通过激活函数。
    • 权重(Weights)构成一个矩阵
    • 输入(Inputs)是一个向量
    • 输出(Outputs)是
  • 并行计算视角:在 GPU 上,成千上万个线程同时运行同一个网络(例如神经纹理采样),但每个线程的输入坐标不同。
    • 这意味着整个 GPU 实际上在执行一次矩阵-矩阵乘法(Matrix-Matrix Multiplication, GEMM)
    • 这种计算模式非常适合 GPU 上的专用硬件加速器(如 NVIDIA Tensor Cores)。

2. Cooperative Vectors (CoopVec):解锁硬件潜能

为了让普通着色器代码也能利用 Tensor Cores,Slang 引入了 Cooperative Vectors。

  • 传统方法的局限:直接使用硬件内建函数(Intrinsics)非常繁琐,需要手动管理线程协作、共享内存打包,且只能在统一控制流(Uniform Control Flow)中使用。
  • Cooperative Vectors 的优势
    • 提供了一种高层抽象,让你能像写普通向量代码一样写矩阵运算。
    • 编译器自动将其映射到高效的硬件指令(如 Tensor Cores),处理底层的线程协作细节。
    • 跨平台:支持 D3D12 (Shader Model 6.9+), Vulkan (NV_cooperative_vector), Metal 4。

3. 使用 CoopVec 进行推理(Inference)

将普通的 MLP 代码改造为硬件加速版本非常简单:

  1. 将输入/输出类型从普通数组 float[] 改为 CoopVec<float, N>
  2. 使用 coopVecMatMulAdd(weights, input, bias, ...) 替代手写的 for 循环矩阵乘法。
  3. 注意:为了获得最佳性能和兼容性,权重通常需要使用 16位浮点数(float16)

4. 使用 CoopVec 进行训练(Training)

在训练中使用硬件加速稍微复杂一些,需要手动实现反向传播逻辑。

  • 挑战
    1. 需要额外的存储空间来累积梯度(Weights Gradients, Bias Gradients)。
    2. CoopVec 目前还不是 Slang 默认的可微分类型,需要手动包装和实现自定义导数。
  • 反向传播实现
    • 权重梯度:使用 coopVecOuterProductAccumulate 计算输入向量和输出梯度的外积,并原子累加到权重梯度矩阵中。
    • 偏置梯度:使用 coopVecReduceSumAccumulate 将所有线程的输出梯度求和,原子累加到偏置梯度向量中。
    • 输入梯度:使用 coopVecMatMul 计算权重矩阵的转置与输出梯度的乘积,得到传给上一层的输入梯度。
  • 性能提升:基准测试显示,使用 CoopVec 加速的训练过程比高度优化的纯软件实现快 3-4 倍

性能技巧 (Performance Tips)

1. 核心挑战:线程发散 (Divergence)

GPU 将 32 个线程编为一组(称为 WarpWave)并同时执行。当 Warp 内的线程执行不同路径时(如 if-else),硬件会串行执行(Serialize)所有路径,导致性能大幅下降。

  • CoopVec 的发散问题
    • Cooperative Vector 操作(如 MatMulAdd)被设计为由整个 Warp 共同执行一次矩阵-矩阵乘法。
    • 编程模型允许每个线程使用不同的权重矩阵(Matrix A/B)。
    • 当 Warp 内的线程使用了不同的矩阵时,硬件会检测这种“矩阵发散”,并串行处理每个使用不同矩阵的线程组。
  • 性能悬崖
    • 如视频中的图所示(双对数坐标轴),当 Warp 内共享同一个矩阵的线程组规模变小时,性能会急剧下降。
  • 解决方案
    • 务必最小化矩阵发散
    • 策略:按材质对绘制调用进行分组(Grouping)、手动排序线程(Sorting)、或使用着色器执行重排序(Shader Execution Reordering)。

2. 关键优化:矩阵布局 (Matrix Layouts)

  • 问题:NVIDIA Tensor Cores 期望矩阵数据以一种特定的、非直观的内存布局存储,这取决于 GPU 架构和数据类型,而不是标准的行主序(Row-Major)或列主序(Column-Major)。
  • 正确流程
    1. 离线转换(Offline Shuffle):在着色器运行之前,就将权重矩阵转换为硬件期望的优化布局(Optimal Layout)
    2. 运行时 API:在程序启动时,通过 API(D3D12 / Vulkan)查询特定布局需要的大小,然后在 GPU 上执行转换操作,将结果保存在 Buffer 中。
    3. 两种布局Inference_Optimal(推理用)和 Training_Optimal(训练用)。
  • 收益:避免了在着色器中进行昂贵的实时“混洗”(Shuffle)操作。

3. 关键优化:层融合 (Layer Fusion)

  • 问题:即使权重矩阵布局正确,向量(输入/输出)在进入和离开 Tensor Core 时仍需要进行混洗。对于一个多层 MLP,朴素实现会是 Unshuffle -> MatMul -> Shuffle -> Activation -> Unshuffle -> MatMul -> Shuffle ...。这些混洗操作非常昂贵。
  • 层融合:编译器的一种优化,旨在移除层与层之间冗余的混洗/反混洗操作。
  • 脆弱性:这种优化很“脆弱”,很容易因为不当的编码方式而失效,导致性能急剧下降(例如 2 倍的性能差距)。
  • 如何避免融合失效
    • 使用向量操作严禁使用 for 循环对向量进行逐元素操作。应使用 maxmin 等向量内建函数来编写激活函数。
    • 使用向量加载严禁逐元素加载偏置(Bias)等数据,应使用向量加载指令。
    • 使用融合的 MatMulAdd:将偏置加法合并到 coopVecMatMulAdd 操作中,而不是单独作为一步操作。

4. 精度选择 (Math Precision)

不同的浮点精度在性能、易用性和精度之间有巨大权衡:

精度性能易用性/开销精度
FP16 (16位浮点)较慢最容易。无需转换,精度高。良好
FP8 (8位浮点)最快容易。无需转换,寄存器占用少。可能不足
INT8 (8位整型)很快最困难。需要量化和缩放,引入额外计算和格式转换开销。取决于量化

建议FP8 通常是性能最好的选择,但需注意其极低的精度是否会影响网络收敛或输出质量。FP16 是最稳妥的起点。

5. 性能总结

  1. 使用矩阵布局 API:确保权重矩阵在进入着色器前就已转换为最优布局。
  2. 最小化矩阵发散:通过排序或分组,确保一个 Warp 内的线程尽可能使用相同的矩阵。
  3. 使用向量内建函数:避免逐元素操作,帮助编译器实现层融合。
  4. 评估精度:从 FP16 开始,如果需要极致性能,再考虑 FP8 或 INT8。

部署 (Deployment)

本节的核心是将之前在 Python (SlangPy) 环境中原型验证的神经渲染代码,迁移到高性能的 C++ 生产环境中。

  • 原型 (Prototyping):Python + SlangPy (用于快速迭代和训练)。
  • 部署 (Deployment):C++ + Slang (用于最终产品,保持核心着色器代码不变)。

1. Slang 代码编译

在 C++ 环境中,我们不再动态加载 .slang 源码,而是预先将其编译为着色器二进制文件。

  • 编译器:使用 slangc(Slang 编译器)命令行工具,或在 C++ 中集成 Slang API。
  • 关键编译标志:必须启用 Cooperative Vector 功能,例如:
    • capabilities p_cooperative_vector
    • capabilities p_cooperative_vector_training
  • 输出:生成目标平台的着色器二进制文件(如 DX12 的 DXIL 或 Vulkan 的 SPIR-V),C++ 应用在运行时直接加载这些二进制文件。

2. 检查硬件支持

在 C++ 应用启动时,必须检查并启用 Cooperative Vector 支持。

  • Vulkan:检查 VK_NV_cooperative_vector 扩展是否受支持。
  • DirectX 12:需要调用 D3D12EnableExperimentalFeatures 来启用实验性着色器模型和 Cooperative Vector 功能,并检查 D3D12_COOPERATIVE_VECTOR_TIER

3. 处理矩阵优化布局 (Matrix Optimal Layouts)

在 C++ 端,我们需要手动完成 Python 库自动处理的权重转换,这个流程在上一节(性能)中已有提及:

  1. 查询大小:调用原生 API (Vulkan/DX12) 查询转换后的矩阵布局需要多少显存。
  2. 上传数据:将标准的行主序(Row-Major)矩阵上传到 GPU。
  3. GPU 转换:在命令缓冲区中调用转换函数(如 vkCmdConvertCooperativeVectorMatrixNV),将矩阵转换为 Inference OptimalTraining Optimal 布局,并存入 Buffer。

4. 从 SlangPy 到 C++ Compute Shader

这是从原型到部署最大的区别,即如何调用 Slang 函数。

  • Python (SlangPy) 的“魔法”
    • SlangPy 允许我们直接在 Python 中"调用" Slang 函数(如 inference(uv))。
    • 它在后台自动为我们生成了一个计算着色器(Compute Shader),遍历所有像素并调用该函数。
  • C++ 的“手动”实现
    1. 创建计算着色器入口:我们必须在 .slang 文件中显式创建一个计算着色器入口函数(例如 main_cs)。
    2. 计算坐标:在此函数中,使用 DispatchThreadID (线程ID) 来计算当前像素的坐标 (x, y)
    3. 调用核心逻辑:使用 (x, y) 坐标调用我们原有的 inference 函数。
    4. 写入输出:将 inference 的返回值写入到输出纹理(UAV)的对应位置。
  • C++ 端:C++ 代码负责绑定所有资源(参数 Buffer、输出纹理),然后调用 Dispatch() 来执行这个 main_cs 计算着色器。

5. 结论

通过以上步骤,我们成功将 Python 原型移植到了 C++ 生产环境。两个版本共享相同的核心 Slang 着色器代码,并产生完全相同的渲染结果,但 C++ 版本提供了部署所需的高性能和低开销


神经纹理压缩 (NTC)

1. 核心思想:从“生成”到“转码”

在前面的课程中,我们尝试使用 MLP(多层感知机)和频率编码来拟合一张图像,但结果比较模糊。要提高质量,要么增加 MLP 的规模(计算成本高昂),要么改变思路。

NTC(Neural Texture Compression)选择了后者,它引入了隐空间纹理(Latent Textures)

  • 旧架构UV 坐标 -> 频率编码 -> MLP -> 颜色
  • 新架构 (NTC)
    1. 隐空间纹理 (Latent Textures):一张或多张低分辨率(例如 2x 或 4x 缩小)、低位深(例如 2-4 bit)的纹理。
    2. 相对位置编码 (Relative Positional Encoding):基于 UV 坐标相对于所采样的隐空间纹素的位置。
    3. MLP 输入采样得到的隐空间特征 + 相对位置编码
  • 角色转变:MLP 不再是一个完整的图像解码器,而是一个“转码器” (Transcoder)。它学习如何将低分辨率的隐空间信号转码(上采样并解码)为高分辨率的最终颜色。这种方式能产生更清晰的结果。

2. NTC 的特性与优势

  • 可变比特率 (Variable Bitrate):通过调整隐空间纹理的数量、分辨率和位深,NTC 可以在 0.5 到 20 bpp (bits per pixel) 之间灵活调整压缩率。
  • 多通道支持 (High Channel Count):可以轻松扩展到 16 个通道,允许将完整的 PBR 材质(BaseColor, Normal, Roughness, Metallic...)压缩到同一个 NTC 容器中。
  • 独立像素解码 (Independent Pixel Decoding):NTC 的设计允许在着色器中按需解码单个像素,而无需在加载时解压整个纹理(尽管这也是一种部署选项)。
  • 无幻觉 (No Hallucinations):与大型 AI 模型不同,NTC 是为每张纹理单独训练的小型 MLP。在低比特率下,它的伪影表现为模糊和细节丢失,而不是生成“多余的手指”之类的虚假内容。

3. 质量对比 (vs. Block Compression)

与 GPU 硬件支持的块压缩(如 BC1, BC7)相比:

  • NTC 完胜:在相似的压缩率下,NTC 的图像质量(PSNR)显著高于 BC1/BC7。对于细节复杂的材质,NTC 的优势尤其明显。
  • 伪影差异
    • BC 伪影:块状效应 (Blockiness)。
    • NTC 伪影模糊 (Blur)。在视觉上,模糊通常比块状更不容易分散注意力。
  • 通道相关性:NTC 在压缩通道间相关的 PBR 材质时效果最好。如果通道不相关(例如,在一个平坦色块上印有法线贴图不相关的文字),细节可能会"泄漏"到其他通道,但这可以通过提高比特率来解决。

4. 训练流程

  1. 像素采样:每一步从图像中随机选取一个子集(例如 64k 像素,组织成小图块以提高效率)。
  2. 前向传播 (Forward Pass):采样隐空间纹理,计算位置编码,通过 MLP 得到预测颜色。
  3. 计算损失 (Loss):计算预测颜色与参考颜色之间的 L2 损失(均方误差)。
  4. 反向传播 (Backward Pass):将损失梯度反向传播,以更新两个目标
    • MLP 的网络权重 (Weights)
    • 隐空间纹理的纹素值 (Latent Texels)
  5. 关键优化:整个前向、反向传播和梯度累积过程被融合在一个高度优化的单一 CUDA/Slang 核心 (Single Kernel) 中,而不是像 PyTorch 那样分步执行。这带来了两个数量级(约 100 倍)的训练速度提升。
  6. 优化器步进:使用优化器(如 Adam)更新参数。整个训练过程在高端 GPU 上通常仅需 30 秒左右。

5. 部署模式 (Deployment Models)

NTC 提供了三种灵活的部署方式,以平衡 VRAM 占用和渲染性能:

5.1. Inference on Sample (采样时推理)

  • 工作方式:在 G-Buffer、Forward Pass 或光追着色器中,用 NTC 解码器(MLP)完全替换传统的 texture() 采样。
  • 滤波:NTC 无法利用硬件三线性滤波。必须使用随机纹理滤波 (Stochastic Texture Filtering, STF),即从滤波核中随机选取一个 texel,这会引入噪声。
  • 优点极大的 VRAM 节省
  • 缺点:着色器变慢(增加了 MLP 计算),且 STF 引入噪声。

5.2. Inference on Load (加载时推理)

  • 工作方式:在游戏加载地图或模型时,一次性将 NTC 纹理解码为未压缩图像,然后重新编码为标准的 BC 格式 (BC7, BC1) 并存入 VRAM。
  • 优点零渲染性能开销,易于集成到现有管线。
  • 缺点完全没有 VRAM 节省。只节省了磁盘空间和下载流量。

5.3. Inference on Feedback (虚拟纹理流送)

  • 工作方式:与虚拟纹理(Virtual Texturing)系统结合。当系统发现需要某个纹理图块(Tile)时,它不是从磁盘加载,而是实时地将 NTC 数据转码 (Transcode) 为 BC 格式,并填入 VT 纹理图集中。
  • 优点:在性能和 VRAM 占用之间取得了良好的折中
  • 缺点:系统更复杂,且 VRAM 中需要同时保留 NTC 数据(用于转码)和 BC 数据(用于渲染)。

6. 总结与未来

NTC 提供了一种远超传统 BC 压缩的方案,其核心优势在于:

  1. 节省磁盘空间/下载流量
  2. 灵活部署:高端硬件(PC/主机)可以使用 "On Sample" 节省 VRAM,低端硬件(移动端)可以使用 "On Load" 保证性能,而美术资源只需制作一套 NTC 格式
  3. 更高清的未来:在 VRAM 预算不变的情况下,NTC 允许美术师使用更高分辨率的纹理。
  4. 可扩展性:未来可以使用感知损失(Perceptual Loss)代替 L2 Loss 进行训练,以获得更好的视觉质量;也可以用 NTC 压缩其他数据,如光照探针网格(Light Probes Grids)

实时神经材质 (Real-Time Neural Materials)

本节以一个实际项目为例,展示了将一个复杂的渲染问题转化为 Neural Shading 问题的完整开发工作流。

1. 动机与挑战

  • 目标:实时渲染极其复杂、精细、由艺术家创造的多层材质。
  • 挑战:这些材质的着色器图(Shader Graphs)非常复杂,计算成本极高,无法在实时帧预算内(如 16ms)运行。
  • 核心问题:我们缺乏一种好的方法,能将这种复杂的着色器图“压缩”或“简化”为一个低成本、固定开销(Fixed-Cost)的模型,同时最大限度地保留其视觉保真度。

2. 迭代过程:从失败到成功

项目组尝试了多种架构,这个迭代过程充分体现了 Neural Shading 的开发实践。

迭代 1:朴素 MLP + 隐空间纹理

  • 架构MLP(光照L, 视角V, 隐空间编码UV) -> 颜色
  • 训练目标:同时训练 MLP 的权重和一张高分辨率隐空间纹理(Latent Texture)的像素值。
  • 结果
    • 低分辨率隐空间纹理:可行,能大致拟合材质。
    • 高分辨率隐空间纹理:训练失败。纹理变得充满噪声,无法收敛。
  • 失败原因:在高分辨率下,需要优化的参数(每个纹素都是一个独立参数)变得过多。训练样本被稀释在数百万个参数中,导致梯度噪声(Noisy Gradients)极其严重,优化器无法找到正确的方向。

迭代 2:编码器-解码器架构 (Encoder-Decoder)

  • 思路:高分辨率隐空间纹理之所以失败,是因为其参数(纹素)之间是完全独立的。我们需要一个更结构化的方式来生成它。
  • 新架构(训练时)
    1. 编码器 (Encoder):一个额外的 MLP,输入是艺术家创作的原始 PBR 纹理(Albedo, Normal 等)。
    2. 解码器 (Decoder):与迭代 1 相同的 MLP。
    3. 流程原始PBR纹理 -> Encoder -> 隐空间纹理 -> Decoder (输入 L, V) -> 颜色
  • 为何成功:编码器(一个小型网络)的参数量远小于它所生成的高分辨率隐空间纹理。这种约束使得训练过程更加稳定,能够成功收敛。
  • 部署技巧(关键)
    1. 训练完成后,编码器 (Encoder) 就可以丢弃了
    2. 我们运行一次编码器,将原始 PBR 纹理烘焙 (Bake) 成一张静态的隐空间纹理。
    3. 运行时:我们只需要部署解码器 (Decoder) 和这张烘焙好的隐空间纹理。这实现了与迭代 1 相同的低运行时开销,但解决了训练不稳定的问题。

迭代 3:注入先验知识 (Injecting Prior Knowledge)

  • 问题:虽然能收敛,但解码器(Decoder)在处理法线贴图(Normal Mapping)强烈的材质时效果不佳。
  • 原因:这个小小的 MLP 浪费了其有限的参数容量 (Limited Capacity) 去重新学习一个我们已知的、复杂的数学运算:如何进行坐标系变换(即将 L 和 V 向量从世界空间转换到切线空间)。
  • 解决方案(混合架构)
    1. 将解码器拆分为两部分:一个微小的可训练层 + 一个固定函数(Fixed-Function)组件。
    2. 可训练层:只负责从隐空间纹理中提取 NormalTangent 向量。
    3. 固定函数:使用标准的着色器代码,利用这些 N, T 向量构建着色坐标系 (Shading Frame),并将 L, V 向量旋转到该坐标系下。
    4. MLP 主体:最后,MLP 接收旋转后的 L, V 向量,现在它可以专注于学习材质外观 (Appearance),而无需关心坐标变换的复杂数学。
  • 结果:在不增加网络参数的情况下,渲染质量显著提升,非常接近参考目标。

3. 训练即优势:超越性能

将材质变为“可训练的”,带来了一些传统着色器难以实现的好处:

  • 自动材质简化 (LOD)
    • 通过调整解码器 MLP 的大小(网络层数/宽度),我们可以得到一个在质量 vs. 成本之间平滑过渡的“刻度盘”。
    • 这种简化是通过训练自动保留最重要的视觉特征的,远优于手动简化着色器图。这对于硬件适配(高/中/低配)和 LOD(近/远景)非常有用。
  • 可扩展性 (Extensibility)
    • 可以轻松地在架构中加入新的学习目标
    • 示例 1:学习重要性采样 (Learned Importance Sampling):在训练时,不仅学习渲染材质,还同时学习如何为该材质生成最优的采样分布(用于光线追踪),并共享同一套隐空间纹理。
    • 示例 2:学习纹理过滤 (Learned Filtering):通过训练一个隐空间纹理的 Mipmap 链,使其在缩小查看时也能匹配超采样的参考结果,从而完美解决高频材质的锯齿问题(与课程开头的 Mipmap 优化思想一致)。

4. 总结:Neural Shading 工作流

这个项目生动地展示了 Neural Shading 的开发工作流:

  1. 识别难题:找到传统着色器难以解决的问题(如材质简化、复杂压缩)。
  2. 转化为优化:将该问题重新定义为一个优化问题(即定义一个损失函数)。
  3. 注入先验:不要让小型网络“从零学起”。注入你已知的数学和图形学知识(如法线变换)到固定函数中,让网络只专注于它擅长的非线性拟合。
  4. 快速迭代:开发过程会充满大量失败的尝试。一个支持快速原型设计(如 SlangPy)和高性能部署(如 Slang C++ API)的统一工具链至关重要。