
问题引出
最近在做基于WebGL的路径追踪时,遇到了一个法线(凹凸)贴图的问题,如下图,凹凸效果走样特别严重。通过问题分析,目前渲染器还缺少对不同纹理过滤类型的实现,今天刚好完成了相关内容,趁热将其记录下来。
小球凹凸效果的问题
小球细节
其他渲染器的效果
原因分析
本文示例里小球的凹凸贴图如下图。通过凹凸贴图计算法线的过程如下:
-
通过对贴图采样得到点P的高度hp;
-
分别右移和上移一个微小的距离得到hp+Δx、hp+Δy;
-
最终的法线可以表示为:
N′=N+α(hp+Δx−hp)T+α(hp+Δy−hp)(N×T)
别忘了归一化:N′′=∥∥N′∥∥N′
上式中α为凹凸强度,T为切向向量。
以上过程,最重要的便是Δx和Δy的选取。已知的是在屏幕空间水平和竖直方向的偏移分别为1/resolutionx和1/resolutiony,resolution为渲染屏幕的分辨率,我们需要通过屏幕空间中的偏移去求每个物体在其所在的UV空间的偏移Δx和Δy,最后采样求得最终的法线。显然Δx和Δy还与视角有关,即当物体离摄像机较近时,偏移很小,而距离增加时,偏移增大。
小球凹凸贴图(1500∗1500)
对于上述的根据视角自适应采样的方法如何实现?这篇文章详细的介绍了纹理采样,其中的基于MipMap的三线性过滤可以满足我们的要求。在我们使用光栅化渲染,当设置纹理的TEXTURE_MIN_FILTER或TEXTURE_MAG_FILTER为LINEAR_MIPMAP_LINEAR时,OpenGL/WebGL会自动的根据当前像素在UV上的变化率选取合适的MipMap,这是已经集成在硬件上的功能。
解决方案一
基于上述的分析,我们知道光栅化的时候,可以直接利用纹理过滤选项,让硬件帮我们完成最佳的采样。对于凹凸贴图,我们可以直接使用内置的微分函数dFdx、dFdy:
float hp = texture2D(bump, uv).r;
float hpdx = texture2D(bump, uv + dFdx(uv)).r;
float hpdy = texture2D(bump, uv + dFdy(uv)),r;
我们怎么将上面的光栅化应用到光线追踪呢?我们可以把法线的结果通过光栅化预计算到FrameBuffer,将计算结果传入光追的Shader,通过坐标变换求得屏幕坐标,采样即可得到法线结果,下图分别为光栅化得到的法线以及最终渲染结果:

但,这种方案有哪些问题呢?
从这种方案的原理出发,很显然,可以预见它有如下的一些问题:
- 仅对摄像机视角内的像素点有效,且无法得到被遮挡的物体的法线结果;
- 折射/反射后失真;
- 视角转动需重新渲染法线结果的FrameBuffer;
- 光线追踪每个像素都会使用低差异序列的抗锯齿采样,而光栅化并无此特性,造成两者实际的渲染点不一致,容易引起物体边缘的不连续。
基于上述问题,引出本文的重点:光线微分法。
光线微分法
感兴趣的朋友可以搜索原论文:《Tracing ray differentials.》Igehy, H.本文结合这篇文章以及实际工程中的一些问题来介绍这个算法。
对于任一射线R可以表示为:
R=⟨P,D⟩
P为射线的起点,D为射线的方向向量。Ray Tracing的第一次求交时起点为相机的位置,求方向时将屏幕坐标考虑进来,令:
d(x,y)=View+xRight+yUp
View为相机的朝向,Right为相机的x轴方向向量,Up为相机的y轴方向向量,因此:
D=∥d∥d=(d⋅d)1/2d
初始化时:
∂x∂P=0
∂x∂D=∂x∂((d⋅d)1/2d)=(d⋅d)3/2(d⋅d)Right−(d⋅Right)d
∂y∂D的求解方法与∂x∂D类似,本文不再列出。
当光线沿着方向D传播时,直到与某点相交时,得到交点P′:
P′=P+tD,求微分,得:
∂x∂P′=∂x∂P+t∂x∂D+∂x∂tD
上式中的∂x∂D和前一步的∂x∂D一致,因为射线直线传播时方向不变,那么如何求∂x∂t?

如上图,射线从点P出发,沿方向D传播,与△ABC相交于点P′。设△ABC的平面方程为Ax+By+Cz=d,则其法线N=[A,B,C],法线方向由三角形确定,与x、y不相关。由几何关系得到:
(P−P′)⋅N=−tN⋅D
⇒t=−N⋅DP⋅N+N⋅Dd
对t求微分,可得:
∂x∂t=−N⋅D(∂x∂P+∂x∂D)⋅N−(N⋅D)2d⋅(N⋅∂x∂D)
接下来我们来分析P′处的UV坐标。已知P′的UV、法线、顶点坐标均为点A、B、C三个顶点内差所得,令点A、B、C处的占比分别为α、β、γ,则满足以下条件:
α+β+γ=1
αA+βB+γC=P′
⇒⎣⎡AxBxCx1AyByCy1AzBzCz1⎦⎤⎣⎡αβγ⎦⎤=⎣⎡P′xP′yP′z1⎦⎤
假定当前的交点是满足上述内差条件的,则可得:
⎣⎡AxBxCxAyByCyAzBzCz⎦⎤⎣⎡αβγ⎦⎤=⎣⎡P′xP′yP′z⎦⎤
且α+β+γ=1。
设M=⎣⎡AxBxCxAyByCyAzBzCz⎦⎤,若M可逆,可得:
⎣⎡αβγ⎦⎤=M−1⎣⎡P′xP′yP′z⎦⎤
由此我们得到了内差系数α、β、γ和内差结果的变换关系。然而上述等式成立的条件是变换矩阵可逆,这个条件有的时候可能不满足,比如所有顶点都在xy平面上,此时所有点的z轴分量为0,矩阵不可逆。
为了解决这个问题,笔者构造了一个新的空间,使得该空间的x轴为三角形其中一边,z轴为与三角形所在平面的法线和新的x轴都成45度的向量,y轴即为两者的正交向量,令新空间的变换矩阵为M1,则变换后的顶点A′、B′、C′分别为:
A′=M1A
B′=M1B
C′=M1C
且A′、B′、C′依旧满足:
⎣⎡A′xB′xC′xA′yB′yC′yA′zB′zC′z⎦⎤⎣⎡αβγ⎦⎤=M1⎣⎡P′xP′yP′z⎦⎤
令M2=⎣⎡A′xB′xC′xA′yB′yC′yA′zB′zC′z⎦⎤,则:
⎣⎡αβγ⎦⎤=M2−1M1⎣⎡P′xP′yP′z⎦⎤
对于P′的UV坐标S′=⎣⎡u′v′1⎦⎤,依然满足内插规则:
αSa+βSb+γSc=S′
设M=M2−1M1,对x求微分:
∂x∂S′=∂x∂αSa+∂x∂βSb+∂x∂γSc
∂x∂S′=M[0]∂x∂P′Sa+M[1]∂x∂P′Sb+M[2]∂x∂P′Sc
M[i]为M的第i行向量。
下两张图分别为使用光线微分和光栅化计算得到的∂x∂S′,为了显示更明显,将其值放大了10倍。


现在我们得到了∂x∂S′,对纹理进行采样时需要利用相关数据计算MipMap的等级。
渲染点P′与其向右和向上一个像素点对应的UV差值为:
ΔTx≈Δx∂x∂S′
ΔTy≈Δy∂y∂S′
MipMap的等级lod可以表示为:
lod=0.5log2[max(Δx⋅Δx,Δy⋅Δy)]
计算出了lod的值,我们需要对纹理进行三线性差值计算:
差值计算的两级MipMap分别为:
lodsub=floor(lod)
lodup=floor(lod)+1
Fup=lod−lodsub
分别采样lodsub和lodup两个等级的结果,再将两者进行线性差值,其中lodup的占比为Fup。
使用光线微分后,渲染的结果如下:


总结
要做出高质量的光线追踪渲染,在做纹理采样时需要应用纹理过滤,光线追踪时由于无法使用诸如dFdx的函数,需要根据射线的表达式手动计算微分,而本文所用的光线微分便为其中一种方法。需要注意的是,本文仅对光线直线传播时进行了分析,当光线发生折射和反射时,光线的方向发生了变化,还需要将∂x∂N和∂y∂N考虑进来,感兴趣的读者可以阅读上面的Paper,这部分的内容我也将在近期分享。
参考
《Tracing ray differentials.》Igehy, H. (1999). SIGGRAPH '99 Proceedings