Shadowmap简易实现
之前一直没有自己实现过阴影,只是概念上有所了解,这次尝试实际编写学习下。
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.完成效果如下。