[ZZ] Shadow Map
如何能够高效的产生更接近真实的阴影一直是视频游戏的一个很有挑战的工作,本文介绍目前所为人熟知的两种阴影技术之一的ShadowMap(阴影图)技术。
ShadowMap技术的概念应该说是最早应用在视频游戏中的阴影实现技术,有着非常高效和快速的特点,在实现阴影的同时只需要相对很小的计算负担。
ShadowMap绘制阴影主要是通过一张额外的阴影贴图来实现的,在早期的3D游戏中人物等动态运动的物体通常不绘制阴影,而场景内遮蔽关系相对确定的静态物体的阴影通常是在建立模型之初便已绘制到场景的贴图之中,这是利用ShadowMap来实现阴影概念的最初形成,而现在我们说到的 ShadowMap只是在游戏绘制时将阴影动态的绘制到一张阴影贴图上,再利用计算好的阴影贴图来绘制场景而已,整个计算只需要将场景绘制两边,而不需要像ShadowVolume一样额外生成新的模型,所以Shadow可以保持很好的性能表现而与场景的复杂度并无太大关系。
ShadowMap的概念很好理解,整个绘制过程分为两个阶段,首先以灯光为视角对场景进行绘制,绘制的结果是将场景内物体相对光源的深度信息写入一张阴影图中(Shadow Map),而不是RGB颜色。第二遍绘制场景时逐像素对比相对光源的深度值与阴影图中的深度,当深度大于阴影图中的深度时,说明该像素位于阴影中,进行相应的阴影混合。因为ShadowMap这种技术的特点,所以非常适合实现锥光源(spot light)下的阴影。对于在点光源(point light)下的利用ShadowMap生成阴影,有一种方法是利用Cubemap,这样六张阴影图可以实现全景点光源的ShadowMap。
生成Shadow Map
以DirectX中Sample为例,用于生成Shadow Map的Texture是格式为D3DFMT_R32F的RenderTarget,32位的浮点数可以保证深度信息的精度。
第一遍的绘制中,设置视角变换和投影矩阵为光源的视角变换和投影矩阵(假设一个相机从光源向外),在Vertex Shader中照常进行顶点空间坐标(这样深度测试会自动得到每个像素最接近光源的点),额外的贴图坐标输出为坐标的z和w坐标。
Depth.xy = oPos.zw;
在Pixel Shader中最终输出的深度:
Color = Depth.x / Depth.y; // Depth is z / w
这个值就是反映场景中在光源照射下的深度信息,值域位于0,1区间,位于近平面时为0,原平面时为1。
渲染场景
第二遍渲染场景,在Vertex Shader中除了完成坐标转换和贴图坐标转换外,需要额外传递几个参数,观察视角下的空间坐标、法线向量以及转换到光源投影空间下的坐标。前两个用于光照计算,后一个用于阴影深度判断。
最后的阴影混合在Pixel Shader中完成,在这里依次判断每个像素是否位于光照影响之下,只需用到该顶点的光向量与光源朝向向量点积的结果与光源照射范围二分之一弧度值的 cos值相比较(通过夹角大小来判断)
如果位于光源照射下,则计算该像素在深度图中的uv坐标,采样,然后比较深度值,判断是否位于阴影之内:
//换算UV坐标
float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 );
ShadowTexC.y = 1.0f – ShadowTexC.y;
//采样并判断深度
LightAmount = (tex2D( g_samShadow, ShadowTexC ) < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
最后根据阴影信息混合颜色即可。
Shadow Map的最大优点是高效率和快速,同样也会存在很多局限性,比如不适合点光源,并且在生成的阴影边缘锯齿化很严重。当然,我们也可以通过多次采样混合阴影边缘或者多次渲染进行高斯模糊来提高效果。
基于Shadow Map的阴影实现
0、简介
Shadow Mapping是一种基于图像空间的阴影实现方法,其优点是实现简单,适应于大型动态场景;缺点是由于shadow map的分辨率有限,使得阴影边缘容易出现锯齿(Aliasing);关于SM的研究很活跃,主要围绕着阴影抗锯齿,出现了很多SM变种,如PSM,LPSM,VSM等等,在这里http://en.wikipedia.org/wiki/Shadow_mapping可以找到很多SM变种的链接;;SM的实现分为两个pass,第一个pass以投射阴影的灯光为视点渲染得到一幅深度图纹理,该纹理就叫Shadow Map;第二个pass从摄像机渲染场景,但必须在ps中计算像素在灯光坐标系中的深度值,并与Shadow Map中的相应深度值进行比较以确定该像素是否处于阴影区;经过这两个pass最终就可以为场景打上阴影。这篇文章主要总结一下自己在实现基本SM的过程中遇到的一些问题以及解决方法,下面进入正题。
1、生成Shadow Map
为了从灯光角度渲染生成Shadow Map,有两个问题需要解决:一是要渲染哪些物体,二是摄像机的参数怎么设置。对于问题一,显然我们没必要渲染场景中的所有物体,但是只渲染当前摄像机视景体中的物体又不够,因为视景体之外的有些物体也可能投射阴影到视景体之内的物体,所以渲染Shadow Map时,这些物体必须考虑进来,否则可能会出现阴影随着摄像机的移动时有时无的现象,综上,我们只需要渲染位于当前摄像机视景体内的所有物体以及视景体之外但是会投射阴影到视景体之内的物体上的物体,把它们的集合称为阴影投射集,为了确定阴影投射集,可以根据灯光位置以及当前的视景体计算出一个凸壳,位于该凸壳中的物体才需要渲染,如图1所示。对于问题二,灯光处摄像机的视景体应该包含阴影投射集中的所有物体,另外应该让物体尽量占据设置的视口,以提高Shadow Map的精度;对于方向光和聚光灯,摄像机的look向量可以设置为光的发射发向,摄像机的位置设置为灯光所在的位置,为了包含阴影集中的所有物体,可以计算阴影投射集在灯光视图空间中的轴向包围盒,然后根据面向光源的那个面设置正交投影参数,就可以保证投射集中的所有物体都位于灯光视景体中,并且刚好占据整个视口,如图2所示。更详细的信息可以参考《Mathematics for 3D Game Programming and Computer Graphics, Third Edition》一书的10.2节。
图1:灯光位置与视景体构成一个凸壳,与该凸壳相交的物体称为阴影投射集
图2:根据阴影投射集在灯光视图空间中的包围盒来计算正交投影参数
2、生成阴影场景
生成了ShadowMap之后,就可以根据相应灯光的视图矩阵和投影矩阵从摄像机角度渲染带有阴影的场景了。下面把相关的shader代码贴上:
vertex shader:
// for projective texturing
uniform mat4 worldMatrix;
uniform mat4 lightViewMatrix;
uniform mat4 lightProjMatrix; varying vec4 projTexCoord; void main()
{
// for projective texture mapping
projTexCoord = lightProjMatrix*lightViewMatrix*worldMatrix*pos; // map project tex coord to [0,1]
projTexCoord.x = (projTexCoord.x + projTexCoord.w)*0.5;
projTexCoord.y = (projTexCoord.y + projTexCoord.w)*0.5;
projTexCoord.z = (projTexCoord.z + projTexCoord.w)*0.5; gl_Position = gl_ModelViewProjectionMatrix * pos;
}
pixel shader(版本一):
// The shadow map
uniform sampler2DShadow shadowDepthMap; varying vec4 projTexCoord; void main()
{
// light computing
vec4 lightColor = Lighting(...); vec4 texcoord = projTexCoord;
texcoord.x /= texcoord.w;
texcoord.y /= texcoord.w;
texcoord.z /= texcoord.w; // depth comparison
vec4 color = vec4(1.0,1.0,1.0,1.0);
float depth = texture(shadowDepthMap,vec3(texcoord.xy,0.0));
if(texcoord.z > depth)
{
// this pixel is in shadow area
color = vec4(0.6,0.6,0.6,1.0);
} gl_FragColor = lightColor*color;
}
这样就实现了最基本的Shadow Mapping,对于每个像素只采样一个深度texel,看看效果吧。
图3:最基本的Shadow Mapping
可以看到效果不尽如人意,主要有三个问题(分别对应上图标记):1、物体的背光面当作阴影处理了;2、正对着光的一些像素也划到阴影区去了(Self-shadowing);3、阴影边缘有比较强的锯齿效果。对于问题一,可以判断当前像素是否背着灯光,方法是求得像素在灯光视图空间中的位置以及法线,然后求一个点积就可以了,对于这种像素,不用进行阴影计算即可;问题二产生的原因是深度误差导致的,当物体表面在灯光视图空间中的倾斜度越大时,误差也越大;解决办法有多种,第一种是在进行深度比较时,将深度值减去一个阈值再进行比较,第二种是在生成shadow map时,只绘制背面,即将背面设置反转,第三种方法是使用OpenGL提供的depth offset,在生成shadow map时,给深度值都加上一个跟像素斜率相关的值;第一种方法阈值比较难确定,无法完全解决Self-shadowing问题,第二种方法在场景中都是二维流形物体时可以工作的很好,第三种方法在绝大多数情况都可以工作得很好,这里使用这种方法,如下面代码所示:
// handle depth precision problem
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(1.0f,1.0f); // 绘制阴影投射集中的物体 glDisable(GL_POLYGON_OFFSET_FILL);
解决第一和第二个问题后的效果如下面所示图4所示:
图4:解决背光面阴影和Self-shadowing问题之后的效果
还有最后一个问题未解决,从上面的图也可看得出来,阴影边缘锯齿比较严重,解决这个问题也有两种较常用方法,第一种方法是用PCF(Percentage Closer Filtering),基本思想是对每个像素从shadow map中采样相邻的多个值,然后对每个值都进行深度比较,如果该像素处于阴影区就把比较结果记为0,否则记为1,最后把比较结果全部加起来除以采样点的个数就可以得到一个百分比p,表示其处在阴影区的可能性,若p为0代表该像素完全处于阴影区,若p为1表示完全不处于阴影区,最后根据p值设定混合系数即可。第二种方法是在阴影渲染pass中不计算光照,只计算阴影,可以得到一幅黑白二值图像,黑色的表示阴影,白色表示非阴影,然后对这幅图像进行高斯模糊,以对阴影边缘进行平滑,以减小据齿效果,最后在光照pass中将像素的光照值与相应二值图像中的值相乘就可以得到打上柔和阴影的场景了;在理论上来说,两种方法都能达到柔和阴影的效果,本文采用的PCF方法,第二种方法后面会尝试,并在效果和速度上与PCF做一下比较,等测试完了会贴到这里来。下面贴上加了PCF的像素shader的代码:
// The shadow map
uniform sampler2DShadow shadowDepthMap; varying vec4 projTexCoord; void main()
{
// light computing
vec4 lightColor = Lighting(...); float shadeFactor = 0.0;
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1, -1));
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1, 1));
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2( 1, -1));
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2( 1, 1)); shadeFactor *= 0.25; // map from [0.0,1.0] to [0.6,1.0]
shadeFactor = shadeFactor * 0.4 + 0.6; gl_FragColor = lightColor*shadeFactor;
}
另外注意要对shadow map纹理设置以下纹理参数:
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,GL_COMPARE_REF_TO_TEXTURE);
glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LEQUAL);
PCF效果如下图 所示,可以看到阴影边缘变柔和了:
图5:柔和的阴影边缘(PCF:采样pattern:左上,左下,右下,右上)
不同的采样pattern对结果也会有影响,比如采用下面的采样pattern效果如图6所示:
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1.5, 0.5));
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(0.5, 0.5));
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1.5, -1.5));
shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(0.5, -1.5));
图6:柔和的阴影边缘(PCF:使用非对齐采样pattern)
最后再贴一个2048分变率的shadow map产生的阴影效果,以作比较,PCF的采样pattern跟图6中一样。
图7:shadow map大小为2048时的阴影效果(PCF采样pattern与图6一样)。
3、结语
关于Shadow Map的变种有很多,不同的变种针对不同情况不同场景提供SM阴影抗锯齿解决方案,本文实现的只是基本的SM,后面考虑实现某种SM变种,进一步提高阴影的效果。
[ZZ] Shadow Map的更多相关文章
- (转)Shadow Map & Shadow Volume
转自:http://blog.csdn.net/hippig/article/details/7858574 shadow volume 这个术语几乎是随着 DOOM3 的发布而成为FPS 玩家和图形 ...
- [工作积累] 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 ...
- Unity基础(5) Shadow Map 概述
这篇是自己看shadow map是的一些笔记,内容稍稍凌乱,如有错误请帮忙纠正 1.常见阴影处理方式 Shadow Map : using Z-Buffer Shadow Mapping 的原理与实践 ...
- Shadow Map 实现极其细节
这里不介绍算法原理,只说说在实现过程中遇到的问题,以及背后的原因.开发环境:opengl 2.0 glsl 1.0. 第一个问题:产生深度纹理. 在opengl中每一次离屏渲染需要向opengl提供 ...
- Shadow Map 原理和改进 【转】
http://blog.csdn.net/ronintao/article/details/51649664 参考 1.Common Techniques to Improve Shadow Dept ...
- Shadow Map阴影贴图技术之探 【转】
这两天勉勉强强把一个shadowmap的demo做出来了.参考资料多,苦头可不少.Shadow Map技术是目前与Shadow Volume技术并行的传统阴影渲染技术,而且在游戏领域可谓占很大优势.本 ...
- GraphicsLab Project之再谈Shadow Map
作者:i_dovelemon 日期:2019-06-07 主题:Shadow Map(SM), Percentage Closer Filtering(PCF), Variance Shadow Ma ...
- Shadow Map -- 点阴影(全方位)
昨晚终于把点阴影(深度CubeMap)程序调通了,思想不难,基本就是在上节定向光阴影基础上稍作修改,但是CG程序不太方便Debug,需要输出中间效果图进行判断,耽搁了一会儿. 过程如下: 1.将深度渲 ...
随机推荐
- java 格式化时间
java.text.DateFormat format1 = new java.text.SimpleDateFormat("yyyy-MM-dd hh:mm:ss"); form ...
- myeclipse中的js文件报错
方法一:myeclipse9 很特殊 和 myeclipse10 不一样,所以myeclipse9 不能使用该方法. 方法二: 为了做一个页面特效,导入了一个jquery文件,怎想,myeclipse ...
- Jpush推送模块
此文章已于 14:17:10 2015/3/24 重新发布到 鲸歌 Jpush推送模块 或以上版本的手机系统. SDK集成步骤 .导入 SDK 开发包到你自己的应用程序项目 • 解压 ...
- 为什么内联函数,构造函数,静态成员函数不能为virtual函数
http://blog.csdn.net/freeboy1015/article/details/7635012 为什么内联函数,构造函数,静态成员函数不能为virtual函数? 1> 内联函数 ...
- Android UI组件
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android=&quo ...
- IoC容器Autofac - Autofac + Asp.net MVC + EF Code First(转载)
转载地址:http://www.cnblogs.com/JustRun1983/archive/2013/03/28/2981645.html 有修改 Autofac通过Controller默认构造 ...
- C#(去、过滤)掉字符中的换行符
本文介绍的方法需要先导入命名空间:usingSystem.Text.RegularExpressions; 字符串里所有的的换行符都去掉:textStr = Regex.Replace(textStr ...
- HTML5标准学习 – DOCTYPE
转自:http://www.cnblogs.com/GrayZhang/archive/2011/03/31/learning-html5-doctype.html 上一篇文章主要讲述了HTML文档的 ...
- loj 1046(bfs)
题目链接:http://acm.hust.edu.cn/vjudge/problem/viewProblem.action?id=26766 思路:由于数据不是很大,我们可以枚举骑士最后聚集的位置,然 ...
- android:scaleType属性
Android:scaleType是控制图片如何resized/moved来匹对ImageView的size. ImageView.ScaleType / android:scaleType值的意义区 ...