軟陰影技術 —— PCSS

Shadow Mapping原理:

一個着色點是否位於陰影中,只需觀察該點是否在能以光源爲視點的相機中看到。若能看到,則shading point不在陰影中,否則在陰影中 。

需要解決的問題:

如何判斷是否在能以光源爲視點的相機中看到?

解決方案:

以光源爲視點生成深度圖(只保留最近距離),其餘點只需與深度圖紋理對應的深度比較即可,大於紋理深度值在陰影中,否則不在。

算法步驟:需要兩個Pass

1.第一個pass:生成以光源爲視點的深度圖

2.第二個pass:以Camera爲視點渲染場景,每個點轉化到光源深度圖紋理空間中,然後與深度圖紋理對應的深度比較即可,大於紋理深度值在陰影中,否則不在。

注意:vulkan的NDC空間z值在【0,1】之間,與opengl中NDC空間Z值在【-1,1】不同。(巨坑)

	const mat4 biasMat = mat4(
	0.5, 0.0, 0.0, 0.0,
	0.0, 0.5, 0.0, 0.0,
	0.0, 0.0, 1.0, 0.0,
	0.5, 0.5, 0.0, 1.0 
    );
    vec4 shadowCoord   = biasMat * lightVP * fs_in.worldPos;
	vec3 shadowCoordUV = shadowCoord.xyz / shadowCoord.w;

 

場景圖:

經典的Shaow mapping算法 ——無Bias:

1.發生 Self-occlusion或Shadow acne

preview

分析:

由於深度圖中的深度是離散保存的,對於p1和p2的判斷就會出錯(p1在陰影中, p2不在陰影中),地面會出現明暗相隔的條紋,不是摩爾紋,這種現象叫Shadow acne.

解決方案:

目前工業界通過給ShadowMap中的深度值添加一個偏移來避免這個問題:

		if(closestdist <  shadowCoord.z - bias)
		{
			 shadow = 1.0;
		 }

注意:還要注意從光源視角來看,多邊形的斜率越大需要的Bias也就越大:

2.Peter panning現象

Bias要根據場景設置合適的值,如果過大就會導致peter-panning現象,陰影懸浮的問題。

解決方案:

使用Second-depth shadow map可以徹底解決Self-occlusion問題。這種算法不止記錄最小的深度,還會記錄第二小的深度,使用它們的中點來做比較。不過由於必須使用封閉的模型以及效率問題(雖然算法的時間複雜度不高,但是實時渲染不相信複雜度:)),這種算法還沒有被應用。

軟陰影

1.PCF(percentage  clost filter)-濾波/抗鋸齒

思想:通過對shading point 周圍N*N的區域進行採樣,然後判斷是否在陰影中,最後求均值,起到filter的作用,獲得軟陰影的效果,同時也可以抗鋸齒。

 

2.PCSS

2.1 基本思想

 PCF進行卷積處理,卷積核的大小該取多少,一般是使用固定的大小,實際上,應該根據局部特徵選擇不同大小的卷積核,達到自適應的效果。PCSS的基本思想就是動態計算卷積核的大小,解決了以前算法軟陰影的一致性問題。原本的濾波算法不管遮擋物和陰影接收物體相距多遠,都生成一致的“軟”的陰影。PCSS 通過一個和光源位置相關的相似三角形來控制軟陰影的採樣搜索範圍。下面這個圖展示了PCSS如何根據光源,遮擋物,和陰影投射目標三者確定搜索範圍

PCSS首先假定光源是一個區域光(area light),是我們最常見的光源。傳統的點光源,聚光燈和平行光,其實都不過是某種模擬,比如點光源模擬小燈泡,聚光燈模擬手電筒,平行光模擬太陽,其實嚴格來說這些都是區域光,軟陰影形成的很大一個原因就是區域光的存在。
爲了在舊的光源體系中加入區域的概念,我們假定光源是一個始終平行於接收面的圓形,這樣,就可以估算根據光源,遮擋物,被遮擋物三者的距離(都是lightShadoMAP紋理空間的距離值)估算陰影的軟硬程度.

計算公式如下:
[公式]

PCSS的主要貢獻在於形成了所謂“動態”的陰影,PCSS確定的這個搜索範圍,也可以看做是某種模糊半徑,或者卷積核的大小,並沒有要求一定按照多重採樣(PCF)的方式來實現,因此可以很好地和VSM,ESM等技術結合。

2.2 算法步驟

  1. 在Shadow map上計算Blocker的深度:常採用在Shadow map上一定區域多次採樣,將對shading Point造成遮擋的採樣點深度累加求平均,作爲Blocker的平均深度值。
  2. 根據上述公式,相似三角形求半影的大小,決定軟硬程度,
  3. 在shadow map區域內進行PCF

2.3 需要解決的問題:

2.3.1 如何確定在Shadow map上採樣區域?


vec2 searchRegionRadius(float vLightViewDepth)
{
    return vec2(parameter.lightRadiusUV) * (vLightViewDepth - parameter.light_zNear) / vLightViewDepth;
}

2.3.2 對採樣率過低的解決方法

使用泊松圓盤採樣, 下圖最左側的部分,其使用了4×4網格模式的PCF採樣。最右側的部分則表示在某一個圓形範圍內的12次泊松分佈。而中間的陰影便使用了該模式進行採樣,但仍然可以發現一定的鋸齒。在右側的陰影中,每個像素在進行採樣時又對泊松分佈進行了隨機的旋轉,因此該陰影效果是最爲平滑的。

泊松圓盤採樣源碼:

float rand(vec2 uv) 
{ 
	// 0 - 1
	const float a = 12.9898, b = 78.233, c = 43758.5453;
	float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
	return fract(sin(sn) * c);
}

void poissonDiskSamples(const in vec2 randomSeed) 
{
  int samples = parameter.numBlockerSample > parameter.numPCFSample ? parameter.numBlockerSample : parameter.numPCFSample;

  float angle_step = PI2 * float(NUM_RINGS) / float(samples);
  float inv_num_samples = 1.0 / float(samples);

  float angle = rand(randomSeed) * PI2;
  float radius = inv_num_samples;
  float radiusStep = radius;

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

最後PCSS的源碼:

#define AMBIENT 0.2
#define MAX_SAMPLE 128
#define NUM_RINGS 10
#define PI 3.141592653589793
#define PI2 6.283185307179586

layout (set = 0, binding = 5) uniform Parameter
{
	float bias;
	float light_zNear;
	float light_zFar;
    float lightRadiusUV;
	int shadowType;
	int numBlockerSample;
	int numPCFSample;
    int alignData;

	vec4  lightColor;
	vec4  lightPos;
	vec4  cameraPos;
} parameter;

//lightUV
float calculateShadow(sampler2D vShadowMap, vec3 vShadowCoord, vec2 vOffSet)
{
	float visibility = 0.0, closestdist = 0.0f; 
	if ((vShadowCoord.z >= 0.0) && (vShadowCoord.z <= 1.0))
	{
		closestdist = texture(vShadowMap, vShadowCoord.st + vOffSet).r;
		visibility  = closestdist < (vShadowCoord.z - parameter.bias) ? 0.0f : 1.0f ;
	}
	return  visibility;
}

//lightUV
float filterPCF(sampler2D vShadowMap, vec3 vShadowCoordUV)
{
	ivec2 texDim = textureSize(vShadowMap, 0).xy;
	float dx = 1.0 / float(texDim.x);
	float dy = 1.0 / float(texDim.y);

	float visibilityFactor = 0.0;
	int count = 0;
	int range = 1;
	
	for (int x = -range; x <= range; x++)
	{
		for (int y = -range; y <= range; y++)
		{
			visibilityFactor += calculateShadow(vShadowMap, vShadowCoordUV, vec2(dx*x, dy*y));
			count++;
		}
	}
	return visibilityFactor / count;
}

// Precomputer Poisson disk for sampling
vec2 poissonDisk[MAX_SAMPLE];

float rand(vec2 uv) 
{ 
	// 0 - 1
	const float a = 12.9898, b = 78.233, c = 43758.5453;
	float dt = dot( uv.xy, vec2( a,b ) ), sn = mod( dt, PI );
	return fract(sin(sn) * c);
}

void poissonDiskSamples(const in vec2 randomSeed) 
{
  int samples = parameter.numBlockerSample > parameter.numPCFSample ? parameter.numBlockerSample : parameter.numPCFSample;

  float angle_step = PI2 * float(NUM_RINGS) / float(samples);
  float inv_num_samples = 1.0 / float(samples);

  float angle = rand(randomSeed) * PI2;
  float radius = inv_num_samples;
  float radiusStep = radius;

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

vec2 searchRegionRadius(float vLightViewDepth)
{
    return vec2(parameter.lightRadiusUV) * (vLightViewDepth - parameter.light_zNear) / vLightViewDepth;
}

vec2 penumbraSize(float vTexCoordDepthUV, float vBlockerDepthUV)
{
	return vec2(parameter.lightRadiusUV) * (max(vTexCoordDepthUV - vBlockerDepthUV, 0.0) / vBlockerDepthUV);
}


float searchBlocker(sampler2D vShadowMap, vec3 vShadowCoordUV,float vLightViewDepth) 
{
  ivec2 textureSize2d = textureSize(vShadowMap, 0).xy;
  float texelSizeX = 1.0 / float(textureSize2d.x);
  float texelSizeY = 1.0 / float(textureSize2d.y);
  float blockerDepth = 0.0;
  int   numBlocker = 0;

  vec2 searchRegionRadiusUV = searchRegionRadius(vLightViewDepth) * vec2(texelSizeX, texelSizeY);

  for(int i = 0; i < parameter.numBlockerSample;++i)
  {
      vec2  sampleCoord = vShadowCoordUV.xy + poissonDisk[i] * searchRegionRadiusUV; 
      float cloestDepth = texture(vShadowMap, sampleCoord).r;
     
	 if(cloestDepth < (vShadowCoordUV.z - parameter.bias))
      {
        blockerDepth += cloestDepth;
        numBlocker += 1;
      }
  }
  if(numBlocker > 0 && numBlocker < parameter.numBlockerSample)
  {
	return blockerDepth / float(numBlocker);
   }
  return  numBlocker == parameter.numBlockerSample ? 0.0f : 1.0f;
}

float pcf(sampler2D vShadowMap,vec3 vShadowCoordUV, vec2 vFilterRadiusUV) 
{
  ivec2 textureSize2d = textureSize(vShadowMap, 0).xy;
  float texelSizeX    = 1.0 / float(textureSize2d.x);
  float texelSizeY    = 1.0 / float(textureSize2d.y);

  float visibility = 0.0;
  for(int i = 0; i < parameter.numPCFSample;++i)
  {
  	vec2  offset      = poissonDisk[i] * vFilterRadiusUV * vec2(texelSizeX, texelSizeY);
    vec2  sampleCoord = vShadowCoordUV.xy + offset;
    float cloestDepth = texture(vShadowMap, sampleCoord).r;

	visibility += (vShadowCoordUV.z - parameter.bias) > cloestDepth ? 0.0 : 1.0;
   }

  return visibility / float(parameter.numPCFSample);
}

float pcss(sampler2D vShadowMap, vec3 vShadowCoordUV, float vLightViewDepth)
{
  //step 1: avg blocker depth
  float avgBlockerDepth = searchBlocker(vShadowMap, vShadowCoordUV, vLightViewDepth);
  if(avgBlockerDepth == 0.0f) return 0.0f; //all blocker is in shadow
  if(avgBlockerDepth == 1.0f) return 1.0f; //all blocker is not in shadow

  //step 2: penumbra size
  vec2 penumbraSizeUV = penumbraSize(vShadowCoordUV.z, avgBlockerDepth); 

  //step 3: pcf
  float shadow =  pcf(vShadowMap, vShadowCoordUV, penumbraSizeUV);
  return shadow;
}

參考文獻:

1,優化點

https://gunay-pi.com/chapter-7-shadows-%E9%98%B4%E5%BD%B1/6/

2.perter acne

https://zhuanlan.zhihu.com/p/363733410

3.多個軟陰影技術

https://zhuanlan.zhihu.com/p/26853641

4.PCSS

https://zhuanlan.zhihu.com/p/365814785

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章