5220 字
26 分钟
计算机图形学笔记(六):现代图形学前沿
NOTE

前五部分把离线渲染、几何、动画、物理四根主轴讲完了。这部分走现代前沿——实时渲染里真正在跑的东西(PBR 管线、硬件光线追踪、GPGPU),以及还在研究前线的东西(神经渲染、XPBD、SPH)。主题很多,但它们之间都是前五部分积木的组合:PBR = Part 4 的渲染方程 + GGX;RTX = Part 4 求交 + BVH 的硬件化;NeRF/3DGS = 把 Part 4 的体积渲染微分化。每节都会指出它靠的是前面哪一块。

目录#

  1. 实时渲染优化 — LOD / 视锥剔除 / 遮挡查询 / 实例化批处理
  2. 基于物理的渲染 (PBR) — Cook-Torrance 完整实现 / 能量守恒 / IBL
  3. 硬件光线追踪(RTX / DXR) — 加速结构 / 着色器表 / 去噪 / ReSTIR
  4. 体积渲染与参与介质 — 体渲染方程 / Ray marching / 云
  5. GPGPU 与计算着色器 — 工作组模型 / 并行前缀和 / GPU 粒子
  6. 神经渲染 — NeRF / 3D Gaussian Splatting
  7. 现代物理仿真 — XPBD / SPH / FEM 概览

三十一、实时渲染优化#

WARNING

实时引擎的帧预算只有 16 ms(60 FPS)或 8 ms(VR/120 Hz)。靠”算得快”走不远,真正的瓶颈永远是少算、不算、批着算。三板斧:LOD(远处少算)、剔除(不可见不算)、实例化(同东西一次算完)。

31.1 层次细节(LOD)#

31.1.1 距离与屏幕空间误差#

距离 LOD 是最朴素的做法:

LODlevel=log2(d/dbase)\text{LOD}_\text{level} = \lfloor \log_2(d / d_\text{base}) \rfloor

但真正正确的指标是屏幕空间误差——远处一个三角形投影到屏幕只占 0.1 像素时,简化它眼睛察觉不到:

ϵscreen=ϵworldfdp\epsilon_\text{screen} = \frac{\epsilon_\text{world} \cdot f}{d \cdot p}

其中 ff 为焦距、pp 为像素尺寸、dd 为视距。ϵworld\epsilon_\text{world} 由 QEM 简化(Part 3 §15.3)给出——这是 Part 3 的网格简化直接产出的量

31.1.2 LOD 链的构造#

// 连续 LOD 链:每级三角数减半,误差单调上升
struct LODLevel {
Mesh mesh;
float distance_threshold; // 切换阈值
float geometric_error; // 世界空间误差
};
class LODChain {
std::vector<LODLevel> levels_;
public:
void build(const Mesh& hi, int num_levels) {
levels_.resize(num_levels);
levels_[0] = { hi, 0.f, 0.f };
for (int i = 1; i < num_levels; ++i) {
// Part 3 §15.3 的 QEM 简化
levels_[i].mesh = qem_simplify(hi, std::pow(0.5f, i));
levels_[i].geometric_error = hausdorff(hi, levels_[i].mesh);
levels_[i].distance_threshold = 10.f * std::pow(2.f, i);
}
}
const Mesh& pick(float distance) const {
for (int i = levels_.size() - 1; i >= 0; --i)
if (distance >= levels_[i].distance_threshold) return levels_[i].mesh;
return levels_[0].mesh;
}
};
TIP

Nanite(UE5)彻底放弃离散 LOD 链,改成 cluster hierarchy:网格切成 128 面的 cluster,运行时按屏幕像素误差在 BVH 里选一个切面。但底层的简化算法仍然是 QEM 的现代变体。

31.2 视锥剔除#

从 MVP 矩阵 PV\mathbf{P}\mathbf{V} 抽出六个平面:

nleft=row3+row0,nright=row3row0, \mathbf{n}_\text{left} = \text{row}_3 + \text{row}_0, \qquad \mathbf{n}_\text{right} = \text{row}_3 - \text{row}_0, \ \dots

用 AABB 的 p-vertex / n-vertex 技巧避免 8 个顶点全测:

bool aabb_in_frustum(const AABB& box, const Plane frustum[6]) {
for (const auto& pl : frustum) {
// 选 p-vertex:沿法线方向最远的 AABB 角点
Eigen::Vector3f p;
p.x() = pl.n.x() >= 0 ? box.max.x() : box.min.x();
p.y() = pl.n.y() >= 0 ? box.max.y() : box.min.y();
p.z() = pl.n.z() >= 0 ? box.max.z() : box.min.z();
if (pl.n.dot(p) + pl.d < 0) return false; // 完全在外侧
}
return true;
}

复杂度:每对象 6 次点积,O(n)。进一步用 BVH 层次剔除可降到 O(log n)。

31.3 遮挡查询与 Hi-Z 剔除#

视锥剔除只排除”看不到的方向”。被前景挡住的物体同样浪费:

  • 硬件遮挡查询GL_SAMPLES_PASSED):上一帧绘制一次代理几何体,统计通过深度测试的片元数。延迟一帧,容易卡渲染流水线。
  • Hi-Z(Hierarchical Z):深度缓冲按 mipmap 降采样,每级存最大深度。测一个 AABB 遮挡只需从对应 mip 读一个 texel 对比——GPU 一次 tex lookup 搞定,现代引擎(Frostbite/UE)的主力。

31.4 实例化批处理#

同一网格 + 不同变换 的场景(树林、砖墙、雪粒子),用实例化把 N 次 draw call 压成 1 次:

// 每实例 attributes:model_matrix(16B×4) + color(16B)
glBindBuffer(GL_ARRAY_BUFFER, instance_vbo);
glBufferData(GL_ARRAY_BUFFER, N * sizeof(InstanceData), data, GL_DYNAMIC_DRAW);
for (int i = 0; i < 4; ++i) { // mat4 拆 4 个 vec4
glEnableVertexAttribArray(3 + i);
glVertexAttribPointer(3 + i, 4, GL_FLOAT, GL_FALSE,
sizeof(InstanceData), (void*)(i * sizeof(float) * 4));
glVertexAttribDivisor(3 + i, 1); // divisor=1 → 每实例更新
}
glDrawElementsInstanced(GL_TRIANGLES, index_count, GL_UNSIGNED_INT, 0, N);
NOTE

DX12 / Vulkan 还有 Indirect Draw + Mesh Shader,把 draw call 参数也放 GPU buffer,由 compute shader 生成,CPU 端零 draw call——这是 Nanite/GPU-Driven 渲染的基础。


三十二、基于物理的渲染(PBR)#

32.1 为什么叫”基于物理”#

经验模型(Phong,见 Part 4 §18.3)能调出好看的反光,但:

  • 不能量守恒 — 能量可以凭空变多
  • 不互易 — 交换入射出射方向结果不一样(违反 BRDF 必要条件)
  • 参数不可迁移 — 一盏灯调好换一盏就假

PBR 从微表面物理模型出发,保证:

  1. 能量守恒:frcosθodωo1\int f_r \cos\theta_o \, d\omega_o \leq 1
  2. 亥姆霍兹互易:fr(ωi,ωo)=fr(ωo,ωi)f_r(\omega_i, \omega_o) = f_r(\omega_o, \omega_i)
  3. 参数来自实测:粗糙度、金属度可以扫描测量

32.2 Cook-Torrance BRDF(完整)#

Part 4 §18.4 给过轮廓。这里给完整公式 + GLSL 实现,做为 Part 6 的 canonical 参考。

32.2.1 微表面模型#

把表面看作无数法向分布的微小镜面。对方向 ωi,ωo\omega_i, \omega_o,只有法向恰好等于半程向量 h=(ωi+ωo)/ωi+ωo\mathbf{h} = (\omega_i + \omega_o)/\|\omega_i + \omega_o\| 的微面才贡献镜面反射。

fspec=D(h)F(ωo,h)G(ωi,ωo)4(nωi)(nωo)f_\text{spec} = \frac{D(\mathbf{h}) \, F(\omega_o, \mathbf{h}) \, G(\omega_i, \omega_o)}{4 (\mathbf{n} \cdot \omega_i)(\mathbf{n} \cdot \omega_o)}

三项分别刻画:

  • DD法向分布 \to 有多少微面的法向朝向 h\mathbf{h}
  • FF菲涅尔 \to 这些微面反多少
  • GG几何项 \to 微面之间自遮挡

32.2.2 GGX/Trowbridge-Reitz 法向分布#

DGGX(h)=α2π[(nh)2(α21)+1]2,α=roughness2D_\text{GGX}(\mathbf{h}) = \frac{\alpha^2}{\pi \left[ (\mathbf{n} \cdot \mathbf{h})^2 (\alpha^2 - 1) + 1 \right]^2}, \qquad \alpha = \text{roughness}^2

选 GGX 是因为它长尾——能画出粗糙金属的”柔高光”,Disney 2012 论文之后成为事实标准。

32.2.3 Smith 几何项#

G(ωi,ωo)=G1(ωi)G1(ωo),G1(ω)=nω(nω)(1k)+kG(\omega_i, \omega_o) = G_1(\omega_i) \, G_1(\omega_o), \qquad G_1(\omega) = \frac{\mathbf{n} \cdot \omega}{(\mathbf{n} \cdot \omega)(1 - k) + k}

直接光的 k=(α+1)2/8k = (\alpha + 1)^2 / 8;IBL 的 k=α2/2k = \alpha^2 / 2(Disney 经验值)。

32.2.4 Schlick 菲涅尔近似#

FSchlick(ωo,h)=F0+(1F0)(1ωoh)5F_\text{Schlick}(\omega_o, \mathbf{h}) = F_0 + (1 - F_0)(1 - \omega_o \cdot \mathbf{h})^5

F0F_0 = 基础反射率。非金属 F00.04F_0 \approx 0.04(几乎无色);金属 F0=albedoF_0 = \text{albedo}(带色反光)—— 这就是金属度参数的来源。

32.2.5 漫反射与能量守恒#

fr=kdalbedoπ+fspec,kd=(1F)(1metallic)f_r = k_d \frac{\text{albedo}}{\pi} + f_\text{spec}, \qquad k_d = (1 - F)(1 - \text{metallic})

关键点:kdk_dFF 决定 —— 镜面反多少,漫反射就少多少,自动能量守恒。金属 metallic = 1kd=0k_d = 0,只有镜面反射。

📌 完整 GLSL 片段着色器#

const float PI = 3.14159265359;
// ---------- 三个核心函数 ----------
float D_GGX(float NdotH, float alpha) {
float a2 = alpha * alpha;
float x = NdotH * NdotH * (a2 - 1.0) + 1.0;
return a2 / (PI * x * x);
}
float G_SchlickGGX(float NdotX, float k) {
return NdotX / (NdotX * (1.0 - k) + k);
}
float G_Smith(float NdotV, float NdotL, float alpha) {
float k = (alpha + 1.0) * (alpha + 1.0) / 8.0; // 直接光
return G_SchlickGGX(NdotV, k) * G_SchlickGGX(NdotL, k);
}
vec3 F_Schlick(float VdotH, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - VdotH, 0.0, 1.0), 5.0);
}
// ---------- 单光源 PBR ----------
vec3 pbr_direct(vec3 N, vec3 V, vec3 L,
vec3 albedo, float roughness, float metallic,
vec3 radiance)
{
vec3 H = normalize(V + L);
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
float NdotH = max(dot(N, H), 0.0);
float VdotH = max(dot(V, H), 0.0);
vec3 F0 = mix(vec3(0.04), albedo, metallic);
float alpha = roughness * roughness;
float D = D_GGX(NdotH, alpha);
float G = G_Smith(NdotV, NdotL, alpha);
vec3 F = F_Schlick(VdotH, F0);
vec3 specular = (D * G * F) / max(4.0 * NdotV * NdotL, 1e-4);
vec3 kS = F;
vec3 kD = (vec3(1.0) - kS) * (1.0 - metallic); // 能量守恒
vec3 diffuse = kD * albedo / PI;
return (diffuse + specular) * radiance * NdotL;
}
TIP

对多光源,循环累加 pbr_direct 即可。IBL 部分不是简单的加法——需要分离漫反射和镜面两路预积分,见下一节。

32.3 基于图像的光照(IBL)#

为什么不能直接对环境贴图做蒙卡尔洛积分?因为每个片元都要采几百次,实时承受不住。IBL 的核心是预积分 + 分离近似:

32.3.1 漫反射辐照度贴图#

对环境贴图做 ΩLi(ωi)cosθdωi\int_\Omega L_i(\omega_i) \cos\theta \, d\omega_i 的预卷积,结果仍是一张立方体贴图(通常 32×32 足够,因为漫反射高频信息低)。运行时采一次。

32.3.2 镜面反射:分离近似(Split-Sum Approximation)#

Epic 2013:

Lofr(ωi,ωo)cosθdωiBRDF LUT (2D)Li(ωi)D(ωi)dωiPrefiltered env mapL_o \approx \underbrace{\int f_r(\omega_i, \omega_o) \cos\theta \, d\omega_i}_{\text{BRDF LUT (2D)}} \cdot \underbrace{\int L_i(\omega_i) \, D(\omega_i) \, d\omega_i}_{\text{Prefiltered env map}}
  • 预过滤环境图:对不同 roughness 做 importance sampling GGX,结果存成 mipmap 链(粗糙度越高采样到越高 mip)。
  • BRDF LUT:只依赖 (nωo,roughness)(\mathbf{n} \cdot \omega_o, \text{roughness}) 的二维查表。

运行时 IBL 只需 3 次贴图采样:

vec3 IBL(vec3 N, vec3 V, vec3 albedo, float roughness, float metallic) {
vec3 F0 = mix(vec3(0.04), albedo, metallic);
vec3 R = reflect(-V, N);
float NdotV = max(dot(N, V), 0.0);
vec3 kS = F_Schlick(NdotV, F0);
vec3 kD = (1.0 - kS) * (1.0 - metallic);
vec3 irradiance = texture(u_irradianceMap, N).rgb;
vec3 diffuse = irradiance * albedo;
// 粗糙度 → mip level
float mip = roughness * (MAX_PREFILTER_MIP - 1.0);
vec3 prefiltered = textureLod(u_prefilterMap, R, mip).rgb;
vec2 brdf = texture(u_brdfLUT, vec2(NdotV, roughness)).rg;
vec3 specular = prefiltered * (F0 * brdf.x + brdf.y);
return kD * diffuse + specular;
}
NOTE

Disney Principled BSDF(2012)在 Cook-Torrance 之上加 sheen / clearcoat / subsurface,UE / Blender / Substance 都基于它的 11 参数体系。原理相同,只是多了几层 BRDF 加和。


三十三、硬件光线追踪(RTX / DXR)#

WARNING

Part 4 讲的是离线光线追踪,每秒可能跑几 fps。RTX 把加速结构构建 + 求交 + BVH 遍历全搬到 GPU 专用硬件单元(RT Core),让实时光追成为现实。API 层面 NVIDIA OptiX / Microsoft DXR / Vulkan KHR Ray Tracing 三家路线几乎同构。

33.1 管线架构#

硬件光追管线有 5 种着色器,代替传统管线的 VS/FS:

着色器作用触发时机Ray Generation发射光线(相当于主循环)每像素一次
Intersection自定义图元求交(三角形以外)BVH 遍历到叶子Any-Hitalpha 裁剪等”要不要采纳这次命中”每次可能命中
Closest-Hit最近命中着色(= Part 4 的 shade)BVH 遍历结束Miss未命中(= 背景 / 环境贴图)遍历结束且无命中

33.2 两级加速结构(TLAS / BLAS)#

  • BLAS(Bottom-Level):每个网格一个 BVH,存三角形。静态网格可预构建。
  • TLAS(Top-Level):场景中所有实例(BLAS + 世界变换)的 BVH。动态物体每帧刷新。

分两级是因为:动态场景里只重建 TLAS 就够(O(实例数)),不用重建每个几何体的 BVH(O(三角数))。

// DXR 伪代码
D3D12_RAYTRACING_GEOMETRY_DESC geom = {};
geom.Type = D3D12_RAYTRACING_GEOMETRY_TYPE_TRIANGLES;
geom.Triangles.VertexBuffer = ...;
geom.Triangles.IndexBuffer = ...;
// BLAS:一次构建
dev->GetRaytracingAccelerationStructurePrebuildInfo(&blas_inputs, &blas_info);
cmd->BuildRaytracingAccelerationStructure(&blas_build, 0, nullptr);
// TLAS:每帧刷新
for (auto& inst : scene_instances) {
D3D12_RAYTRACING_INSTANCE_DESC& d = tlas_instances[i];
d.AccelerationStructure = inst.blas_gpu_va;
memcpy(d.Transform, inst.world_matrix, sizeof(float) * 12);
d.InstanceMask = 0xFF;
d.InstanceContributionToHitGroupIndex = inst.material_id;
}

33.3 着色器表(Shader Binding Table)#

硬件光追怎么知道命中某个三角形后跑哪个 Closest-Hit?靠一张 SBT 表:material_id → 着色器索引。这就是 Part 4 §19 BVH 叶子节点的硬件一般化版本。

33.4 低采样去噪(核心难题)#

实时光追每像素只能发 1–4 条光线(否则帧率不够)。方差巨大,需要时空重建:

  • SVGF(Schied 2017):空间双边滤波 + 时域复用,主流离线电影也在用
  • ReSTIR(Bitterli 2020):重采样——对每像素维护一个小的候选”重要光路”蓄水池,邻居互相借光样本,等效采样数翻倍。是 Cyberpunk / Alan Wake 2 路径追踪的关键。
  • 神经去噪(OIDN / OptiX Denoiser) 吃 (noisy RT, albedo, normal) 输出干净图,细节最多。
WARNING

ReSTIR / SVGF 都假设场景时序稳定——快速相机旋转、disocclusion 会破坏时域复用,导致”鬼影 / 拖影”。工程上要配 motion vector + depth 判据回退到空间滤波。

33.5 动态全局光照(DDGI / ReSTIR GI)#

实时 GI 的主流方案:

  • DDGI(Dynamic Diffuse Global Illumination,Majercik 2019):场景里布一张稀疏的 probe 网格,每 probe 存一张 8×8 的辐照度图。着色时三线性插值。UE5 Lumen 的 diffuse GI 近亲。
  • ReSTIR GI:把 ReSTIR 的蓄水池思路扩展到路径追踪 —— 整条路径(而不只是一次直接光采样)加入蓄水池。

三十四、体积渲染与参与介质#

NOTE

体积渲染不只是”画云”。CT/MRI 医学影像、光雾、次表面散射(皮肤/蜡/叶子)全是它的应用。核心方程把 Part 4 的渲染方程从”表面上一次事件”推广到”光沿路径的连续积分”。

34.1 体渲染方程#

光线 r(t)=o+tω\mathbf{r}(t) = \mathbf{o} + t\omega 在介质里的辐射度:

L(o,ω)=0dT(0,t)σs(r(t))Ls(r(t),ω)dt+T(0,d)Lbg(r(d),ω)L(\mathbf{o}, \omega) = \int_0^d T(0, t)\, \sigma_s(\mathbf{r}(t))\, L_s(\mathbf{r}(t), \omega)\, dt + T(0, d)\, L_\text{bg}(\mathbf{r}(d), \omega)
  • 透射率 T(0,t)=exp(0tσt(r(s))ds)T(0, t) = \exp\left(-\int_0^t \sigma_t(\mathbf{r}(s))\, ds\right)(Beer-Lambert 定律)
  • 消光 σt=σa+σs\sigma_t = \sigma_a + \sigma_s(吸收 + 散射)
  • 入散射 Ls=4πp(ω,ω)L(r(t),ω)dωL_s = \int_{4\pi} p(\omega', \omega) L(\mathbf{r}(t), \omega') d\omega'
  • 相位函数 pp 常用 Henyey-Greenstein: p(cosθ)=1g24π(1+g22gcosθ)3/2p(\cos\theta) = \frac{1 - g^2}{4\pi (1 + g^2 - 2g\cos\theta)^{3/2}}

34.2 Ray Marching 求解#

实时里几乎全用黎曼和离散化——均匀步长沿光线采样:

Eigen::Vector3f ray_march(const Ray& ray, float t_min, float t_max,
const Volume& vol, const std::vector<Light>& lights) {
Eigen::Vector3f L = Eigen::Vector3f::Zero();
float T = 1.0f; // 透射率
float dt = 0.1f;
int steps = int((t_max - t_min) / dt);
for (int i = 0; i < steps; ++i) {
float t = t_min + i * dt;
Eigen::Vector3f p = ray.o + t * ray.d;
float density = vol.sample(p); // 从 3D 纹理采样
if (density <= 0) continue;
float sigma_t = vol.extinction * density;
float dT = std::exp(-sigma_t * dt); // 本步透射率衰减
// 直接光采样(只对每盏光算一次阴影 ray march)
Eigen::Vector3f Ls = Eigen::Vector3f::Zero();
for (const auto& light : lights) {
Eigen::Vector3f L_dir = (light.pos - p).normalized();
float T_light = shadow_transmittance(p, light.pos, vol); // 同法
float cos_theta = (-ray.d).dot(L_dir);
float phase = henyey_greenstein(cos_theta, vol.g);
Ls += light.color * T_light * phase * vol.scattering * density;
}
L += T * (1.0f - dT) * Ls; // 本步入散射贡献
T *= dT;
if (T < 0.01f) break; // 早期终止
}
return L;
}

34.3 云渲染的工程技巧#

离线的 ray march 每像素几百步。游戏要实时,靠这些 trick:

  • 3D 噪声纹理 + Perlin 混合做基础云密度,天气贴图做宏观覆盖率
  • 大步长 + 抖动:64–128 步配合蓝噪声抖动起点,消除带状条纹
  • 双尺度:低分辨率(1/4 分辨率)体 ray march + 高分辨率 upsample
  • Horizon Zero Dawn 2015 GDC 演讲是云渲染的经典资料
TIP

体渲染和 NeRF 本质一样 —— 都是”沿光线累积密度加权辐射”。NeRF 的不同只是把 (σ,Ls)(\sigma, L_s) 从采样纹理换成查询 MLP


三十五、GPGPU 与计算着色器#

35.1 工作组模型#

GPU 把线程按三层组织:

  • Thread:最小单位,一段 shader
  • Work Group(本地工作组):local_size_{x,y,z},共享一块片上 shared memory(~48 KB),可 barrier 同步
  • Dispatch:num_groups_{x,y,z},一次 glDispatchCompute

关键约束:同组内可同步,跨组不能。要跨组同步必须额外发起 dispatch。

#version 430
layout(local_size_x = 16, local_size_y = 16) in;
layout(rgba32f, binding = 0) uniform image2D img;
shared float tile[16][16]; // 组内共享
void main() {
ivec2 gid = ivec2(gl_GlobalInvocationID.xy); // 全局坐标
ivec2 lid = ivec2(gl_LocalInvocationID.xy); // 组内坐标
tile[lid.y][lid.x] = imageLoad(img, gid).r;
barrier(); // 组内同步
// ... 用 tile 做 blur / reduce
}

35.2 并行前缀和(Scan)#

Prefix sum 是无数并行算法的基石——stream compaction、排序、BVH 构建全要用。Blelloch 算法 O(n)O(n) 复杂度 O(logn)O(\log n) 深度:

输入 [3, 1, 7, 0, 4, 1, 6, 3]
Upsweep(树形归约):
步长 1: [3, 4, 7, 7, 4, 5, 6, 9]
步长 2: [3, 4, 7,11, 4, 5, 6,14]
步长 4: [3, 4, 7,11, 4, 5, 6,25] # 根为总和
末位清零 → [3, 4, 7,11, 4, 5, 6, 0]
Downsweep(反向散播):
输出 [0, 3, 4,11,11,15,16,22] # exclusive scan
WARNING

算法核心:先算总和、再反向分发。比朴素的 O(nlogn)O(n \log n) 的 Hillis-Steele 算法工作总量少一半。

35.3 GPU 粒子系统#

全 GPU 粒子 / Emit / Sort / Draw 都在 compute shader 里跑,CPU 零拷贝。核心抽象是两个 free-list:

// ========= Update Pass =========
layout(std430, binding = 0) restrict buffer Particles { Particle p[]; };
layout(std430, binding = 1) restrict buffer Counters {
uint alive_count; uint dead_count; uint alive_list[];
};
uniform float dt;
uniform vec3 gravity;
void main() {
uint i = gl_GlobalInvocationID.x;
if (i >= p.length()) return;
Particle pt = p[i];
pt.life -= dt;
if (pt.life > 0.0) {
pt.vel += gravity * dt;
pt.pos += pt.vel * dt;
p[i] = pt;
uint slot = atomicAdd(alive_count, 1u); // 原子分配
alive_list[slot] = i;
} else {
atomicAdd(dead_count, 1u); // 回收
}
}

要点:

  • atomicAdd 做无锁计数分配
  • alive_list 只收集还活着的粒子索引,后续 draw 用 indirect draw 读这个 list 长度
  • 发射 pass 从 dead_count 回收死粒子槽位,避免动态增长

三十六、神经渲染#

WARNING

神经渲染不是”画个图让 GAN 生成”。真正的 NeRF 和 3DGS 的数学基础仍然是体积渲染方程——它们只是把”场景表示”从传统几何+贴图换成了可微分的参数化结构。

36.1 NeRF(Mildenhall 2020)#

核心思路:用 MLP 把 3D 位置 + 视角方向映到密度 + 颜色:

FΘ:(x,ω)(σ,c)F_\Theta: (\mathbf{x}, \omega) \mapsto (\sigma, \mathbf{c})

渲染时对光线做 ray marching(和 §34 一样):

C^(r)=i=1NTi(1eσiδi)ci,Ti=exp(j<iσjδj)\hat{\mathbf{C}}(\mathbf{r}) = \sum_{i=1}^N T_i (1 - e^{-\sigma_i \delta_i}) \mathbf{c}_i, \qquad T_i = \exp\left(-\sum_{j<i} \sigma_j \delta_j\right)

监督信号:多视图照片。对渲染的像素 C^\hat{\mathbf{C}} 与真实照片 Cgt\mathbf{C}_\text{gt} 做 L2 loss,反向传播穿过整条 ray march 回到 MLP 权重。因为 ray march 是纯可微分加权和,梯度可传。

关键工程点#

  • 位置编码 γ(x)=[sin20πx,cos20πx,,sin2Lπx,cos2Lπx]\gamma(x) = [\sin 2^0 \pi x, \cos 2^0 \pi x, \dots, \sin 2^L \pi x, \cos 2^L \pi x] 让 MLP 能学高频细节
  • 分层采样:粗网络预测密度 → 重要性采样细网络,避开空间中大量空白
  • :原版 NeRF 单场景训练 1–2 天。加速版 Instant-NGP(Müller 2022)用多尺度哈希网格,5 秒训练

36.2 3D Gaussian Splatting(Kerbl 2023)#

把场景表示为上百万个显式 3D 高斯,每个高斯有位置 μ\boldsymbol{\mu}、协方差 Σ\boldsymbol{\Sigma}、不透明度 α\alpha、球谐 RGB。

核心优势:

  • 光栅化而非 ray march:每帧把所有高斯投影到屏幕,按深度做 α\alpha blend。GPU 友好。
  • 显式 —— 可编辑、可动画、可导出。神经网络只用在训练优化中,推理零 MLP。
  • 质量 > NeRF,速度 >> NeRF(100 FPS vs 0.1 FPS)

屏幕空间投影:3D 高斯 Gi(x;μi,Σi)G_i(\mathbf{x}; \boldsymbol{\mu}_i, \boldsymbol{\Sigma}_i) 投影到 2D 后仍是高斯(仿射变换保高斯性):

Σi2D=JWΣiWJ\boldsymbol{\Sigma}_i^\text{2D} = \mathbf{J} \mathbf{W} \boldsymbol{\Sigma}_i \mathbf{W}^\top \mathbf{J}^\top

J\mathbf{J} 是投影矩阵的雅可比,W\mathbf{W} 是 view 矩阵。

渲染(前向合成):把所有高斯按深度排序,前到后合成:

C(x,y)=iciαiGi2D(x,y)j<i(1αjGj2D(x,y))C(x, y) = \sum_i \mathbf{c}_i \alpha_i G_i^\text{2D}(x, y) \prod_{j < i} (1 - \alpha_j G_j^\text{2D}(x, y))
TIP

3DGS 的优化用 自适应致密化:梯度大的高斯分裂、位置近的合并、不透明度太低的删掉。这让百万高斯的数量可以动态调整,对高频细节自动变多、平坦区域自动变少。

36.3 对比#

NeRFInstant-NGP3D Gaussian Splatting表示MLP 隐式哈希网格 + 小 MLP显式高斯点云
训练~24 h~5 s~30 min推理0.1 FPS~10 FPS~100 FPS
可编辑容易底层体渲染方程体渲染方程屏幕空间 alpha blend

三十七、现代物理仿真#

37.1 XPBD(eXtended PBD)#

Part 5 §29.2 的 PBD 好用但刚度非物理——同样的 kk 换迭代次数就软硬不同。XPBD (Macklin 2016) 把拉格朗日乘子作为显式状态:

Δxi=C+α~λjwjjC2+α~wiiC,α~=αΔt2\Delta \mathbf{x}_i = -\frac{C + \tilde{\alpha} \lambda}{\sum_j w_j \|\nabla_j C\|^2 + \tilde{\alpha}} \, w_i \nabla_i C, \qquad \tilde{\alpha} = \frac{\alpha}{\Delta t^2}

其中 α=1/k\alpha = 1/k柔度(compliance),直接对应物理弹性。α~=0\tilde{\alpha} = 0 退化为 PBD。刚度从此和迭代次数解耦,UE5 Chaos Cloth / Houdini Vellum 都基于它。

37.2 SPH(Smoothed Particle Hydrodynamics)#

把流体离散为粒子,用核函数加权平均邻居估计场变量:

A(x)jmjρjAjW(xxj,h)A(\mathbf{x}) \approx \sum_j \frac{m_j}{\rho_j} A_j \, W(\|\mathbf{x} - \mathbf{x}_j\|, h)

WW 常用 Poly6 / Spiky 核。离散 Navier-Stokes(Part 5 §30.2):

ai=1ρipi+ν2vi+g\mathbf{a}_i = -\frac{1}{\rho_i} \nabla p_i + \nu \nabla^2 \mathbf{v}_i + \mathbf{g}

每项梯度/拉普拉斯用 WW 的导数解析表达,纯局部计算,GPU 友好。

工程流程:

  1. 空间哈希 bucketize 粒子(Part 5 §27 的邻居查找)
  2. 每粒子求邻居 → 估计密度 ρi\rho_i
  3. 由状态方程 pi=k(ρiρ0)p_i = k(\rho_i - \rho_0) 算压强 → 压强梯度力
  4. 加粘性、表面张力、重力
  5. 积分(辛欧拉 / Verlet,见 Part 5 §28)

PBF(Position Based Fluids,Macklin 2013)把 SPH 的”压强力”改写成 PBD 风格的密度约束投影,稳定性和大步长都更好,是 Houdini / Flip Fluids 的主流。

37.3 FEM(有限元)概览#

弹簧质点把物体离散为质点+弹簧,FEM 离散为四面体单元,每个单元上定义位移插值。应力-应变关系(线弹性):

σ=λtr(ε)I+2με\boldsymbol{\sigma} = \lambda \, \text{tr}(\boldsymbol{\varepsilon}) \mathbf{I} + 2\mu \boldsymbol{\varepsilon}

λ,μ\lambda, \mu 是拉梅常数(可由杨氏模量 EE、泊松比 ν\nu 推出)。Co-rotational FEM 把旋转部分先通过 polar decomposition 提取出来,避免大变形下线性弹性的”体积爆炸”。

Corotated / Stable Neo-Hookean / ARAP 是业界主力的超弹性模型,Ten Minute Physics / PhysX / Chaos 都在用。


尾声:把六部分拼起来#

Part工具在 Part 6 里被谁复用
Part 2 光栅化透视除法 / 深度测试§31 剔除 / §36 3DGS 投影
Part 4 光线追踪渲染方程 / BVH / 蒙卡洛§32 PBR / §33 RTX / §34 体渲染 / §36 NeRF
Part 6 前沿上面的全部组合
NOTE

六部分笔记的主线其实很简单:前五部分造积木,Part 6 搭城堡。真正难的不是任何单个算法,而是知道在什么场景下选哪个积木。下次看到一个新论文(比如”基于 Gaussian Splatting 的实时 relighting”),第一反应应该是:这是把 §36 的 3DGS 和 §32 的 PBR 拼起来了。积木法一分解,就不神秘了。

延伸阅读清单#

主题推荐
光线追踪《PBRT》第 4 版 / Ray Tracing Gems I & II(免费 PDF)
神经渲染NeRF / Instant-NGP / 3D Gaussian Splatting 三篇原论文
GPU 架构《GPU Gems》1-3 卷(免费) / 《Real-Time Collision Detection》
  • 完 —
计算机图形学笔记(六):现代图形学前沿
https://kyc001.github.io/posts/计算机图形学笔记六/
作者
kyc001
发布于
2025-07-25
许可协议
CC BY-NC-SA 4.0