unity的阴影实现方式是采用Shdowmap技术,但是一直不知道其中的原理。
它的原理并不复杂,假设有一个摄像机在灯光的位置,从灯光的位置往物体看,这时候会有一张光源空间的深度信息图,这就是Shadow Map。凡是物体的深度值大于Shadow Map上的深度值的都是被遮挡的部分,表示处于阴影中。
所需知识点:
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:斜波深度偏差问题
// 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:阴影锯齿问题
// 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:超出深度图的区域
// 解决摄像机投影空间为黑色的问题
// NDC空间下是-1到1,转到屏幕空间时深度为0到1,大于1的不处理阴影
// 只是不知道为什么是摄像机背后方向的地面为黑色
if (v.proj.z > 1.0f)
{
shadowCol = 0.0f;
}