GAMES202筆記:基於SH的GI方案


基於Spherical Harmonics的實時全局照明
本文主要內容:頻率與基函數、球面諧波函數、預計算輻射傳輸、球面均勻采樣、構建正交基、SH實現、球面諧波函數的旋轉及實現。

1. 頻率與基函數

1.1 傅里葉級數展開

任何一個函數可以寫成常數和一系列基函數(不同頻率sin和cos項)的線性組合,基函數數量越多越接近於原函數的形狀:

在這里插入圖片描述

\[f(x)=\frac{A}{2}+\frac{2 A \cos (t \omega)}{\pi}-\frac{2 A \cos (3 t \omega)}{3 \pi}+\frac{2 A \cos (5 t \omega)}{5 \pi}-\frac{2 A \cos (7 t \omega)}{7 \pi}+\cdots \]

1.2 頻率

在空間上圖像信號數值的變化是否劇烈,任何一張圖(也就是二維函數)的頻率,也就是頻域上對應的內容可以用一張頻譜表示出來:

在這里插入圖片描述

頻譜最中心處是低頻內容,我們可以做一個filtering(濾波),從而去除一系列頻率上的內容,我們對這張圖用一個低通濾波器,從而把高頻的內容去除掉:

在這里插入圖片描述

卷積:卷積是一個模糊操作,在圖上取任意一點,取點的周圍一定區域內的像素值進行加權平均並將結果寫回這個點,這就是卷積。
在空間域上做卷積等於在函數上做卷積;等於在頻域上做原圖頻譜和卷積核頻譜 的乘積操作:

在這里插入圖片描述

對於任意的product integral(兩個函數先乘積在積分),我們將其認為是做了一個卷積操作。

\[\int_{\Omega} f(x) g(x) \mathrm{d} x \]

空間域上的兩個信號\(f(x)\)\(g(x)\)進行一個卷積,等於在頻域上讓兩個信號相乘;如果兩個信號有一個信號是低頻的,那么頻域上相乘后得到的結果也是低頻的,最終相乘在積分的結果也是低頻。的可以總結為:積分之后的頻率取決於積分前最低的頻

1.3 基函數

\[f(x)=\sum_{i} c_{i} \cdot B_{i}(x) \]

一個函數可以描繪成其他函數的線性組合,如\(f(x)\)可以描繪成一系列的\(B_i\)函數乘以各自對應的系數最終再相加在一起,這一系列的函數\(B_i\)就是基函數。

2. 球諧函數

SH(Spherical Harmonics)是一系列基函數,系列中的每個函數都是二維函數,並且每個二維函數都是定義在球面上的,階數越高,頻率越高。

在這里插入圖片描述

下圖是對SH的可視化,與一維的傅里葉一樣,SH也存在不同頻率的函數,但不同頻率的函數個數也不同,頻率越高所含有的基函數越多:

在這里插入圖片描述

\(l\)表示的是階數,通常第\(l\)階有\(2l+1\)個基函數,前\(n\)階有\(n^2\)個基函數。
越偏白的藍色地方值越大,越黑的地方值越小;而黃色中則表示偏白的地方表示其絕對值大,偏黑的地方表示絕對值小;藍色表示正,黃色表示負。

2.1 投影

由於一個函數\(f(\omega)\)可以由一系列基函數和系數的線性組合表示,那么怎么確定基函數前面的系數,這就需要通過投影操作:

\[c_{i}=\int_{\Omega} f(\omega) B_{i}(\omega) \mathrm{d} \omega \]

我們知道函數\(f(\omega)\),通過對應的基函數\(B_i(\omega)\)進行投影操作,從而求出各基函數對應的系數\(C_i\),與以下操作是同一個道理:在空間中想描述一個向量,可以xyz三個坐標來表達,把xyz軸當做三個基函數,把向量投影到xyz軸上,得到三個系數就是三個坐標。

2.2 重建

知道基函數對應的系數,就能用系數和基函數恢復原來的函數。
由於基函數的階可以是無限個的,越高的階可恢復的細節就越好,但一方面是因為更多的系數會帶來更大的存儲壓力、計算壓力,而描述變化比較平滑的環境漫反射部分,用3階SH就足夠。

\[c_{i}=\int_{\Omega} f(\omega) B_{i}(\omega) \mathrm{d} \omega \]

對於重建diffuse來說這里的\(f(\omega)\)對應環境光照。

2.3 球諧函數的正交性

\[\begin{aligned} &\int_{\Omega} B_{i}(\mathbf{i}) \cdot B_{j}(\mathbf{i}) \mathrm{di}=\mathbf{1} \quad(\mathbf{i}=\boldsymbol{j}) \\ &\int_{\Omega} B_{i}(\mathbf{i}) \cdot B_{j}(\mathbf{i}) \mathrm{di}=\mathbf{0} \quad(\mathbf{i} \neq j) \end{aligned} \]

旋轉一個基函數之后,得到的函數就不再是一個基函數(因為基函數有嚴格的朝向等限制),但是旋轉球諧函數等價於同階基函數的線性組合。這意味着可以簡單地進行投影、重建、旋轉。

2.4 內容補充

diffuse BRDF類似於一個低通濾波器,使用一些低頻信息就可以恢復出原始內容。因為積分之后的頻率取決於積分前最低的頻率,當diffuse BRDF使用低頻信息即可恢復內容時,也就意味着無論光照項是多么復雜,其本應該用多高頻的基函數去表示,但我們希望得到的是其與BRDF之積的積分,所以可以使用比較低頻的基函數去描述燈光。
前3階球諧基函數:

\[\begin{aligned} &Y_{0}^{0}(\theta, \varphi)=\frac{1}{2} \sqrt{\frac{1}{\pi}} \\ &Y_{1}^{-1}(\theta, \varphi)=\frac{1}{2} \sqrt{\frac{3}{2 \pi}} \sin \theta e^{-i \varphi} \\ &Y_{1}^{0}(\theta, \varphi)=\frac{1}{2} \sqrt{\frac{3}{\pi}} \cos \theta \\ &Y_{1}^{1}(\theta, \varphi)=\frac{-1}{2} \sqrt{\frac{3}{2 \pi}} \sin \theta e^{i \varphi} \\ &Y_{2}^{-2}(\theta, \varphi)=\frac{1}{4} \sqrt{\frac{15}{2 \pi}} \sin ^{2} \theta e^{-2 i \varphi} \\ &Y_{2}^{-1}(\theta, \varphi)=\frac{1}{2} \sqrt{\frac{15}{2 \pi}} \sin \theta \cos \theta e^{-i \varphi} \\ &Y_{2}^{0}(\theta, \varphi)=\frac{1}{4} \sqrt{\frac{5}{\pi}}\left(3 \cos ^{2} \theta-1\right) \\ &Y_{2}^{1}(\theta, \varphi)=\frac{-1}{2} \sqrt{\frac{15}{2 \pi}} \sin \theta \cos \theta e^{i \varphi} \\ &Y_{2}^{2}(\theta, \varphi)=\frac{1}{4} \sqrt{\frac{15}{2 \pi}} \sin ^{2} \theta e^{2 i \varphi} \end{aligned} \]

3. 預計算輻射傳輸

PRT(Precomputed Radiance Transfer)思想:將光照切割成Lighting, Light Transport兩部分:

\[L(\mathbf{0})=\int_{\Omega} \underbrace{L(\mathbf{i})}_{\text {lighting }} \underbrace{V(\mathbf{i}) \rho(\mathbf{i}, \mathbf{0}) \max (0, \boldsymbol{n} \cdot \mathbf{i})}_{\text {light transport }} \mathrm{di} \]

首先,因為在Diffuse的情況下,BRDF幾乎是一個常數,可以把它提到外面:

\[L(\mathbf{0})=\rho \int_{\Omega} L(\mathbf{i}) V(\mathbf{i}) \max (0, \boldsymbol{n} \cdot \mathbf{i}) \mathrm{d} \mathbf{i} \]

而Lighting項可以寫成基函數的形式:

\[L(\mathbf{i}) \approx \sum l_{i} B_{i}(\mathbf{i}) \]

同樣的,Light transport項也可以寫成基函數求和的形式:

\[T\left(\mathbf{i}\right) \approx \sum_{} l_{q} B_{q}\left(\mathbf{i}\right) \]

\(l_i\)\(l_q\)是常數,可以提出來:

\[L(\mathbf{0})=\sum_{i} \sum_{q} l_{i} l_{q} \int_{\Omega^{+}} B_{i}\left(\mathbf{i}\right) B_{q}\left(\mathbf{i}\right) \mathrm{d} \mathbf{i} \]

因為球諧的正交性,只有\(i = q\)的時候,\(B_{i}\left(\mathbf{i}\right) B_{q}\left(\mathbf{i}\right)\)相乘才有值,所以這個函數的復雜度仍是O(n)。
下面將基函數形式的Lighting項帶入渲染方程:

\[\begin{aligned} &{L(\mathbf{0}) \approx \rho \sum l_{i} \int_{\Omega} \mathrm{B}_{i}(\mathbf{i}) V(\mathbf{i}) \max (0, \boldsymbol{n} \cdot \mathbf{i}) \mathrm{di}} \\ &L(\mathbf{0}) \approx \rho \sum l_{i} T_{i} \end{aligned} \]

對於積分中的部分來說,相當於light transport乘與一個基函數,這就成了lighting transport投影到一個基函數的系數。因為對light transport做了預計算,visibility成為了常量,只能對靜止物體進行計算。
一個SH基函數旋轉后都可以被同階的SH基函數線性組合得到,因此可以解決光源旋轉問題。
現在將lighting這個球面函數,通過SH的基函數用一堆系數來表示,這些系數排成一行也就是組成了向量,因此光照變成了一個向量:

在這里插入圖片描述

投影到SH空間:

\[l_{i}=\int_{\Omega} L(\mathbf{i}) \cdot B_{i}(\mathbf{i}) \mathrm{di} \]

從SH空間重建:

\[L(\mathbf{i}) \approx \sum l_{i} B_{i}(\mathbf{i}) \]

4. 球面均勻采樣

4.1 Hammersley采樣

Hammersley這是一種均勻分布的2D隨機采樣,他將十進制轉換成二進制,再將二進制轉換到[0,1]之間的小數,這一過程被稱作 Radical Inverse。具體表示方法見下圖:

在這里插入圖片描述

Hammersley采樣點集合:

\[p_{i}=\left(x_{i}, y_{i}\right)=(i / N, \phi(i)) \]

\(N\)是我們的采樣點總數,\(\phi(i)\)就是Radical Inverse之后得到的小數,代碼實現如下:

float RadicalInverse( uint bits )
{
	//reverse bit
	//高低16位換位置
	bits = (bits << 16u) | (bits >> 16u); 
	//A是5的按位取反
	bits = ((bits & 0x55555555) << 1u) | ((bits & 0xAAAAAAAA) >> 1u);
	//C是3的按位取反
	bits = ((bits & 0x33333333) << 2u) | ((bits & 0xCCCCCCCC) >> 2u);
	bits = ((bits & 0x0F0F0F0F) << 4u) | ((bits & 0xF0F0F0F0) >> 4u);
	bits = ((bits & 0x00FF00FF) << 8u) | ((bits & 0xFF00FF00) >> 8u);
	return  float(bits) * 2.3283064365386963e-10;
}

vec2 Hammersley(uint i,uint N)
{
	return vec2(float(i) / float(N), RadicalInverse(i))
}

4.2 2D坐標到球坐標的映射函數

對於球面來說,如果用方位角\((\theta, \phi)\)直接拿Hammersley均勻采樣,會出現兩極密度高,中間密度低的情況:

在這里插入圖片描述

通過求球面概率密度分布函數的反函數,可以對球面均勻采樣的映射函數:

\[\left\{\begin{array}{l} \theta=\arccos \left(1-2 \xi_{x}\right) \\ \phi=2 \pi \xi_{y} \end{array}\right.,\xi_{x}, \xi_{y} \sim \text { Uniform }[0,1] \]

現在,在2D平面坐標\((\xi_{x},\xi_{y})\)做Hammersley均勻采樣,然后再映射回球坐標系即可得到均勻的分布:

在這里插入圖片描述

PS:我們可以很容易的得到 \(cos\theta=1-2 \xi_{x}\)

4.3 球坐標系與直角坐標系的轉換

直角坐標系\((x,y,z)\)轉球坐標系\((r, \theta, \varphi)\)

\[\begin{gathered} r=\sqrt{x^{2}+y^{2}+z^{2}} \\ \theta=\arccos \frac{z}{r} \\ \varphi=\arctan \frac{y}{x} \end{gathered} \]

球坐標系\((r, \theta, \varphi)\)轉直角坐標系\((x,y,z)\)

\[x=r \sin \theta \cos \varphi\\ y=r \sin \theta \sin \varphi\\ z=r \cos \theta \]

現在我們可以結合2D坐標到球坐標的映射函數寫出輸入\((\xi_{x},\xi_{y})\),輸出\((x,y,z)\)的方法:

vec3 HemisphereSample_uniform(float u, float v) 
{
	float phi = v * 2.0 * PI;
	float cosTheta = 1.0 - u; // float cosTheta = u;//一回事兒
	float sinTheta = sqrt(1.0 - cosTheta * cosTheta);
	return vec3(cos(phi) * sinTheta, sin(phi) * sinTheta, cosTheta);
}

注意這里的\(1-2 \xi_{x}\)被改為了\(1- \xi_{x}\),所以是半球空間。球面空間只需要將float cosTheta = 1.0 - u;改為float cosTheta = 1.0 - 2u;

5. 根據單位法向量構建正交基

根據反射定律和折射定律,我們知道如何求解反射方向和折射方向。用蒙特卡洛方法渲染時,需要生成與法向量或反射方向呈一定概率分布的方向采樣:

在這里插入圖片描述

通過上文中HemisphereSample_uniform函數可以生成一個直角坐標系下的向量,而且是在正上半球(結合Hammersley可以生成正上半球的均勻采樣向量)。我們可以把它當做是生成在切線空間的,因此我們還需要計算一個正交基,將它轉換回世界空間:

在這里插入圖片描述

將基下的向量與正交基投影,可以得到原向量,下面提供正交基的一種計算方法:

// [ Duff et al. 2017, "Building an Orthonormal Basis, Revisited" ]
float3x3 GetTangentBasis( float3 TangentZ )
{
	const float Sign = TangentZ.z >= 0 ? 1 : -1;
	const float a = -rcp( Sign + TangentZ.z );
	const float b = TangentZ.x * TangentZ.y * a;
	
	float3 TangentX = { 1 + Sign * a * Pow2( TangentZ.x ), Sign * b, -Sign * TangentZ.x };
	float3 TangentY = { b,  Sign + a * Pow2( TangentZ.y ), -TangentZ.y };
	
	return float3x3( TangentX, TangentY, TangentZ );
}

6. 實現

6.1 編碼球面紋理信息

以光照探針所在點為相機位置,記錄周圍環境到Cubemap:

在這里插入圖片描述

之后的操作有兩種。閆老師提到,濾波后一次采樣等於沒濾波多次采樣。所以我們可以不濾波,直接用隨機向量多次采樣來計算漫反射;也可以先濾波,然后進行一次采樣。下面采用先濾波,在進行一次采樣的方法。

在這里插入圖片描述

6.2 預計算SH系數\(l_{i}\)

投影\(L(\mathbf{i})\)到SH空間:

\[l_{i}=\int_{\Omega} L(\mathbf{i}) \cdot B_{i}(\mathbf{i}) \mathrm{di} \]

6.1.2 濾波環境貼圖

float3 PrefilterEnvMap(float3 NormalDir)
{
	float3 FilteredColor = 0;
	float Weight = 0;
	// 根據法線得到正交基
	float3x3 TangentToWorld = GetTangentBasis(NormalDir);
	const uint NumSamples = 4096;
	for( uint i = 0; i < NumSamples; i++ )
	{
		// 計算二維隨機向量
		float2 R = Hammersley(i, NumSamples);
		// 通過球面映射轉換到直角坐標系得到隨機向量
		float3 E = HemisphereSample_uniform(R.x, R.y);
		// 把隨機向量轉換為世界空間
		float3 H = mul(E, TangentToWorld);
		// 得到cos項
		float NoL = saturate(dot(NormalDir, H));
		if( NoL > 0 )
		{
		    FilteredColor += AmbientCubemap.Sample(H, 0).rgb * NoL;
		    Weight += NoL;
		}
	}
	
	return FilteredColor / max( Weight, 0.001 );
}

可以用一個低分辨率的Cubemap來存儲,因為漫反射頻率非常低。

6.1.2 求出Spherical Harmonics

前3階基函數,可以用計算的結果替代常量計算部分:

float[9] SHcosineLobe(Vector3 normal) 
{
	float[] sh = new float[9];
	float x = normal.x;
	float y = normal.y;
	float z = normal.z;
	
	sh[0] = 1.0f / 2.0f * Sqrt(1.0f / PI);
	
	sh[1] = Sqrt(3.0f / (4.0f * PI)) * z;
	sh[2] = Sqrt(3.0f / (4.0f * PI)) * y;
	sh[3] = Sqrt(3.0f / (4.0f * PI)) * x;
	
	sh[4] = 1.0f / 2.0f * Sqrt(15.0f / PI) * x * z;
	sh[5] = 1.0f / 2.0f * Sqrt(15.0f / PI) * z * y;
	sh[6] = 1.0f / 4.0f * Sqrt(5.0f / PI) * (-x * x - z * z + 2 * y * y);
	sh[7] = 1.0f / 2.0f * Sqrt(15.0f / PI) * y * x;
	sh[8] = 1.0f / 4.0f * Sqrt(15.0f / PI) * (x * x - z * z);
	
	return sh;
}

計算一次采樣的SH結果:

float3[9] GetSHSampleColor(float3 Dir)
{
	float3[9] Result;
	float3 color = AmbientCubemap.Sample(Dir, 0);
	float[9] SHValue = SHcosineLobe(Dir);
	for(uint i = 0; i < 9; ++i)
	{
		Result[i] = SHValue[i] * Color;
	}
	return Result;
}

下面需要生成很多隨機采樣方向,計算球諧向量,然后取平均。這里可以用CS的Barrier操作,只分一個線程組,等待組內所有采樣都記錄下之后,進行平均,類似Mipmap一樣一級一級兩兩平均:

// 采樣次數
const THREAD_COUNT = 256;
// 暫存顏色
float3[THREAD_COUNT] CollectedData;

float3[9] GetResult(uint id)
{
   // 權重
   Weight = 4.0f * PI;
   // 計算一個采樣方向,類似上文中的半球空間
   float3 SampleWay = SphereSample_uniform(Hammersley(id, THREAD_COUNT)).xyz;
   // 采樣出球諧顏色
   float3[9] Col = GetSHSampleColor(SampleWay);

   [unroll]
   for(uint i = 0; i < 9; ++i)
   {
       // 把第i項球諧顏色存儲
       CollectedData[id] = Col[i] * Weight;

       uint threadID = THREAD_COUNT;
       while(threadID > 0)
       {
           threadID /= 2;
           bool bInside = id < threadID;
           float3 data = 0.0f;
           // 等當前線程組所有線程都把第i項存到CollectedData中
           GroupMemoryBarrierWithGroupSync();
           if(isInside)
           {   
               // 取均值
               data = CollectedData[id * 2] + CollectedData[id * 2 + 1];
               data *= 0.5f;
           }
           //等待所有可取均值的線程完成
           GroupMemoryBarrierWithGroupSync();
           if(isInside)
           {
               // 存儲均值
               CollectedData[id] = data;
           }
       }

       if(id == 0)
       {
           // 最終結果
           Col[i] = CollectedData[0];
       }
   }
}

代碼中的Weight其實是蒙特卡洛方法中的\(\frac{1}{p\left(X_{i}\right)}\)

\[\int f(x) \mathrm{d} x=\frac{1}{N} \sum_{i=1}^{N} \frac{f\left(X_{i}\right)}{p\left(X_{i}\right)} \]

因為是球面均勻采樣所以PDF是\(\frac{1}{4\pi}\)
現在我們得到了最終的9個SH Color。

6.3 運行時從SH空間重建光照

\[L(\mathbf{i}) \approx \sum l_{i} B_{i}(\mathbf{i}) \]

float3 GetDiffuseSH(float3[9] SHResult, float3 Normal)
{
	float[9] Bi = SHcosineLobe(Normal);
	float3 Col = 0.0f;
	for(uint i = 0; i < 9; ++i)
	{
		Col += SHResult[i] * Bi[i];
	}
	return Col;
}

得到效果

在這里插入圖片描述

7. 球諧的旋轉

PRT 的一個問題是如果 lighting 部分是預計算的,那就只適用於靜態環境光下的靜態物體渲染;環境光或者物體只要有變化,PRT 就不得不進行重新預計算;但得益於 SH 的旋轉不變性,我們至少可以讓 SH Lighting 適用於動態旋轉的情形而不必重新預計算。
環境光旋轉和物體旋轉在 PRT 渲染中是等價的,只是說看相對於哪個東西來看待旋轉而已。

7.1 SH系數旋轉的性質

  • SH系數的旋轉是對SH系數的線性變換
  • 對每一階SH系數的旋轉可以分別進行

第一條性質意味着給定SH系數\({t}=\left(t_{0}, \ldots, t_{k-1}\right)\),我們希望將某個旋轉\(R\)作用到它上面,則存在\(k\)階方陣\(M_R\)使得\(M_R{t}\)就正好是我們所需要的新SH系數。

第二條性質意味着我們可以分別處理每一階的SH系數。比如我們使用了前3階SH來進行投影,那么就有\(l=0\) 對應的1個系數、 \(l=1\) 對應的3個系數以及\(l=2\) 對應的5個系數,加起來共9個SH系數。\(l=0\)的SH是個常量函數,不需要處理; \(l=1\)時的3個系數可以用一個3階方陣進行變換;\(l=2\) 時的5個系數則可以用一個5階方陣進行變換。

也就是說運行時根據光源旋轉解出每階(\({i}\))對應的\(M_{R}\),就能在\(l_{i}\)不變的情況下得到\(L_i\)

7.2 方陣\(M_R\)的計算方法

\(l=1\)為例(更高階可類推):
\(R\)是我們希望進行的旋轉操作的旋轉矩陣, \(P\)是求出任意三維方向向量所對應的5個SH值的投影函數,則對任意方向向量 \(x\)\(M_R\)應滿足:

\[{M}_{R} {P}({x})={P}({R} {x}) \]

先用\(R\) 來變換\(x\),再將變換結果投影到SH上,等價於先把 \(x\) 投影到SH上,之后再用 \(R\)去變換投影結果。
假設隨便帶入3個方向向量\({n}_{0},{n}_{1},{n}_{2}\),這個等式都應該成立:

\[\left\{\begin{array}{l} {M}_{R} {P}\left({n}_{0}\right)={P}\left({R} {n}_{0}\right) \\ {M}_{R} {P}\left({n}_{1}\right)={P}\left({R} {n}_{1}\right) \\ {M}_{R} {P}\left({n}_{2}\right)={P}\left({R} {n}_{2}\right) \\ \end{array}\right. \]

因為\({P}({n}_{i})\)\({P}({R}{n}_{i})\)都是列向量,我們可以把這一坨式子寫成矩陣形式:

\[{M}_{R}\left[{P}\left({n}_{0}\right), {P}\left({n}_{1}\right), {P}\left({n}_{2}\right)\right]=\left[{P}\left({R} {n}_{0}\right), {P}\left({R} {n}_{1}\right), {P}\left({R} {n}_{2}\right)\right] \]

\({A}=\left[{P}\left({n}_{0}\right), {P}\left({n}_{1}\right), {P}\left({n}_{2}\right)\right]\)如果\({A}\)是可逆的,那么我們可以很容易地把\(M_R\)找出來:

\[{M}_{R}=\left[{P}\left({R} {n}_{0}\right), {P}\left({R} {n}_{1}\right), {P}\left({R} {n}_{2}\right)\right] {A}^{-1} \]

也就是說,只要\({n}_{0},{n}_{1},{n}_{2}\)選取的值使\({A}\)可逆,就可以解出\(M_R\)

7.3 實現方法

7.3.1 預計算

預計算\({A}^{-1}\),選取\(2 l+1\)個方向向量 \({n}_{0}, \ldots,{n}_{2l}\)使得 \({A}=\left[{P}\left({n}_{0}\right), \ldots, {P}\left({n}_{2 l}\right)\right]\) 可逆。求出\({A}^{-1}\)並存儲。

同時存儲向量 \({n}_{0},...,{n}_{2l}\)

7.3.2 運行時

  1. 計算主光源的旋轉矩陣\(R\),將\(R\)應用於 \({n}_{0}, \ldots,{n}_{2l}\),得到\({n}_{0}^{\prime}, \ldots, {n}_{2 l}^{\prime}\)
  2. \(P\)計算出\({n}_{0}^{\prime}, \ldots, {n}_{2 l}^{\prime}\)對應的SH投影值,得到\(2 l+1\)\(2 l+1\)維列向量,將這些列向量構造成一個\(2 l+1\)階方陣\({S}\)
  3. 設待變換的SH系數用\(2 l+1\)維列向量 \(x\) 表示,計算\({S}{A}^{-1}x\) 即可。可以寫成\({S}({A}^{-1}x)\)來節省一些計算量。

這里就相當於,對於向量\({n}_{0},...,{n}_{2l}\),已知\({A}^{-1}\),這兩個在運行時已經變成了常量,只需要帶入變量\(R\)得到\(M_R\)即可。然后根據:
\({M}_{R} {P}({x})={P}({R} {x})\),將\(M_R\)\({P}({x})\)相乘即可得到旋轉后的\(L_i\)

7.3.4 偽代碼

float3 GetDiffuseSH(float3[9] SHResult, float3 Normal, float3x3 RotationMatrix)
{
    // 1. 運行時步驟1
	float3 L1_n0 = mul(RotationMatrix, L1N0);
	float3 L1_n1 = mul(RotationMatrix, L1N1);
	float3 L1_n2 = mul(RotationMatrix, L1N2);
	
	float3 L2_n0 = mul(RotationMatrix, L2N0);
	float3 L2_n1 = mul(RotationMatrix, L2N1);
	float3 L2_n2 = mul(RotationMatrix, L2N2);
	float3 L2_n1 = mul(RotationMatrix, L2N3);
	float3 L2_n2 = mul(RotationMatrix, L2N4);

    // 2.運行時步驟2
    float3 S_L1_n0 = SHcosineLobe_L1(L1_n0);
    float3 S_L1_n1 = SHcosineLobe_L1(L1_n1);
    float3 S_L1_n2 = SHcosineLobe_L1(L1_n2);

    float[5] S_L2_n0 = SHcosineLobe_L2(L2_n0);
	float[5] S_L2_n1 = SHcosineLobe_L2(L2_n1);
	float[5] S_L2_n2 = SHcosineLobe_L2(L2_n2);
	float[5] S_L2_n3 = SHcosineLobe_L2(L2_n3);
	float[5] S_L2_n4 = SHcosineLobe_L2(L2_n4);

    float[9] S1 = float3x3(
    S_L1_n0.x, S_L1_n1.x, S_L1_n2.x,
    S_L1_n0.y, S_L1_n1.y, S_L1_n2.y,
    S_L1_n0.z, S_L1_n1.z, S_L1_n2.z,
    );
    
    float[25] S2 = float[25]{
        S_L2_n0[0],S_L2_n1[0],S_L2_n2[0],S_L2_n3[0],S_L2_n4[0],
        S_L2_n0[1],S_L2_n1[1],S_L2_n2[1],S_L2_n3[1],S_L2_n4[1],
        S_L2_n0[2],S_L2_n1[2],S_L2_n2[2],S_L2_n3[2],S_L2_n4[2],
        S_L2_n0[3],S_L2_n1[3],S_L2_n2[3],S_L2_n3[3],S_L2_n4[3],
        S_L2_n0[4],S_L2_n1[4],S_L2_n2[4],S_L2_n3[4],S_L2_n4[5],
    };

    // 3.運行時步驟3
    float[9] MR_L1 = mul(S1, A_Inv_L1);
    float[25] MR_L2 = mul(S2, A_Inv_L2);

    float[9] Bi = SHcosineLobe(Normal);

    float L0 = Bi[0];

    float3 L1 = float3(Bi[1], Bi[2], Bi[3]);
    L1 = mul(MR_L1, L1);

    float[5] L2 = float[5]{Bi[4], Bi[5], Bi[6], Bi[7], Bi[8]};
    L2 = mul(MR_L2, L2);
	
    return SHResult[0] * L0 + 
    SHResult[1] * L1[0] + SHResult[2] * L1[1] + SHResult[3] * L1[2] + 
    SHResult[4] * L2[0] + SHResult[5] * L2[1] + SHResult[6] * L2[2] + SHResult[7] * L2[4] + SHResult[8] * L2[5];
}

8. 游戲引擎中的實現

Unity中的Light Probe,Tetrahedral Tessellations方案。UE4的Indirect Light Cache和VLM均是基於球諧的,只不過在差值方式,存儲等方面有區別。這里先不寫了,文章有點長

引用

主要來源 GAMES202
截圖來自於GAMES 202 PPT和Unity引擎內
http://corysimon.github.io/articles/uniformdistn-on-sphere/
https://www.cnblogs.com/KillerAery/p/15335369.html
https://airguanz.github.io/2018/11/20/SH-PRT.html
https://zhuanlan.zhihu.com/p/51267461
https://zhuanlan.zhihu.com/p/103715075
https://zhuanlan.zhihu.com/p/49746076
https://zhuanlan.zhihu.com/p/362460950
https://zhuanlan.zhihu.com/p/373697912
https://zhuanlan.zhihu.com/p/149217557
https://zhuanlan.zhihu.com/p/351071035


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM