1、原理
阴影实时渲染是计算机图形学的高级技术。它能提高场景的真实感。两种通用的阴影渲染技术分别是地图阴影(shadow map)和体积阴影(shadow volumes)。地图阴影的优势在于效率很高,因为地图阴影只需要渲染场景两次(一般来说)。并且不需要进行几何处理和产生额外的mesh。无论多复杂的场景,使用地图阴影总能保持很好的性能。
地图阴影的概念很直观。首先,从光线的方向把整个场景渲染到一张纹理上。通过特定的顶点和像素渲染器渲染得到的这张纹理就被叫做地图阴影(shadow map)。这张纹理的特点是其上每一个像素记录的不是该像素的颜色,而是像素的深度。记某个像素点的深度为Di。
接下来再一次渲染场景,本次渲染过程中,计算每一个与地图阴影对应的每个像素点到光源的距离,记作Dj。然后把Di与Dj进行比较,如果两者匹配,该像素不在阴影中。如果Di小于Dj,该像素在阴影中。
最后,根据比较结果,像素渲染器更新每一个像素的颜色。渲染地图阴影最适合的灯光是聚光灯(spotlight),因为地图阴影通过投影场景到其上面而被渲染。
程序初始化阶段,范例创建一张D3DFMT_R32F格式的纹理来承载shadowmap,选择这种格式的纹理是因为地图阴影只需要一个通道(记录深度),32位能够提供足够的精度。渲染由两个部分组成:
1、创建阴影地图。
2、通过阴影地图渲染场景。
2、创建地图阴影(ShadowMap)
地图阴影首先将用于承载地图阴影的纹理作为渲染目标。
LPDIRECT3DSURFACE9 pOldRT = NULL; V( pd3dDevice->GetRenderTarget( 0, &pOldRT ) ); LPDIRECT3DSURFACE9 pShadowSurf; if( SUCCEEDED( g_pShadowMap->GetSurfaceLevel( 0, &pShadowSurf ) ) ) { pd3dDevice->SetRenderTarget( 0, pShadowSurf ); SAFE_RELEASE( pShadowSurf ); } |
创建地图阴影的两个渲染器分别是VertShader和PixShader。VertShader将输入的坐标转化到灯光的投影空间:
void VertShadow( float4 Pos : POSITION, float3 Normal : NORMAL, out float4 oPos : POSITION, out float2 Depth : TEXCOORD0 ) { oPos = mul( Pos, g_mWorldView ); oPos = mul( oPos, g_mProj ); } |
g_mProj就是灯光投影空间,它的定义是:
D3DXMatrixPerspectiveFovLH( &g_mShadowProj, g_fLightFov, 1, 0.01f, 100.0f); |
转化到这个投影空间就意味着摄像机从位于灯光的位置,且朝着灯光的方向望去。然后,VertexShader又传递纹理坐标和投影的z和w到像素渲染器:
Depth.xy = oPos.zw; |
此时,像素渲染器每个像素都有了独一无二的z和w值。像素渲染器输出z/w值到渲染目标。
void PixShadow( float2 Depth : TEXCOORD0, out float4 Color : COLOR ) { Color = Depth.x / Depth.y; } |
这个值代表了场景中每个像素的深度,它的取值范围在0~1之间。在近剪裁面处,它的值是0,在远剪裁面处,他的值是1。渲染完成后,阴影地图就包含了每个像素的深度值。
3、渲染场景
带阴影的场景通过VertScene和PixScene两个渲染器渲染得到。VertScene将场景中的顶点转化到到投影坐标系,传递纹理坐标到像素渲染器。另外,它还输出:各顶点和法线在视(view space)内的坐标vPos和vNormal。再另外,它还输出:各顶点在灯光投影空间内的坐标vPosLight。
void VertScene( float4 iPos : POSITION, float3 iNormal : NORMAL, float2 iTex : TEXCOORD0, out float4 oPos : POSITION, out float2 Tex : TEXCOORD0, out float4 vPos : TEXCOORD1, out float3 vNormal : TEXCOORD2, out float4 vPosLight : TEXCOORD3 ) {
vPos = mul( iPos, g_mWorldView ); oPos = mul( vPos, g_mProj ); vNormal = mul( iNormal, (float3x3)g_mWorldView ); Tex = iTex; vPosLight = mul( vPos, g_mViewToLightProj ); } |
此时的g_mProj是g_VCamera.GetProjMatrix()。也就是说是观察者摄像机的投影方向(注意区别于1中g_mProj)。其中,vPos和vNormal是在后续程序中用于光线计算。vPosLight是shadowmap的纹理坐标。vPosLight这个坐标是如何获得的呢?如果观察者相机位于灯光处且观察方向与灯光方向相同,通过将世界坐标变换到灯光视坐标,再进行灯光投影变换得到!
const D3DXMATRIX *pmView = g_bCameraPerspective ? g_VCamera.GetViewMatrix() : &mLightView; ...... D3DXMATRIXA16 mViewToLightProj; mViewToLightProj = *pmView; D3DXMatrixInverse( &mViewToLightProj, NULL, &mViewToLightProj ); D3DXMatrixMultiply( &mViewToLightProj, &mViewToLightProj, &mLightView ); D3DXMatrixMultiply( &mViewToLightProj, &mViewToLightProj, &g_mShadowProj ); V( g_pEffect->SetMatrix( "g_mViewToLightProj", &mViewToLightProj ) ); |
像素着色器测试每个像素是否在阴影内。首先,像素着色其检测该点是否位于光圈中(聚光灯形成的那个亮的区域)。
if( dot( vLight, g_vLightDir ) > g_fCosTheta ) |
其中,g_vLightDir是灯光的方向,vLight是某像素到灯光的方向。
float3 vLight = normalize( float3( vPos - g_vLightPos ) ); |
如果位于光圈内,检查它是否位于阴影内。这是通过把vPosLight(灯光位置在灯光投影空间内的坐标)转化到0到1的范围内,
float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 ); |
目的是为了将它与shadowmap的贴图匹配。
由于在贴图空间内,坐标轴y是向下的,与投影坐标系内正好相反,所以要把投影坐标系里的y值反向。
ShadowTexC.y = 1.0f - ShadowTexC.y; |
接下来,通过该坐标对shadowmap进行检查。
首先获得shadowmap上某点的颜色值,此时的颜色值代表的是深度(前面处理过的)。
tex2D(g_samShadow, ShadowTexC ) |
然后把它和当前渲染的图片对应shadowmap上的“某点”的像素值与灯泡的距离
vPosLight.z / vPosLight.w |
进行比较,如果shadowmap的深度大,则该点没有在阴影中。
没在阴影中的话,它的‘源值’(sourcevals = source values),意思应该就是说,光就强。设其源值为1.0.否则为0,表示光线弱.
float sourcevals; sourcevals = ((tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON) < (vPosLight.z / vPosLight.w))? 0.0f: 1.0f; |
最后,由这个‘源值’来控制该点的Diffuss。从而渲染出带阴影的场景。
Diffuse = (sourcevals * ( 1 - g_vLightAmbient ) + g_vLightAmbient ) * g_vMaterial; |
其中SDK的例子中还使用了2×2邻近点过滤的办法什么意思呢?就是说shadowmap上的像素点和当前渲染的图片的像素点不是一一对应的。它是从shadowmap上取4个点
tex2D( g_samShadow, ShadowTexC ) tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 0) ) tex2D( g_samShadow, ShadowTexC + float2(0, 1.0/SMAP_SIZE) ) tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 1.0/SMAP_SIZE) ) |
仔细一看就知道,这四点分别是对应点,对应点左边,下边和左下边的点。比较完以后就存入
float sourcevals[4]; |
中。然后用‘邻近点过滤’的办法来综合处理这四个点来得到一个‘光总量’:
float LightAmount |
然后再用‘光总量’来控制Diffuss,从而渲染出带阴影的场景。光总量的获得也很简单,lerp是个HLSL的函数,原型为ret lerp(x,y,s),含义是:ret = x + s(y - x)。leprs是是texelpos的小数部分, SMAP_SIZE * ShadowTexC的结果带有小数部分,小数部分表示shadowmap和场景渲染对应点之间错开的一小段距离。此时邻近点过滤的方法认为,该点的对错开的距离与权重成反比。
float2 texelpos = SMAP_SIZE * ShadowTexC; float2 lerps = frac( texelpos ); ...... float LightAmount = lerp( lerp( sourcevals[0], sourcevals[1], lerps.x ), lerp( sourcevals[2], sourcevals[3], lerps.x ), lerps.y ); |