这两天勉勉强强把一个shadowmap的demo做出来了。参考资料多,苦头可不少。Shadow Map技术是目前与Shadow Volume技术并行的传统阴影渲染技术,而且在游戏领域可谓占很大优势。本篇是第一辑。——ZwqXin.com
Shadow Map的原理很简单,但是实现起来到处是雷。当然这只是我的体会。恩,不过就是“从光源处看场景,那些看不见的区域全部都该是阴影”。很容易看出,与针对特定模型的Shadow Volume不同,Shadow Map是针对场景的。这就是说,对一个光源应用一次Shadow Map,该光源所“看”到的所有对象——都在作阴影判断的范围内。当然,不针对模型说明了不用再像Shadow Volume那样需要考究和处理模型的形状、几何信息,工作成本下去了,效率上来了,因此在不那么注重阴影准确度的游戏里可是很受欢迎。准确度?恩,是的,有得必有失,这样两者至今才能依然肩并肩。Shadow Map注重对场景信息的采样,采样,也就是说要考究锯齿处理——这也是Shadow Map技术不断求突破的核心。当然还有别的便利,譬如对透明纹理的阴影生成支持,shadow volume前天一直在找可以实现这种效果的方法,但是暂时没找着——譬如一张纯黑背静的树的图片做Billboard,shadow volume处理的是一个矩形,得出的阴影也只是个矩形;但是Shadow Map中可以预先处理这张图片(剔除背景色)再做MAP操作,可以得出树轮廓的阴影——Billboard在Shadow Volume眼里是一个矩形的“模型”(可能有别的技法可以改变它的这种“看法”,知情者可否相告呢?),但在shadow map眼里是“场景”的一部分,它完全可以融合在这个大环境中生成阴影贴图。
两步走。首先是获得光源视觉下场景深度,保存在一张纹理上。通常是通过上一篇所说的矩阵变换,到达光源视点下的屏幕投影空间,截屏,把该截屏的深度信息写入一张纹理上(通常用专门的深度纹理,它可以用来作深度比较)。之后一步,回到普通的相机视觉,对相机所看到的每一点,找到刚才深度纹理对应的那个点——把该点像素在纹理图的深度作为判断该点是否应该处于阴影区域的依据。怎样判断?和“光源 - 该像素点”的距离做比较咯。实际上这个比较在做深度图的时候就可以出来了啊~为什么要等到做完深度图再专门拿出来比较呢?因为在光视觉下看到的“点的集合”跟照相机视觉下看到的“点集合”是不一样的。通俗点讲,两种视觉下看到的场景不一样(除非你照相机本来就在光源,视线一致),这么说来,可以“点与点对应”(注意,不是唯一对应)的那部分仅仅是这两个“点集合”的交集而已.......比两者中任一者都要来得小。我们于是只处理这部分可以一一对应起来的点,也只能处理它们——这里就隐含了一个巨大的问题了:那些在相机中可以看到,但是在光源视觉下看不到的场景怎么办?这部分场景的点做不了比较啊!是的,我思考这个问题颇久了,然后我在下篇将会讲到这个问题。
比较出来,把距离大于对应深度的那些部分涂黑就可以了。比较部分,老实说,我觉得手工比较很麻烦,毕竟这种对应关系是很难确定的。幸好OPENGL有专门的API给我们来做这事情,呵呵。完成这种比较有几个条件:1.上面提到了深度纹理就是条件之一,它能直接储存深度;2.深度纹理比较命令。它们的位置看起来是这样的:
- GenTex() //一般可在初始化里调用
- {
- ..................
- glGenTextures(1, &shadowmapid);
- glBindTexture(GL_TEXTURE_2D, shadowmapid);
- glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, fbowidth, fboheight, 0,
- GL_DEPTH_COMPONENT, GL_UNSIGNED_INT, NULL);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
- ...............
- }
- GenShadow()
- {
- glBindTexture(GL_TEXTURE_2D, shadowmapid);
- 渲染场景到纹理
- .....
- }
- CastShadow
- {
- glBindTexture(GL_TEXTURE_2D, shadowmapid);
- //深度纹理比较的API
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_R_TO_TEXTURE);
- glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);
- //glTexParameteri(GL_TEXTURE_2D, GL_DEPTH_TEXTURE_MODE, GL_INTENSITY);
- ...............//正常地再渲染一次场景
- }
同样是设置纹理特性(glTexParameteri),为什么不一起放在生成纹理时就操作呢?因为上面说过那个理由:我们不是比较整张深度图 嘛(当然我不知道它内部是不是一比较就得全部比较的)。
涂黑部分,固定管道下见过是依赖于上面注释掉的那句。它能把比较结果X(两种结果,X =1代表距离大于深度,要涂黑;X = 0则相反,代表距离小于等于[GL_LEQUAL]深度,比较“通过”,不用处理)应用到纹理的所有通道(GL_INTENSITY大概表示这意思吧)。接下来用ALPHA测试存到透明通道里的结果,大于0.99则涂黑便可(我所看的那教程上就是这样做的);如果你跟我一样用shader来更灵活地处理像素,则可注释掉上面GL_INTENSITY那句,在我们的像素shader里,用sampler2DShadow采样上面那深度纹理,用shadow2DProj就同样能获取这个结果X了。
最后附录一下与GL_INTENSITY相关的几个纹理“像素值储存格式”(参考这里:平民程序):
GL_INTENSITY: RGBA = (X, X, X, X) 把结果储存到所有通道
GL_ALPHA: RGBA = (0, 0, 0, X) 仅把结果储存到Alpha通道
GL_LUMINANCE: RGBA = (X, X, X, 1)把结果储存到3个Color通道