之前一直沒有自己實現過陰影,只是概念上有所了解,這次通過Demo進行實際編寫操作。
總的來說沒有什么可以優化的,倒是對於窗戶這種可用面片代替的物體似乎能優化到貼圖上,之前arm有個象棋屋的demo做過這個
來說回Shadowmap,主要思想是通過深度圖可得到世界坐標位置,所以光源位置渲染一張場景深度圖以得到光源位置像素點的世界坐標,
再對比主相機的像素點世界坐標,如果兩個世界坐標距離小於誤差則說明兩者都能看見這個點,則這個點不在陰影內,否則在陰影區域內
當然實際做起來有許多更高效的做法。而A相機內的像素點如何切換到B相機這樣的問題,可以通過投影變換來實現,也就是
Camera.main.WorldToViewportPoint。將世界坐標位置轉換為視口坐標,0-1的范圍,直接適用於貼圖采樣。
算是講的比較簡單直白,網上一些操作我都省了,那么來看看具體的操作步驟。
1.在光源子節點上掛載一個相機,用正交顯示即可,但光源相機要可看見待投射陰影對象的模型。然后給這個光源相機掛載相機渲染深度的腳本,這里偷懶直接用OnRenderImage。

public class LightShadowMapFilter : MonoBehaviour { public Material mat; void Awake() { GetComponent<Camera>().depthTextureMode |= DepthTextureMode.Depth; } void OnRenderImage(RenderTexture source, RenderTexture destination) { Graphics.Blit(source, destination, mat); } }
2.這個腳本需要一個材質球參數,這個材質球的shader即為返回深度的Shader,但為了方便這里直接返回世界坐標信息,如下:

Shader "ShadowMap/DepthRender"//渲染深度 { Properties { } SubShader { Tags { "RenderType"="Opaque" } LOD 100 ZTest Always Cull Off ZWrite Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; }; sampler2D _CameraDepthTexture;//光源相機傳入的深度圖 #define NONE_ITEM_EPS 0.99//如果沒有渲染到物體就比較麻煩,所以設置一個EPS閾值 v2f vert(appdata_img v) { v2f o = (v2f)0; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.texcoord.xy; return o; } float4 GetWorldPositionFromDepthValue(float2 uv, float linearDepth)//通過深度得到世界坐標位置 { float camPosZ = _ProjectionParams.y + (_ProjectionParams.z - _ProjectionParams.y) * linearDepth; float height = 2 * camPosZ / unity_CameraProjection._m11; float width = _ScreenParams.x / _ScreenParams.y * height; float camPosX = width * uv.x - width / 2; float camPosY = height * uv.y - height / 2; float4 camPos = float4(camPosX, camPosY, camPosZ, 1.0); return mul(unity_CameraToWorld, camPos); } fixed4 frag (v2f i) : SV_Target { float rawDepth = SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture, i.uv); float linearDepth = Linear01Depth(rawDepth); if (linearDepth > NONE_ITEM_EPS) return 0;//如果沒有物體則返回0 return fixed4(GetWorldPositionFromDepthValue(i.uv, linearDepth).xyz, 1);//如果有物體則返回世界坐標信息 } ENDCG } } }
3.編輯器內給光源相機綁定RenderTexture到RenderTarget,直接在Project面板里創建,分辨率設置為1024即可,光源這部分就結束了。
4.接下來開始處理主相機的邏輯。根據官方論壇的信息camera WorldToViewportPoint的等價實現在這兒:
https://forum.unity.com/threads/camera-worldtoviewportpoint-math.644383/
由於合並到一個矩陣內不直觀,最后一步投影坐標到NDC再到視口的操作放到shader中去處理。
那么先處理光源深度圖和光源相機VP矩陣傳入的腳本,也就是論壇帖子里前兩步操作,如下:

using System.Collections; using System.Collections.Generic; using System; using UnityEngine; public class ShadowMapArgumentUpdate : MonoBehaviour { public RenderTexture lightWorldPosTexture; public Camera depthCamera; void Update() { var viewMatrix = depthCamera.worldToCameraMatrix; var projMatrix = GL.GetGPUProjectionMatrix(depthCamera.projectionMatrix, false) * viewMatrix; Shader.SetGlobalTexture("_LightWorldPosTex", lightWorldPosTexture); Shader.SetGlobalMatrix("_LightProjMatrix", projMatrix); } }
注意投影矩陣要經過GL類的轉換函數處理,防止OpenGL和DX不一致。隨后這個腳本掛載至主相機或某個GameObject上都可
需要掛載的RenderTexture和深度相機就是之前創建的。
5.最后是接收Shadowmap的shader。並對兩個世界坐標的像素位置進行距離上的比較,當然比較深度大小更常規一些。

Shader "ShadowMap/ShadowMapProcess" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_fog #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; float2 uv : TEXCOORD0; float3 worldPos : TEXCOORD1; }; sampler2D _LightWorldPosTex; sampler2D _MainTex; matrix _LightProjMatrix; float4 _MainTex_ST; //https://forum.unity.com/threads/camera-worldtoviewportpoint-math.644383/ float3 Proj2ViewportPosition(float4 pos) { float3 ndcPosition = float3(pos.x / pos.w, pos.y / pos.w, pos.z / pos.w); float3 viewportPosition = float3(ndcPosition.x*0.5 + 0.5, ndcPosition.y*0.5 + 0.5, -ndcPosition.z); return viewportPosition; } v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); o.worldPos = mul(unity_ObjectToWorld, v.vertex).xyz;//世界坐標 return o; } #define EPS 0.01//兩個像素點最小距離差 fixed4 frag (v2f i) : SV_Target { float4 col = tex2D(_MainTex, i.uv); float3 lightViewportPosition = Proj2ViewportPosition(mul(_LightProjMatrix, float4(i.worldPos,1))); //這個就是Camera.main.WorldToViewport的后半部分處理 float4 lightWorldPos = tex2D(_LightWorldPosTex, lightViewportPosition.xy); //既然是視口坐標了直接采樣貼圖即可 if (lightWorldPos.a > 0 && distance(i.worldPos, lightWorldPos) > EPS) return col * 0.2; //兩個像素點距離大於誤差則為陰影,當然小問題是免不了的,這個只出於學習目的。 return col;//不在陰影內則返回原始像素 } ENDCG } } }
6.完成效果如下。