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

[3] Variance Shadow map

[4] Nvidia-Variance Shadow Mapping

[5] 切换到ESM-KlayGE游戏引擎

原文地址:https://www.cnblogs.com/idovelemon/p/10988985.html

时间: 2024-10-10 08:18:38

GraphicsLab Project之再谈Shadow Map的相关文章

Shadow Map阴影贴图技术之探 【转】

这两天勉勉强强把一个shadowmap的demo做出来了.参考资料多,苦头可不少.Shadow Map技术是目前与Shadow Volume技术并行的传统阴影渲染技术,而且在游戏领域可谓占很大优势.本篇是第一辑.——ZwqXin.comShadow Map的原理很简单,但是实现起来到处是雷.当然这只是我的体会.恩,不过就是“从光源处看场景,那些看不见的区域全部都该是阴影”.很容易看出,与针对 特定模型的Shadow Volume不同,Shadow Map是针对场景的.这就是说,对一个光源应用一次

阴影映射(Shadow Map)的研究(三)

阴影映射(Shadow Map)的研究(三) 最近为了自己制作的项目可是吃了不少苦头,这其中关键的一点就是想要实现阴影映射(Shadow Map).为了实现目标,我参考了网络上很多相关的资料,也看了一些案例,最终花了我一个月的时间将这个效果实现了. 阴影映射这样的效果,其实在即将发布的Qt 3D中已经有相关的介绍,KDAB中有一篇文章<Shadow Mappingin Qt3D 2.0>就在Qt 3D的框架上实现了阴影映射.不过当时这个效果是假定目标机器支持OpenGL 3.0规范的,目前大部

[ZZ] Shadow Map

Shadow Map 如何能够高效的产生更接近真实的阴影一直是视频游戏的一个很有挑战的工作,本文介绍目前所为人熟知的两种阴影技术之一的ShadowMap(阴影图)技术.     ShadowMap技术的概念应该说是最早应用在视频游戏中的阴影实现技术,有着非常高效和快速的特点,在实现阴影的同时只需要相对很小的计算负担.     ShadowMap绘制阴影主要是通过一张额外的阴影贴图来实现的,在早期的3D游戏中人物等动态运动的物体通常不绘制阴影,而场景内遮蔽关系相对确定的静态物体的阴影通常是在建立模

Shadow Map阴影贴图技术之探

这两天勉勉强强把一个shadowmap的demo做出来了.参考资料多,苦头可不少.Shadow Map技术是目前与Shadow Volume技术并行的传统阴影渲染技术,而且在游戏领域可谓占很大优势.本篇是第一辑.--ZwqXin.comShadow Map的原理很简单,但是实现起来到处是雷.当然这只是我的体会.恩,不过就是"从光源处看场景,那些看不见的区域全部都该是阴影".很容易看出,与针对特定模型的Shadow Volume不同,Shadow Map是针对场景的.这就是说,对一个光源

C++ Primer 学习笔记_74_面向对象编程 --再谈文本查询示例[续/习题]

面向对象编程 --再谈文本查询示例[续/习题] //P522 习题15.41 //1 in TextQuery.h #ifndef TEXTQUERY_H_INCLUDED #define TEXTQUERY_H_INCLUDED #include <iostream> #include <fstream> #include <sstream> #include <vector> #include <set> #include <map&g

[工作积累] shadow map问题汇总

1.基本问题和相关 Common Techniques to Improve Shadow Depth Maps: https://msdn.microsoft.com/en-us/library/windows/desktop/ee416324(v=vs.85).aspx Cascaded Shadow Maps https://msdn.microsoft.com/en-us/library/windows/desktop/ee416307(v=vs.85).aspx Soft shadow

【Go语言】【13】再谈GO语言的结构体

本文从如下四个方面再领着大家认识结构体 匿名结构体和匿名成员的结构体 值传递和引用传递 再谈嵌套结构体 面向对象 1.匿名结构体和匿名成员的结构体 如上篇所述,一个结构体需要先声明,再初始化,最后把初始化后的结构体赋值给其它变量,例如: /*声明结构体*/ type employee struct{ name,address string height,weight float64 } /*初始化结构体,并赋给变量emp*/ emp := employee{name:"eagle",

OpenCV2马拉松第9圈——再谈对比度(对比度拉伸,直方图均衡化)

收入囊中 lookup table 对比度拉伸 直方图均衡化 葵花宝典 lookup table是什么东西呢? 举个例子,假设你想把图像颠倒一下,f[i] = 255-f[i],你会怎么做? for( int i = 0; i < I.rows; ++i) for( int j = 0; j < I.cols; ++j ) I.at<uchar>(i,j) = 255 - I.at<uchar>(i,j); 大部分人应该都会这么做.或者: for( i = 0; i &

再谈word2003编程

再谈word2003编程 近期因为项目需要,写了许多word2003编程的东东.有时候遇到难题想查sdk说明,很难找到中文解释,对于e文不好的我来说,简直是天书.想必很多人多有感慨.     下面列出内容是一些常用的内容说明,希望对大家有帮助.     那就开始吧注意,下文的WAPP是我定义的word文档工程变量 的 //合并单元格    table.Cell(2, 2).Merge(table.Cell(2, 3));  //单元格分离     object Rownum = 2;     o