
一如既往,本文是筆者閱讀《基于物理的渲染:從理論到實踐》的總結(jié),文章不會面面俱到地描述書中所有的東西,只會把筆者認為重要的東西整理出來。既是一種復習,也希望能對讀者有幫助。整理范圍:第2章+第3章的前5節(jié)。
由于作者水平所限,文中如果有錯誤或者沒有解釋到位的地方,還請讀者不吝指正。
交點類(Interaction)
這是第2.10節(jié)的內(nèi)容,也是第2章的最后一節(jié)。之所以認為這個重要,有兩個原因:1、PBR意味著對光線和表面的交點進行著色。2、之前的內(nèi)容都是向量、法線、光線等等非常簡單而基礎的東西,不值得拿出來再寫一遍。
pbrt中的交點分為兩種,一個是表面交點(SurfaceInteraction),這個應該是我們所熟悉的交點。另一個是介質(zhì)交點(MediumInteraction),這個東西目前還沒有講到,猜測應該是和體積渲染有關(guān),計算光在介質(zhì)中反射或者折射的信息。于是,兩種交點就提取出了共有的基類:Interaction。
Interaction
Interaction類中的成員有:
Point3f p;
Float time;
Vector3f pError;
Vector3f wo;
Normal3f n;
MediumInterface mediumInterface;
一個個來解釋:
-
Point3f p:這是交點的位置信息。這非常容易理解,交點類都沒有交點的信息算什么! -
Float time:時間。不知道是什么時間,也許是到這個點的時間,也許是計算得到這個點所用的時間。 -
Vector3f pError:我們計算交點信息的時候用的是浮點數(shù),浮點數(shù)不可能表示數(shù)學意義上的每一個數(shù),所以計算的時候會產(chǎn)生誤差。這個pError保存的就是一個保守的誤差范圍(就是誤差絕對不會超過的范圍)。 -
Vector3f wo:這是光線方向的相反方向?;蛘哒f是光線反射進入攝像機的方向。我們計算交點是假設有一條光線從攝像機的位置發(fā)出,然后和表面相交。這個wo就保存了這條光線的反方向,也就是前一篇文章中的。
-
Normal3f n:交點的法線。 -
MediumInterface mediumInterface:介質(zhì)接口。是交點所處的環(huán)境,或者說是光在什么介質(zhì)中穿行。目前為止還沒用到。
SurfaceInteraction
SurfaceInteraction類中包含了交點的幾何信息,這個類保存了許多跟點有關(guān)的計算需要用到的幾何信息,這樣在計算的時候就不用去管這個點在哪個物體上,所以你會看到:這個類居然連TM的Shape、BSDF、BSSRDF指針都有??!
好了,不吐槽了,正經(jīng)的。
SurfaceInteraction類的成員包括:
Point2f uv;
Vector3f dpdu, dpdv;
Normal3f dndu, dndv;
const Shape *shape = nullptr;
上面的這些代碼定義的是關(guān)于幾何表面的信息,其中,Shape是幾何表面,這個在第3章中有介紹,都是曲面和曲線(所以我才去啃了一個禮拜的《曲線和曲面的微分幾何》?。?。要理解這些變量,需要理解曲面是怎么表示的。參考曲面的定義(不了解的讀者可以參考我的前一篇文章。),曲面是由,
兩個參數(shù)映射成的三維空間中的點。這里的uv變量就是這兩個參數(shù)的值,而dpdu和dpdv則是曲面關(guān)于
,
的偏導數(shù)。dndu和dndv則是曲面的法線關(guān)于
,
的偏導數(shù)。這些數(shù)據(jù)意味著當前點附近的點的位置的變化情況以及法線的變化情況,在計算中非常有用。
struct {
Normal3f n;
Vector3f dpdu, dpdv;
Normal3f dndu, dndv;
} shading;
著色幾何信息,說實話我到現(xiàn)在還不知道有啥用。講了一大堆怎么設置,怎么計算,結(jié)果有啥用沒說,很蛋疼的感覺。
剩余的成員:
const Primitive *primitive = nullptr;
BSDF *bsdf = nullptr;
BSSRDF *bssrdf = nullptr;
mutable Vector3f dpdx, dpdy;
mutable Float dudx = 0, dvdx = 0, dudy = 0, dvdy = 0;
// Added after book publication. Shapes can optionally provide a face
// index with an intersection point for use in Ptex texture lookups.
// If Ptex isn't being used, then this value is ignored.
int faceIndex = 0;
BSDF和BSSRDF不用說,梳理I中講過,faceIndex是面片的索引。這世上絕大多數(shù)模型都不能用完美的數(shù)學公式來表示,通常的方法是采用三角形面片來近似,一個模型有許多三角形面片,而這個faceIndex指的就是面片的索引。具體有什么用,暫時還不知道。剩余的變量都沒有說明,所以也不去糾結(jié)。
球面
球面的表示和交點計算算是最難的一部分,也不是說它本身有多難,而是它要很多微分幾何的知識,比如說至少得知道第一基本形式和第二基本形式都是啥,有啥用。
廢話不多說,一起來看。
球面的表示
球面的坐標方程是
這個誰都知道,但是,這個沒用!我們不僅要知道交點在哪,還要知道交點附近的點的變化趨勢是什么樣的。甚至,我們還需要知道如何將紋理貼到球表面上去,這些信息上面的方程給不了,所以,我們采用的是極坐標表示方式,因為它正好有兩個參數(shù)(對應,
非常方便),而且計算偏導數(shù)也方便。
球的極坐標方程如下:
其中的取值范圍是
到
,
的取值范圍是
到
。幾何意義上,
是和z軸正方向的夾角,而
是和x軸正方向,沿著
方向旋轉(zhuǎn)的夾角,如下圖所示(下圖中沒有標出
角,但是把按照上面描述的,把
比對圖片帶入計算就可以發(fā)現(xiàn)是這樣的):

進一步,這只是球面的數(shù)學表示,要用類怎么表示呢?首先要想到,我們需要兩個參數(shù)和,這兩個參數(shù)是不是其中的和?顯然不是,因為如果和是和的話,那么又是什么?所以,和不是參數(shù)方程中的任何一個變量,我們?nèi)〉们蛎嫔系狞c,首先一條就是:這是一個確定的球!也就是說,、、都是固定值,只有和才是參數(shù)。
于是,球面關(guān)于參數(shù)方程是:
代入到極坐標方程中就是
雖然看上去很復雜,但這就是球關(guān)于的參數(shù)方程。其中,
都是固定值,
和
是
的取值范圍,
是
能取得最大值,也就是說
的取值范圍是
。
所以,Shpere類的成員就有:
const Float radius;
const Float zMin, zMax;
const Float thetaMin, thetaMax, phiMax;
其中的zMin和zMax是與thetaMin和thetaMax對應的z坐標最小值和最大值。Sphere的構(gòu)造函數(shù)是這樣初始化這些值的:
Sphere(const Transform *ObjectToWorld, const Transform *WorldToObject,
bool reverseOrientation, Float radius, Float zMin, Float zMax,
Float phiMax)
: Shape(ObjectToWorld, WorldToObject, reverseOrientation),
radius(radius),
zMin(Clamp(std::min(zMin, zMax), -radius, radius)),
zMax(Clamp(std::max(zMin, zMax), -radius, radius)),
thetaMin(std::acos(Clamp(std::min(zMin, zMax) / radius, -1, 1))),
thetaMax(std::acos(Clamp(std::max(zMin, zMax) / radius, -1, 1))),
phiMax(Radians(Clamp(phiMax, 0, 360))) {}
但是這個初始化代碼有個問題,那就是thetaMin、thetaMax和zMin、zMax的對應關(guān)系弄反了。從上面的式子我們知道,,很明顯,
在區(qū)間
是單調(diào)遞減函數(shù),所以,當
取最小值的時候,對應的z應該取最大值才對。
光線與球面的交點
數(shù)學上,計算光線和球面的交點非常簡單,將光線的方程
代入球面方程中計算就好了(其中,是光線的起始點,
是光線的方向,這兩個是固定值,
是參數(shù)變量)。但是,實際應用的過程不會這樣簡單。
- 計算出的交點需要用到我們之前定義的SurfaceInterface類,我們需要填充很多信息,包括切向量,法向量以及偏導數(shù)等等。
- 數(shù)學中我們可以認為光線是無限長的,但是實際應用過程中,我們沒那么多資源去計算無限長的光線,所以光線類(pbrt中是Ray)就會有一個最大長度的限制,也就是
的最大值tMax。計算過程中,我們需要舍棄交點值的
大于這個tMax的情況。
- 數(shù)學意義上的數(shù)是精確數(shù),擁有無限的精度。計算機中用的是浮點數(shù),不管是單精度還是雙精度,都是一種近似數(shù),不能精確地計算出交點的位置。在多次使用這種近似數(shù)計算之后,誤差就會累積,甚至可能出現(xiàn)原本應該是交點,結(jié)果卻不是,或者反過來,這些情況。所以,誤差也需要考慮進去。
- ……
有意思,從數(shù)學的抽象到落地成代碼的過程從來都是最有意思的部分!
計算交點t值
第一步,先計算交點,用的方程不是極坐標參數(shù)方程,而是普通的笛卡爾坐標系中的球的隱式方程:
將光線的方程代入其中可以得到:
整理得到:
這是一元二次方程的標準形式(),利用求根判別式就可以知道到底有沒有交點,有幾個交點。
這個過程在代碼中的實現(xiàn)是這樣的:
EFloat a = dx * dx + dy * dy + dz * dz;
EFloat b = 2 * (dx * ox + dy * oy + dz * oz);
EFloat c = ox * ox + oy * oy + oz * oz - EFloat(radius) * EFloat(radius);
// Solve quadratic equation for _t_ values
EFloat t0, t1;
if (!Quadratic(a, b, c, &t0, &t1)) return false;
EFloat是pbrt中自定義的一個結(jié)構(gòu),與內(nèi)置的float相比,最大的區(qū)別就是EFloat里包含了誤差,可以理解成帶誤差的float。代碼的實現(xiàn)和我們的數(shù)學推理基本一致,它采用了一個Quadratic函數(shù)來求解t的值,如果沒有實數(shù)根,那么整個交點檢測的函數(shù)就返回false。
t值的有效性(t是否超出[0, tMax]范圍)
即便我們計算出t值了,這個t值也不一定是有效的,它必須在光線的“射程”范圍內(nèi)。因為計算出的t值用的是pbrt自定義的“帶誤差的float”結(jié)構(gòu),所以,實際檢測的時候我們需要考慮:
- 如果兩個交點t0,t1都不在[0, tMax]的范圍內(nèi),那么兩個交點都舍棄,返回false,意味著沒有交點。
- 先檢測t0,如果t0在[0, tMax]范圍內(nèi),那么就認為光線和球面的交點是t0。如果t0檢測失敗,那么就檢測t1,如果t1滿足范圍要求,就認為t1是光線和球面的交點。
寫成代碼就是:
// Check quadric shape _t0_ and _t1_ for nearest intersection
if (t0.UpperBound() > ray.tMax || t1.LowerBound() <= 0) return false;
EFloat tShapeHit = t0;
if (tShapeHit.LowerBound() <= 0) {
tShapeHit = t1;
if (tShapeHit.UpperBound() > ray.tMax) return false;
}
可以這樣實現(xiàn)是因為在計算t0和t1的時候已經(jīng)內(nèi)含了排序,t0的值必定是小于t1的。
t值的有效性(是否超出了球面的
和
范圍)
我們在定義球面的時候確定了的最小值和最大值,這就確定了z坐標的最小值和最大值,因為
和
之間有著非常明確的關(guān)系。同樣,對
的范圍我們也進行了限制,這就又給交點的計算帶來了麻煩,我們必須確定這個交點在球面的范圍之內(nèi)。
// Compute sphere hit position and $\phi$
pHit = ray((Float)tShapeHit);
// Refine sphere intersection point
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius;
phi = std::atan2(pHit.y, pHit.x);
if (phi < 0) phi += 2 * Pi;
// Test sphere intersection against clipping parameters
if ((zMin > -radius && pHit.z < zMin) || (zMax < radius && pHit.z > zMax) ||
phi > phiMax) {
if (tShapeHit == t1) return false;
if (t1.UpperBound() > ray.tMax) return false;
tShapeHit = t1;
// Compute sphere hit position and $\phi$
pHit = ray((Float)tShapeHit);
// Refine sphere intersection point
pHit *= radius / Distance(pHit, Point3f(0, 0, 0));
if (pHit.x == 0 && pHit.y == 0) pHit.x = 1e-5f * radius;
phi = std::atan2(pHit.y, pHit.x);
if (phi < 0) phi += 2 * Pi;
if ((zMin > -radius && pHit.z < zMin) ||
(zMax < radius && pHit.z > zMax) || phi > phiMax)
return false;
}
忽略減少誤差的操作(Refine spher intersection point的過程),我們來看怎么計算值的。因為
,所以
,代碼中也是這么計算的,并且為了確保
值在
之間,對計算出的小于0的數(shù)還加上了
進行調(diào)整。
然后是判斷交點的z值是否在的范圍內(nèi),
值是否在
范圍內(nèi)。如果不在,那么就換一個交點試試,如果另一個交點也不在,那就表示沒有交點,交點檢測失敗了,返回false。
交點鄰域的變化情況
交點鄰域的變化情況主要指兩個:
1、點在鄰域內(nèi)的變化情況。主要由點關(guān)于和
的偏導數(shù)(
,
)組成,暗示了在
和
兩個維度上,微小變化量會帶來什么樣的點位置改變。
2、法向量的變化情況。主要由法向量關(guān)于和
的偏導數(shù)(
,
)組成,暗示了在
和
兩個維度上,微小變化量會帶來法向量的什么改變。
計算這些量的前提是,我們必須有u和v的值才行,從上面關(guān)于球面的方程中,我們很容易就能推導出:
可以通過交點的z值公式,利用反余弦函數(shù)求出,具體的代碼是:
// Find parametric representation of sphere hit
Float u = phi / phiMax;
Float theta = std::acos(Clamp(pHit.z / radius, -1, 1));
Float v = (theta - thetaMin) / (thetaMax - thetaMin);
接著求p關(guān)于u和v的偏導數(shù)。
代碼實現(xiàn)如下:
// Compute sphere $\dpdu$ and $\dpdv$
Float zRadius = std::sqrt(pHit.x * pHit.x + pHit.y * pHit.y);
Float invZRadius = 1 / zRadius;
Float cosPhi = pHit.x * invZRadius;
Float sinPhi = pHit.y * invZRadius;
Vector3f dpdu(-phiMax * pHit.y, phiMax * pHit.x, 0);
Vector3f dpdv =
(thetaMax - thetaMin) *
Vector3f(pHit.z * cosPhi, pHit.z * sinPhi, -radius * std::sin(theta));
代碼中,為了減少除法的消耗,將z的倒數(shù)保存起來,計算了和
,然后利用上面推導的公式,計算出想要的值。
接著就是最困難的部分了:如何計算法向量的偏導數(shù)?
我們用一個被稱為溫加頓方程(Weingarten equations)的東西來計算法向量的偏導數(shù)。溫加頓方程(Weingarten equations)是用和
的線性組合來表示法向量的偏導數(shù)。那么,為什么能用線性組合來表示呢?這就要用到上一篇文章的知識了。假設你已經(jīng)了解了曲線和曲面的相關(guān)知識,特別是第一基本形式和第二基本形式的相關(guān)知識。
在曲面中,法向量n意味著一個法向量場,表示的是表面的所有法向量的集合。而在代碼中,它僅僅表示了一個向量,但是這沒法計算變化了,所以,我們還是要放到整個曲面的意義上去。
法向量的計算方式是,也就是說,法向量與曲面在這點上的切平面垂直,并且它是單位向量。然后,對
兩邊求導可得,
,就意味著
。而
又垂直于切平面,所以
必然在其平面內(nèi),于是,它就可以表示成
的線性組合。
接著,由第二基本形式可以得到,即
由于也在切平面內(nèi),我們可以用
線性組合的方式來表示
:
將和
帶入到
的表達式中得:
這里的E,F(xiàn),G都是第一基本形式的系數(shù),具體是。將上述的方程組表示成矩陣就是
于是
逆矩陣可以使用余子式來計算
最后得到
最終,求和
的公式就是:
觀察代碼,我們發(fā)現(xiàn),它的過程與我們推導出的結(jié)果完全一致:
// Compute sphere $\dndu$ and $\dndv$
Vector3f d2Pduu = -phiMax * phiMax * Vector3f(pHit.x, pHit.y, 0);
Vector3f d2Pduv =
(thetaMax - thetaMin) * pHit.z * phiMax * Vector3f(-sinPhi, cosPhi, 0.);
Vector3f d2Pdvv = -(thetaMax - thetaMin) * (thetaMax - thetaMin) *
Vector3f(pHit.x, pHit.y, pHit.z);
// Compute coefficients for fundamental forms
Float E = Dot(dpdu, dpdu);
Float F = Dot(dpdu, dpdv);
Float G = Dot(dpdv, dpdv);
Vector3f N = Normalize(Cross(dpdu, dpdv));
Float e = Dot(N, d2Pduu);
Float f = Dot(N, d2Pduv);
Float g = Dot(N, d2Pdvv);
// Compute $\dndu$ and $\dndv$ from fundamental form coefficients
Float invEGF2 = 1 / (E * G - F * F);
Normal3f dndu = Normal3f((f * F - e * G) * invEGF2 * dpdu +
(e * F - f * E) * invEGF2 * dpdv);
Normal3f dndv = Normal3f((g * F - f * G) * invEGF2 * dpdu +
(f * F - g * E) * invEGF2 * dpdv);
當然,最后保存關(guān)于交點的信息以及集中時,參數(shù)t的值。
// Initialize _SurfaceInteraction_ from parametric information
*isect = (*ObjectToWorld)(SurfaceInteraction(pHit, pError, Point2f(u, v),
-ray.d, dpdu, dpdv, dndu, dndv,
ray.time, this));
// Update _tHit_ for quadric intersection
*tHit = (Float)tShapeHit;
直接把計算得到的信息都保存進SurfaceInteraction結(jié)構(gòu)里,很直觀,然后將這些信息都轉(zhuǎn)換到世界空間中。
關(guān)于誤差
誤差的主要來源是浮點數(shù)計算時的舍入誤差,每一次計算都會有誤差,不管是加減還是乘除法。這點書中第三章的最后介紹得非常詳細,之后會總結(jié)出來。
總結(jié)
雖然看上去并不多,但是花了很多時間在微分幾何上,就為了理解溫加頓方程。這一篇文章像是如何實現(xiàn)數(shù)學公式,事實上也是,圖形和交點,怎么能沒計算呢?想到后面還有茫茫多的計算,不禁打個冷戰(zhàn),想想是不是要再寫篇文章壓壓驚?
參考資料
Physically Based Rendering
pbrt源碼,版本3
Differiential Geometry of Curves and Surfaces