Shadow Mapping

786 篇文章 9 订阅
480 篇文章 12 订阅
  1、什么是Shadow Mapping?

      Shadow Mapping是由Lance Williams于1978年在一篇"Casting curved shadows on curved surfaces"的文章中提出的,这篇文章是Shadow Map技术之根源。其实原理很简单,如果光源和目标点之间的连线没有任何物体阻挡的话,则目标点没有在阴影中;如果有物体遮挡,则目标点处在阴影中。而Shadow Map,就是一张记录了每个象素处用于比较遮挡关系信息的Textur。

      产生这个ShadowTexture的方法很简单,以SpotLight为例,把3D Camera放到光源的位置,把DepthTest打开,渲染场景,在PixShader中把每个象素的深度信息或者光源和此象素距离信息写到 RenderTarget上,由于DepthTest是打开的,保证了最终写到RenderTarget上的均是物体上未处在阴影中的点的深度值,实质完全可以等效为最终的DepthBuffer。

    得到这个Show Map之后,如何最终生成阴影呢?在PixShader对每个pixel进行处理时,算出当前象素与灯当的距离Dc,与存在 Shdow Map中的引像素的值Dz进行比较,如果Dc > Dz,则在阴影中,反之则被灯光照亮。

2、Shadow Map之HLSL的实现

    在Direct SDk中有Shadow Map的Sample,下面的Shader和Sample里面空全一样,只是加了一些注释便于理解。

    (1)生成Shadow Map的VS和PS
  1. //-----------------------------------------------------------------------------
  2. // Vertex Shader: VertShadow
  3. void VertShadow( float4 Pos : POSITION,
  4.                 float3 Normal : NORMAL,
  5.                 out float4 oPos : POSITION,
  6.                 out float2 Depth : TEXCOORD0 )
  7. {
  8.     //从模型坐标系变换到观察坐标系
  9.     oPos = mul( Pos, g_mWorldView );
  10.   //进行投影变换
  11.   oPos = mul( oPos, g_mProj );
  12.   //把投影坐标系的ZW值赋给Depth,作为PixelShader中的输出,这里的Z还是齐次坐标,这里不直接输出Z/W,我的理解是让Z和W都在 Rasterizer中进行线性插
  13.   //值,这样可以增加最终生成的Shadow Map的精度。
  14.     Depth.xy = oPos.zw;
  15. }
  16. //-----------------------------------------------------------------------------
  17. // Pixel Shader: PixShadow
  18. void PixShadow( float2 Depth : TEXCOORD0,
  19.                 out float4 Color : COLOR )
  20. {
  21.     // 把 z / w的值作为Color值输出,写到RenderTarget上,此时的RT formate是D3DFMT_R32F
  22.   //把Z/W目的是把齐次坐标Z变换到三维空间的非齐次坐标,范围则是[-1,1]
  23.     Color = Depth.x / Depth.y;
  24. }
(2)用Shadow Map生成Shadow
  1. //-----------------------------------------------------------------------------
  2. // Vertex Shader: VertScene
  3. // Desc: Process vertex for scene
  4. //-----------------------------------------------------------------------------
  5. void VertScene( float4 iPos : POSITION,
  6.                 float3 iNormal : NORMAL,
  7.                 float2 iTex : TEXCOORD0,
  8.                 out float4 oPos : POSITION,
  9.                 out float2 Tex : TEXCOORD0,
  10.                 out float4 vPos : TEXCOORD1,
  11.                 out float3 vNormal : TEXCOORD2,
  12.                 out float4 vPosLight : TEXCOORD3 )
  13. {
  14.     vPos = mul( iPos, g_mWorldView );
  15.     oPos = mul( vPos, g_mProj );
  16.     vNormal = mul( iNormal, (float3x3)g_mWorldView );
  17.     Tex = iTex;
  18.     //把当前顶点位置变换到以光源为Camera的投影空间,
  19.     vPosLight = mul( vPos, g_mViewToLightProj );
  20. }
  21. //-----------------------------------------------------------------------------
  22. // Pixel Shader: PixScene
  23. // Desc: Process pixel (do per-pixel lighting) for enabled scene
  24. //-----------------------------------------------------------------------------
  25. float4 PixScene( float2 Tex : TEXCOORD0,
  26.                 float4 vPos : TEXCOORD1,
  27.                 float3 vNormal : TEXCOORD2,
  28.                 float4 vPosLight : TEXCOORD3 ) : COLOR
  29. {
  30.     float4 Diffuse;
  31.     // 计算光源到当前象素方向向量并单位化
  32.     float3 vLight = normalize( float3( vPos - g_vLightPos ) );
  33.     //  dot( vLight, g_vLightDir )为光源到当前象素方向向量和光的方向向量之间的夹角余旋值,由于是spotlight,因此必须要在spotlight可照射的范围内。因为角
  34.     //度越小余旋值越大,因此这里是大于
  35.     if( dot( vLight, g_vLightDir ) > g_fCosTheta )
  36.     {
  37.         // Pixel is in lit area. Find out if it's
  38.         // in shadow using 2x2 percentage closest filtering
  39.         //从投影空间坐标转化为纹理空间坐标,也就是找到投影空间中的点和纹理空间中的点的对应关系
  40.       //除以w,xy坐标便处在(-1,1)的范围内,乘0.5加0.5,则变换到了(0,1)的范围,因texture space的u,v坐标是(0,1)的
  41.         float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 );
  42.       //在投影坐标系中,Y轴是向上的,而在纹理空间中Y轴向下,因此要作以下处理
  43.         ShadowTexC.y = 1.0f - ShadowTexC.y;
  44.         // 在texel space中对应的象素坐标
  45.         float2 texelpos = SMAP_SIZE * ShadowTexC;
  46.      
  47.         // 取得小数部分       
  48.         float2 lerps = frac( texelpos );
  49.         //这里使用的是2x2 percentage closest filtering,因此是采的邻近的四个点,判断它们是否在阴影中,
  50.         float sourcevals[4];
  51.         sourcevals[0] = (tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
  52.         sourcevals[1] = (tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 0) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
  53.         sourcevals[2] = (tex2D( g_samShadow, ShadowTexC + float2(0, 1.0/SMAP_SIZE) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
  54.         sourcevals[3] = (tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 1.0/SMAP_SIZE) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;
  55.      
  56.         // 用lerps
  57.         float LightAmount = lerp( lerp( sourcevals[0], sourcevals[1], lerps.x ),
  58.                                   lerp( sourcevals[2], sourcevals[3], lerps.x ),
  59.                                   lerps.y );
  60.         // 计算光照,如果完全在阴影中,则LightAmount为0,这里只计算了Diffuse color,没有高光
  61.         Diffuse = ( saturate( dot( -vLight, normalize( vNormal ) ) ) * LightAmount * ( 1 - g_vLightAmbient ) + g_vLightAmbient )
  62.                   * g_vMaterial;
  63.     } else
  64.     {
  65.         Diffuse = g_vLightAmbient * g_vMaterial;
  66.     }
  67.     return tex2D( g_samScene, Tex ) * Diffuse
  68. }

3、Shdow Map 的优缺点

    优点:简单,不需要知道场景中Object的Geometry,不需要Stencil Buffer,每个灯光只需多渲染一个Pass。

    缺点:当Shadow Map分辨率不够高时,或灯光与物体隔得很近时,在边缘处会产生Aliasing,锯齿,因此,很多改进shadow Map的算法都围绕着如何消除锯齿作文章。

4、Shadow Map的改进

    关于Shadow Map的改进,又出了很多的paper和技术,比如:Percentage Shadow map,  使用bloom filter对Shadow Map进行模糊处理.以及siggraph 2002 中Marc Stamminger和 George Drettakis提出的Perspective Shadow Map.以及Adaptive Shadow Map等等。

 

    阴影算法,在3D渲染中是很重要的一部分。阴影算法大致可以分为以下三类:基于ray tracing,基于shadow volume,基于shadow map(Z buffer).

    Ray tracing可以很自然地实现shadow,不需要特殊处理,但是ray tracing一般都用于离线渲染。Shadow Volume在实时渲染中也有应用,但是Shadow Volume依赖于geometry,而且Volume的生成是比较麻烦的事情。因此在实时渲染中,还是简单的Shadow map运用得最多,基于Shadow Map的论文也是层出不穷。

Shadow Map分为两个pass:

(1) 以灯光的位置作为视点,渲染整个场景,把深度值(Z值)写到一张texture(shadow map)中。

(2)以相机所在的位置作为视点,渲染整个场景,在PS中把每个象素P(x,y,z)转换到灯光所在的视空间中对应P`(x`,y`,z`),用(x`,y`)作为uv去采样shadow map中此点的z值Zmap,在与z`比较,如果z` > Zmap,此像素便在阴影中,如果z` < Zmap此像素便不在阴影中。

Shadow Map优点是简单,易于实现。但是Shadow map有alias(走样、锯齿)的问题。为了解决这个问题,很多人提出了很多改进的办法。

1、Percentage-Closer Filtering:

Percentage-Closer Filtering出自论文“Rendering Antialiased Shadows with Depth Maps”,NV的Sample 里面也有,该文采用”Percentage-closer filtering”的滤波方法来解决Shaodow map的走样问题。

其思想很简单,拿文章里的一个图为例,假如某像素转换到灯光视图空间中的Z值为49.8,把这个值与在Shadow Map中3X3的区域的Z值比较,如果49.8小于在Shadow map中对应的Z值,则记为0,表示不在阴影中,反之则记为1。这样得到了右边所示的3X3区域大小的9个值,在对这9个值取平均,得到0.55,以这个值作为在pixel shader中的阴影权值。此方法能够在一定程度上解决alias的问题,而且有软阴影效果。

2、Perspective Shadow Maps

  Perspective Shadow Maps来自论文:“Perspective Shadow Maps,SIGGRAPH 2002 by Stamminger and Drettakis”。

该论文把Shadow map的alias问题分为两类:Perspective alias和Project alias. Project alias是因为当灯光照射方向与物体表面夹角比较小时,使得多个pixel对应Shadow map中一个texel,产生alias问题,可以增大shadow map来解决此问题。Perspective alias产生的原因是因为透视透影会产生近大远小的效果,这使得近处的物体有可能多个pixel对应着Shadow map中一个texel, 产生alias问题。Perspective Shadow map就是用于解决这一问题。

Perspective Shadow Maps其实思想还是比较容易理解的。在生成shadow map时,首先将物体以及灯光变换到perspective space中,在perspective space中,整个空间是一个长方体,没有了近大远小的问题,在这个空间中,再以常规方法,以灯光作为视点,生成shadow map.

Perspective Shadow Maps有很多局限性,对光源的位置和类型都有要求,很多情况需要特殊处理,源文中列了一些需要特殊处理的情况。正因为这些限制,使得实现起来比较复杂。但是此论文开了解决Perspective alias的先河,有不少后续文章都是借鉴了此文思想。

Light Space Perspective Shadow Maps

该文出自论文:“Light Space Perspective Shadow Maps”,这篇论文是以Perspective Shadow Maps为基础的,是对其的改进。

“Light Space Perspective Shadow Maps”与Perspective Shadow maps的区别是,它在产生shadow map之前,不是先以Camera的View Frustrum作透视投影,而是在和灯光方向垂直的方向构建View Frustrum,以此View Frustrum把灯光和场景转换到Perspective space中,再计算Shadow map.这样的好处在于,平行光源转换后依然是平行光,点光源被转换成了平行光源,克服了Perspective Shadow Maps中的一些问题。

Parallel-Split Shadow Maps for Large-scale Virtual Environments

    该方法出自论文:” Parallel-Split Shadow Maps for Large-scale Virtual Environments”,

附件: PSSM.jpg

  如上图示,Parallel-Split Shadow Maps把View Frustrum按照Z的范围分成三个部份,再分别为这三个部分各自生成Shaodw Map。假如光源不是平行光,可先用Light space Shadow Map的方法转换到Light Space,此时光源便是平行光了。Parallex-Split Shadow Maps是关键在于如何对View Frustrum作合理的切分。

Variance Shadow Maps.

Variance Shadow Maps,来自William Donnelly和Andrew Lauritzen的“Variance Shadow Maps”。该方法利用概率论中的期望值、方差和切比雪夫不等式,实在是巧妙。

在前面的Percentage closer filter方法中,我们不能用纹理过滤方法(如高期滤波等)对Shadow Map进行预处理,因为预处理之后结果就会变得不正确,不能反映像素是否在阴影之中,因此只能采样一定范围取差值求平均。而Variance Shadow Maps就没有这个限制。它的方法其实很简单,分为如下几步:

(1)              像生成一般shadow map一样渲染,但是除了把每个像素的深度值d,以及深度的平主 d2记录下来。

(2)              利用一种虑波方法对Shadow map进行处理。

(3)              从相机处渲染场景,在PS中把每个象素P(x,y,z)转换到灯光所在的视空间中对应P`(x`,y`,z`),用(x`,y`)作为uv去采样shadow map中此点的z值和z2记为D和F,采样时可以采用硬件支持的Blinear或Trilinear和AF去采,此时:

期望值E(z) = D,E(z2)=F

那么方差就等于: variance = E(z2) - E(z)2 = F – D2

再根据切比雪夫不等式,计算出阴影参数即可,具体的公式可查阅论文或Nvida对应的Sample。(

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值