Shadowmap核心思想

一.前言
這個教程主要面對DirectX9.0的初學者,文中代碼說明部分以DirectX9.0c SDK(August2006)中的ShadowMap Sample 爲例進行講解。如果沒有D3D矢量運算基礎,HLSL,或是對D3D流程不熟悉的朋友推薦《3D遊戲程序設計入門》(翁雲兵翻譯)這本電子文檔,圖書推薦《Visual C++/DirectX9 3D遊戲開發導引》(葉至軍)作爲入門讀物,另外directx9_c的幫助文件永遠是開發人員的參考手冊。
二.介紹
陰影貼圖(Shadow Map)是Williams在1978年提出的,可以說是所有陰影處理中最簡單的方法,圖1-1展示了使用這一方法的渲染效果.


圖1-1:圖中爲一立方體,在一聚光燈照射下地面呈現的陰影效果

三.原理
在陰影貼圖算法中,每個光源都相當於一個獨立的攝像機,都有個一個獨立的陰影貼圖。
(比如說場景需要兩盞燈光,那麼就需要建立三個攝像機,一號二號攝像機的位置和朝向與兩個燈光相同,三號攝像機爲真正渲染成最終畫面的攝像機。)
這裏的陰影貼圖(IDirect3DTexture9)不同於我們以往使用的D3DFMT_A8R8G8B8類型的彩色貼圖,而是D3DFMT_R32F即每點存儲32位灰度的格式。此外每個陰影貼圖還要使用深度模板(DepthStencilSurface)。

圖2-2展示了一個局外視角觀察的場景中攝像機,物體,燈光的全境。


圖2-2:攝像機座標系,世界座標系,燈光座標系。


立方體下的陰影可以用如下方法描述:
1.首先以燈光爲視點,渲染出一幅帶深度緩衝的平面陰影貼圖(Shadow Map),圖1-3展示的是以燈光爲視點我們我看到的平面陰影貼圖。



圖1-3:以燈光爲攝像機位置,我們所看到的圖像。

此平面圖就是一張32位灰度經過深度模版處理過的陰影貼圖,灰度代表以燈光視點出發,穿過投影面這點,形成的射線經過的所有場景中的點中最近的那個點地深度值,把該值通過某種映射變爲灰度值,並且該條射線所有穿過的點灰度值都被設定爲了這個灰度。
可以看到在陰影貼圖中我們是看不到任何陰影的,這符合我們的常識,圖中的顏色灰度記錄了從燈光視角點出發一條射線上所有頂點的Z深度值信息(我們把Z深度轉換爲灰度顏色渲染成圖,這些灰度我們成爲“深度相對灰度”),那些被遮擋的頂點恰恰是產生陰影的點。(有關Z深度解釋請查看前面提供的參考書)。

2.下面我們的任務就是要在正常座標系中找出這些被遮擋的點,然後把它們渲染成黑色。就完成了我門全部的工作。我們發現這些在燈光攝像機下的被遮擋點何其它點在正常攝像機下的區別是:把正常攝像機座標系的點變換到燈光座標系,然後投影變換,得到了和圖1-3相同視角的圖.


圖1-4:沒有陰影貼圖的正常攝像機視角
那些被遮擋點代表深度相對灰度與陰影貼圖的代表灰度的顏色相比是大的,而其它亮區的點經過正常攝像機到燈光攝像機變換,燈光攝像機投影變換這兩次座標系變換後,深度相對灰度的顏色與陰影貼圖的灰度的顏色相比是相同的。

圖1-5顯示了這一比較渲染過程,A,B,C三點都是正常攝像機可以看到的點,但在燈光攝像機下被投影成了一條線上灰度相同爲A:Zlight=2,B:Zlight=2,C:Zlight=2 (均爲C點的深度相對灰度)的點,並被製成陰影貼圖。我們把正常攝像機下的點變換到燈座標系A,B,C,投影化,仍然保留了原來的深度相對灰度A:Zcamera=7,B:Zcamera=6,C:Zcamera=2 ,用原來的深度相對灰度和陰影圖同樣點的灰度作比較就可以得出該點是否處在陰影中的結論了。


3.我們就可以用Shader把這些點單獨處理成陰影。
四.代碼及註釋
我們主要分析DirectX9.0c SDK(August2006)中的ShadowMap例子。該源文件在
\Microsoft DirectX SDK (August 2006)\Samples\C++\Direct3D\ShadowMap中

這裏簡單提一下DXUT的結構:
DXUT入口函數是WinMain()

WinMain()依次調用: 
DXUTSetCursorSettings();//設定鼠標指針
DXUTInit();//初始化DXUT管理結構
DXUTCreateWindow();//創建窗口
                  設定消息處理函數爲用戶函數MsgProc()
MsgProc()再把消息傳遞KeyboardProc(),MouseProc()
DXUTCreateDevice()//創建D3D設備接口,
其中調用用戶函數OnCreateDevice()OnResetDevice()
DXUTMainLoop();//主循環
                 掉用OnFrameMove(),OnFrameRender()實現在處理運動,和渲染
DXUTGetExitCode()//銷燬接口
OnLostDevice(),OnDestroyDevice()

在MainLoop()之前我們都可以加入我們自己的初始化函數,比如InitializeDialogs()。
DXUTSetCallbackDeviceCreated( OnCreateDevice );
    DXUTSetCallbackDeviceReset( OnResetDevice );
    DXUTSetCallbackDeviceLost( OnLostDevice );
    DXUTSetCallbackDeviceDestroyed( OnDestroyDevice );
    DXUTSetCallbackMsgProc( MsgProc );
    DXUTSetCallbackKeyboard( KeyboardProc );
    DXUTSetCallbackMouse( MouseProc );
    DXUTSetCallbackFrameRender( OnFrameRender );
DXUTSetCallbackFrameMove( OnFrameMove );
是爲了讓DUXT在該調用的時候調用我們自己寫的函數,比如OnCreateDevice()

IsDeviceAcceptable()是測試硬件支持的用戶函數,
ModifyDeviceSettings()是更改設備調用的用戶函數,之後調用OnResetDevice()

我們的主要工作將集中在:
1.Main()裏的初始化
2. OnCreateDevice()OnResetDevice()
3.OnFrameMove(),OnFrameRender()
其中OnFrameRender()調用RenderScene()來輔助完成渲染。

ShadowMap.cpp文件

1。初始化:
首先定義ShadowMap貼圖的大小爲512*512
#define SHADOWMAP_SIZE 512

然後設定正常攝像機的位置
D3DXVECTOR3 vFromPt   = D3DXVECTOR3( 0.0f, 5.0f, -18.0f );
    D3DXVECTOR3 vLookatPt = D3DXVECTOR3( 0.0f, -1.0f, 0.0f );
    g_VCamera.SetViewParams( &vFromPt, &vLookatPt );

設定燈光攝像機的位置
vFromPt = D3DXVECTOR3( 0.0f, 0.0f, -12.0f );
    vLookatPt = D3DXVECTOR3( 0.0f, -2.0f, 1.0f );
    g_LCamera.SetViewParams( &vFromPt, &vLookatPt );

設定燈光的錐體張開角度大小
g_fLightFov = D3DX_PI / 2.0f;

燈光漫反射和位置
g_Light.Diffuse.r = 1.0f;
    g_Light.Diffuse.g = 1.0f;
    g_Light.Diffuse.b = 1.0f;
    g_Light.Diffuse.a = 1.0f;
    g_Light.Position = D3DXVECTOR3( -8.0f, -8.0f, 0.0f );
    g_Light.Direction = D3DXVECTOR3( 1.0f, -1.0f, 0.0f );
    D3DXVec3Normalize( (D3DXVECTOR3*)&g_Light.Direction, (D3DXVECTOR3*)&g_Light.Direction );
    g_Light.Range = 10.0f;
    g_Light.Theta = g_fLightFov / 2.0f;
g_Light.Phi = g_fLightFov / 2.0f;

OnCreateDevice()& OnResetDevice( )
創建效果接口
D3DXCreateEffectFromFile()

創建頂點聲明
CreateVertexDeclaration()

加載網格模型並把模型頂點格式變爲頂點聲明格式
g_Obj[i].m_Mesh.Create()
g_Obj[i].m_Mesh.SetVertexDecl()

設定正常攝像機和燈光攝像機的投影矩陣
g_VCamera.SetProjParams( D3DX_PI/4, fAspectRatio, 0.1f, 100.0f );
g_LCamera.SetProjParams( D3DX_PI/4, fAspectRatio, 0.1f, 100.0f );

創建默認紋理
CreateTexture(&g_pTexDef)

把燈光的漫反射,張角傳遞給Shader
SetVector( "g_vLightDiffuse", (D3DXVECTOR4 *)&g_Light.Diffuse ) );
SetFloat( "g_fCosTheta", cosf( g_Light.Theta ) ) );

創建ShadowMap紋理
V_RETURN( pd3dDevice->CreateTexture( SHADOWMAP_SIZE, SHADOWMAP_SIZE,
                                         1, D3DUSAGE_RENDERTARGET,
                                         D3DFMT_R32F,
                                         D3DPOOL_DEFAULT,
                                         &g_pShadowMap,
                                         NULL ) );

創建深度模版
V_RETURN( pd3dDevice->CreateDepthStencilSurface( SHADOWMAP_SIZE,
                                                     SHADOWMAP_SIZE,
                                                     D3DFMT_D24X8,
                                                     D3DMULTISAMPLE_NONE,
                                                     0,
                                                     TRUE,
                                                     &g_pDSShadow,
                                                     NULL ) );
設定ShadowMap的投影矩陣
D3DXMatrixPerspectiveFovLH( &g_mShadowProj, g_fLightFov, 1, 0.01f, 100.0f);

OnFrameMove()
更新模型位置,更新攝像機位置

OnFrameRender()
這是程序的重頭戲

首先判斷燈光是否爲自用移動狀態,爲了便於說明,以下我們僅以自由燈光爲例
if( g_bFreeLight )

把燈光攝像機變換矩陣賦予mLightView
   mLightView = *g_LCamera.GetViewMatrix();

然後把設備下的舊的RenderTarget,和DepthStencilSurface保存起來,並把新的RenderTarget設爲ShadowMap紋理下的表面,把DepthStencilSurface設爲我們要返回的目標,目的是把圖像渲染到ShadowMap紋理中,用g_pDSShadow保存深度信息,流程見圖1-6



然後調用RenderScene( pd3dDevice, true, fElapsedTime, &mLightView, &g_mShadowProj )
開始渲染ShadowMap紋理和深度模版

這裏我們把原程序改一下以便理解
渲染ShadowMap時,可以不進行SetVector( "g_vLightPos", &v4 )和SetVector( "g_vLightDir", &v4 )的操作。(SetVector( "g_vLightPos", &v4 )和SetVector( "g_vLightDir", &v4 )是第二次渲染正常攝像機時候需要傳讀的參數)
直接進行SetTechnique( "RenderShadow" )然後以燈光攝像機爲視點開始渲染深度

下面進入RenderShadow 的Shader代碼段

頂點處理
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 );//進行投影變換
Depth.xy = oPos.zw;//深度爲投影變換後的ZW分量
}

像素處理
void PixShadow( float2 Depth : TEXCOORD0,//輸入深度
out float4 Color : COLOR )//輸出顏色灰度
{
Color = Depth.x / Depth.y;//把深度兩個分量相除,得到顏色灰度,這就是我們說的深度到灰度的映射關係
}

下面我們分析一下代碼在做什麼

假定輸入頂點經過燈光攝像機座標系變化後的位置爲(x,y,z),

下面進行投影變換



該灰度被渲染到了ShadowMap紋理中,然後我們取出舊的RenderTarget面,和深度模版面,渲染正常攝像機的場景
在調用RenderScene( pd3dDevice, false, fElapsedTime, pmView, g_VCamera.GetProjMatrix() )前我們多了一個矩陣mViewToLightProj,這個矩陣目的是把正常攝像機座標系中的點變換到世界座標系下,在變換到燈光攝像機座標系下,在做投影變換。



圖中a點是最下方黑點的正常攝像機座標系下的向量,通過世界變換矩陣變到了b向量,再通過燈光攝像機矩陣變到了c,c在通過投影矩陣變到了燈光攝像機的投影空間。這個投影空間的深度灰度要比陰影貼圖的深度灰度大,因爲c的長度大於黑點在陰影貼圖中的深度d, 所以那個黑點是位於陰影區域的。

RenderScene():
設置SetTechnique( "RenderScene" )
正常攝像機的Shader代碼段
頂點處理
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 );//把正常攝像機座標系的頂點變到燈光投影面中
}
float4 PixScene( float2 Tex : TEXCOORD0,//座標
float4 vPos : TEXCOORD1,//頂點輸出位置
float3 vNormal : TEXCOORD2,//頂點法線
float4 vPosLight : TEXCOORD3 ) : COLOR//燈光攝像機的投影
{float4 Diffuse;
float3 vLight = normalize( float3( vPos - g_vLightPos ) );取得燈到點的向量

//如果這個向量在燈光主軸上的投影大於一個值則說明這個點超出了照射範圍
//應該給處理爲暗點
if( dot( vLight, g_vLightDir ) > g_fCosTheta ) // Light must face the pixel (within Theta)

//以下是投影空間到紋理空間的變換
{float2 ShadowTexC = 0.5 * vPosLight.xy / vPosLight.w + float2( 0.5, 0.5 );
        ShadowTexC.y = 1.0f - ShadowTexC.y;
        float2 texelpos = SMAP_SIZE * ShadowTexC;   

//以下分了四種情況進行灰度的判斷。並進行了空域濾波
float2 lerps = frac( texelpos );
        float sourcevals[4];
        sourcevals[0] = (tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f; 
        sourcevals[1] = (tex2D( g_samShadow, ShadowTexC + float2(1.0/SMAP_SIZE, 0) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f; 
        sourcevals[2] = (tex2D( g_samShadow, ShadowTexC + float2(0, 1.0/SMAP_SIZE) ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f; 
        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; 
        float LightAmount = lerp( lerp( sourcevals[0], sourcevals[1], lerps.x ),
                                  lerp( sourcevals[2], sourcevals[3], lerps.x ),
                                  lerps.y );


//如果是純陰影的話我們可以知道LightAmount的值是0,如果是燈光照射面的話//LightAmount的值是1,如果是影的邊緣的話,LightAmount值介於1和0之間。
Diffuse = ( saturate( dot( -vLight, normalize( vNormal ) ) ) * LightAmount * ( 1 - g_vLightAmbient ) + g_vLightAmbient )
                  * g_vMaterial;

//如果在燈照範圍之外的點進行下面處理
} else
    {
        Diffuse = g_vLightAmbient * g_vMaterial;
    }

//返回這個像素點在紋理貼圖的像素顏色,並乘了一個代表明暗的漫反射係數代表光照
return tex2D( g_samScene, Tex ) * Diffuse;
}



說明:
如果不考慮空域濾波,我們可以讓LightAmount =tex2D( g_samShadow, ShadowTexC ) + SHADOW_EPSILON < vPosLight.z / vPosLight.w)? 0.0f: 1.0f;

代碼的意思是,tex2D( g_samShadow, ShadowTexC )-----陰影貼圖上的該像素點的深度相應灰度。
vPosLight.z / vPosLight.w-----正常攝像機變到燈光投影后Z,W分量相除結果。
想一下這個變換公式:


就會發現我們對vPosLight.z / vPosLight.w進行了同樣的深度灰度映射,只不過我們沒有再渲染紋理。這樣兩個經過同樣處理的值可以進行比較了。比較的結果就是該點是否處於陰影區域的判據。
然後我們讓陰影區域中的象素點漫反射乘0渲染,在燈光區域中的象素點漫反射乘1渲染。

到此爲止ShadowMap 的思想和核心代碼已經講述完畢了。希望大家多研究研究代碼還是有好處的,越學會覺得自己知識越貧乏,這就對了.

 

原文鏈接http://school.ogdev.net/ArticleShow.asp?id=5724&categoryid=9

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章