# 第十六课: 阴影贴图
# 第十六课:阴影贴图(Shadow mapping)
第十五课中已经学习了如何创建光照贴图。光照贴图可用于静态对象的光照,其阴影效果也很不错,但无法处理运动的对象。
阴影贴图是目前(截止2012年)最好的生成动态阴影的方法。此法最大的优点是易于实现,缺点是想完全**正确**地实现不大容易。
本课首先介绍基本算法,探究其缺陷,然后实现一些优化。由于撰写本文时(2012),阴影贴图技术还在被广泛地研究;我们将提供一些指导,以便你根据自身需要,进一步改善你的阴影贴图。
## 基本的阴影贴图
基本的阴影贴图算法包含两个步骤。首先,从光源的视角将场景渲染一次,只计算每个片断的深度。接着从正常的视角把场景再渲染一次,渲染时要测试当前片断是否位于阴影中。
“是否在阴影中”的测试实际上非常简单。如果当前采样点比阴影贴图中的同一点离光源更远,那说明场景中有一个物体比当前采样点离光源更近;即当前片断位于阴影中。
下图可以帮你理解上述原理:
![](https://box.kancloud.cn/2015-11-02_5636f3095f82c.png)
## 渲染阴影贴图
本课只考虑平行光——一种位于无限远处,其光线可视为相互平行的光源。故可用正交投影矩阵来渲染阴影贴图。正交投影矩阵和一般的透视投影矩阵差不多,只不过未考虑透视——因此无论距离相机多远,物体的大小看起来都是一样的。
## 设置渲染目标和MVP矩阵
十四课中,大家学习了把场景渲染到纹理,以便稍后从shader中访问的方法。
这里采用了一幅1024x1024、16位深度的纹理来存储阴影贴图。对于阴影贴图来说,通常16位绰绰有余;你可以自由地试试别的数值。注意,这里采用的是深度纹理,而非深度渲染缓冲区(这个要留到后面进行采样)。
```
<pre class="calibre16">```
<span class="token2">// The framebuffer, which regroups 0, 1, or more textures, and 0 or 1 depth buffer.</span>
GLuint FramebufferName <span class="token">=</span> <span class="token6">0</span><span class="token1">;</span>
<span class="token3">glGenFramebuffers</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>FramebufferName<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindFramebuffer</span><span class="token1">(</span>GL_FRAMEBUFFER<span class="token1">,</span> FramebufferName<span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Depth texture. Slower than a depth buffer, but you can sample it later in your shader</span>
GLuint depthTexture<span class="token1">;</span>
<span class="token3">glGenTextures</span><span class="token1">(</span><span class="token6">1</span><span class="token1">,</span> <span class="token">&</span>depthTexture<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glBindTexture</span><span class="token1">(</span>GL_TEXTURE_2D<span class="token1">,</span> depthTexture<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glTexImage2D</span><span class="token1">(</span>GL_TEXTURE_2D<span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span>GL_DEPTH_COMPONENT16<span class="token1">,</span> <span class="token6">1024</span><span class="token1">,</span> <span class="token6">1024</span><span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span>GL_DEPTH_COMPONENT<span class="token1">,</span> GL_FLOAT<span class="token1">,</span> <span class="token6">0</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glTexParameteri</span><span class="token1">(</span>GL_TEXTURE_2D<span class="token1">,</span> GL_TEXTURE_MAG_FILTER<span class="token1">,</span> GL_NEAREST<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glTexParameteri</span><span class="token1">(</span>GL_TEXTURE_2D<span class="token1">,</span> GL_TEXTURE_MIN_FILTER<span class="token1">,</span> GL_NEAREST<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glTexParameteri</span><span class="token1">(</span>GL_TEXTURE_2D<span class="token1">,</span> GL_TEXTURE_WRAP_S<span class="token1">,</span> GL_CLAMP_TO_EDGE<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glTexParameteri</span><span class="token1">(</span>GL_TEXTURE_2D<span class="token1">,</span> GL_TEXTURE_WRAP_T<span class="token1">,</span> GL_CLAMP_TO_EDGE<span class="token1">)</span><span class="token1">;</span>
<span class="token3">glFramebufferTexture</span><span class="token1">(</span>GL_FRAMEBUFFER<span class="token1">,</span> GL_DEPTH_ATTACHMENT<span class="token1">,</span> depthTexture<span class="token1">,</span> <span class="token6">0</span><span class="token1">)</span><span class="token1">;</span>
<span class="token3">glDrawBuffer</span><span class="token1">(</span>GL_NONE<span class="token1">)</span><span class="token1">;</span> <span class="token2">// No color buffer is drawn to.</span>
<span class="token2">// Always check that our framebuffer is ok</span>
<span class="token4">if</span><span class="token1">(</span><span class="token3">glCheckFramebufferStatus</span><span class="token1">(</span>GL_FRAMEBUFFER<span class="token1">)</span> <span class="token">!=</span> GL_FRAMEBUFFER_COMPLETE<span class="token1">)</span>
<span class="token4">return</span> <span class="token6">false</span><span class="token1">;</span>
```
```
MVP矩阵用于从光源的视角绘制场景,其计算过程如下:
- 投影矩阵是正交矩阵,可将整个场景包含到一个AABB(axis-aligned box, 轴向包围盒)里,该包围盒在X、Y、Z轴上的坐标范围分别为(-10,10)、(-10,10)、(-10,20)。这样做是为了让整个场景始终可见,这一点在“再进一步”小节还会讲到。
- 视图矩阵对场景做了旋转,这样在观察坐标系中,光源的方向就是-Z方向(需要温习\[第三课\]
-
模型矩阵可设为任意值。
```
<pre class="calibre16">```
glm<span class="token1">:</span><span class="token1">:</span>vec3 lightInvDir <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">0.5</span>f<span class="token1">,</span><span class="token6">2</span><span class="token1">,</span><span class="token6">2</span><span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Compute the MVP matrix from the light's point of view</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 depthProjectionMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span>ortho<span class="token"><</span>float<span class="token">></span><span class="token1">(</span><span class="token">-</span><span class="token6">10</span><span class="token1">,</span><span class="token6">10</span><span class="token1">,</span><span class="token">-</span><span class="token6">10</span><span class="token1">,</span><span class="token6">10</span><span class="token1">,</span><span class="token">-</span><span class="token6">10</span><span class="token1">,</span><span class="token6">20</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 depthViewMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">lookAt</span><span class="token1">(</span>lightInvDir<span class="token1">,</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">0</span><span class="token1">,</span><span class="token6">0</span><span class="token1">,</span><span class="token6">0</span><span class="token1">)</span><span class="token1">,</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">0</span><span class="token1">,</span><span class="token6">1</span><span class="token1">,</span><span class="token6">0</span><span class="token1">)</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 depthModelMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">mat4</span><span class="token1">(</span><span class="token6">1.0</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 depthMVP <span class="token">=</span> depthProjectionMatrix <span class="token">*</span> depthViewMatrix <span class="token">*</span> depthModelMatrix<span class="token1">;</span>
<span class="token2">// Send our transformation to the currently bound shader,</span>
<span class="token2">// in the "MVP" uniform</span>
<span class="token3">glUniformMatrix4fv</span><span class="token1">(</span>depthMatrixID<span class="token1">,</span> <span class="token6">1</span><span class="token1">,</span> GL_FALSE<span class="token1">,</span> <span class="token">&</span>depthMVP<span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">[</span><span class="token6">0</span><span class="token1">]</span><span class="token1">)</span>
```
```
## Shaders
这一次渲染中所用的着色器很简单。顶点着色器仅仅简单地计算一下顶点的齐次坐标:
```
<pre class="calibre16">```
#version <span class="token6">330</span> core
<span class="token2">// Input vertex data, different for all executions of this shader.</span>
<span class="token3">layout</span><span class="token1">(</span>location <span class="token">=</span> <span class="token6">0</span><span class="token1">)</span> <span class="token4">in</span> vec3 vertexPosition_modelspace<span class="token1">;</span>
<span class="token2">// Values that stay constant for the whole mesh.</span>
uniform mat4 depthMVP<span class="token1">;</span>
void <span class="token3">main</span><span class="token1">(</span><span class="token1">)</span><span class="token1">{</span>
gl_Position <span class="token">=</span> depthMVP <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexPosition_modelspace<span class="token1">,</span><span class="token6">1</span><span class="token1">)</span><span class="token1">;</span>
<span class="token1">}</span>
```
```
fragment shader同样简单:只需将片断的深度值写到location 0(即写入深度纹理)。
```
<pre class="calibre16">```
#version <span class="token6">330</span> core
<span class="token2">// Ouput data</span>
<span class="token3">layout</span><span class="token1">(</span>location <span class="token">=</span> <span class="token6">0</span><span class="token1">)</span> out float fragmentdepth<span class="token1">;</span>
void <span class="token3">main</span><span class="token1">(</span><span class="token1">)</span><span class="token1">{</span>
<span class="token2">// Not really needed, OpenGL does it anyway</span>
fragmentdepth <span class="token">=</span> gl_FragCoord<span class="token1">.</span>z<span class="token1">;</span>
<span class="token1">}</span>
```
```
渲染阴影贴图比渲染一般的场景要快一倍多,因为只需写入低精度的深度值,不需要同时写深度值和颜色值。显存带宽往往是影响GPU性能的关键因素。
## 结果
渲染出的纹理如下所示:
![](https://box.kancloud.cn/2015-11-02_5636f309770f6.png)
颜色越深表示z值越小;故墙面的右上角离相机更近。相反地,白色表示z=1(齐次坐标系中的值),离相机十分遥远。
## 使用阴影贴图
## 基本shader
现在回到普通的着色器。对于每一个计算出的fragment,都要测试其是否位于阴影贴图之“后”。
为了做这个测试,需要计算:**在创建阴影贴图所用的坐标系中**,当前片断的坐标。因此要依次用通常的`MVP`矩阵和`depthMVP`矩阵对其做变换。
不过还需要一些技巧。将depthMVP与顶点坐标相乘得到的是齐次坐标,坐标范围为\[-1,1\],而纹理采样的取值范围却是\[0,1\]。
举个例子,位于屏幕中央的fragment的齐次坐标应该是(0,0);但要对纹理中心进行采样,UV坐标就应该是(0.5,0.5)。
这个问题可以通过在片断着色器中调整采样坐标来修正,但用下面这个矩阵去乘齐次坐标则更为高效。这个矩阵将坐标除以2(主对角线上\[-1,1\] -> \[-0.5, 0.5\]),然后平移(最后一行\[-0.5, 0.5\] -> \[0,1\])。
```
<pre class="calibre16">```
glm<span class="token1">:</span><span class="token1">:</span>mat4 <span class="token3">biasMatrix</span><span class="token1">(</span>
<span class="token6">0.5</span><span class="token1">,</span> <span class="token6">0.0</span><span class="token1">,</span> <span class="token6">0.0</span><span class="token1">,</span> <span class="token6">0.0</span><span class="token1">,</span>
<span class="token6">0.0</span><span class="token1">,</span> <span class="token6">0.5</span><span class="token1">,</span> <span class="token6">0.0</span><span class="token1">,</span> <span class="token6">0.0</span><span class="token1">,</span>
<span class="token6">0.0</span><span class="token1">,</span> <span class="token6">0.0</span><span class="token1">,</span> <span class="token6">0.5</span><span class="token1">,</span> <span class="token6">0.0</span><span class="token1">,</span>
<span class="token6">0.5</span><span class="token1">,</span> <span class="token6">0.5</span><span class="token1">,</span> <span class="token6">0.5</span><span class="token1">,</span> <span class="token6">1.0</span>
<span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 depthBiasMVP <span class="token">=</span> biasMatrix<span class="token">*</span>depthMVP<span class="token1">;</span>
```
```
终于可以写vertex shader了。和之前的差不多,不过这次要输出两个坐标。
- `gl_Position`是当前相机所在坐标系下的顶点坐标
- `ShadowCoord`是上一个相机(光源)所在坐标系下的顶点坐标
```
<pre class="calibre16">```
<span class="token2">// Output position of the vertex, in clip space : MVP * position</span>
gl_Position <span class="token">=</span> MVP <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexPosition_modelspace<span class="token1">,</span><span class="token6">1</span><span class="token1">)</span><span class="token1">;</span>
<span class="token2">// Same, but with the light's view matrix</span>
ShadowCoord <span class="token">=</span> DepthBiasMVP <span class="token">*</span> <span class="token3">vec4</span><span class="token1">(</span>vertexPosition_modelspace<span class="token1">,</span><span class="token6">1</span><span class="token1">)</span><span class="token1">;</span>
```
```
fragment shader就很简单了:
- `texture2D( shadowMap, ShadowCoord.xy ).z` 是光源到距离最近的遮挡物之间的距离。
- `ShadowCoord.z`是光源和当前片断之间的距离
……因此,若当前fragment比最近的遮挡物还远,那意味着这个片断位于(这个最近的遮挡物的)阴影中
```
<pre class="calibre16">```
float visibility <span class="token">=</span> <span class="token6">1.0</span><span class="token1">;</span>
<span class="token4">if</span> <span class="token1">(</span> <span class="token3">texture2D</span><span class="token1">(</span> shadowMap<span class="token1">,</span> ShadowCoord<span class="token1">.</span>xy <span class="token1">)</span><span class="token1">.</span>z <span class="token"><</span> ShadowCoord<span class="token1">.</span>z<span class="token1">)</span><span class="token1">{</span>
visibility <span class="token">=</span> <span class="token6">0.5</span><span class="token1">;</span>
<span class="token1">}</span>
```
```
我们只需把这个原理加到光照计算中。当然,环境光分量无需改动,毕竟这只分量是个为了模拟一些光亮,让即使处在阴影或黑暗中的物体也能显出轮廓来(否则就会是纯黑色)。
```
<pre class="calibre16">```
color <span class="token">=</span>
<span class="token2">// Ambiant : simulates indirect lighting</span>
MaterialAmbiantColor <span class="token">+</span>
<span class="token2">// Diffuse : "color" of the object</span>
visibility <span class="token">*</span> MaterialDiffuseColor <span class="token">*</span> LightColor <span class="token">*</span> LightPower <span class="token">*</span> cosTheta<span class="token">+</span>
<span class="token2">// Specular : reflective highlight, like a mirror</span>
visibility <span class="token">*</span> MaterialSpecularColor <span class="token">*</span> LightColor <span class="token">*</span> LightPower <span class="token">*</span> <span class="token3">pow</span><span class="token1">(</span>cosAlpha<span class="token1">,</span><span class="token6">5</span><span class="token1">)</span><span class="token1">;</span>
```
```
## 结果——阴影瑕疵(Shadow acne)
这是目前的代码渲染的结果。很明显,大体的思想是实现了,不过质量不尽如人意。
![](https://box.kancloud.cn/2015-11-02_5636f309850d1.png)
逐一检查图中的问题。代码有两个工程:`shadowmaps`和`shadowmaps_simple`,任选一项。simple版的效果和上图一样糟糕,但代码比较容易理解。
## 问题
## 阴影瑕疵
最明显的问题就是**阴影瑕疵**:
![](https://box.kancloud.cn/2015-11-02_5636f309a9231.png)
这种现象可用下面这张简单的图解释:
![](https://box.kancloud.cn/2015-11-02_5636f309b78a2.png)
通常的“补救措施”是加上一个误差容限(error margin):仅当当前fragment的深度(再次提醒,这里指的是从光源的坐标系得到的深度值)确实比光照贴图像素的深度要大时,才将其判定为阴影。这可以通过添加一个偏差(bias)来办到:
```
<pre class="calibre16">```
float bias <span class="token">=</span> <span class="token6">0.005</span><span class="token1">;</span>
float visibility <span class="token">=</span> <span class="token6">1.0</span><span class="token1">;</span>
<span class="token4">if</span> <span class="token1">(</span> <span class="token3">texture2D</span><span class="token1">(</span> shadowMap<span class="token1">,</span> ShadowCoord<span class="token1">.</span>xy <span class="token1">)</span><span class="token1">.</span>z <span class="token"><</span> ShadowCoord<span class="token1">.</span>z<span class="token">-</span>bias<span class="token1">)</span><span class="token1">{</span>
visibility <span class="token">=</span> <span class="token6">0.5</span><span class="token1">;</span>
<span class="token1">}</span>
```
```
效果好多了::
![](https://box.kancloud.cn/2015-11-02_5636f309c3824.png)
不过,您也许注意到了,由于加入了偏差,墙面与地面之间的瑕疵显得更加明显了。更糟糕的是,0.005的偏差对地面来说太大了,但对曲面来说又太小了:圆柱体和球体上的瑕疵依然可见。
一个通常的解决方案是根据斜率调整偏差:
```
<pre class="calibre16">```
float bias <span class="token">=</span> <span class="token6">0.005</span><span class="token">*</span><span class="token3">tan</span><span class="token1">(</span><span class="token3">acos</span><span class="token1">(</span>cosTheta<span class="token1">)</span><span class="token1">)</span><span class="token1">;</span> <span class="token2">// cosTheta is dot( n,l ), clamped between 0 and 1</span>
bias <span class="token">=</span> <span class="token3">clamp</span><span class="token1">(</span>bias<span class="token1">,</span> <span class="token6">0</span><span class="token1">,</span><span class="token6">0.01</span><span class="token1">)</span><span class="token1">;</span>
```
```
阴影瑕疵消失了,即使在曲面上也看不到了。
![](https://box.kancloud.cn/2015-11-02_5636f309dafe0.png)
还有一个技巧,不过这个技巧灵不灵得看具体的几何形状。此技巧只渲染阴影中的背面。这就对厚墙的几何形状提出了硬性要求(请看下一节——阴影悬空(Peter Panning),不过即使有瑕疵,也只会出现在阴影遮蔽下的表面上。【译者注:在迪斯尼经典动画[《小飞侠》](http://movie.douban.com/subject/1296538/)中,小飞侠彼得·潘的影子和身体分开了,小仙女温蒂又给他缝好了。】
![](https://box.kancloud.cn/2015-11-02_5636f309f216c.png)
渲染阴影贴图时剔除正面的三角形:
```
<pre class="calibre16">```
<span class="token2">// We don't use bias in the shader, but instead we draw back faces,</span>
<span class="token2">// which are already separated from the front faces by a small distance</span>
<span class="token2">// (if your geometry is made this way)</span>
<span class="token3">glCullFace</span><span class="token1">(</span>GL_FRONT<span class="token1">)</span><span class="token1">;</span> <span class="token2">// Cull front-facing triangles -> draw only back-facing triangles</span>
```
```
渲染场景时正常地渲染(剔除背面)
```
<pre class="calibre16">```
<span class="token3">glCullFace</span><span class="token1">(</span>GL_BACK<span class="token1">)</span><span class="token1">;</span> <span class="token2">// Cull back-facing triangles -> draw only front-facing triangles</span>
```
```
代码中也用了这个方法,和“加入偏差”联合使用。
## 阴影悬空(Peter Panning)
现在没有阴影瑕疵了,但地面的光照效果还是不对,看上去墙面好像悬在半空(因此术语称为“阴影悬空”)。实际上,加上偏差会加剧阴影悬空。
![](https://box.kancloud.cn/2015-11-02_5636f30a12687.png)
这个问题很好修正:避免使用薄的几何形体就行了。这样做有两个好处:
- 首先,(把物体增厚)解决了阴影悬空问题:物体比偏差值要大得多,于是一切麻烦烟消云散了
- 其次,可在渲染光照贴图时启用背面剔除,因为现在,墙壁上有一个面面对光源,就可以遮挡住墙壁的另一面,而这另一面恰好作为背面被剔除了,无需渲染。
缺点就是要渲染的三角形增多了(每帧多了一倍的三角形!)
![](https://box.kancloud.cn/2015-11-02_5636f30a24a1c.png)
## 走样
即使是使用了这些技巧,你还是会发现阴影的边缘上有一些走样。换句话说,就是一个像素点是白的,邻近的一个像素点是黑的,中间缺少平滑过渡。
![](https://box.kancloud.cn/2015-11-02_5636f30a3c173.png)
## PCF(percentage closer filtering,百分比渐近滤波)
一个最简单的改善方法是把阴影贴图的`sampler`类型改为\*\*`sampler2DShadow`\*\*。这么做的结果是,每当对阴影贴图进行一次采样时,硬件就会对相邻的纹素进行采样,并对它们全部进行比较,对比较的结果做双线性滤波后返回一个\[0,1\]之间的float值。
例如,0.5即表示有两个采样点在阴影中,两个采样点在光明中。
注意,它和对滤波后深度图做单次采样有区别!一次“比较”,返回的是true或false;PCF返回的是4个“true或false”值的插值结果
![](https://box.kancloud.cn/2015-11-02_5636f30a4b0dd.png)
可以看到,阴影边界平滑了,但阴影贴图的纹素依然可见。
## 泊松采样(Poisson Sampling)
一个简易的解决办法是对阴影贴图做N次采样(而不是只做一次)。并且要和PCF一起使用,这样即使采样次数不多,也可以得到较好的效果。下面是四次采样的代码:
```
<pre class="calibre16">```
<span class="token4">for</span> <span class="token1">(</span>int i<span class="token">=</span><span class="token6">0</span><span class="token1">;</span>i<span class="token"><</span><span class="token6">4</span><span class="token1">;</span>i<span class="token">++</span><span class="token1">)</span><span class="token1">{</span>
<span class="token4">if</span> <span class="token1">(</span> <span class="token3">texture2D</span><span class="token1">(</span> shadowMap<span class="token1">,</span> ShadowCoord<span class="token1">.</span>xy <span class="token">+</span> poissonDisk<span class="token1">[</span>i<span class="token1">]</span><span class="token">/</span><span class="token6">700.0</span> <span class="token1">)</span><span class="token1">.</span>z <span class="token"><</span> ShadowCoord<span class="token1">.</span>z<span class="token">-</span>bias <span class="token1">)</span><span class="token1">{</span>
visibility<span class="token">-</span><span class="token">=</span><span class="token6">0.2</span><span class="token1">;</span>
<span class="token1">}</span>
<span class="token1">}</span>
```
```
`poissonDisk`是一个常量数组,其定义看起来像这样:
```
<pre class="calibre16">```
vec2 poissonDisk<span class="token1">[</span><span class="token6">4</span><span class="token1">]</span> <span class="token">=</span> vec2<span class="token1">[</span><span class="token1">]</span><span class="token1">(</span>
<span class="token3">vec2</span><span class="token1">(</span> <span class="token">-</span><span class="token6">0.94201624</span><span class="token1">,</span> <span class="token">-</span><span class="token6">0.39906216</span> <span class="token1">)</span><span class="token1">,</span>
<span class="token3">vec2</span><span class="token1">(</span> <span class="token6">0.94558609</span><span class="token1">,</span> <span class="token">-</span><span class="token6">0.76890725</span> <span class="token1">)</span><span class="token1">,</span>
<span class="token3">vec2</span><span class="token1">(</span> <span class="token">-</span><span class="token6">0.094184101</span><span class="token1">,</span> <span class="token">-</span><span class="token6">0.92938870</span> <span class="token1">)</span><span class="token1">,</span>
<span class="token3">vec2</span><span class="token1">(</span> <span class="token6">0.34495938</span><span class="token1">,</span> <span class="token6">0.29387760</span> <span class="token1">)</span>
<span class="token1">)</span><span class="token1">;</span>
```
```
这样,根据阴影贴图采样点个数的多少,生成的fragment会随之变明或变暗。
![](https://box.kancloud.cn/2015-11-02_5636f30a5be97.png)
常量700.0确定了采样点的“分散”程度。散得太密,还是会发生走样;散得太开,会出现**条带**(截图中未使用PCF,以便让条带现象更明显;其中做了16次采样)
![](https://box.kancloud.cn/2015-11-02_5636f30a7e7dc.png)
![](https://box.kancloud.cn/2015-11-02_5636f30aa7296.png)
## 分层泊松采样(Stratified Poisson Sampling)
通过为每个像素分配不同采样点个数,我们可以消除这一问题。主要有两种方法:分层泊松法(Stratified Poisson)和旋转泊松法(Rotated Poisson)。分层泊松法选择不同的采样点数;旋转泊松法采样点数保持一致,但会做随机的旋转以使采样点的分布发生变化。本课仅对分层泊松法作介绍。
与之前版本唯一不同的是,这里用了一个随机数来索引`poissonDisk`:
```
<pre class="calibre16">```
<span class="token4">for</span> <span class="token1">(</span>int i<span class="token">=</span><span class="token6">0</span><span class="token1">;</span>i<span class="token"><</span><span class="token6">4</span><span class="token1">;</span>i<span class="token">++</span><span class="token1">)</span> <span class="token1">{</span>
int index <span class="token">=</span> <span class="token2">// A random number between 0 and 15, different for each pixel (and each i !)</span>
visibility <span class="token">-</span><span class="token">=</span> <span class="token6">0.2</span><span class="token">*</span><span class="token1">(</span><span class="token6">1.0</span><span class="token">-</span><span class="token3">texture</span><span class="token1">(</span> shadowMap<span class="token1">,</span> <span class="token3">vec3</span><span class="token1">(</span>ShadowCoord<span class="token1">.</span>xy <span class="token">+</span> poissonDisk<span class="token1">[</span>index<span class="token1">]</span><span class="token">/</span><span class="token6">700.0</span><span class="token1">,</span> <span class="token1">(</span>ShadowCoord<span class="token1">.</span>z<span class="token">-</span>bias<span class="token1">)</span><span class="token">/</span>ShadowCoord<span class="token1">.</span>w<span class="token1">)</span> <span class="token1">)</span><span class="token1">)</span><span class="token1">;</span>
<span class="token1">}</span>
```
```
可用如下代码(返回一个\[0,1\]间的随机数)产生随机数
```
<pre class="calibre16">```
float dot_product <span class="token">=</span> <span class="token3">dot</span><span class="token1">(</span>seed4<span class="token1">,</span> <span class="token3">vec4</span><span class="token1">(</span><span class="token6">12.9898</span><span class="token1">,</span><span class="token6">78.233</span><span class="token1">,</span><span class="token6">45.164</span><span class="token1">,</span><span class="token6">94.673</span><span class="token1">)</span><span class="token1">)</span><span class="token1">;</span>
<span class="token4">return</span> <span class="token3">fract</span><span class="token1">(</span><span class="token3">sin</span><span class="token1">(</span>dot_product<span class="token1">)</span> <span class="token">*</span> <span class="token6">43758.5453</span><span class="token1">)</span><span class="token1">;</span>
```
```
本例中,`seed4`是参数`i`和`seed`的组成的vec4向量(这样才会是在4个位置做采样)。参数seed的值可以选用`gl_FragCoord`(像素的屏幕坐标),或者`Position_worldspace`:
```
<pre class="calibre16">```
<span class="token2">// - A random sample, based on the pixel's screen location.</span>
<span class="token2">// No banding, but the shadow moves with the camera, which looks weird.</span>
int index <span class="token">=</span> <span class="token3">int</span><span class="token1">(</span><span class="token6">16.0</span><span class="token">*</span><span class="token3">random</span><span class="token1">(</span>gl_FragCoord<span class="token1">.</span>xyy<span class="token1">,</span> i<span class="token1">)</span><span class="token1">)</span><span class="token">%</span><span class="token6">16</span><span class="token1">;</span>
<span class="token2">// - A random sample, based on the pixel's position in world space.</span>
<span class="token2">// The position is rounded to the millimeter to avoid too much aliasing</span>
<span class="token2">//int index = int(16.0*random(floor(Position_worldspace.xyz*1000.0), i))%16;</span>
```
```
这样做之后,上图中的那种条带就消失了,不过噪点却显现出来了。不过,一些“漂亮的”噪点可比上面那些条带“好看”多了。
![](https://box.kancloud.cn/2015-11-02_5636f30ad5703.png)
上述三个例子的实现请参见tutorial16/ShadowMapping.fragmentshader。
## 深入研究
即使把这些技巧都用上,仍有很多方法可以提升阴影质量。下面是最常见的一些方法:
## 早优化(Early bailing)
不要把采样次数设为16,太大了,四次采样足矣。若这四个点都在光明或都在阴影中,那就算做16次采样效果也一样:这就叫过早优化。若这些采样点明暗各异,那你很可能位于阴影边界上,这时候进行16次采样才是合情理的。
## 聚光灯(Spot lights)
处理聚光灯这种光源时,不需要多大的改动。最主要的是:把正交投影矩阵换成透视投影矩阵:
```
<pre class="calibre16">```
glm<span class="token1">:</span><span class="token1">:</span>vec3 <span class="token3">lightPos</span><span class="token1">(</span><span class="token6">5</span><span class="token1">,</span> <span class="token6">20</span><span class="token1">,</span> <span class="token6">20</span><span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 depthProjectionMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span>perspective<span class="token"><</span>float<span class="token">></span><span class="token1">(</span><span class="token6">45.0</span>f<span class="token1">,</span> <span class="token6">1.0</span>f<span class="token1">,</span> <span class="token6">2.0</span>f<span class="token1">,</span> <span class="token6">50.0</span>f<span class="token1">)</span><span class="token1">;</span>
glm<span class="token1">:</span><span class="token1">:</span>mat4 depthViewMatrix <span class="token">=</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">lookAt</span><span class="token1">(</span>lightPos<span class="token1">,</span> lightPos<span class="token">-</span>lightInvDir<span class="token1">,</span> glm<span class="token1">:</span><span class="token1">:</span><span class="token3">vec3</span><span class="token1">(</span><span class="token6">0</span><span class="token1">,</span><span class="token6">1</span><span class="token1">,</span><span class="token6">0</span><span class="token1">)</span><span class="token1">)</span><span class="token1">;</span>
```
```
大部分都一样,只不过用的不是正交视域四棱锥,而是透视视域四棱锥。考虑到透视除法,采用了texture2Dproj。(见“第四课——矩阵”的脚注)
第二步,在shader中,把透视考虑在内。(见“第四课——矩阵”的脚注。简而言之,透视投影矩阵根本就没做什么透视。这一步是由硬件完成的,只是把投影的坐标除以了w。这里在着色器中模拟这一步操作,因此得自己做透视除法。顺便说一句,正交矩阵产生的齐次向量w始终为1,这就是为什么正交矩阵没有任何透视效果。)
用GLSL完成此操作主要有两种方法。第二种方法利用了内置的`textureProj`函数,但两种方法得出的效果是一样的。
```
<pre class="calibre16">```
<span class="token4">if</span> <span class="token1">(</span> <span class="token3">texture</span><span class="token1">(</span> shadowMap<span class="token1">,</span> <span class="token1">(</span>ShadowCoord<span class="token1">.</span>xy<span class="token">/</span>ShadowCoord<span class="token1">.</span>w<span class="token1">)</span> <span class="token1">)</span><span class="token1">.</span>z <span class="token"><</span> <span class="token1">(</span>ShadowCoord<span class="token1">.</span>z<span class="token">-</span>bias<span class="token1">)</span><span class="token">/</span>ShadowCoord<span class="token1">.</span>w <span class="token1">)</span>
<span class="token4">if</span> <span class="token1">(</span> <span class="token3">textureProj</span><span class="token1">(</span> shadowMap<span class="token1">,</span> ShadowCoord<span class="token1">.</span>xyw <span class="token1">)</span><span class="token1">.</span>z <span class="token"><</span> <span class="token1">(</span>ShadowCoord<span class="token1">.</span>z<span class="token">-</span>bias<span class="token1">)</span><span class="token">/</span>ShadowCoord<span class="token1">.</span>w <span class="token1">)</span>
```
```
## 点光源(Point lights)
大部分是一样的,不过要做深度立方体贴图(cubemap)。立方体贴图包含一组6个纹理,每个纹理位于立方体的一面,无法用标准的UV坐标访问,只能用一个代表方向的三维向量来访问。
空间各个方向的深度都保存着,保证点光源各方向都能投射影子。T
## 多个光源组合
该算法可以处理多个光源,但别忘了,每个光源都要做一次渲染,以生成其阴影贴图。这些计算极大地消耗了显存,也许很快你的显卡带宽就吃紧了。
## 自动光源四棱锥(Automatic light frustum)
本课中,囊括整个场景的光源四棱锥是手动算出来的。虽然在本课的限定条件下,这么做还行得通,但应该避免这样的做法。如果你的地图大小是1Km x 1Km,你的阴影贴图大小为1024x1024,则每个纹素代表的面积为1平方米。这么做太蹩脚了。光源的投影矩阵应尽量紧包整个场景。
对于聚光灯来说,只需调整一下范围就行了。
对于太阳这样的方向光源,情况就复杂一些:光源**确实**照亮了整个场景。以下是计算方向光源视域四棱锥的一种方法:
潜在阴影接收者(Potential Shadow Receiver,PSR)。PSR是这样一种物体——它们同时在【光源视域四棱锥,观察视域四棱锥,以及场景包围盒】这三者之内。顾名思义,PSR都有可能位于阴影中:相机和光源都能“看”到它。
潜在阴影投射者(Potential Shadow Caster,PSC)= PSR + 所有位于PSR和光源之间的物体(一个物体可能不可见但仍然会投射出一条可见的阴影)。
因此,要计算光源的投影矩阵,可以用所有可见的物体,“减去”那些离得太远的物体,再计算其包围盒;然后“加上”位于包围盒与广元之间的物体,再次计算新的包围盒(不过这次是沿着光源的方向)。
这些集合的精确计算涉及凸包体的求交计算,但这个方法(计算包围盒)实现起来简单多了。
此法在物体离开视域四棱锥时,计算量会陡增,原因在于阴影贴图的分辨率陡然增加了。你可以通过多次平滑插值来弥补。CSM(Cascaded Shadow Map,层叠阴影贴图法)无此问题,但实现起来较难。
## 指数阴影贴图(Exponential shadow map)
指数阴影贴图法试图借助“位于阴影中的、但离光源较近的片断实际上处于‘某个中间位置’”这一假设来减少走样。这个方法涉及到偏差,不过测试已不再是二元的:片断离明亮曲面的距离越远,则其越显得黑暗。
显然,这纯粹是一种障眼法,两物体重叠时,瑕疵就会显露出来。
## LiSPSM(Light-space perspective Shadow Map,光源空间透视阴影贴图)
LiSPSM调整了光源投影矩阵,从而在离相机很近时获取更高的精度。这一点在“duelling frustra”现象发生时显得尤为重要。所谓“duelling frustra”是指:点光源与你(相机)距离远,『视线』方向又恰好与你的视线方向相反。离光源近的地方(即离你远的地方),阴影贴图精度高;离光源远的地方(即离你近的地方,你最需要精确阴影贴图的地方),阴影贴图的精度又不够了。
不过LiSPSM实现起来很难。详细的实现方法请看参考文献。
CSM(Cascaded shadow map,层叠阴影贴图)CSM和LiSPSM解决的问题一模一样,但方式不同。CSM仅对观察视域四棱锥的各部分使用了2~4个标准阴影贴图。第一个阴影贴图处理近处的物体,所以在近处这块小区域内,你可以获得很高的精度。随后几个阴影贴图处理远一些的物体。最后一个阴影贴图处理场景中的很大一部分,但由于透视效应,视觉感官上没有近处区域那么明显。
撰写本文时,CSM是复杂度/质量比最好的方法。很多案例都选用了这一解决方案。
## 总结
正如您所看到的,阴影贴图技术是个很复杂的课题。每年都有新的方法和改进方案发表。但目前为止尚无完美的解决方案。
幸运的是,大部分方法都可以混合使用:在LiSPSM中使用CSM,再加PCF平滑等等是完全可行的。尽情地实验吧。
总结一句,我建议您坚持尽可能使用预计算的光照贴图,只为动态物体使用阴影贴图。并且要确保两者的视觉效果协调一致,任何一者效果太好/太坏都不合适。