實時陰影技術(Real-time Shadows)


Shadow Mapping 基本實現


Shadow Mapping 基本原理:

  1. 陰影生成 Pass:
    • 額外設置一個攝像機在光源位置(Light Camera,光源攝像機),並且朝光照方向看去。
    • 用一張 Texture(稱為 陰影貼圖 Shadow Map)來記錄 Light Camera 所看到的像素深度(每個像素位置只記錄所見最近深度,而不用做別的 shading 計算)來作為遮擋深度。
// shadowVertex.glsl
// ...
void main(void) {
  vNormal = aNormalPosition;
  vTextureCoord = aTextureCoord;
  gl_Position = uLightMVP * vec4(aVertexPosition, 1.0);
}
// shadowFragment.glsl
// ...
void main(){
  gl_FragColor = pack(gl_FragCoord.z);
}

如圖,Shadow Map 記錄了 Light Camera 所看到的最近深度圖,顏色越深,離攝像機越近:

  1. 渲染 Pass:
    • 主攝像機需要渲染屏幕每個像素時,該像素對應的世界坐標進行 Light Camera 的MVP變換后能得到在 Light Camera 屏幕空間中的對應位置 \(shadowCoord = (x',y',z')\)
    • Shadow Map 里用\((x',y')\)采樣得到的遮擋深度 \(depth\) 與深度值 \(z'\) 做比較: 若 \(depth < z'\)(意味着該像素的光被遮擋),這時就可以對該像素降低可見度(Visibility)。
// phongVertex.glsl
// ...
void main(void) {
  vNormal = (uModelMatrix * vec4(aNormalPosition, 0.0)).xyz;
  vTextureCoord = aTextureCoord;
  vFragPos = (uModelMatrix * vec4(aVertexPosition, 1.0)).xyz;
  vPositionFromLight = uLightMVP * vec4(aVertexPosition, 1.0);
  gl_Position = uProjectionMatrix * uViewMatrix * uModelMatrix * vec4(aVertexPosition, 1.0);
}
// phongFragment.glsl 
// ...
void main(){
    // 歸一化坐標
    vec3 projCoords = vPositionFromLight.xyz / vPositionFromLight.w;
	vec3 shadowCoord = projCoords * 0.5 + 0.5;
    // Shadow
	float visibility = 1.0;
	float depthInShadowmap = unpack(texture2D(shadowMap,shadowCoord.xy).rgba); //將rgba四通道(32位)的值unpack成float類型的數值
	if(depthInShadowmap < shadowCoord.z){
    	visibility = 0.0;
	}
    // blinnPhong光照着色
    vec3 color = blinnPhong();
    
    gl_FragColor = vec4(color * visibility,1.0);
}

如圖為主攝像機每個像素經過變換后比較深度的結果,其中綠色點意味着深度 \(depth \approx z’\) (沒有遮擋光照),非綠色點意味着 \(depth < z'\)(被遮擋了光照):

Shadow Bias


直接使用Shadow Map可能會在不應該出現陰影的位置出現一些黑白條紋相間的現象(稱為 Shadow Acne):

其本質原因在於,Shadow Map 是一個二維數組,離散的存儲方式很難完全表示實際的幾何信息。尤其當光照方向不垂直於平面時,遮擋深度的采樣會和實際深度產生偏差(如圖一個不受遮擋的幾何平面,但黑色加粗部分卻被Shadow Mapping方法認為是被遮擋的):

解決方法:

  • 直接給采樣陰影深度加一個 偏移量 Bias(相當於把陰影深度往遠處加,從而更不容易產生遮擋)。

// phongFragment.glsl
//...
void main(){
    // 歸一化坐標
    vec3 projCoords = vPositionFromLight.xyz / vPositionFromLight.w;
	vec3 shadowCoord = projCoords * 0.5 + 0.5;
    // Shadow Bias
	const float BIAS = 0.005;
    // Shadow
	float visibility = 1.0;
	float depthInShadowmap = unpack(texture2D(shadowMap,shadowCoord.xy).rgba); //將rgba四通道(32位)的值unpack成float類型的數值
	if(depthInShadowmap + BIAS < shadowCoord.z){
    	visibility = 0.0;
	}
    // blinnPhong光照着色
    vec3 color = blinnPhong();
    
    gl_FragColor = vec4(color * visibility,1.0);
}

Peter Panning 問題 & 簡單 Trick

然而由於增加了Bias,可能會導致 Peter Panning 現象:往往在物體縫隙間發生漏光。

解決方法:

  • 避免使用單薄的幾何體(例如薄牆、薄地面);只要幾何體厚度大於Bias,影子邊界便會產生在幾何體內部,從而不易看見影子與幾何體的分離現象。

有一種有別於Bias的方法(但實際上也是殊途同歸):

  • 不使用Bias

  • 第一個Pass(Light Camera記錄深度的那個)設置成僅渲染背面(正面剔除)

這樣可以讓一些具有厚度的幾何體背面作為深度記錄,從而部分避免了幾何體正面的 Shadow Acne現象。實際上這個跟使用了Bias+加厚幾何體思想是差不多的,區別只不過在於:前者是低門限加一個偏移,后者則是直接給出高門限

Slope Scale Based Depth Bias

通過上面知道,Bias 過小時可能不能解決 Shadow Acne 現象,Bias 過大時又可能導致嚴重的 Peter Panning問題。

Slope Scale Based Depth Bias :為了盡可能減少由於 Bias 過大過小引起的問題,采取了根據平面傾角的一種自適應 Bias(例如:當光線與平面垂直時,Bias應該為0;當光線與平面的夾角越小,則Bias應越大)。

float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

Cascade Shadow Map(CSM)


當 Shadow Mapping 應用在大型場景中時,一張正常分辨率大小(如1024×1024)的貼圖用來記錄整個大型場景的陰影深度信息是非常不精確的,尤其是在靠近主攝像機的地方所看到的陰影將是嚴重失真的(一塊塊柵格)。

Cascade Shadow Map(CSM) 借鑒了 LOD 的思想,對主攝像機的視錐體范圍進行了划分,然后對每個划分區域采用相同分辨率大小的 Shadow Map,這意味着:離主攝像機比較近的地方往往區域面積是比較小的,這樣 Shadow Map 能夠表示的深度信息就更精確些,看到的陰影效果也更真實;離主攝像機比較遠的地方往往區域面積是比較大的,雖然使用相同分辨率的 Shadow Map 能夠表示的深度信息比較不精確,但對於遠處的物體來說,這種不准確是可以接受的。

CSM 的代價很明顯,即划分了多少層區域,就要使用多少倍數量的 Shadow Map;但 CSM 已經算是比較廉價且視覺效果不錯的大型場景 Shadow 解決方案,在工業界得到廣泛應用。

CSM(一層Shadow Map)的效果:

CSM(三層Shadow Map)的效果:

視錐分割

首先需要對視錐體划分成若干層(每一層在主攝像機觀察空間下的 z值范圍 分別從哪到哪),有如下方法:

  • 通過自定義比例來分割

  • 通過對數比例分割:這是較理想的比例公式,能夠減少 Shadow Perspective Alias 問題的出現(下面是公式推理的內容)。

Shadow Perspective Alias:透視投影會產生近大遠小的效果,可能會讓近處物體的多個像素對應着 Shadow Map 中的一個紋素,從而產生 Alias。

假設,\(dp\) 為主攝像機近平面上的一小段距離, \(ds\) 為 ShadowMap 平面上的一小段距離。

通過下圖的分析,通過幾何關系容易得:

\(\frac{dp}{ds} = \frac{n\tan(\theta)dz}{z}/ ds\)

\(\varphi\)\(\theta\) 為物體平面法線的角度參數,則 \(\tan(\theta)=\frac{\sin(\theta)}{\cos(\theta)}=\frac{\cos(\varphi)}{\cos(\theta)}\)

\(n\) 為主攝像機近平面距離,\(f\) 為主攝像機遠平面距離。

我們期望各層 Shadow Map 的投影和主攝像機圖像分辨率都呈現盡量相同的比例(即比例與 \(z\) 無關):

\(\frac{dp}{ds} = C_0\)

接着,我們假設所有物體表面法線都是一樣的,也就是說 \(tan(\theta)\) 也將是一個常數。

這個假設是針對這類物體:一個橫跨了多層 Shadow Map 具有相同朝向的物體(如一長塊牆面),在各層 Shadow Map 交界處很容易被看出邊界的artifact問題(上下兩層 Shadow Map 精准度不同)。

最終整理得到這么一個式子:

\(\frac{1}{z}\frac{dz}{ds} = C\)

解一下微分方程得:

分別代入 \(z=n,s=0\)\(z=f,s=1\) 解得。

\(z=n \cdot \left(\frac{f}{n}\right)^{s}\)

最終,把若干層 Shadow Map 視為若干段 \(s\),代入得:

\(z_i=n \cdot (\frac{f}{n})^{\frac{i}{N}},i=1,...,N\)

但是對數分割是一種理論上的理想分割,在場景物體從近到遠分布均勻的情況下才有比較好的表現,而在實際應用中是很少出現以上理想情況的。因為使用對數比例計算出來的一層 CSM 通常距離很小,在多數情況下都是比較空曠的,這就造成了大量的浪費。

因此通常會與線性分割進行一個混合,公式如下:

\(z_{i}=\operatorname{lerp}\left(n *(\frac{f}{n})^{\frac{i}{N}}, n+\frac{i*(f-n)}{N}, \lambda\right), i=1, \ldots, N \ , \lambda \in [0,1]\)

\(\lambda\) 是一個 \([0,1]\) 區間的控制參數,用以在對數和線性之間進行插值

計算包圍盒

分割好各層視錐體后,我們需要選擇對應的 Shadow Map 來恰好包圍住視錐體(即各層的 Light Camera 的光錐體剛好包圍住視錐體)。

有如下做法:

  • 使用最緊湊的方形包圍盒:分割后的視錐體的8個頂點在世界空間上做的View變換后,取它們最大最小x值y值z值來作為 Light Camera 的遠近平面參數設置。

然而這種做法很容易導致遠近平面大小頻繁變化,在視錐體發生變化的情況下容易出現陰影邊緣閃爍的瑕疵(shimmering edge effect or shadow flickering)

shimmering shadow edges

  • 使用固定的最大方形包圍:既然頻繁變化容易導致陰影抖動,那么就干脆使用一個固定的最大方形包圍盒來包圍任意角度下的視錐體。代價則是會包含有更多視錐體以外的位置,降低了Shadow Map的利用率。
image-20211010222035636

此外,還有使用球形包圍盒的方法來構造光源正交矩陣,優勢是可以通過很低廉的算法來進行層級選擇:即像素點坐標到球心的距離與半徑比較。

層級選擇

有了若干層 Shadow Map 后,渲染某個shading poing時該如何判斷點在哪個層級:

  • 直接通過視錐分割的z值范圍來判斷所在是哪個層級

  • 通過各層 Light Camera 的 View 變換和 Projection 變換,得到點在該層 Shadow Map 的 UV 坐標,當 UV 坐標在 [0,1] 范圍內時則說明在該層級內

前面我們在視錐分割已經確定了z值划分范圍,直接簡單根據shading point的z值來判斷層級不是更好嗎?這是因為每一層的 Shadow Map 其實多多少少包含了更遠一層的部分陰影信息,但是它的精准度明顯要比更遠層的 Shadow Map 要好,因此通過uv坐標判斷點所屬層,就可以盡量命中較近層的 Shadow Map。

不過,如果僅選擇單個層級,會容易出現各層級陰影交界處出現陰影效果劇變的問題,這時候也可以混合上下兩層 Shadow Map 來讓交界可以過渡變化。

Percentage Closer Filtering(PCF)


Shadow Mapping 還存在 陰影鋸齒(Shadow Aliasing) 問題:

Percentage Closer Filtering(PCF)正是解決陰影鋸齒的方案,它的核心想法是計算陰影時不是考慮單個采樣點,而是在一定范圍內進行多重采樣,這樣可以讓陰影的邊緣不那么鋸齒,因為 Visibility 不再是非0即1,而是帶有漸變的取值。

分布采樣函數

vec2 disk[NUM_SAMPLES]; // 經過分布采樣函數運算后得到NUM_SAMPLES個采樣坐標

在對周圍一定范圍內若干個坐標進行采樣的時候,可以通過分布采樣函數來確定 NUM_SAMPLES 個采樣位置,為了讓陰影邊緣更加柔和,我們可以用一些較好的分布采樣函數。

均勻圓盤分布采樣(Uniform-Disk Sample):圓范圍內隨機取一系列坐標作為采樣點;看上去比較雜亂無章,采樣效果的 noise 比較嚴重。

泊松圓盤分布采樣(Poisson-Disk Sample):圓范圍內隨機取一系列坐標作為采樣點,但是這些坐標還需要滿足一定約束,即坐標與坐標之間至少有一定距離間隔。

// 均勻圓盤分布
void uniformDiskSamples( const in vec2 randomSeed ) {
  // 隨機種子
  float randNum = rand_2to1(randomSeed);
  // 隨機取一個角度
  float sampleX = rand_1to1( randNum ) ;
  float angle = sampleX * PI2;
  // 隨機取一個半徑
  float sampleY = rand_1to1( sampleX ) ;
  float radius = sqrt(sampleY);
  for( int i = 0; i < NUM_SAMPLES; i ++ ) {
    disk[i] = vec2(radius * cos(angle) , radius * sin(angle));
    // 繼續隨機取一個半徑
    sampleX = rand_1to1( sampleY ) ;
    radius = sqrt(sampleY);
    // 繼續隨機取一個角度
    sampleY = rand_1to1( sampleX ) ;
    angle = sampleX * PI2;
  }
}
// 泊松圓盤分布
void poissonDiskSamples( const in vec2 randomSeed ) {
  // 初始弧度
  float angle = rand_2to1( randomSeed ) * PI2;
  // 初始半徑
  float INV_NUM_SAMPLES = 1.0 / float( NUM_SAMPLES );
  float radius = INV_NUM_SAMPLES;
  // 一步的弧度
  float ANGLE_STEP = 3.883222077450933;// (sqrt(5)-1)/2 *2PI
  // 一步的半徑
  float radiusStep = radius;

  for( int i = 0; i < NUM_SAMPLES; i ++ ) {
    disk[i] = vec2(cos(angle),sin(angle)) * pow( radius, 0.75 );
    radius += radiusStep;
    angle += ANGLE_STEP;
  }
}

PCF 算法過程

Percentage Closer Filtering(PCF) 的算法過程:

  1. 計算 Visibility 時,原本對 Shadow Map 的一次坐標采樣換成對周圍一定范圍內若干個坐標進行采樣。
  2. 各個采樣結果同樣用來與 \(z'\) 做比較,最后取比較結果的平均作為 Visibility。
float visibility_PCF(sampler2D shadowMap, vec4 coords) {
  const float bias = 0.005;
  float sum = 0.0;
  // 初始化泊松分布
  poissonDiskSamples(coords.xy);
  // 采樣
  for(int i = 0;i<NUM_SAMPLES;++i){
    float depthInShadowmap = unpack(texture2D(shadowMap,coords.xy+disk[i]*0.001).rgba);
    sum += ((depthInShadowmap + bias)< coords.z?0.0:1.0);
  }
  // 返還平均采樣結果
  return sum/float(NUM_SAMPLES);
}

Percentage Closer Soft Shadows(PCSS)


Shadow Mapping 還存在硬陰影(Hard Shadow)的問題,因為現實世界的影子往往是軟陰影(Soft Shadow)

一個現實觀察是,當投影物與陰影之間的距離越遠,則陰影越軟(如下圖:筆尖陰影由於與筆尖的距離較近,因此陰影邊緣較為銳利;而遠處筆身陰影則因與筆身距離較遠,陰影邊緣較為發散且模糊)。

這是因為較大的光源面會有一些區域被遮蔽一部分光又接受一部分光,從而產生半影(Penumbra),直觀看就是沒那么暗的邊緣處陰影。

Penumbra Size

用二維平面的圖去描述,實際上就是光源段 \(w_{Light}\) 兩端與遮擋物連直線后打在被投影物上的即是 半影段 \(w_{Penumbra}\) ,也就是說這段半影需要有漸變的陰影效果。假如我們用 PCF 算法中的圓盤半徑大小等同於這個半影段的尺寸 \(w_{Penumbra}\),就能實現這段的漸變陰影效果(可以想想為什么)。

現在,由下圖的幾何關系容易推出:

\(w_{\text {Penumbra }}=\left(d_{\text {Receiver }}-d_{\text {Blocker }}\right) \cdot w_{\text {Light }} / d_{\text {Blocker }}\)

其中,\(w_{Light}\) 是光源面積尺寸,\(d_{Blocker}\) 是遮擋物的深度,\(d_{Receiver}\) 是被投影物(實際上就是shading point)的深度。

但是 PCF 算法的圓盤半徑大小是固定的,因此處處的邊緣看起來都帶有相同的漸變范圍,這和我們看到的筆尖陰影現象不符合(近處邊緣漸變應該更少些,遠處邊緣漸變應該多些),所以我們可以只要根據不同位置動態地修改圓盤半徑大小(實際上就是動態地計算 \(w_{Penumbra}\) ),這個也就是PCSS的核心部分。

我們不能簡單把一個投影點變換成Shadow Map的坐標后,直接拿單個坐標采樣 ShadowMap 的深度來作為 \(d_{Blocker}\) 。這是因為投影點的單次采樣實際上就是單一直線連向了光源面的中心,而這條直線要是沒有碰到遮擋物(即 \(d_{Blocker}=d_{Receiver}\) ),從而得出該投影點為全亮的結論。

但實際很多場景中(如下圖),投影點和光源面處處連線后會發現有相當一部分光線會碰到遮擋物,因此該投影點應該屬於半影范圍內。

為此,我們可以對 ShadowMap 的一定范圍內進行多重采樣,每次采樣得到的深度若小於 \(d_{Receiver}\) 則認為遇到遮擋物並算入平均遮擋深度的貢獻,這樣多重采樣之后得到的平均遮擋深度就作為 \(d_{Blocker}\)

如何確定采樣的范圍半徑呢?兩個參數決定:\(w_{Light}\) 的尺寸、投影點與光源的距離(可以結合上圖推理一下為什么)

\(SampleSize=w_{Light}\cdot z_{Receiver} \cdot c\)

這樣,計算 Blocker 平均遮擋深度的整個過程為:

float findBlocker( sampler2D shadowMap, vec2 uv, float zReceiver ) {  float dBlocker = zReceiver * 0.01;  const float wLight = 0.006;  const float c = 100.0;	  float sampleSize = wLight * zReceiver * c;  float sum = 0.01;	// 取0.01一是為了避免出現0除問題,二是當多重采樣沒有貢獻時的dBlocker/sum將等於zReceiver  for(int i = 0;i<BLOCKER_SEARCH_NUM_SAMPLES;++i){    float depthInShadowmap = unpack(texture2D(shadowMap,uv+disk[i]*sampleSize).rgba);    if(depthInShadowmap < zReceiver){      dBlocker += depthInShadowmap;      sum += 1.0;    }  }  return dBlocker/float(sum);}

PCSS 算法過程

Percentage Closer Soft Shadows(PCSS) 的算法過程:

  1. Blocker Search:通過多重采樣,計算出平均遮擋深度 \(d_{Blocker}\)

  2. Penumbra Size:計算圓盤半徑大小 \(w_{\text {Penumbra }}=\left(d_{\text {Receiver }}-d_{\text {Blocker }}\right) \cdot w_{\text {Light }} / d_{\text {Blocker }}\)

  3. Filtering:通過多重采樣,計算出平均 Visibility(實際上就是調用PCF算法)

float visibility_PCSS(sampler2D shadowMap, vec4 coords){
  poissonDiskSamples(coords.xy);
  // STEP 1: avgblocker depth
  float dBlocker = findBlocker(shadowMap,coords.xy,coords.z);
  // STEP 2: penumbra size
  const float wLight = 0.006;
  float penumbra = (coords.z-dBlocker)/dBlocker * wLight;
  // STEP 3: filtering
  const float bias = 0.005;
  float sum = 0.0;
  for(int i = 0;i<PCF_NUM_SAMPLES;++i){
    float depthInShadowmap = unpack(texture2D(shadowMap,coords.xy+disk[i]*penumbra).rgba);
    sum += ((depthInShadowmap + bias)< coords.z?0.0:1.0);
  }
  return sum/float(PCF_NUM_SAMPLES);
}

PCF算法效果圖:

PCSS算法效果圖:

Variance Soft Shadow Mapping(VSSM)


PCSS、PCF 的算法都需要多重采樣,尤其 PCSS 需要兩個多重采樣(第一步的Blocker Search和第三步的PCF),這使得算法速度較慢。

為了避免多重采樣的計算,Variance Soft Shadow Mapping(VSSM) 假定一定范圍內的深度的分布符合 正態分布(Normal Distribution) ,那么只要知道該段范圍的 均值(實際上就是期望值)E方差 Var,就能先得到該范圍的正態分布模型(即知道對應的 概率密度函數 PDF)。

\(PDF(x) = \frac{1}{\sqrt{2 \pi} \sigma} \exp \left(-\frac{(x-\mu)^{2}}{2 \sigma^{2}}\right)\)

其中,\(\mu = E\)\(\sigma^2 = Var\)

接着可以通過該正態分布模型的 累計分布函數(即 CDF),就能快速推算出該范圍內有多少比例的 x 大於(或小於)給定的某個值。

\(CDF(x) = \int^x_{-\infin} PDF(t) \mathrm{d}t\)

Variance Soft Shadow Mapping(VSSM) :簡單來說,VSSM 算法就是依據 ShadowMap 的深度符合正態分布的假設來快速完成 PCSS 中的第一步(Blocker Search)和第三步(PCF算法)的一種陰影算法。

VSSM效果圖:

計算平均值 & 方差

為了快速查詢得到某段范圍的均值、方差,我們可以先選以下一種數據結構來快速查詢 Shadow Map 某段范圍的均值(期望值)\(E(X)\)

  • 硬件 Mipmap:當 Shadow Map 更新時,需要重新生成 Mipmap,不過GPU硬件實現的 Mipmap 算法非常快的開銷非常小;查詢某段方形范圍時,需要根據方形中心所在的位置(相對於周圍四個紋素的坐標)、上下層級做三線性插值(Trillinear interpolation),得到的結果即是近似的均值(期望值)。

  • 前綴和數組(Summed Area Tables/SAT):當 Shadow Map 更新時,需要重新進行二維前綴和計算;需要編寫 Compute Shader 實現該算法,比Mipmap方法更慢一些,但百分百精准;查詢某段方形范圍時,就可以通過如下圖方法快速查詢得到某段范圍的總和,除掉范圍面積就能得到均值(期望值)。

我們需要存儲 \(E(X)\)\(E(X^2)\)​ ,這樣就能計算某段范圍的平均值、方差:

  1. 平均值 \(E(X)\)

  2. 方差 \(Var(X)=E(X^2)-E^2(X)\)

\(E(X^2)\) 即 ShadowMap 每個紋素再求個平方后作為額外的ShadowMap,然后再生成 Mipmap 或 SAT。

計算累計分布函數(CDF)

有了上面的期望值與方差,我們就能確定一個正態分布。但是它對應的 CDF 函數是沒有解析解的,而有數值解(稱為 Error Function),但是計算比較繁瑣。

切克比夫不等式(Chebyshev’s Inequality)\(P(x>t) \leq \frac{\sigma^{2}}{\sigma^{2}+(t-\mu)^{2}}\)

實際上這個切克比夫不等式不僅可用在正態分布,其它的很多分布也是可以套用這個不等式的。

將這個不等式改造一下,就成了一個大膽的近似公式:

\(P(x>t) \approx \frac{\sigma^{2}}{\sigma^{2}+(t-\mu)^{2}}\)

注意:這里求的是 \(x>t\) 的部分,即 \(P(t)=1-CDF(t)\)

當然這個近似公式肯定不是精確的,但是計算開銷非常小,也就被用在 VSSM 算法中。

加速 Blocker Search 算法

PCSS 算法中的 Blocker Search 步驟:在一定范圍內多重采樣,每次采樣得到的深度若小於 \(d_{Receiver}\) 則認為遇到遮擋物並算入平均遮擋深度的貢獻,這樣多重采樣之后得到的平均遮擋深度 \(z_{occ}\) 就作為 \(d_{Blocker}\)

如下圖5X5的采樣結果若設 \(d_{Receiver}\) 為7,那么平均遮擋深度 \(z_{occ}\) 則為紅色部分的平均值。

設該采樣范圍的面積為 \(N\),無遮擋的面積占有 \(N_1\),有遮擋的面積則占有 \(N_2\) ,則有:

\(\frac{N_{1}}{N} z_{\text {unocc }}+\frac{N_{2}}{N} z_{o c c}=z_{A v g}\)

我們做出兩個假設:

  • \(\frac{N_1}{N} = P(x>d_{Receiver})\)\(\frac{N_2}{N} = 1-P(x>d_{Receiver})\) ;這個假設基於認為深度分布為正態分布,通過切克比夫不等式獲得近似解(即上面兩節的內容)。

  • \(z_{unocc} = d_{Receiver}\) ;這個假設基於認為絕大部分沒被遮擋的情況都屬於同一個深度(相當於在同一個垂直於光方向的平面),即可認為均為深度 \(d_{Receiver}\)

那么 VSSM 加速該算法的公式表示為:

\(d_{Blocker} = z_{occ} = \frac{N\cdot z_{Avg} - N_1 \cdot z_{unocc}}{N_2} = \frac{E(x)-P(x>d_{Receiver})\cdot d_{Receiver}}{1-P(x>d_{Receiver})}\)

加速 PCF 算法

PCF 算法中的多重采樣:每次采樣得到的遮擋物深度用來與 \(z'\) 做大小比較(小於 \(z'\) 則視為被遮擋,大於 \(z'\) 則視為全亮),最后取比較結果的平均作為 Visibility。

我們做出一個假設:

  • \(不被遮擋的概率 = P(x > z')\) ;這個假設基於認為深度分布為正態分布,通過切克比夫不等式獲得近似解。

那么 VSSM 加速該算法的公式表示為:

\(Visibility = P(x>z') \cdot 1 + (1-P(x>z')) \cdot 0 = P(x>z')\)

VSSM 的缺陷

VSSM 的主要缺陷表現:

  • 並不是任何深度的分布都是符合正態分布模型的,例如對於圖右的簡單幾何體反而用正態分布表示會很不適合。
  • 漏光(Light Leaking)現象,在一些應當被陰影完全遮蔽的內部有可能仍產生亮度。

  • 在加速 Blocker Search 算法中的假設 \(z_{unocc} = d_{Receiver}\) 基於認為絕大部分沒被遮擋的情況都屬於同一個深度,但實際上有些不被遮擋的地方深度並不等於 \(d_{Reveiver}\)

Moment Shadow Mapping


Moment Shadow Mapping 正是為了解決 VSSM 缺陷的一種算法,它主要想法是:使用高階的矩去描述一個分布的 CDF。這樣就能通過記錄 m 階的矩,就能復原成足夠接近實際 CDF 函數的效果,從而能適應不同的深度分布模型(有些地方可能接近正態分布,有些地方可能奇奇怪怪的分布)。

Moment Shadow Mapping將使用最簡單的形式來標識矩:\(z,z^2,z^3,z^4,...\)

實際上,VSSM 本質便是記錄 2 階的矩來復原 CDF 函數,而 Moment Shadow Mapping 一般使用4階的矩就已經足夠接近實際 CDF 了。

雖然 Moment Shadow Mapping 效果相當不錯,很好的解決了 VSSM 絕大部分缺陷,但是它仍需要相當的額外空間開銷和重建矩的額外性能開銷。

Distance Field Soft Shadows


Distance Field Soft Shadows 是與 Shadow Mapping 系列技術(PCF、PCSS、VSSM、Moment Shadow Mapping)截然不同的陰影技術路線,它主要想法是:

將點 \(o\)(Shading Point)與光源面中心點 \(p_{light}\) 相連形成一條方向為 \(l\) 的中心線段,而這條中心線上各個點 \(p_i\) 都可以通過 SDF 查得與其最近幾何物體的距離並且推算出安全角度(點\(o\) 能打到光源面的直線與中心線的最大夾角)為 \(\theta_i = arcsin \frac{\operatorname{SDF}(p_i)}{p_i-o}\)

SDF 相關可以看幾何(Geometry)部分,這里假定已經對場景生成了 SDF 信息。

那么所有這些點中對應的安全角度之中取最小的安全角度 \(\theta = min\{\theta_i\}\) ,這個安全角度與最大角度的比例決定了光源面的光照覆蓋率,也就決定了點 \(o\) 的Visibility。

使用 Distance Field Soft Shadows 的好處很多:

  • 計算陰影很快(假設已經生成了SDF的情況下,比傳統Shadow Mapping類技術是要快的多)
  • 陰影質量很高,而且完美解決 Shadow Ance / Peter Panning / 采樣噪聲等傳統Shadow Mapping會出現的問題

然而代價是:

  • SDF 需要預計算,這就意味着場景物體需要是靜態的,當然也可以使用一些算法使能和動態物體相結合,盡量減少重新生成SDF的成本。
  • SDF 需要較大的存儲空間(一般采用三維數組表示空間各個網格的SDF值,但是可以使用八叉樹等空間數據結構或者其它方法做進一步優化)。

計算安全角度

計算某個點 \(p_i\) 的安全角度時,直觀的幾何關系便是:

\(\theta_i = \arcsin \frac{\operatorname{SDF}(p_i)}{p_i-o}\)

而在實踐中,往往會使用:

\(\theta_i = \min \left\{\frac{k \cdot \operatorname{SDF}(p_i)}{p_i-o}, 1.0\right\}\)

這樣的近似公式實際效果相當接近原幾何關系,而且也能減少復雜的 arcsin 運算開銷,最后它還能通過 \(k\) 這個參數來調整陰影的硬軟程度。

如下圖分別為 \(k=32\)\(k=8\)\(k=2\) 的效果:

Distance Field Soft Shadows 算法過程

具體算法過程:

  1. \(o\) 點(shading point)設為第一個步進點,即 \(p_0 = o\)

  2. 每次算出下一個步進點 \(p_{i+1} = p_{i} + l \cdot SDF(p_{i})\) 並記錄安全角度 \(\theta_i = \min \left\{\frac{k \cdot \operatorname{SDF}(p_i)}{p_i-o}, 1.0\right\}\)

  3. 重復 "步驟2",直到滿足 \(l \cdot (p_{i+1}-p_{light}) < 0\) (即意味着已經步進到光源點背面了)

  4. 取所有次步進的最小安全角度 \(\theta = min\{\theta_i\}\) ,則可見度則為 \(Visibility = \frac{\theta}{c}\) (其中 \(c\) 為點 \(o\) 與光源面連接的最大角度)

參考



免責聲明!

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



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