GraphicsLab Project之再谈Shadow Map
作者:i_dovelemon
日期:2019-06-07
主题:Shadow Map(SM), Percentage Closer Filtering(PCF), Variance Shadow Map(VSM)
引言
对于3D场景来说,阴影的重要性不言而喻。随着时代的发展,各种各样的阴影绘制技术被提出(如Shadow Volume和Shadow Map)。在之前的博文中,我们讨论过PSSM。这是一种为了解决大场景阴影贴图透视走样方法而提出的算法。它主要是将场景切割,用多张Shadow Map来组织阴影。这个算法核心是多张Shadow Map的组织,而不是Shadow Map本身。
介绍
一般情况下,我们不做任何特殊处理,产生的Shadow Map,我们称之为Standard Shadow Map(SSM)(参考文献[1])。如果不做任何处理的使用SSM,这样势必会给场景中的阴影带来很多的锯齿,比较难看(如图1种的SSM)。究其原因,是我们在计算一个像素是否被阴影覆盖的时候,只有单纯的覆盖和不覆盖两种情况,而贴图本身是一种离散的数据情况,所以会给阴影产生锯齿。
所以,针对这种情况,人们想到了不使用覆盖和不覆盖这两种情况进行阴影的表达,而是用这个像素被覆盖的程度(percentage)(参考文献[2])。就像我们对于有锯齿的图形,会在边缘加上一点alpha渐变来解决一样,通过覆盖程度的不同,会给阴影产生一个柔和的边缘(如图1中的PCF)。
但PCF本身需要通过对Shadow Map进行多次采样求平均值来进行,想要比较好的效果,采样半径需要比较大。这样势必会造成性能损耗。所以在PCF的基础上,人们通过dithering的技术,减少采样次数来实现类似的效果。
SSM和PCF都需要Shadow Map中存放的是像素对应的深度值。而在进行阴影计算的时候,需要从中获取到对应的深度值。这就导致我们的Shadow Map无法利用硬件提供的mipmapping和linear filtering等手段进行filtering。
所以,人们想到通过其他的手段来让我们能够对Shadow Map进行filtering。也就是本文将要着重介绍的Variance Shadow Map(VSM)。
Variance Shadow Map
VSM的技术是由William Donnelly(参考文献[3])等人提出的一种方案。通过他们的方案我们就能够对Shadow Map进行mipmapping和filtering,甚至还可以进行blur。所以这样的方法就能够很好的产生柔和的阴影(如图1中的VSM)。原理部分请参考原始论文和NVIDIA的一篇简述(参考文献[4])。
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
[4] Nvidia-Variance Shadow Mapping
GraphicsLab Project之再谈Shadow Map的更多相关文章
- GraphicsLab Project之Diffuse Irradiance Environment Map
作者:i_dovelemon 日期:2020-01-04 主题:Rendering Equation,Irradiance Environment Map,Spherical Harmonic 引言 ...
- GraphicsLab Project学习项目
作者:i_dovelemon 日期:2016 / 05 / 30 主题:3D,Graphics 引言 进公司以来,主要在学习的就是如何保证代码的质量,以前热爱的图形学也放置了.但是,作为游戏程序员,特 ...
- 再谈js对象数据结构底层实现原理-object array map set
如果有java基础的同学,可以回顾下<再谈Java数据结构—分析底层实现与应用注意事项>:java把内存分两种:一种是栈内存,另一种是堆内存.基本类型(即int,short,long,by ...
- [ZZ] Shadow Map
Shadow Map 如何能够高效的产生更接近真实的阴影一直是视频游戏的一个很有挑战的工作,本文介绍目前所为人熟知的两种阴影技术之一的ShadowMap(阴影图)技术. ShadowMap技术 ...
- Shadow Map阴影贴图技术之探 【转】
这两天勉勉强强把一个shadowmap的demo做出来了.参考资料多,苦头可不少.Shadow Map技术是目前与Shadow Volume技术并行的传统阴影渲染技术,而且在游戏领域可谓占很大优势.本 ...
- (转)Shadow Map & Shadow Volume
转自:http://blog.csdn.net/hippig/article/details/7858574 shadow volume 这个术语几乎是随着 DOOM3 的发布而成为FPS 玩家和图形 ...
- C++ Primer 学习笔记_44_STL实践与分析(18)--再谈迭代器【下】
STL实践与分析 --再谈迭代器[下] 三.反向迭代器[续:习题] //P355 习题11.19 int main() { vector<int> iVec; for (vector< ...
- [工作积累] shadow map问题汇总
1.基本问题和相关 Common Techniques to Improve Shadow Depth Maps: https://msdn.microsoft.com/en-us/library/w ...
- Unity基础6 Shadow Map 阴影实现
这篇实现来的有点墨迹,前前后后折腾零碎的时间折腾了半个月才才实现一个基本的shadow map流程,只能说是对原理理解更深刻一些,但离实际应用估计还需要做很多优化.这篇文章大致分析下shadow ma ...
随机推荐
- React Native 隐藏组件思路
In your render function:{ this.state.showTheThing && <TextInput/>} Then just do: this. ...
- jquery特效(1)—点击展示与隐藏全文
下班了~~~我把今天整理的一个jquery小特效发一下,个人觉得比较简单,嗖嗖的就写出来了~~~ 下面先来看最终的动态效果: 一.来看一下主体框架程序: <!DOCTYPE html> & ...
- codeforces 664B B. Rebus(乱搞题)
题目链接: B. Rebus time limit per test 1 second memory limit per test 256 megabytes input standard input ...
- python 基础之第五天
###########window路径写法########## In [1]: winpath = 'C:\tmp' In [2]: print winpath C: mp In [3]: winpa ...
- mysql审计实现方法
Mysql版本: 5.6.24-72.2 一.通过init-connect + binlog 实现MySQL审计功能 基本原理: 由于审计的关键在于DML语句,而所有的DML语句都可以通过binlog ...
- Swift扩展
Swift中的「扩展」(extensions)和OC中的categories类似,只是Swift中的「扩展」没有名字.Swift中的「扩展」可以向一个已有的类/结构体/枚举类型添加新功能,这包括在没有 ...
- CQOI2017 部分题解
部分题解是指没写那道算几. BZOJ上目前没有day2的题面D2T2的图. BZOJ4813 小Q的棋盘 显然可以$O(n^2)$DP,然而可以$O(n)$贪心:只有一条从根出发的一条链上的边可以只经 ...
- 「UOJ#117」 欧拉回路
欧拉回路 - 题目 - Universal Online Judge 题意: 给定有向图或无向图,求一条欧拉回路. 题解 心路历程:woc什么傻哔东西->哇真香我的吗!(逃 首先我知道很多人把欧 ...
- bzoj1799同类分布——数位DP
题目:https://www.lydsy.com/JudgeOnline/problem.php?id=1799 数位DP. 1.循环方法 预处理出每个位数上,和为某个数,模某个数余某个数的所有情况: ...
- 是否要从单片机转为嵌入式Linux?
作者:嵌入式老鸟火哥 授权转载于公众号嵌入式老鸟的职场之道(ID: ict_embedded),有增加内容和修改. 最近很多童鞋投票并咨询如何从单片机转为嵌入式Linux开发.看来读者圈中做单片机,R ...