天天看點

ShadowMap渲染陰影方法及問題

ShadowMap基于的原理:SM算法是一個2-pass繪制算法,第一pass從光源視點繪制場景,生成SM紋理,第2pass從視點視圖按正常方法繪制場景

從光源的位置觀察場景,這時候我們看不到的地方就是該有陰影的地方,于是可以使用比較像素到光源距離的方法來确定某個像素是否在陰影之中。

于是我們需要記錄我們看得到的像素的距離值,以便做比較。

首先,建立在光源位置處的觀察坐标系,這一步應該在CPU階段完成,這裡為描述友善,寫為HLSL代碼

這裡假設觀察方向指向原點

float3 dirZ = -normalize(lightPos);

float3 up = float3(0,0,1);

float3 dirX = cross(up, dirZ);

float3 dirY = cross(dirZ, dirX);

接下來,把場景中所有頂點變換到這個光源-觀察空間中(light-view-space)

實際中應使用矩陣進行變換,這裡直接做

   float4 vPos;

   vInPos.xyz-=vLightPos.xyz; //首先是平移變換

   vPos.x=dot(vInPos,x_dir);   //接下來是分别繞3個軸旋轉

   vPos.y=dot(vInPos,y_dir);

   vPos.z=dot(vInPos,z_dir);

   vPos.w=1;

然後進行light-view-space空間裡的投影變換,所使用的矩陣要根據光源的特點進行改變,如FOV等

最後将這時得到的結果渲染到一張紋理上,把它稱作shadowmap

這張紋理一般使用R16F,或者R32F格式,而一些內建顯示卡中或者OPENGL不支援這樣的格式,還可以使用整數紋理,這時需要對深度值進行壓縮。

渲染結束後,在紋理中儲存的就是在光源處能看見的像素到光源的距離了,為了節省shader指令,可以使用距離的平方

即在浮點紋理中為 return dot(vLightVec,vLightVec);

在整數紋理中為  float fDepth=dot(vLightVec,vLightVec);

return float4( floor(fDepth) / 256.0f, frac(fDepth), frac(fDepth), frac(fDepth), frac(fDepth));

通過把frac(fDepth)同時寫入藍色和alpha通道,這樣可以節省一條指令,否則需要另一個指令來填充這些通道

mov r2.gba r0.g   //r0.g包含frac(fDepth)

vertex shader

float4x4 matViewProjection;

float4x4 matProjection;

float4 vLightPos;

float fDistScale;

float fTime0_X;

struct VS_OUTPUT

{

float4 Position : POSITION0;

float3 vLightVec:TEXCOORD0;

};

VS_OUTPUT vs_main(float4 vInPos:POSITION )

{

VS_OUTPUT output;

output.Position = mul(vInPos, matViewProjection );

//光源運動,做demo時應在cpu階段完成

float3 vLightPos;

vLightPos.x=cos(1.321*fTime0_X);

vLightPos.z=sin(0.923*fTime0_X);

vLightPos.xz=normalize(vLightPos.xz)*100;

vLightPos.y=100;

//建立在光源處的坐标系,應在cpu完成

float3 z_dir=-normalize(vLightPos);

float3 up=float3(0,0,1);

float3 x_dir=cross(up,z_dir);

float3 y_dir=cross(z_dir,x_dir);

//将頂點變換到光源空間中,實際中應用矩陣變換

float4 vPos;

vInPos.xyz-=vLightPos.xyz;

vPos.x=dot(vInPos,x_dir);

vPos.y=dot(vInPos,y_dir);

vPos.z=dot(vInPos,z_dir);

vPos.w=1;

//在光源空間中投影,實際中應根據FOV等因素構造特殊的投影矩陣,這裡用預設的

output.Position=mul(vPos,matProjection);

//fDistScale用于把距離值規格化到[0,1]之間,fDistScale一般取1/farZClip

output.vLightVec=fDistScale*vInPos;

return( output );

pixel shader

float4 ps_main(float3 vLightVec:TEXCOORD0) : COLOR0

{

//壓縮深度值到整數紋理

float fDepth=dot(vLightVec,vLightVec);

return float4(floor(fDepth)/256.0f,frac(fDepth),frac(fDepth),frac(fDepth));

}

到這裡準備工作就結束了,接下來就是要渲染陰影的時候了

這時回到正常觀察位置即camera位置,不再在light-view-space中觀察

将場景中每個頂點再變換到light-view-space中,這是為了要找到頂點在light-view-space中的位置,以便與紋理中的距離值做比較

将已經變換的頂點再進行投影,再将投影平面坐标變換到紋理坐标空間,即把範圍為[-1,1]的x,y坐标變換到[0,1]的範圍中去,以便比對相應的紋素texel

然後就是光照,既然有陰影必然有光照效果,而且應該是逐像素光照,可以使用任何光照模型,phong,blinn或者Oren-Nayar

最後的距離比較決定是否為陰影,光照計算,都在像素shader中進行

float fBackProjCut;

float fKa;

float fKd;

float fKs;

float4 vLightColor;

float fShadowBias;

sampler ShadowMap;

sampler SpotLight;

float4 ps_main(

float3 vNormal:TEXCOORD0,

float3 vViewVec:TEXCOORD1,

float3 vLightVec:TEXCOORD2,

float4 vShadowCrd:TEXCOORD3) : COLOR0

{

vNormal=normalize(vNormal);

float fDepth=dot(vLightVec,vLightVec);

vLightVec=normalize(vLightVec);;

float fDiffuse=saturate(dot(vLightVec,vNormal));

float fSpecular=pow(saturate(dot(reflect(-normalize(vViewVec),vNormal),vLightVec)),16);

float3 vShadowMap=tex2Dproj(ShadowMap,vShadowCrd);

float fClosestDepth=vShadowMap.r*256+vShadowMap.g;

//光照圖,聚光燈

float fSpotLight=tex2Dproj(SpotLight,vShadowCrd).r;

//把像素的深度值與對應紋理中的深度值比較,得出陰影值0(無) or 1(有)

float fShadow=(fDepth-fShadowBias<fClosestDepth);

//cut back projection保證不會照亮聚光燈背後的像素

fShadow=fShadow*(vShadowCrd.w>fBackProjCut);

fShadow*=fSpotLight;

return fKa*vLightColor+(fKd*fDiffuse*vLightColor+fKs*fSpecular)*fShadow;

}

ShadowMap渲染陰影方法及問題
ShadowMap渲染陰影方法及問題

由于SM算法基于圖像空間,是以有一些缺陷,如果視點與光源位置差異很大,會産生明顯走樣,陰影邊緣處會出現明顯的階梯狀,這可以使用靠近百分比過濾PCF來改善,因為SM紋理中每個紋素texel可能不是投影到單一螢幕像素上,紋理分辨率越低,走樣越嚴重。

這裡的算法也不适用于點光源,僅僅在聚光燈時有效,要用于點光源,則需要3D紋理,進行6次渲染,過幾天把這個做一下。

繼續閱讀