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));
- }
- 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;
- }
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& lt;fClosestDepth); //cut back projection保證不會照亮聚光燈背后的像 素 fShadow=fShadow*(vShadowCrd.w>fBackProjCut); fShadow*=fSpotLight; return fKa*vLightColor+(fKd*fDiffuse*vLightColor+fKs*fSpecular)*fShadow;
}
由於SM算法基於圖像空間,所以有一些缺陷,如果視點與光源位置差異很大,會產生明顯走樣,陰影邊緣處會出現明顯的階梯狀,這可以使用靠近百分比過濾PCF來改善,因為SM紋理中每個紋素texel可能不是投影到單一屏幕像素上,紋理分辨率越低,走樣越嚴重。
這里的算法也不適用於點光源,僅僅在聚光燈時有效,要用於點光源,則需要3D紋理,進行6次渲染,過幾天把這個做一下。