1、 软阴影的实现尝试软阴影( Soft - Shadow),并非一种图形学算法或技术的代名词。怎么说呢,它是图形学大师们孜孜不倦地追求的更逼近真实的阴影效果 。 ZwqX在已有的阴影 DEMO 上作改进,是当时很自然的想法。而选择了Shadow Map Demo2 ,不知道,算不算是一个失策。Cascaded ShadowMap 技术,从本质上说,只是分区间地调整投影矩阵,根据视距离使用不同分辨率的阴影图,促使效率与效果平衡而已。这是 Shadow Map 改进算法的一条路,但并非唯一。回想Shadow Map 阴影贴图技术之探 ,里面我简单地用乒乓式的采样,实现了一下 3X3 的阴影图模糊 P
2、CF。虽然结果看上去很那个,但这昭示着 Shadow Map 改进算法的另一条路 图形学中的图像处理,调整阴影图。在我的上一个阴影 DEMO 中,主要的 shader 只有一个,它是在 Shadow - Casting阶段完成场景阴影图的分层次贴附。在 CPU 上完成的是 ShadowMap - Generating 阶段,因为那里没有涉及像素层面,完全不需要 GPU 的并行计算能力。但如果要对一张贴图的内容进行像素级的处理恩,这是通常的想法。但当时脑中另一个突然一闪的想法,让我决定先行对其进行捕获 场景后处理。一般说的 DefferedShading 并不等同于这个意义,但就字面来说它们也有
3、那么一点共通。恩,题外话先免了,先暗自打个小算盘:场景后处理,我是想在最终画面上动手脚。在 OpenGL 流水线的尾部,无论是否双缓冲,必然是把显存上每个像素格的值传向显示器屏幕上的每个“真像素” 上,表示成颜色(譬如 LCD 是用彩色滤光片等结构对此转换的,可以理解为每个像素上又有红绿蓝三色子像素 balabala嘛,其实我也不怎么理解啊这些硬件的- -,呃离题了,回来)。如果把这个覆盖屏幕的“结果” 当作贴在一个 Screen-Aligned-Quad(屏幕大矩形)的一张纹理,我们在显存-屏幕这个过程之前进行拦截,获得并处理这个“ 纹理”后再让它传向屏幕。有这方面基础的朋友都知道,FBO
4、又该出来表演了学一学,FBO 。把 Shadow - Casting 的结果写入 FBO 绑定的一个纹理中,然后自己画一个 Screen-Aligned-Quad,贴这个纹理 对这个过程启用 shader 处理。最后虽然屏幕上本质只有这么一个矩形,但是众多本该直接映射在屏幕上的场景元素已经完美地“被代表” 了。看上去丝毫没有河蟹爬过的痕迹完全没有。依然是模糊处理。高斯模糊。01 /02 uniform int BSceneWidth;03 uniform int BSceneHeight;04 uniform float ishoriz;05 06 uniform int samplecoun
5、t;07 uniform float weights;08 uniform float sigma;09 10 uniform sampler2D BScenemap;11 12 void main()13 14 gl_FragData0 = BlurFilter(BScenemap, gl_TexCoord0.xy) ;15 其中 BlurFilter 函数是对当前像素,在水平或垂直方向上采样(samplecount ),采样值乘以一个高斯分布值而已(公式也就网上常见的,sigma 为参数),跟 PCF 也就差不多那回事。很明显,我们需要在水平方向和垂直方向都模糊一次,这里用 2 个 PAS
6、S。我们只需要模糊阴影,因此把阴影部分和场景部分分开写入纹理(MRT),最后再统一。1 / CastingShader, render to two textures (MRT)2 gl_FragData0 = vec4(gl_Color.rgb * diffuse.rgb * texColor.rgb, 1.0);3 4 gl_FragData1 = shadeFactor;1 /PostProcessingShader , as a texture to the screen-aligned quad2 uniform sampler2D Scenemap;3 uniform sample
7、r2D Shademap;4 5 void main()6 7 vec4 shadeFact = texture2D(Shademap, gl_TexCoord1.xy); 8 gl_FragColor = texture2D(Scenemap, gl_TexCoord0.xy) * shadeFact;9 写到 FBO 纹理后,屏幕矩形绑定之:软是软了,就边界效果而言是很好的(这里图片压缩了看上去糟糕而已)但有几个问题:1.很明显的边界问题,阴影与场景分离了,SampleNum 越大分离得越明显;2 随着SampleNum 增加帧率狂降。效果和效率都不行。首先,这种高采样带宽的东西本来就是帧
8、率杀手,更不用说高斯分布权数的计算实在够呛。其次因为场景和阴影分离,而模糊的只是对于相机“可见” 的那部分阴影,所以图中间隔线是活生生被当作阴影边界了。后来也考虑过再加入图像处理中的“膨胀”运算 形态学运算小结 ,但是有点吃力不讨好的感觉,无论膨胀多少次,还是能依稀见到狭缝,加上效率问题,宣告此法的破产。左-右,上-下依次 32p4p4pass1, 32p2p2pass1, 32p2p2pass2, 64p2p2pass1(自己猜,我什么都忘记了哈)还是好好用别的方法吧软阴影与反锯齿有着深刻的关系,PCF 在 Graphics 上原本就是一种反锯齿技术,而它在阴影算法上往往作为简捷的软阴影达成
9、途径。VSM( Variance Shadow Map)同样旨在解决这一问题,但它并非像以往一样对阴影图的后处理,而是重新定义阴影图。ZwqX1. 锯齿与反锯齿锯齿感是在 Casting 的时候产生的。我们确实是相当于把一张阴影图(深度图)贴在场景上了,但具体的操作依然是一条深度比较指令。譬如,传统的 SM 中,根据场景物体像素在光源视觉下的深度得到的阴影图,在 Casting 阶段对应相机视觉中的像素判断是否属于阴影图中的像素 这本来就是一个 TRUE OR FALSE 的判断,若 TRUE 证明非阴影,像素颜色值不变;若 FALSE 则涂黑,以表明该处阴影。NewValue = OldVa
10、lue * (isShadow? 0 : 1)即使把 0 替换成 shadowMapValue 或是运用距离值判断,也改变不了这里的根源:把视场景分割成阴影区和非阴影区。双方强烈的撕扯总会造成霹雳的裂痕。我们一般使用的纹理在贴在奇形怪状的表面上时产生的扭曲同样会导致锯齿,可为什么却不那么难接受呢?原因是 filtering。初学纹理贴图的时候,一定会接触到 Texture Filtering,尤其在发生欠采样和过采样的时候,纹理映射的策略。在 OpenGL 1.x 中,GL_LINEAR 和GL_NEAREST 应该是最为熟悉的,尤其是线性插值 GL_LINEAR,让我们贴纹理的时候能得到比较
11、好的性效比:1 glTexParameteri(GL_TEXTURE_2D_ARRAY_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR);2 glTexParameteri(GL_TEXTURE_2D_ARRAY_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR);贴纹理过程的采样能通过简单的线性插值完成,再把插出来的值映射到恰当的位置去想想对于深度图这会是什么情况:低分辨率的阴影图上一个 4X4 格子,对应场景中9X9 大小区域,恰好区域覆盖阴影边界,深度插值后某像素获得了表示“半影” 的灰度数值。但是要“贴”到场景时,需要经过一个 TR
12、UE OR FALSE 过滤器,这个“ 半影”像素依然要选择:自己究竟属于影子,还是非影子。一旦整个边界的像素选择完成,由于阴影判断式对所有像素等效,它们会发现自己“整齐” 地划出了一条边界线,锯齿边界线。2. Variance Shadow MapVSM(Variance Shadow Map)的提出正基于此。其实它本质也是一种基于概率的方法,把以上的 TRUE OR FALSE 过滤器过程改作一个可见性估计过程嘛,你也知道概率学上的东西有很多微妙的地方 让 texture filtering 作用于每个像素的方差(Variance)而非其本身的值,于此建立的切比雪夫不等式作为这个可见性估计
13、式。ZwqXin 本身概率论学得不深,为了避免误人子弟,这许概率数学原理我就 PASS 了。每个像素经过这样折腾一下,取切比雪夫不等式的上限(UpperBound,即上式pmax(t))为结果,它大概揭示了当前像素属于“非阴影区” 的程度(当这个 UpperBound 越接近 0 时代表此像素越可能“属于阴影”)。UpperBound 计算式的三个参数,t 是像素的实际深度(根据 GPU GEMS3,这里取的是像素距离光源的距离 distToLight), 是当前像素深度的期望 E(x)(取的是直接在阴影图上采样得到的插值后的 深度,好吧,囧), 2 是方差( 2 = E(x2)- E(x)2
14、)。首先查查什么是 “切比雪夫不等式”( Chebyshevs inequality )。是的,上过概率课,所以至少听过这个名词(听说,那是因为期末考后潜意识对自身的遗忘魔法):in any data sample or probability distribution, “nearly all“ the values are “close to“ the mean value the precise statement being that no more than 1/k2 of the distributions values can be more than k standard de
15、viationsaway from the mean. WIKI。上面式子应该是另一种表达形式吧(吧?)。在我看来,切比雪夫不等式的上限是关于 t 的函数式,描述了一个具体的量 t 在随机集合 X 上的位置的概率范围。Casting 过程中,一个像素往深度图 Shadow Map 上采样,姑且认为这是一个随机选择的过程;像素若是根据其纹理坐标(光源投影矩阵合理所得)进行的采样,纹理上应该只有唯一的一个点与之对应 当然,即使取隔壁的点也不是不可,不过它作为准确值的概率没有那个“唯一的点”大 把可能取的值作为集合 X,那 X 就是一个以“唯一的点”为中心的圆,使该点作为集合的期望值 。(期望的计算
16、式子 E(X) = x1 * p(x1) + x2 * p(x2) + ,稍微考虑一下就明白)。问题是 t,它作为实际值,在我们的纹理采样过程中是不可能出现的,甚至我觉得它根本无法得出,为什么取“像素距离光源的距离 distToLight”呢?它的计算不存在于纹理采样过程中,但也不可能就说它就是“真实值” 啊哪怕表义如此。对此,可看看GPU GEMS3 8.1 中作者的解释,不过我是没有太理解而已(尽管没有更好的)。于是,既然摆脱的 TRUE OR FALSE 的抑制,我们完全可以动用 texture filtering 去取那个“唯一的点 ”期望值 E(x)了。这不,为了计算方差,我们还需要
17、一个 E(x2)类似地,我们只需要在 ShadowMap Generating 阶段把像素深度值的平方也保存起来就可以了这就是 Variance Shadow Map,需要一张纹理的两个通道,分别存储深度和深度平方。CASTING 阶段把各值传入切比雪夫不等式计算上限值这不是确切的“ 属于非阴影区的概率”,但它是唯一明确能得到的值,这个不等式误差是 VSM 致命弱点的根源。这个上限值概率作为“阴影因子” 乘在场景像素上,造出了肉眼可见的“半影”区,同时大大地增强了阴影的边缘的轮廓感(难看的锯齿减少)。(CSM 一般效果,没加 PCF 模糊,注意看的是阴影外形)(对比。 CSM+VSM 可见没有了突兀的轮廓, filter 的效果显著)(对比。没有 VSM 效果时,远观时 (splitNum = 4)轮廓已经很糟糕了,但 VSM 下能显示完好轮廓)