做者: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
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