GraphicsLab Project之再談Shadow Map

做者:i_dovelemonhtml

日期:2019-06-07git

主題:Shadow Map(SM), Percentage Closer Filtering(PCF), Variance Shadow Map(VSM)github

 

引言

  對於3D場景來講,陰影的重要性不言而喻。隨着時代的發展,各類各樣的陰影繪製技術被提出(如Shadow Volume和Shadow Map)。在以前的博文中,咱們討論過PSSM。這是一種爲了解決大場景陰影貼圖透視走樣方法而提出的算法。它主要是將場景切割,用多張Shadow Map來組織陰影。這個算法核心是多張Shadow Map的組織,而不是Shadow Map自己。算法

 

介紹

  通常狀況下,咱們不作任何特殊處理,產生的Shadow Map,咱們稱之爲Standard Shadow Map(SSM)(參考文獻[1])。若是不作任何處理的使用SSM,這樣勢必會給場景中的陰影帶來不少的鋸齒,比較難看(如圖1種的SSM)。究其緣由,是咱們在計算一個像素是否被陰影覆蓋的時候,只有單純的覆蓋和不覆蓋兩種狀況,而貼圖自己是一種離散的數據狀況,因此會給陰影產生鋸齒。app

  因此,針對這種狀況,人們想到了不使用覆蓋和不覆蓋這兩種狀況進行陰影的表達,而是用這個像素被覆蓋的程度(percentage)(參考文獻[2])。就像咱們對於有鋸齒的圖形,會在邊緣加上一點alpha漸變來解決同樣,經過覆蓋程度的不一樣,會給陰影產生一個柔和的邊緣(如圖1中的PCF)。性能

  但PCF自己須要經過對Shadow Map進行屢次採樣求平均值來進行,想要比較好的效果,採樣半徑須要比較大。這樣勢必會形成性能損耗。因此在PCF的基礎上,人們經過dithering的技術,減小採樣次數來實現相似的效果。.net

  SSM和PCF都須要Shadow Map中存放的是像素對應的深度值。而在進行陰影計算的時候,須要從中獲取到對應的深度值。這就致使咱們的Shadow Map沒法利用硬件提供的mipmapping和linear filtering等手段進行filtering。3d

  因此,人們想到經過其餘的手段來讓咱們可以對Shadow Map進行filtering。也就是本文將要着重介紹的Variance Shadow Map(VSM)。orm

 

Variance Shadow Map

  VSM的技術是由William Donnelly(參考文獻[3])等人提出的一種方案。經過他們的方案咱們就可以對Shadow Map進行mipmapping和filtering,甚至還能夠進行blur。因此這樣的方法就可以很好的產生柔和的陰影(如圖1中的VSM)。原理部分請參考原始論文和NVIDIA的一篇簡述(參考文獻[4])。htm

  VSM的大體步驟以下所示:

  1.建立一個雙通道及以上的的RenderTarget(個人Demo爲了簡單,直接使用的是RGBA四通道的貼圖)。VSM對精度要求比較高,因此RenderTarget須要fp16或者fp32的精度(我用的是fp32)。接下來就和SSM同樣,從燈光的視角來渲染場景,而後保存兩個不一樣的值:depth,depth*depth。注意depth須要歸一化到[0,1]的範圍來保持精度。

  2.對產生的Shadow Map進行Blur操做,完畢以後再產生Mipmap鏈。

  3.在進行陰影計算的時候,根據Shadow Map中存放的depth和depth*depth,使用硬件提供的mipmapping和filtering,自動的計算出一階動差(平均值)M1和二階動差M2。

  4.根據當前像素在光源空間中的深度與M1進行比較,若是當前光源深度小於M1,就表示當前像素不在陰影中。

  5.反之,就在陰影裏面。那麼根據以下幾個公式,求出當前像素的覆蓋率(percentage):

  $pmax = \frac{\sigma^2}{\sigma^2 + (t - M_1)^2}(t爲當前像素深度)$

  $\sigma^2 = M_2 - M_1^2$

  6.而後使用pmax來繪製陰影。

 

代碼

  本文的Demo項目可在這裏找到,這裏不在給出詳細的代碼。

  如下是模型繪製陰影時的Shader(sceneGrassSD.vs和glb_sceneGrassVSMSD.fs):

#version 330

in vec3 glb_attr_Pos;
in vec3 glb_attr_Normal;
in vec2 glb_attr_TexCoord;

uniform mat4 glb_unif_ShadowM;
uniform mat4 glb_unif_WorldM;
uniform mat4 glb_unif_Trans_Inv_WorldM;

out vec3 vs_Vertex;
out vec3 vs_Normal;
out vec2 vs_TexCoord;

uniform float glb_unif_Timer;
uniform float glb_unif_WindPower;
uniform float glb_unif_WindSpeed;
uniform vec3 glb_unif_WindDir;
uniform float glb_unif_HeightPower;

vec3 calc_wind_animation(vec2 uv, vec3 pos) {
    float height = pow(uv.y, glb_unif_HeightPower);
    float offset = height * glb_unif_WindPower * sin(uv.y * glb_unif_WindSpeed + glb_unif_WindSpeed * glb_unif_Timer);
    return pos + glb_unif_WindDir * offset;
}

void main() {
	mat4 shadowM = glb_unif_ShadowM;

    vec3 pos = calc_wind_animation(glb_attr_TexCoord, glb_attr_Pos);
	gl_Position = shadowM * glb_unif_WorldM * vec4(pos, 1.0);
	vs_Vertex = vec3(gl_Position.xyz) / gl_Position.w;
	//vs_Normal = (glb_unif_Trans_Inv_WorldM * vec4(glb_attr_Normal, 0.0)).xyz;
	vs_Normal = vec3(0.0, 1.0, 0.0);
	vs_TexCoord = glb_attr_TexCoord;
}
#version 330

// Input attributes
in vec3 vs_Vertex;
in vec3 vs_Normal;
in vec2 vs_TexCoord;

out vec3 oColor;

// Uniform
uniform vec3 glb_unif_ParallelLight_Dir;

// Constant value
uniform float glb_unif_MinOffset;
uniform float glb_unif_MaxOffset;

uniform sampler2D glb_unif_MaskMap;

void main() {
	vec4 mask = texture(glb_unif_MaskMap, vs_TexCoord, 0);
	if (mask.w < 0.5 || vs_TexCoord.y > 0.9) discard;

	float depth = vs_Vertex.z;
	depth = depth + 1.0;
	depth = depth / 2.0;

	oColor = vec3(depth, depth * depth, 0.0);
}

  

  如下是進行陰影計算時的Shader(floorL.vs和glb_floorVSML.fs):

#version 330

// Input attributes
layout (location = 0) in vec3 glb_attr_Pos;
layout (location = 2) in vec3 glb_attr_Normal;
layout (location = 3) in vec3 glb_attr_Tangent;
layout (location = 4) in vec3 glb_attr_Binormal;
layout (location = 5) in vec2 glb_attr_TexCoord;
layout (location = 6) in vec2 glb_attr_LightMapTexCoord;

// Output attributes
out vec4 vs_Vertex;
out vec3 vs_Normal;
out vec3 vs_Tangent;
out vec3 vs_Binormal;
out vec2 vs_TexCoord;
out vec2 vs_SecondTexCoord;

uniform mat4 glb_unif_ProjM;
uniform mat4 glb_unif_ViewM;

uniform mat4 glb_unif_WorldM;
uniform mat4 glb_unif_Trans_Inv_WorldM;

void main() {
	gl_Position = glb_unif_ProjM * glb_unif_ViewM * glb_unif_WorldM * vec4(glb_attr_Pos, 1.0);
	vs_Vertex = (glb_unif_WorldM * vec4(glb_attr_Pos, 1.0));

    vs_Normal = (glb_unif_Trans_Inv_WorldM * vec4(glb_attr_Normal, 0.0)).xyz;
    vs_Tangent = (glb_unif_Trans_Inv_WorldM * vec4(glb_attr_Tangent, 0.0)).xyz;
    vs_Binormal = (glb_unif_Trans_Inv_WorldM * vec4(glb_attr_Binormal, 0.0)).xyz;

	vs_TexCoord = glb_attr_TexCoord;
    vs_SecondTexCoord = glb_attr_LightMapTexCoord;
}
#version 450

// Input attributes
in vec4 vs_Vertex;
in vec3 vs_Normal;
in vec3 vs_Tangent;
in vec3 vs_Binormal;
in vec2 vs_TexCoord;
in vec2 vs_SecondTexCoord;

// Output color
out vec4 oColor;

// Uniform
uniform vec3 glb_unif_Albedo;
uniform float glb_unif_Roughness;
uniform float glb_unif_Metallic;
uniform samplerCube glb_unif_DiffusePFC;
uniform samplerCube glb_unif_SpecularPFC;
uniform sampler2D glb_unif_AOMap;
uniform mat4 glb_unif_ShadowM;
uniform sampler2D glb_unif_ShadowMap;

vec3 calc_view() {
	vec3 view = vec3(0.0, 0.0, 0.0);
	view = normalize(glb_unif_EyePos - vs_Vertex.xyz);
	return view;
}

vec3 calc_light_dir() {
	vec3 light_dir = vec3(0.0, 0.0, 0.0);
	light_dir = -glb_unif_ParallelLight_Dir;
	return light_dir;
}

vec3 calc_direct_light_color() {
	vec3 light = vec3(0.0, 0.0, 0.0);
	light = light + glb_unif_ParallelLight;

	return light;
}

float calculateVSMShadowFactor(vec3 pos, vec3 eyePos, vec3 lookAt, mat4 shadowM, sampler2D shadowMap) {
	float shadowFactor = 1.0;

	vec4 lightSpacePos = shadowM * vec4(pos, 1.0);
	lightSpacePos.xyz /= lightSpacePos.w;
	lightSpacePos.xyz /= 2.0;
	lightSpacePos.xyz += 0.5;

	if (lightSpacePos.x < 0.0 || 
		lightSpacePos.x > 1.0 ||
		lightSpacePos.y < 0.0 ||
		lightSpacePos.y > 1.0) {
		// Out of shadow
		shadowFactor = 1.0;
	} else {
		vec2 shadowMoments = texture(shadowMap, lightSpacePos.xy).xy;
		if (lightSpacePos.z < shadowMoments.x) {
			// Out of shadow
			shadowFactor = 1.0;
		} else {
			float variance = shadowMoments.y - shadowMoments.x * shadowMoments.x;
			float pmax = variance / (variance + pow(lightSpacePos.z - shadowMoments.x, 2.0));
			shadowFactor = pmax;
		}
	}

	return shadowFactor;
}

void main() {
	oColor = vec4(0.0, 0.0, 0.0, 0.0);

	vec3 normalInWorld = vec3(0.0, 0.0, 0.0);
	vec3 normalInTangent = vec3(0.0, 0.0, 0.0);
    normalInWorld = normalize(vs_Normal);

	vec3 view = calc_view();

	vec3 light = calc_light_dir();

	vec3 h = normalize(view + light);

	vec3 albedo = glb_unif_Albedo;
	float roughness = glb_unif_Roughness;
	float metallic = glb_unif_Metallic;
	vec3 emission = vec3(0.0, 0.0, 0.0);
	float ao = 1.0;

	vec3 direct_light_color = calc_direct_light_color();

	vec3 direct_color = glbCalculateDirectLightColor(normalInWorld, view, light, h, albedo, roughness, metallic, direct_light_color);

	float shadow_factor = calculateVSMShadowFactor(vs_Vertex.xyz, glb_unif_EyePos, glb_unif_LookAt, glb_unif_ShadowM, glb_unif_ShadowMap);

	oColor.xyz = (direct_color * ao) * shadow_factor + glb_unif_GlobalLight_Ambient * albedo * ao;

	float alpha = 1.0;
	oColor.w = alpha;
}

 

總結

  上面給出瞭如何實現一個VSM,相對於SSM和PCF,它的效率要差點,可是效果會好不少。除了這個好處以外,咱們知道SSM有Shadow Bias的問題,使用VSM能夠徹底避免掉這個問題。固然VSM也有它本身的缺點,好比精度要求高,容易出現light bleeding等等。除了VSM以外,還有其餘的Shadow Map技術,也可以支持對Shadow Map進行Filtering和Blur(如ESM)。這裏有一篇文章(參考文獻[5])對比了各個算法的優缺點,你們能夠參考下。

 

參考文獻

[1] Tutorial 16: Shadow mapping

[2] GPU Gems 1 Chapter 11: Shadow map antialiasing

[3] Variance Shadow map

[4] Nvidia-Variance Shadow Mapping

[5] 切換到ESM-KlayGE遊戲引擎

相關文章
相關標籤/搜索