基于CPU实现的Shadow Map(阴影图)技术--(Cg语言实现)

Shadow Map是一种基于深度图(depth map)的阴影生成方法,由Lance Williams于1978年在文章“Casting curved shadows on curved surfaces”中首次提出。该方法的主要思想是:在第一遍渲染场景时,将场景的深度信息存放在纹理图片上,这个纹理图片称为深度图;然后在第二次渲染场景时,将深度图中的信息length1取出,和当前顶点与光源的距离length2做比较,如果length1小于length2,则说明当前顶点被遮挡处于阴影区,然后在片段着色程序中,将该顶点设置为阴影颜色。


13.1 什么是depth map

深度图是一张2D图片,每个像素都记录了从光源到遮挡物(遮挡物就是阴影生成物体)的距离,并且这些像素对应的顶点对于光源而言是“可见的”。这里的“可见”像素是指,以光源为观察点,光的方向为观察方向,设置观察矩阵并渲染所有遮挡物,最终出现在渲染表面上的像素。

Depth map中像素点记录的深度值记为lenth1;然后从视点的出发,计算物体顶点V到光源的距离,记为length2;比较length1与length2的大小,如果length2>length1,则说明顶点所对应的depth texure上的像素点记录的深度值,并不是V到光源的距离,而是V和光源中间某个点到光源的距离,这意味着“V被遮挡”。

在一些教程中,往往将depth map翻译成阴影贴图(shdaow texture),这实在是一个误解,不光误解了两个名称,也混淆了2种阴影算法。阴影贴图的英文为Shadow texture,就是将日常所见的阴影保存为纹理图片; Depth texture保存的是“从视点到物体顶点的距离,通常称为深度值”。图 39左边的子图来自wikipid上shadow map网页,请注意,下面表述为depth map;右边的子图则是一张普通的shadow texture。


此外, Shadow texture不但表示阴影贴图,也代表了一种阴影渲染方法,其实就是将阴影贴图作为纹理投影到物体上,投影的方法采用前面所讲述的texture projective方法。


13.2 Shadow map与shadow texture的区别

在很多中文资料中,论述Shadow map技术时,容易将Shadow map与shadow texture这两个不同的概念混淆;

在英文中map有映射和图片的双重含义在内,shadow map技术称为“shadow map”在英文中应该是准确的。中文翻译shadow map为阴影图,例如“实时计算机图形学第二版153页,第6.12.4节便将shadow map翻译为阴影图”,这种翻译已经是既成事实,那么我们也延续这种翻译方式。但是一定要知道“阴影图”和shadow texture所谓的阴影贴图是完全不同的两个概念。Shadow map以depth map为技术基础,通过比较“光源可见点到光源的深度”和“任何点到光源的深度”来判断点是否被物体遮挡;而shadow texture技术,将生成的阴影图形作为投影纹理来处理,也就是将一张阴影图投影映射到一个物体上(阴影接收体)。这种方法的缺点在于:设计者必须确认哪个物体是遮挡物,哪个物体是阴影接受体,并且不能产生自阴影现象(将一个物体的阴影贴图贴到物体身上,这是多么怪异)。


13.3 Shadow map原理与实现流程

使用Shadow Map技术渲染阴影主要分两个过程:生成depth map(深度图)和使用depth map进行阴影渲染。

生成depth map的流程为:

1. 以光源所在位置为相机位置,光线发射方向为观察方向进行相机参数设置;

2. 将世界视点投影矩阵worldViewProjMatrix传入顶点着色程序中,并在其中计算每个点的投影坐标,投影坐标的Z值即为深度值(将Z值保存为深度值只是很多方法中的一种)。在片段shadow程序中将深度值进行归一化,即转化到【0,1】区间。然后将深度值赋给颜色值(Cg最的颜色值范围在0-1之间)。

这里有一点要留心:depth map中保存的深度值到底是什么?很多文献都将depth map深度值解释成Z Buffer中的Z值,我对这种解释一直持怀疑态度!并不是说这种解释不对,而是指“这种解释有以偏概全的嫌疑”。我们通常所说的距离是指笛卡尔坐标空间中的欧几里得距离(Euclidean distance),Z值本身并不是这个距离(参阅第2.4.2节),此外我在研究GPU算法的过程中,看到的关于depth map中保存的深度值的计算方法远不止一种,有些直接计算顶点到视点的距离,然后归一化到【0,1】空间,同样可以有效的用于深度比较。由此可见,depth map中保存的深度值,是衡量“顶点到视点的距离”相对关系的数据,计算深度值的重点在于“保证距离间相对关系的正确性”,至于采用什么样的计算方法倒在其次。

3. 从frame buffer中读取颜色值,并渲染到一张纹理上,就得到了depth map。注意:在实际运用中,如果遇到动态光影,则depth map通常是实时计算的,这就需要场景渲染两次,第一次渲染出depth map,然后基于depth map做阴影渲染。渲染depth map的顶点着色程序和片段着色程序分别为:

代码18 渲染depth map的顶点着色程序

void main_v(float4 position : POSITION,

out float4 oPosition : POSITION,

out float2 depth : TEXCOORD0,

uniform float4x4 worldViewProj )

{

oPosition = mul(worldViewProj, position);

// 存放深度值

depth.x = oPosition.z;

depth.y = oPosition.w;

}

代码19 渲染depth map的片段着色程序

void main_f(float2 depth : TEXCOORD0,

out float4 result : COLOR,

uniform float pNear ,

uniform float pFar,

uniform float depthOffset )

{

float depthNum = 0.0;

//归一化到0-1空间

depthNum = (depth.x - pNear) / (pFar - pNear);

depthNum += depthOffset;

result.xyz = depthNum.xxx;

result.w = 1.0;

}

在代码 19的片段着色程序中,有一个外部输入变量depthOffset,该变量表示深度值的偏移量,这时因为:将深度值写入纹理颜色,会导致数据精度的损失,所以需要加上一个深度偏移量。这个偏移量自己设定,通常是0.01之类的微小数据。

使用depth map进行阴影渲染的流程为:

1. 将纹理投影矩阵传入顶点着色程序中。注意,这个纹理投影矩阵,实际上就是产生深度图时所使用的worldViewProjMatrix矩阵乘上偏移矩阵(具体参见第13章),根据纹理投影矩阵,和模型空间的顶点坐标,计算投影纹理坐标和当前顶点距离光源的深度值length2(深度值的计算方法要和渲染深度图时的方法保持一致)。

2. 将depth map传入片段着色程序中,并根据计算好的投影纹理坐标,从中获取颜色信息,该颜色信息就是深度图中保存的深度值lenght1。

3. 比较两个深度值的大小,若length2大于length1,则当前片断在阴影中;否则当前片断受光照射。顶点着色程序和片段着色程序如下所示:

代码20 使用depth map进行阴影渲染的顶点着色程序

void main_v(float4 position : POSITION,

float4 normal : NORMAL,

float2 tex : TEXCOORD,

out float4 outPos : POSITION,

out float4 outShadowUV : TEXCOORD0,

uniform float4x4 worldMatrix,

uniform float4x4 worldViewProj,

uniform float4x4 texViewProj)

{

outPos = mul(worldViewProj, position);

float4 worldPos = mul(worldMatrix, position);

// 计算投影纹理坐标

outShadowUV = mul(texViewProj, worldPos);

}

代码21 使用depth map进行阴影渲染的片段着色程序

void main_f(float4 position : POSITION,

float4 shadowUV : TEXCOORD0,

out float4 result : COLOR

uniform sampler2D shadowMap ,

uniform float pNear ,

uniform float pFar,

uniform float depthOffset,

uniform int pixelOffset)

{

//计算当前顶点和光源之间的距离(相对)

float lightDistance = (shadowUV.z -pNear) / (pFar - pNear);

lightDistance = lightDistance - depthOffset;

shadowUV.xy = shadowUV.xy/ shadowUV.w;

//进行多重采样,减小误差

float4 depths = float4(

tex2D(shadowMap, shadowUV.xy + float2(-pixelOffset, 0)).x,

tex2D(shadowMap, shadowUV.xy + float2(pixelOffset, 0)).x,

tex2D(shadowMap, shadowUV.xy + float2(0, -pixelOffset)).x,

tex2D(shadowMap, shadowUV.xy + float2(0, pixelOffset)).x);

float centerdepth = tex2D(shadowMap, shadowUV.xy).x;

//进行深度比较

float l_Lit = (lightDistance >= centerdepth? 0 : 1);

l_Lit += (lightDistance >= depths.x? 0 : 1);

l_Lit += (lightDistance >= depths.y? 0 : 1);

l_Lit += (lightDistance >= depths.z? 0 : 1);

l_Lit += (lightDistance >= depths.w? 0 : 1);

l_Lit *= 0.2f;

result = float4(l_Lit, l_Lit, l_Lit, 1.0);

}

图 40展示了使用shadow map方法得到的阴影渲染效果。


Shadow map方法的优点是可以使用一般用途的图形硬件对任意的阴影进行绘制,而且创建阴影图的代价与需要绘制的图元数量成线性关系,访问阴影图的时间也固定不变。此外,可以在基于该方法进行改进,创建软阴影效果。所谓软阴影就是光学中的半影区域。如果实时渲染软阴影,并运用到游戏中,是目前光照渲染领域的一个热门研究方向。

但Shadow map方法同样存在许多不足之处:

其一:阴影质量与阴影图的分辨率有关,所以很容易出现阴影边缘锯齿现象;

其二:深度值比较的精确度和正确性,有赖于depth map中像素点的数据精度,当生成深度图时肯定会造成数据精度的损失。要知道,深度值最后都被归一化到0,1空间中,所以看起来很小的精度损失也会影响数据比较的正确性,尤其是当两个点相聚非常近时,会出现z-fighting现象。所以往往在深度值上加上一个偏移量,人为的弥补这个误差;

其三:自阴影走样(Self-shadow Aliasing),光源采样和屏幕采样通常并不一定在完全相同的位置,当深度图保存的深度值与观察表面的深度做比较时,其数值可能会出现误差,而导致错误的效果,通常引入偏移因子来避免这种情况;

其四:这种方法只适合于灯类型是聚光灯(Spot light )的场合。如果灯类型是点光源(Point light)的话,则在第一步中需要生成的不是一张深度纹理,是一个立方深度纹理(cube texture)。如果灯类型是方向光(Directional light)的话,,则产生深度图时需要使用平行投影坐标系下的worldViewProjMatrix矩阵;

当前广泛使用的阴影算法中有一种被称之为模板(stencil)阴影算法。模板阴影算法在游戏中得到广泛的使用,在当前主流的开源图形引擎中,基本都集成了该算法。为了对比shadow map方法,特地在本书的附录C中对其进行阐述。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值