阴影映射(Shadow Mapping)示例

这个示例展示了如何从一个单向光源实现一个基本的阴影映射,而阴影贴图的视场和投影可以匹配相机的视锥体。你可以使用这个示例让一个大场景中的多个动态对象投下动态阴影。

原文地址:http://creators.xna.com/en-US/sample/shadowmapping1。

什么是阴影映射(Shadow Mapping)?

阴影映射是指一种产生阴影的技术,你可以将对象离开光源的距离存储在一张纹理中。 当绘制场景时,你可以使用这个离开光源的距离判断要绘制的像素是否在存储在阴影贴图中的值之后。

阴影映射技术需要绘制场景两次。第一次从光源视场中绘制可以产生阴影的所有对象,这些对象叫做遮蔽体(occluders)。这意味着你需要创建一个位于场景光源位置和光源方向的视矩阵和投影矩阵。对象的深度信息存储在一个渲染目标中。渲染目标通常使用的格式是一个32位浮点类型的SurfaceFormat.Single,它可以以32位的精度存储对象的深度值。但是有些显卡不支持这个格式,所以如果你使用的是这种类型的显卡,你必须使用16位浮点数的SurfaceFormat.HalfSingle。更老的显卡完全不支持浮点数格式的渲染目标,需要一个诸如 SurfaceFormat.Rgba32之类的表面格式,在这种情况中,要将距离放置在4个8位的通道中。

第二步是从相机视场中绘制场景。判断每个像素离开光源的距离。然后采样存储在阴影贴图中的值。如果阴影贴图中的距离小于像素离开光源的距离,你就知道这个像素被另一个物体遮挡,它会处于阴影之中。

从光源视场和相机视场绘制场景

上图表示如何从光源视场和相机视场中绘制场景。红色矩形表示最终的后备缓冲的内容。蓝色矩形表示存储在阴影贴图中的内容。注意观察位于橙色球体后面的灰色矩形只有特定部分才位于阴影中。

当使用阴影贴图绘制场景时会有一些显示错误。一个错误与存储深度值的表面格式有关。你使用的这些值限制了判断离开光源距离的精度。这会导致某些距离接近的地方阴影会不正确。这个问题在脸部的自阴影中最为明显 ,这种错误叫做shadow acne(译者:acne意味痤疮粉刺,很形象)。下图展示了一个shadow acne例子。通常可以使用一个bias偏移量修正这些错误。

shadow acne

另一个场景的错误是源自渲染目标中像素的限制和渲染目标覆盖的范围。如果你将一个很大的区域绘制到一个渲染目标中(就像这个示例中做的那样),那么一个图素(texel)覆盖的区域就很大。当镜头拉近时,你会注意到阴影边缘不是平滑的,这种情况叫做锯齿(aliasing),是由覆盖场景的阴影贴图分辨率不够造成的,下图展示了锯齿的例子。

锯齿

另一个错误只会发生在将一个对象的一部分绘制到阴影贴图的情况中,这会导致阴影中产生一个洞,这种情况通常发生在当对象位于阴影贴图的边缘时。本例中这种情况发生在当相机移动到模型后面时,此时相机的视锥体不包含模型的某一部分,所以,阴影贴图也不包含模型的某一部分,如下图所示。

空洞

示例简介

阴影映射算法的第一步是从相机视角绘制场景。要做到这步,你需要视矩阵和投影矩阵。要最大限度地使用渲染目标的分辨率,这个视矩阵和投影矩阵要对应一个尽量小的视锥体。在本例中,视矩阵和投影矩阵对应相机视锥体的最小包围盒。这可以让阴影贴图只包含用户可见的部分,同时仍然包含完整的可见部分。

匹配视锥体

上图显示了相机视锥体,相机视锥体的角用来获取匹配光线方向的最小包围盒。这个技术是用于单向光源的基本阴影映射技术,但它还有一些限制,我们会在最后加以讨论。

最小Shader配置

Vertex Shader Model 2.0

Pixel Shader Model 2.0

示例控制

本示例使用以下键盘和手柄控制。

动作 键盘控制 手柄控制
旋转相机 UP、DOWN、LEFT和RIGHT方向键 右摇杆
移动相机 W, S, A和D 左摇杆
退出 ESC 或 ALT+F4 BACK

工作原理

本示例有4个主要部分:

  1. 创建渲染目标和深度缓冲
  2. 计算光源视矩阵和投影矩阵CreateLightViewProjectionMatrix
  3. 创建阴影贴图CreateShadowMap
  4. 使用阴影贴图DrawModelsWithShadow绘制场景

要创建一个纹理储存对象的深度,你需要使用RenderTarget2D创建一个渲染目标。这个示例使用一个长宽都为2048的浮点数类型的纹理SurfaceFormat.Single,这可以提高结果的精度。在写入阴影贴图时还需要使用DepthStencilBuffer,它存储了场景的z值,让你可以在阴影贴图中只存储离开光源最近的点。

要计算光源的视矩阵和投影矩阵,CreateLightViewProjectionMatrix方法获取光线方向上最匹配的相机视锥体BoundingBox。这需要根据光线方向旋转相机视锥体顶角,将顶角转换到光源空间。然后通过获取包围盒后表面中点的MinZ值计算光源的位置。

要创建视矩阵,需要使用光源旋转的逆矩阵将光源位置从光源空间转换到世界空间。然后使用光源位置和方向构建视矩阵。投影矩阵基于前面获得的包围盒的大小进行计算,其中X方向为宽度,Y方向为高度,Z方向为近裁平面和远裁平面间的距离。因为使用的是一个单向光,你需要使用正交矩阵。光线是平行的,所以阴影也需要平行。将光源的视矩阵和投影矩阵相乘获得光源的view projection矩阵。

要创建阴影贴图,CreateShadowMap方法首先调用SetRenderTarget方法设置渲染目标。然后保存当前深度模板缓存,设置图形设备的阴影深度模板缓存。然后将渲染目标清除为白色,因为1表示在阴影贴图中距离最远的对象(即位于远裁平面的对象),任何近于远裁平面的对象在阴影贴图中的值都会小于1。shader中的CreateShadowMap technique绘制场景中所有有阴影的几何体。CreateShadowMap_VertexShader将顶点转换到光源空间。CreateShadowMap_PixelShader将深度值写到阴影贴图中。

最后,你需要从相机视角绘制场景,这是在DrawWithShadowMap方法中实现的。使用shader中的DrawWithShadowMap technique绘制场景中的每个对象。 DrawWithShadowMap_VertexShader通过世界矩阵变换顶点并将结果存储在Output.WorldPos中。然后在DrawWithShadowMap_PixelShader中使用这个值确定像素在光照空间中的位置。像素的位置与存储在阴影贴图中的值作比较,这些值也是存储在光源空间中的。如果像素的深度大于存储在阴影贴图中的值,你就知道这个像素在某些对象之后—这个对象是一个遮蔽体(occluder)—需要绘制到阴影贴图。这样,你就知道了这个像素应该在阴影中。

示例的局限

这个示例展示了基本的技术,而且还有一些视觉错误。因为使用的是相机视锥体,所以绘制到阴影贴图中的场景的分辨率与此贴图相同。这会导致距离相机远的对象会采样阴影贴图中大量的像素,但距离相机近的对象需要一个更高的分辨率以防止出现锯齿。你可以在这个示例中找到几个处理精度和分辨率的技术,如果自己想加以改变,可以有两个选择:你可以提高阴影贴图的大小,或扩展视锥体的大小。

在视锥体之外的对象不会绘制到阴影贴图中,这会导致屏幕之外的对象不会在可视区域内投下阴影。一个简单但并不完美的方法是扩展相机投影矩阵创建的包围盒,使它包含更大的范围。还有很多高级的方法可以获取所有可能的遮蔽体,创建一个正确匹配所有遮蔽体和视锥体的包围盒。

源代码ShadowMapping.rar下载,注释已翻译成中文。

文件下载(已下载 2288 次)

发布时间:2010/6/7 下午1:41:21  阅读次数:12015

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号