Featured image of post 计算机图形学 project:物理渲染

计算机图形学 project:物理渲染

这是一篇学习笔记

封面图:最终加了 emission 材质的洞穴场景($\sim 20000\text{ SSP}$)。

期末 project 提供了 GPU 渲染(hlsl)的仓库,其中实现了基本的 UI 和数据绑定,主要代码是填 shader.hlsl。原仓库:https://github.com/lihe50hz/ShortMarch

接下来的内容是 project report 的草稿。或许在困惑于实现细节时能提供一些参考。

Thanks to my teammate zzt. I wouldn’t have gone through such a long waymarch and completed this huge project without you.

Introduction

我们在 ShortMarch 的基础上,写了一个基于 Ray Tracing 的物理渲染器。这个渲染器支持面的各种材料参数和贴图,支持两类光源,并且通过设计重要性采样公式降低了蒙特卡洛采样的方差,还加了一些其他小的功能。

我们发现这个渲染器尤其适合渲染 MC 中的场景。它支持了 MC 中大部分的图形功能,能够产出很有氛围的漂亮图片。

Personal Contribution

我实现的部分主要为渲染器(hlsl)主体。包括:

  • Base, Ray tracing 核心逻辑
  • Material
    • 透射材质
    • principled BSDF(including metallic, roughness, transmission, IOR, subsurface, clear coat, sheen and emission)
    • multi-layer material(对于 clear coat 单独的可调的 roughness)
  • Texture(与我的队友合作完成)
    • 纯色材质
    • 纹理(颜色)贴图(含 alpha),法线贴图
  • 重要性采样,除了 RR 以外:
    • 包括关于 material 和 光源的 MIS
  • 点光源与面光源
  • anti-aliasing
  • tone mapping
  • scene creation

注意在面光源存在的情况下,alpha shadow 是自动满足要求的。

我所编写/修改的代码有:

  • shader.hlsl 中的大部分内容
  • app.cppmaterial.h 等文件中向 shader.hlsl 传必要数据的操作

我的队友主要负责:

  • Special Visual Effects 中的景深、动态模糊
  • HDR 天空
  • Volumetric Rendering 中的一部分功能

Method

核心逻辑

在给出的 hlsl 模板代码的基础上,我填充了 ClosestHitMain 里的逻辑。由于递归层数限制为 31,所以我选择只在 ClosestHitMain 里处理单次光线弹射的逻辑,而单独写一个函数 Calculatewhile 循环处理多轮弹射。在多轮弹射的过程中,我记录累计的颜色,以及当前的 throughput 系数 coef

ClosestHitMain 中,我大致做了以下几步:

  1. 得到当前三角形相关的信息,例如法线以及根据顶点法线插值
  2. 处理体渲染(不是我的工作)
  3. 做俄罗斯轮盘
  4. 获取材质
  5. 做 MIS,决定这一条光线走哪个分量
  6. 光源采样
  7. 决定弹射方向

材质

我实现的材质的属性包括:

  • roughness
  • metallic
  • emission
  • transmission with IoR,这个 transmission 表示透光率,是三个实数,它和漫反射互相影响能量分配,不干扰 specular。可以理解为是水、玻璃这种。
  • alpha,这个 alpha 是单个实数,表示物体的“幽灵程度”。alpha=0 表示完全不可见,alpha=1 表示完全可见。常用于处理一个面上部分透明的情况,例如 MC 中的树叶。
  • clear coat,包括强度和粗糙度
  • sheen
  • subsurface

为了下面涉及的 BSDF 的数值稳定性,我限制最小的 roughness 为 0.01。

材质的着色计算思路遵循以下层次结构:

Step 1. 只有镜面反射与漫反射。根据基本的 rendering equation: $$ L_o(p,w_o)=c_{\rm emission}+\int_\Omega L_i(p,w_i)f(p,\omega_i,\omega_o)(n\cdot\omega_i)\mathrm{d}\omega_i $$ 使用 Cook–Torrance,记 $h$ 为 half vector: $$ f(p,\omega_i,\omega_o)=f_{\rm specular}(p,\omega_i,\omega_o)+f_{\rm diffuse}(p,\omega_i,\omega_o) $$

specular 分量: $$ f_{\rm specular}(p,\omega_i,\omega_o)=\frac{F(\omega_o,h)D(h)G(\omega_o,\omega_i)}{4(n\cdot\omega_i)(n\cdot\omega_o)+\epsilon} $$

使用 Fresnel-Schlick 近似: $$ F(\omega_o,h)=F_0+(1-F_0)(1-\omega_o\cdot h)^5 $$

基础反射率($F$ 是一个 float3,但是公式上我们可以只考虑 RGB 中的一个分量,因此变量类型上的问题我就不多加说明了): $$ F_0=(1-\text{metallic})\left(\frac{\eta-1}{\eta+1}\right)^2+\text{metallic}\cdot c_{\rm base} $$

微表面法线分布使用 GGX(GTR2): $$ D(h)=\frac{\alpha^2}{\pi((n\cdot h)^2(\alpha^2-1)+1)^2}\quad\text{where }\alpha=\text{roughness}^2 $$

shadowing-masking function: $$ G(\omega_o,\omega_i)=G_1(\omega_o)G_1(\omega_i) $$

$$ G_1(\omega)=\frac{n\cdot\omega}{(1-k)(n\cdot\omega)+k}\quad\text{where }k=\frac{(\alpha+1)^2}8 $$

diffuse 分量,主要考虑的是与 specular 的能量分配分配: $$ f_{\rm diffuse}(p,\omega_i,\omega_o)=\frac1\pi(1-\text{metallic})(1-F(\omega_o,h))c_{\rm base} $$

Step 2. 优化 diffuse 分量,考虑 sheen 和 subsurface。 $$ f_{\rm diffuse}(p,\omega_i,\omega_o)=\text{deduction terms}\cdot(f_{\text{diffuse with subsurface}}(p,\omega_i,\omega_o)+f_{\rm sheen}(p,\omega_i,\omega_o)) $$ subsurface 部分有两个分量: $$ f_{\text{diffuse with subsurface}}(p,\omega_i,\omega_o)=(1-I_\text{subsurface})f_{\rm base}(p,\omega_i,\omega_o)+I_\text{subsurface}\cdot f_{\text{subsurface}}(p,\omega_i,\omega_o) $$ 我同时也稍微改了以下 base 分量的计算,用 Disney diffuse: $$ f_{\rm base}(p,\omega_i,\omega_o)=\frac1\pi(1+(F_{\rm D90}-1)(1-n\cdot\omega_i)^5)(1+(F_{\rm D90}-1)(1-n\cdot\omega_o)^5)c_{\rm base}\quad\text{where }F_{\rm D90}=0.5+2\cdot\text{roughness}^2\cdot(\omega_i\cdot h)^2 $$ subsurface 用 Hanrahan-Krueger: $$ f_{\text{subsurface}}(p,\omega_i,\omega_o)=1.25\left(F_{\rm SS90}\cdot\left(\frac{1}{(n\cdot\omega_i)(n\cdot \omega_o)}-0.5\right)+0.5\right)\quad\text{where }F_{\rm SS90}=\text{roughness}^2\cdot(\omega_i\cdot h)^2 $$ sheen: $$ f_{\rm sheen}(p,\omega_i,\omega_o)=(\text{tint}\cdot c_{\rm base}+(1-\text{tint}))\cdot\text{sheen}\cdot(1-(\omega_i\cdot h)^5) $$ Step 3. 考虑透射。镜面反射的分量不变,剩余的部分分配给透射和漫反射。因此 $f_{\rm diffuse}$ 需要乘上一个 $(1-c_{\rm transmission})$。

折射的分量是确定方向的。对于处于面的异侧的 $\omega_i$ 和 $\omega_o$: $$ f_{\rm transmissive}(p,\omega_i,\omega_o)=c_{\rm transmission}(1-\text{metallic})(1-F(n,\omega_i))\frac{1}{\eta^2}\frac{\delta(\omega_o-\omega_r)}{n\cdot\omega_i} $$ $\omega_r$ 根据 Snell’s law 计算。

其中 $\eta$ 是当前面的折射率,例如如果当前面的正面是空气,背面是玻璃,从玻璃外向里看,那么 $\omega_i$ 在玻璃内,$\omega_o$ 在玻璃外。这时能量应当衰减约 $2.25$ 倍。相对地,如果从玻璃里面往外面看,会因为全反射现象只能看到一个圆锥内的事物,并且会变亮。

Step 4. 清漆。清漆是在所有上述项之上的一层只有 specular 且按照几何法线做 specular 的层。它有强度 $I_{\rm coat}$ 和粗糙度 $\text{roughness}_{\rm coat}$。在加入它之后, $$ f(p,\omega_i,\omega_o)=f_{\rm coat}(p,\omega_i,\omega_o)+(1-F_{\rm coat}(n,\omega_i))(1-F_{\rm coat}(n,\omega_o))(f_{\rm specular}(p,\omega_i,\omega_o)+f_{\rm diffuse}(p,\omega_i,\omega_o)+f_{\rm transmissive}(p,\omega_i,\omega_o)) $$ 这个系数是因为光线既要穿入一层清漆,又要穿出一层清漆。

其中 $$ f_{\rm specular}(p,\omega_i,\omega_o)=I_{\rm coat}\frac{F_{\rm coat}(\omega_o,h)D_{\rm coat}(h)G_{\rm coat}(\omega_o,\omega_i)}{4(n\cdot\omega_i)(n\cdot\omega_o)+\epsilon} $$ 清漆使用不太一样的 $F$, $D$, $G$. 其中

  • $F$ 中固定 $F_0=0.04$

  • $G$ 中固定 $\alpha=0.25$,并使用 Smith-GGX: $$ G_1(v) = \frac{2 (n \cdot \omega)}{(n \cdot \omega) + \sqrt{\alpha^2 + (1-\alpha^2)(n \cdot \omega)^2}} $$

  • $D$ 使用 GTR1 分布: $$ D(h)=\frac{(\alpha^2-1)(n\cdot h)}{\pi\ln(\alpha^2)((n\cdot h)^2(\alpha^2-1)+1)} $$

Step 5. alpha。直接以 $1-\text{alpha}$ 的概率穿过当前这个面,方向不变。

材质相关的采样

首先以以下判断分支选择分量:

  • 以 $p_{\rm coat}=I_{\rm coat}\operatorname{luminance}(F_{\rm coat}(n,\omega_o))$ 的概率选择 clear coat
  • 以 $p_{\rm specular}=\operatorname{luminance}(F(n,\omega_o))+0.1\cdot (1-\text{roughness})$ 的概率选择 specular
  • 以 $p_{\rm transmissive}=\operatorname{luminance}(c_{\rm transmission})$ 的概率选择 transmissive
  • 否则选择 diffuse

其中 $\operatorname{luminance}((r,g,b))=0.2126r+0.7152g+0.0722b$.

为了避免漏下分量或在重要性采样计算结果时除以过小的数导致数值稳定性问题,这些 $p$ 都 clamp 到 $[0.05,0.95]$。

diffuse 采样:下文中 $\xi_1,\xi_2\sim U(0,1)$。 $$ \begin{aligned} \varphi &= 2\pi \xi_1,\\ r &= \sqrt{\xi_2},\\ x &= r\cos\varphi,\quad y=r\sin\varphi,\quad z=\sqrt{1-r^2},\\ \omega_i &= (x,y,z),\\ p_{\rm diffuse}(\omega_i) &= \frac{z}{\pi}. \end{aligned} $$ specular 采样: $$ \begin{aligned} \varphi &= 2\pi \xi_1,\\ \cos\theta &= \sqrt{\frac{\xi_2}{\alpha^2 - \xi_2(\alpha^2 - 1)} },\\ h &= (\sin\theta\cos\varphi,\sin\theta\sin\varphi,\cos\theta),\\ \omega_i &= 2(\omega_o\cdot h)h-\omega_o\\ p_{\rm specular}(\omega_i) &= \frac{D(h)(n\cdot h)}{4(\omega_o \cdot h)}, \end{aligned} $$ clear coat 采样: $$ \begin{aligned} \varphi &= 2\pi \xi_1,\\ \cos\theta &= \sqrt{\frac{1-\alpha^{2\xi_2}}{1-\alpha^2}},\\ h &= (\sin\theta\cos\varphi,\sin\theta\sin\varphi,\cos\theta),\\ \omega_i &= 2(\omega_o\cdot h)h-\omega_o\\ p_{\rm coat}(\omega_i) &= \frac{D_{\rm coat}(h)(n\cdot h)}{4(\omega_o \cdot h)}, \end{aligned} $$ 特别地,specular 和 clear coat 的采样需要额外的 reject sampling 保证 $\omega_o$ 不跑到面的另一侧去。

对于每个分量,我在计算贡献时,考虑的系数是它对应 BSDF 中的那个分量,并除以随机到这个分量和这个 $\omega_i$ 的概率密度。容易证明,这样蒙特卡洛是无偏的。

光源采样

我只对于以下分量做光源采样:

  • diffuse
  • roughness > 0.1 的 specular
  • coat roughness > 0.1 的 clear coat

这是因为,roughness 过小的镜面反射,以及折射,它们的 BSDF 都可以认为成是一个 Dirac $\delta$ 函数,这种情况光源采样反而会使噪点严重,或者出现严重的辉光/fireflies。

光源采样的过程如下:

  • 枚举每个点光源 $q$,令 $r=|q-p|$, $\omega_i=(q-p)/r$ $$ \Delta L_o=\text{attenuation}\cdot f(p,\omega_i,\omega_o)(n\cdot\omega_i)\frac{c_{\rm light}}{r^2} $$

  • 枚举每个面光源,随机上面一点 $q=(1-\sqrt{\xi_1})p_0+\sqrt{\xi_1}\sqrt{\xi_2}p_1+\sqrt{\xi_1}(1-\sqrt{\xi_2})p_2$,令 $A$ 为面积,$n^\prime$ 为面光源的法线 $$ \Delta L_o=\text{attenuation}\cdot f(p,\omega_i,\omega_o)(n\cdot\omega_i)(n^\prime\cdot\omega_i)\frac{c_{\rm emission}}{r^2}A $$

  • 将这些贡献加到累计颜色中,并根据情况 disable/enable 下一跳的 emission 是否要计入。

若干解释:

  • 光源采样独立于材质采样的分量。无论材质采样的分量随机到的是哪一个,光源采样总是把这些全部算进去。
  • 这里的 $f$ 只算我上面说的这些分量
  • $\rm attenuation$ 这样计算:我使用一个利用 anyhitTraceRay 过程,将 RayDesc 中的 TMax 设置为 $r-\epsilon$。在 anyhit 中,得到当前击中点的材质,并算出它的 alpha 和 transmission 分量的总系数,乘给 attenuation。注意,这里我相当于假设了所有的透射材质的 ior 都等于 1,这也算是个 compromise,因为考虑折射了就没法光源采样了。
  • 设置下一跳是否要计入 emission,我在 RayPayload 中用 bool count_emission 记录:
    • 如果是透射,则不变
    • 如果这一次材质采样选择的分量是 diffuse / roughness > 0.1 的 specular / roughness > 0.1 的 clear coat 这三种计算光源采样的分量,下一跳不计入
    • 否则计入

Anti-aliasing

这个很简单,就用抖动采样(对于一个像素,每次随机一个 $[0,1]\times[0,1]$ 内的点,把光线往对应方向打),数学上就是对的

Tone mapping

对于 RGB 的每个分量,都使用映射 $$ x\mapsto\frac{Cx}{1+Cx} $$ 来将 $[0,+\infty)$ 中的数值映射到 $[0,1]$。实践上 $C=3$ 是个不错的选择。

材质处理

传入材质分为以下几步:

  1. 利用 tiny_obj_loader 载入 objmtl
  2. external 中的 math_mesh 增加对应的,需要传给 Entity.cpp 的东西
  3. app.cpp 中通过 resource binding 传给 shader.hlsl
  4. shader.hlsl 中用 SampleLevel 与插值得到的 uv 坐标得到材质

Scene creation

我们在 final report 上 feature 了几张 MC 场景的渲染。这些是我选择 MC 自然生成的地形,或者手动在 MC 里搭建了一些场景后,用 mineways 工具导出为 obj,然后再手动修改其中部分材质 mtl 配置所完成的。

Discussion

关于我实现的部分,主要有两方面内容还值得进一步讨论:

  • 透射材质的能量分配问题。理论上,镜面反射、漫反射、透射的能量分配应当根据一个统一的 fresnel 项,但是这里我透射用的 fresnel 项是由 $n\cdot \omega_i$ 计算的,和前两者的 $h\cdot \omega_o$ 不一致。这应该主要是由于 Schlick 近似导致的。一个更加严格的实现是 https://www.cs.cornell.edu/~srm/publications/EGSR07-btdf.pdf,不过比较可惜我来不及实现了。
  • 光源采样的问题。我的实现中,穿过透射材质是不改变 count_emission。根据 AI 的建议,说会出现“墙上的黑色玻璃球问题”,但是我认为如果穿过透射也算就会 double count,所以我最终还是不算。光源采样时不考虑折射,是不得已而为之的近似。存在 MNEE 和 BDPT,以及 photon mapping 等进阶做法。
  • 在同时有顶点法线插值和法线贴图时,计算最终法线似乎有点小问题,直接对几何法线算出的 $T$ 和 $B$ 关于插值后 $N$ 做正交化似乎会导致很轻微的 artifact,我也没管了。

roughness 和 metallic 的组合:从左往右 metallic 递增,从前往后 roughness 递增,天花板为面光源,中间有个点光源

alpha 和 transmission 的对比:左边是 alpha=0, transmission=(0,0,0), 中间是 alpha=0.5, transmission=(0,0,0),右边是 alpha=0, transmission=(0.5,0.5,0.5)

specular 和 clear coat 的对比:左边是粗糙无清漆, 中间是光滑(with roughness=0.05)无清漆,右边是粗糙有清漆(with 强度=0.5, roughness=0.05)

sheen:左边是无 sheen,中间是有 sheen,tint=0,右边是有 sheen,tint=1。为了凸显效果我把 sheen 的强度调为 10 了。可以关注 matball 的底座部分

subsurface:左中右分别是 subsurface = 0/0.5/1.0

具有折射的玻璃

从玻璃内向外看

Cornell Box in MC

材质贴图与法线贴图:一个鞋子

MC 场景:村庄

MC 场景:洞穴

透过彩色玻璃板看点光源

动态模糊(队友工作)

发光体渲染(队友工作)

体渲染(队友工作)

MC 中透过活板门的丁达尔效应(队友工作)

封面图丢给 GPT 去除 fireflies 后的结果,有失真

Licensed under CC BY-NC-SA 4.0

评论

使用 Hugo 构建
主题 StackJimmy 设计