unity的阴影实现方式是采用Shdowmap技术,但是一直不知道其中的原理。

它的原理并不复杂,假设有一个摄像机在灯光的位置,从灯光的位置往物体看,这时候会有一张光源空间的深度信息图,这就是Shadow Map。凡是物体的深度值大于Shadow Map上的深度值的都是被遮挡的部分,表示处于阴影中。

unity的shadow在哪 unity shadowmap原理_深度图

所需知识点:
1.模型空间到屏幕空间的变换过程
http://www.idivecat.com/archives/631

基本思路:
1.获取灯光空间中场景深度图
2.接受阴影的模型,需将顶点信息变换到灯光投影空间中,然后对比当前片元深度和深度图的深度,深度比较深度图的大,则在阴影中。

细节都在代码的注释里,方便以后查阅。

获取深度图的shader

Shader "DeapthTextureShader"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
	}
	SubShader
	{
		Tags { "RenderType" = "Opaque" }

		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag

			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
			};

			struct v2f
			{
				float4 pos : SV_POSITION;
				float2 depth : TEXCOORD1;
			};

			sampler2D _MainTex;
			float4 _MainTex_ST;

			v2f vert(appdata v)
			{
				v2f o;
				// 模型空间变换到裁剪空间,值越是【-1,1】
				o.pos = UnityObjectToClipPos(v.vertex);
				// 记录裁剪空间中的z,w分量
				o.depth = o.pos.zw;
				return o;
			}

			fixed4 frag(v2f i) : SV_Target
			{
				// 顶点到片元有一个差值的过程,投影空间中不是线性的,除以w后深度值会不准确
				// 所以在片元中除以w时的深度值才是正确的
				float depth = i.depth.x / i.depth.y;
				// depth时的点,值域已经是【0,1】了,因为负数的点在已经被裁剪掉了
				// float是4个字节,刚好对应RGBA的4个分量,把一个浮点数编码到RGBA四个通道上
				// 编码输入,解码输出值域都是【0,1】,提高深度值的精度
				fixed4 col = EncodeFloatRGBA(depth);
				return col;
			}
			ENDCG
		}
	}
}

接受阴影的shader

Shader "ShadowMapShader"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
	}

	SubShader
	{
		Pass
		{
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			sampler2D _MainTex;
			half4 _MainTex_ST;

			struct appdata
			{
				half4 vertex :		POSITION;
				half2 texcoord :	TEXCOORD0;
			};

			struct v2f {
				half4 pos:		SV_POSITION;
				half2 uv:		TEXCOORD0;
				half4 proj :	TEXCOORD1;
			};

			float4x4 _WorldToProjectionMatrix; //【世界空间】变换【摄像机投影空间】矩阵
			sampler2D _DepthTexture;

			v2f vert(appdata v)
			{
				v2f o;
				o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
				o.uv = TRANSFORM_TEX(v.texcoord, _MainTex);
				// 将顶点从【模型空间】变换到【投影空间】
				half4 worldPos = mul(unity_ObjectToWorld, v.vertex);
				o.proj = mul(_WorldToProjectionMatrix, worldPos);
				return o;
			}

			fixed4 frag(v2f v) : COLOR
			{
				fixed4 col = tex2D(_MainTex, v.uv);
				// /w后,顶点从MVP变换后的立方体中值域是【-1,1】
				v.proj.xyz = v.proj.xyz / v.proj.w;
				// 但是要用这个变化过的坐标值来查找深度图中的深度,就得做值域转换
				// 这个过程可以放到C#端,减少计算频率
				v.proj.xyz = v.proj.xyz * 0.5 + 0.5;
				// 通过当前像素坐标位置,获取深度图颜色
				fixed4 depthCol = tex2D(_DepthTexture, v.proj.xy);
				// 通过深度图颜色,获取深度值
				half shadowDepth = DecodeFloatRGBA(depthCol);
				// 通过【当前片元的深度值】与【当前片元坐标对应深度图的深度值】进行比较
				half shadowScale = 1;
				if (v.proj.z > shadowDepth)
				{
					shadowScale = 0.4f;
				}
				return col * shadowScale;
			}
			ENDCG
		}
	}
}

在灯光下创建相机

using UnityEngine;


public class DepthTextureCamera : MonoBehaviour
{
    public int m_lightIndex;
    private Light m_light;
    private Camera m_camera;
    private RenderTexture m_rt;

    void Start()
    {
        m_light = GetComponent<Light>();
        // 如果深度图的分辨率比较低,那么很多个像素会取到同一个深度图上的点
        m_rt = new RenderTexture(1024, 1024, 0);
        m_rt.wrapMode = TextureWrapMode.Clamp;

        m_camera = new GameObject().AddComponent<Camera>();
        m_camera.name = "DepthCamera";
        m_camera.depth = -1;
        m_camera.backgroundColor = Color.white;
        m_camera.clearFlags = CameraClearFlags.Color; ;
        
        if(m_light.type == LightType.Directional)
        {
            m_camera.orthographic = true;
        }
        else
        {
            m_camera.orthographic = false;
        }
        m_camera.orthographicSize = 10;
        m_camera.farClipPlane = 50;
        m_camera.targetTexture = m_rt;
        // _camera渲染时 将带有RenderType标签的shader替换为DeapthTextureShader
        m_camera.SetReplacementShader(Shader.Find("DeapthTextureShader"), "RenderType");
        m_camera.transform.SetParent(transform);
        m_camera.transform.localPosition = Vector3.zero;
        m_camera.transform.localRotation = Quaternion.identity;
        m_camera.enabled = false;
    }

    void Update()
    {
        m_camera.Render();
        // GL.GetGPUProjectionMatrix写法兼容dx和gl的投影转换
        Matrix4x4 tm = GL.GetGPUProjectionMatrix(m_camera.projectionMatrix, false);
        //【世界空间】变换【摄像机空间】矩阵 右乘 【摄像机空间】变换【摄像机投影空间】矩阵
        // 结果为:【世界空间】变换【摄像机投影空间】矩阵
        tm = tm * m_camera.worldToCameraMatrix;

        Shader.SetGlobalMatrix("_WorldToProjectionMatrix", tm);
        Shader.SetGlobalTexture("_DepthTexture", m_rt);
    }
}

问题1:斜波深度偏差问题

unity的shadow在哪 unity shadowmap原理_unity的shadow在哪_02


 

// slope scale based depth bias。基于坡度比例的深度偏移
// 深度图的分辨率有限,那么片元在获取深度信息时,会出现多个片元获取同一个深度信息
// 而灯光在绘制深度图时是有一定角度,这就导致某些片元间隔的出现一部分深度比深度图大,一部分深度比深度图小
// 此时抬高地面就可以,或者将深度图的深度信息增大一点点,这个距离就是shadow bias
// 这个值怎么确定呢,太大太小都不行,此时根据前人的经验总结,根据光源方向和法线方向的夹角确定
// 灯光方向和法线一样时,dot为1,比如光线无角度直接照射地面,那么此时取最小的0.005
fixed3 worldLightDir = normalize(_WorldSpaceLightPos0.xyz);
fixed3 worldNormalDir = v.normal;
half bias = max(0.05 * (1.0 - dot(worldNormalDir, worldLightDir)), 0.005);

问题2:阴影锯齿问题

unity的shadow在哪 unity shadowmap原理_unity的shadow在哪_03

unity的shadow在哪 unity shadowmap原理_d3_04

// PCF滤波(百分比渐进滤波percentage-closer filtering)
// 对某个位置相邻的N个片元也进行采样,叠加阴影值得到平均值
// 1 1 1 1 1
// 1 1 1 1 1
// 1 1 0 1 1
// 1 1 1 1 1
// 1 1 1 1 1
float PercentCloaerFilter(float2 texSize, float2 xy, float sceneDepth, float bias, int filterSize)
{
	float shadow = 0.0;
	// 映射到【0,1】时,一个像素的大小
	float2 unitSize = 1 / texSize;
	for (int x = -filterSize; x <= filterSize; ++x)
	{
		for (int y = -filterSize; y <= filterSize; ++y)
		{
			// 获取偏移uv坐标
			float2 uv_offset = float2(x, y) * unitSize;
			// 通过当前像素uv坐标位置,获取深度图颜色, 通过深度图颜色,获取深度值
			float depth = DecodeFloatRGBA(tex2D(_DepthTexture, xy + uv_offset));
			// 通过【当前片元的深度值】与【当前片元坐标对应深度图的深度值】进行比较
			// 将当前片元是阴影色叠加
			shadow += (sceneDepth - bias > depth ? 1.0 : 0.0);
		}
	}
	// 得到当前片元阴影平均色
	float total = (filterSize * 2 + 1) * (filterSize * 2 + 1);
	shadow /= total;
	return shadow;
}

问题3:超出深度图的区域

unity的shadow在哪 unity shadowmap原理_深度图_05

// 解决摄像机投影空间为黑色的问题
// NDC空间下是-1到1,转到屏幕空间时深度为0到1,大于1的不处理阴影
// 只是不知道为什么是摄像机背后方向的地面为黑色
if (v.proj.z > 1.0f)
{
	shadowCol = 0.0f;
}

参考来源:
https://www.jianshu.com/p/9f767f952bb0