- 思路
- 实践
- 在方向光的位置,放一个正交相机
- 调整光源相机参数
- 将光源投影空间的正交视锥体画出来
- 投射阴影
- 接收阴影
- 改进
- 超出Shadow map的默认为光照
- 添加光照处理
- 添加PCF柔滑整体边缘
- 添加SDFs_Like效果
- 添加柔和阴影边缘衰减到光照 MixToLight
- CommandBuffer版
- CSharp
- 坑1
- 坑2
- 坑3
- 坑4 - 已修正
- 坑5
- 坑6- 已修正
- 坑7
- Shader
- 尝试给半透明添加接收阴影
- 待改进
- 已实现 SSSM
- 扩展
- Project
- GGB
- References
最近一个月天天熬夜学习,感觉身体有点吃不消。 还是要合理作息才能发挥大脑最佳状态。。。
今天忙了一整天,搬了好多东西,累死了,还有之前买了个汽车的袋子,现在不常开车,就先把车子罩起来吧。
搞了好久,然后一路上喷了好多酒精杀毒啊,现在疫情没有完全消灭之前,一刻都不能放松,为了自己,为了家人,为了国家,我们公民有义务做好防疫工作。
好了。终于又空继续学习了。
前一篇我学习、翻译了:OpenGL - 阴影映射 - Tutorial 16 : Shadow mapping,OpenGL的ShadowMap原理,又参考了别的一些资料,所有了解就动手实践吧。
代码我就尽量不优化了,方便理解。特别是shader的各种外部传进来的参数,其实很多可以放到一个float4或是int4。
后面重写了另一篇,可以公开可下载的 Project 的 : Unity Shader - Custom Shadow Map (New Version)
思路这里允许我再次简单的描述shadow map的实现思路简介。(你可以看我前面提的,翻译的那篇文章)
以方向光为例。
- 在方向光的位置,放一个正交相机(因为是平行光,正交相机就好)。
- 调整光源相机参数:大小,near, far。
- 投射阴影:在渲染我们场景主体几何体之前,我们在方向光空间下,用刚刚设好的正交相机渲染一张深度图,这张图就叫:shadow map。几何体是否投射阴影,意思就是渲染该几何体时,是否写入到光源空间下的 shadowMap 纹理的深度值。
- 接收阴影:在到渲染主体的几何体时,在VS(Vertex shader)顶点着色器将顶点转换到方向光光源空间下,将这个坐标,假设为float4 shadowCoord传到FS阶段,硬件会处理插值,在FS(Fragment shader)片段着色器中将获取这个片段在光源空间下的坐标,采样之前渲染出来的shadow map,然后用shadowCoord与shadow map对应位置的深度值比较一下大小,如果shadow map中的值小于shadowCoord.z / shadowCoord.w的值,那就说明当前绘制的片段处于该方向光的阴影中。几何体是否接收阴影因为这是否处理上述说明的运算。
// 方向光
public Light directionalLight;
....
// 使用正交相机,因为方向光是平行光
collectShadowMapCam = directionalLight.gameObject.AddComponent();
...
调整光源相机参数
collectShadowMapCam.backgroundColor = Color.white;
collectShadowMapCam.clearFlags = CameraClearFlags.SolidColor;
collectShadowMapCam.orthographic = true;
collectShadowMapCam.orthographicSize = 10;
collectShadowMapCam.nearClipPlane = 0.3f;
collectShadowMapCam.farClipPlane = 20;
collectShadowMapCam.enabled = false;
将光源投影空间的正交视锥体画出来
private void OnDrawGizmos()
{
if (collectShadowMapCam == null || directionalLight == null) return;
Gizmos.color = Color.cyan;
float size = collectShadowMapCam.orthographicSize;
var l_near = collectShadowMapCam.nearClipPlane;
var l_far = collectShadowMapCam.farClipPlane;
var l_pos = directionalLight.transform.position;
var l_up = directionalLight.transform.up;
var l_forward = directionalLight.transform.forward;
var l_right = directionalLight.transform.right;
Vector3 tl_n = l_pos + l_forward * l_near - l_right * size + l_up * size;
Vector3 bl_n = l_pos + l_forward * l_near - l_right * size - l_up * size;
Vector3 tr_n = l_pos + l_forward * l_near + l_right * size + l_up * size;
Vector3 br_n = l_pos + l_forward * l_near + l_right * size - l_up * size;
Vector3 tl_f = tl_n + l_forward * l_far - l_forward * l_near;
Vector3 bl_f = bl_n + l_forward * l_far - l_forward * l_near;
Vector3 tr_f = tr_n + l_forward * l_far - l_forward * l_near;
Vector3 br_f = br_n + l_forward * l_far - l_forward * l_near;
// near
Gizmos.DrawLine(bl_n, tl_n);
Gizmos.DrawLine(tl_n, tr_n);
Gizmos.DrawLine(tr_n, br_n);
Gizmos.DrawLine(br_n, bl_n);
// left
Gizmos.DrawLine(tl_n, tl_f);
Gizmos.DrawLine(bl_n, bl_f);
// right
Gizmos.DrawLine(tr_n, tr_f);
Gizmos.DrawLine(br_n, br_f);
// far
Gizmos.DrawLine(bl_f, tl_f);
Gizmos.DrawLine(tl_f, tr_f);
Gizmos.DrawLine(tr_f, br_f);
Gizmos.DrawLine(br_f, bl_f);
}
效果:
我们需要用到 Camera.SetReplacementShader
,相关使用可以参数我之前写的一篇:Unity - RenderWithShader, SetReplacementShader, ResetReplacementShader 测试
下面是CSharp脚本层设置好shadow map 的尺寸,阴影强度,光源空间的投影矩阵等
public enum ShadowMapResolution // shadow map 分辨率的量级
{
VeryLow = 128,
Low = 256,
Medium = 512,
High = 1024,
VeryHigh = 2048
}
private static readonly int _CustomShadowMap_hash = Shader.PropertyToID("_CustomShadowMap");
private static readonly int _CustomShadowMapLightSpaceMatrix_hash = Shader.PropertyToID("_CustomShadowMapLightSpaceMatrix");
private static readonly int _CustomShadowStrengthen_hash = Shader.PropertyToID("_CustomShadowStrengthen");
// 投射shadow map shader
public Shader shadowMapCasterShader;
// 分辨率级别
public ShadowMapResolution resolution = ShadowMapResolution.Medium;
// 阴影强度
[Range(0, 1)] public float customShadowStrengthen = 0.5f;
...
collectShadowMapCam.SetReplacementShader(shadowMapCasterShader, "MyShadowMap");
...
var rtSize = (int)resolution;
if (shadowMapRT == null || shadowMapRT.width != rtSize)
{
if (shadowMapRT != null) RenderTexture.ReleaseTemporary(shadowMapRT);
shadowMapRT = RenderTexture.GetTemporary(rtSize, rtSize, 0, RenderTextureFormat.RG16);
shadowMapRT.name = "_CustomShadowMap";
collectShadowMapCam.targetTexture = shadowMapRT;
Shader.SetGlobalTexture(_CustomShadowMap_hash, shadowMapRT);
}
collectShadowMapCam.Render();
var lightSpaceMatrix = GL.GetGPUProjectionMatrix(collectShadowMapCam.projectionMatrix, false);
lightSpaceMatrix = lightSpaceMatrix * collectShadowMapCam.worldToCameraMatrix;
// 光源投影矩阵
Shader.SetGlobalMatrix(_CustomShadowMapLightSpaceMatrix_hash, lightSpaceMatrix);
// 阴影的光照衰减强度
Shader.SetGlobalFloat(_CustomShadowStrengthen_hash, customShadowStrengthen);
接着是shader层接收这些参数,在Camera.SetReplacementShader
后,再Camera.Render
得到shadow map RT。
注意我们的shadow map RT是:RenderTextureFormat.RG16
,还有SetReplacementShader
我们是对"MyShadowMap"
的shader tag来替换,这里是为了做测试所以才自己弄了个指定的tag,其实使用会Unity的"LightMode"="ShadowCaster"
方式比较好,这样其他的Unity内置对象也对投射到我们的阴影图里。
// jave.lin 2020.04.10 - 投射阴影
Shader "Custom/ShadowMapCaster" {
CGINCLUDE
#include "UnityCG.cginc"
struct a2v {
float4 vertex : POSITION;
};
struct v2f {
float4 vertex : SV_POSITION;
float depth : TEXCOORD0;
};
v2f vert (a2v v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.depth = COMPUTE_DEPTH_01;
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 result = fixed4(EncodeFloatRG(i.depth),0,0);
return result;
}
ENDCG
SubShader {
Tags { "MyShadowMap"="1" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
COMPUTE_DEPTH_01
、EncodeFloatRG
都是unity内置的宏,和函数
// x = 1 or -1 (-1 if projection is flipped)
// y = near plane
// z = far plane
// w = 1/far plane
uniform vec4 _ProjectionParams;
#define COMPUTE_DEPTH_01 -(UnityObjectToViewPos( v.vertex ).z * _ProjectionParams.w)
// Encoding/decoding [0..1) floats into 8 bit/channel RG. Note that 1.0 will not be encoded properly.
inline float2 EncodeFloatRG( float v )
{
float2 kEncodeMul = float2(1.0, 255.0);
float kEncodeBit = 1.0/255.0;
float2 enc = kEncodeMul * v;
enc = frac (enc);
enc.x -= enc.y * kEncodeBit;
return enc;
}
从shader的shadowCaster可以看到,只写将depth深度编码到一个float2
,并写入RG
通道。
运行结果如下: 这就是我们目前的shadow map纹理
我目前没去制作SSSM(Screen Space Shadow Map),所以我在绘制每一个需要接收阴影的对象的shader上添加逻辑处理即可:
ReceiveShadow.shader
// jave.lin 2020.04.10 接收阴影
Shader "Custom/ReceiveShadow" {
Properties {
_MainTex ("MainTex", 2D) = "white" {}
_MainColor ("MainColor", Color) = (1, 1, 1, 1)
}
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
fixed4 _MainColor;
sampler2D _CustomShadowMap; // shadow map 纹理
float4x4 _CustomShadowMapLightSpaceMatrix; // shadow map 光源空间矩阵
float _CustomShadowStrengthen; // shadow 强度
struct a2v {
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
float2 uv : TEXCOORD0;
float4 shadowCoord : TEXCOORD1;
};
// 获取光照衰减系数
float GetAtten(v2f i) {
float2 uv = i.shadowCoord.xy / i.shadowCoord.w;
uv = uv * 0.5 + 0.5; // (-1,1)->(0,1)
float fragDepth = i.shadowCoord.z / i.shadowCoord.w;
#if defined (SHADER_TARGET_GLSL)
fragDepth = fragDepth * 0.5 + 0.5; // (-1,1)->(0,1)
#elif defined (UNITY_REVERSED_Z)
fragDepth = 1 - fragDepth; // (1,0)->(0,1)
#endif
float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv).xy);
float atten = 1;
if (fragDepth > shadowMapDepth) {
atten = lerp(1, 0, _CustomShadowStrengthen);
}
return atten;
}
// 着色处理
fixed4 shading(v2f i, float atten) {
// code here:
// ambient
// diffuse
// specular
// etc ...
return tex2D(_MainTex, i.uv) * _MainColor * atten;
}
v2f vert (a2v v) {
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = v.uv;
// 取得当前绘制顶点相对光源空间下的坐标,即:阴影映射坐标
o.shadowCoord = mul(_CustomShadowMapLightSpaceMatrix, mul(unity_ObjectToWorld, v.vertex));
return o;
}
fixed4 frag (v2f i) : SV_Target {
float atten = GetAtten(i);
return shading(i, atten);
}
ENDCG
SubShader {
Tags { "RenderType"="Opaque" "MyShadowMap"="1" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
主要理解vert
中的:
// 取得当前绘制顶点相对光源空间下的坐标,即:阴影映射坐标
o.shadowCoord = mul(_CustomShadowMapLightSpaceMatrix, mul(unity_ObjectToWorld, v.vertex));
和frag中的:GetAtten
函数
float atten = GetAtten(i);
运行效果: 基本上一个最最最基础的shadow map例子实现了
屏幕像素,对应超出了:光源投影视锥体范围的,都默认成有阴影了。 我们想要的是超出shadow map范围的都默认在光照。 下shader中的 GetAtten
函数添加一行代码即可:
float GetAtten(v2f i) {
float2 uv = i.shadowCoord.xy / i.shadowCoord.w;
uv = uv * 0.5 + 0.5; // (-1,1)->(0,1)
// clamp to edge color : 1
if (uv.x > 1 || uv.y > 1 || uv.x (0,1)
#endif
if (fragDepth > 1) return 1;
}
留意:
// clamp to edge color : 1
if (uv.x > 1 || uv.y > 1 || uv.x (0,1)
// clamp to edge color : 1
if (uv.x > 1 || uv.y > 1 || uv.x (0,1)
#endif
float atten = 1;
if (_CustomShadowSmoothType == 0) {// hard
float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv).xy);
if (fragDepth shadowMapDepth) {
if (_CustomShadowAutoStrengthen) { // 测试效果
// auto strengthen
fixed l = Luminance(UNITY_LIGHTMODEL_AMBIENT.rgb);
atten = l * 0.5;
} else {
atten = lerp(1, 0, _CustomShadowStrengthen);
}
}
} else if (_CustomShadowSmoothType == 1) {// PCF
float2 offset = 0;
float minus_step = strengthen / 9.0;
for(int i = -1; i center_shadowMapDepth) {
distance = saturate(fragDepth- _CustomShadowBias - center_shadowMapDepth);
distance /= _CustomShadowBlurDistance;
}
float2 offset = 0;
float w = 0;
float idx = 0;
int i =0, j=0;
for(i = -2; i 0 && appliedAtten) {
half3 hDir = normalize(viewDir + lightDir);
fixed HdotN = max(0, dot(hDir, i.worldNormal));
specular = pow(HdotN, 64);
}
fixed4 combinedCol = 0;
// ambient 是模拟各个方向的光所以不需要atten
combinedCol.xyz = ambient +
diffuse * atten * albedo.rgb * _LightColor0.rgb * _MainColor.rgb +
specular * atten * _LightColor0.rgb;
return combinedCol;
} else {
return tex2D(_MainTex, i.uv) * _MainColor * atten;
}
}
主要留意shader的:
bool appliedAtten = appliedAtten = atten == 1; // Hard
if (_CustomShadowMixToLight) {
// PCFs_Like || SDFs_Like
if (_CustomShadowSmoothType == 1 || _CustomShadowSmoothType == 2) {
appliedAtten = true; // 意味着模糊边缘的话,specular会乘上atten来衰减
}
}
值针对有边缘模糊的处理,Hard
边缘是不处理的。
效果:
主要我封装了一个测试用的类
CSharp// jave.lin 2020.04.13 - CmdBuff_ReplacementRender
[Serializable]
public class CmdBuff_ReplacementRender
{
private static int _MvpMatrix_hash = Shader.PropertyToID("_MvpMatrix");
private static int _MvMatrix_hash = Shader.PropertyToID("_MvMatrix");
private static int _InvFar_hash = Shader.PropertyToID("_InvFar");
public enum ReplaceType // 替换类型
{
CheckType, CheckTypeAndValue, IngoreType_ForAll
}
public enum DrawType // CommandBuffer的绘制类型
{
DrawRender,
DrawMesh
}
private static void GetRenders(CmdBuff_ReplacementRender replacement, List replaceList, List sourceList)
{
var checkTag = replacement.replaceTag;
var checkValue = replacement.replaceType == ReplaceType.CheckTypeAndValue ? replacement.replaceMat.GetTag(checkTag, false) : string.Empty;
var renderArr = GameObject.FindObjectsOfType();
if (replacement.replaceType == ReplaceType.IngoreType_ForAll)
{
replaceList.AddRange(renderArr);
}
else if (replacement.replaceType == ReplaceType.CheckType)
{
foreach (var item in renderArr)
{
if (!string.IsNullOrEmpty(item.sharedMaterial.GetTag(checkTag, false)))
{
replaceList.Add(item);
}
else
{
sourceList.Add(item);
}
}
}
else if (replacement.replaceType == ReplaceType.CheckTypeAndValue)
{
foreach (var item in renderArr)
{
if (!string.IsNullOrEmpty(checkValue) && item.sharedMaterial.GetTag(checkTag, false) == checkValue)
{
replaceList.Add(item);
}
else
{
sourceList.Add(item);
}
}
}
}
private static void GetMesh(CmdBuff_ReplacementRender replacement, List replaceList, List sourceList)
{
var checkTag = replacement.replaceTag;
var checkValue = replacement.replaceType == ReplaceType.CheckTypeAndValue ? replacement.replaceMat.GetTag(checkTag, false) : string.Empty;
var renderArr = GameObject.FindObjectsOfType();
if (replacement.replaceType == ReplaceType.IngoreType_ForAll)
{
foreach (var item in renderArr)
{
replaceList.Add(new MeshInfo { mesh = item.gameObject.GetComponent().sharedMesh, trans = item.transform, srcMat = item.material, tempReplaceMat = new Material(replacement.replaceShader) });
}
}
else if (replacement.replaceType == ReplaceType.CheckType)
{
foreach (var item in renderArr)
{
var addItem = new MeshInfo { mesh = item.gameObject.GetComponent().sharedMesh, trans = item.transform, srcMat = item.material, tempReplaceMat = new Material(replacement.replaceShader) };
if (!string.IsNullOrEmpty(item.sharedMaterial.GetTag(checkTag, false)))
{
replaceList.Add(addItem);
}
else
{
sourceList.Add(addItem);
}
}
}
else if (replacement.replaceType == ReplaceType.CheckTypeAndValue)
{
foreach (var item in renderArr)
{
var addItem = new MeshInfo { mesh = item.gameObject.GetComponent().sharedMesh, trans = item.transform, srcMat = item.material, tempReplaceMat = new Material(replacement.replaceShader) };
if (!string.IsNullOrEmpty(checkValue) && item.sharedMaterial.GetTag(checkTag, false) == checkValue)
{
replaceList.Add(addItem);
}
else
{
sourceList.Add(addItem);
}
}
}
}
public ReplaceType replaceType;
public string replaceTag;
public bool drawSrcRender = true;
public DrawType drawType;
public Shader replaceShader;
private Camera camEventHolder; // camEventHolder 和 renderCam 不一定是同一个,因为camEventHolder是拿来挂载事件的,而renderCam是来用绘制的
private CommandBuffer cmdBuff;
[SerializeField] private Material replaceMat;
[SerializeField] private Camera renderCam; // camEventHolder 和 renderCam 不一定是同一个,因为camEventHolder是拿来挂载事件的,而renderCam是来用绘制的
public CameraEvent? CamEvent => lastCamEvent;
private CameraEvent? lastCamEvent;
[SerializeField] private List replaceList = new List();
[SerializeField] private List sourceList = new List();
[SerializeField] private List replaceList_meshFilter = new List();
[SerializeField] private List sourceList_meshFilter = new List();
[Serializable]
public class MeshInfo
{
public Mesh mesh;
public Transform trans;
public Material srcMat;
public Material tempReplaceMat;
}
public string Name => Name;
private string name;
public CmdBuff_ReplacementRender(Camera camEventHolder, CameraEvent? camEvent = null, Shader replaceShader = null, string name = "none-name")
{
this.camEventHolder = camEventHolder;
this.replaceShader = replaceShader;
this.replaceMat = new Material(this.replaceShader);
this.name = name;
this.lastCamEvent = camEvent;
this.cmdBuff = new CommandBuffer();
this.cmdBuff.name = $"{this.lastCamEvent} - {this.name}";
if (this.lastCamEvent.HasValue)
{
this.camEventHolder.AddCommandBuffer(this.lastCamEvent.Value, this.cmdBuff);
}
}
public void Clear()
{
if (cmdBuff != null) cmdBuff.Clear();
}
public void UpdateVP(Camera renderCam)
{
if (drawType == DrawType.DrawMesh && cmdBuff != null)
{
this.renderCam = renderCam;
// CommandBuffer.SetViewProjectionMatrices
// https://docs.unity3d.com/ScriptReference/Rendering.CommandBuffer.SetViewProjectionMatrices.html
/*
using UnityEngine;
using UnityEngine.Rendering;
// Attach this script to a Camera and pick a mesh to render.
// When entering Play mode, this will render a green mesh at
// origin position, via a command buffer.
[RequireComponent(typeof(Camera))]
public class ExampleScript : MonoBehaviour
{
public Mesh mesh;
void Start()
{
var material = new Material(Shader.Find("Hidden/Internal-Colored"));
material.SetColor("_Color", Color.green);
var tr = transform;
var camera = GetComponent();
// Code below does the same as what camera.worldToCameraMatrix would do. Doing
// it "manually" here to illustrate how a view matrix is constructed.
//
// Matrix that looks from camera's position, along the forward axis.
var lookMatrix = Matrix4x4.LookAt(tr.position, tr.position + tr.forward, tr.up);
// Matrix that mirrors along Z axis, to match the camera space convention.
var scaleMatrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1, 1, -1));
// Final view matrix is inverse of the LookAt matrix, and then mirrored along Z.
var viewMatrix = scaleMatrix * lookMatrix.inverse;
var buffer = new CommandBuffer();
buffer.SetViewProjectionMatrices(viewMatrix, camera.projectionMatrix);
buffer.DrawMesh(mesh, Matrix4x4.identity, material);
camera.AddCommandBuffer(CameraEvent.BeforeSkybox, buffer);
}
}
* */
//var tr = renderCam.transform;
//var lookMatrix = Matrix4x4.LookAt(tr.position, tr.position + tr.forward, tr.up);
//var scaleMatrix = Matrix4x4.TRS(Vector3.zero, Quaternion.identity, new Vector3(1, 1, -1));
//var viewMatrix = scaleMatrix * lookMatrix.inverse;
//cmdBuff.SetViewProjectionMatrices(viewMatrix, GL.GetGPUProjectionMatrix(renderCam.projectionMatrix, false));
//cmdBuff.SetViewProjectionMatrices(renderCam.worldToCameraMatrix, GL.GetGPUProjectionMatrix(renderCam.projectionMatrix, false));
// 在这里设置GLOBAL没有用,因为new Materials为获取到
//cmdBuff.SetGlobalFloat(_InvFar_hash, 1.0f / renderCam.farClipPlane);
}
}
public void Update(RenderTexture rt, CameraEvent? camEvent = null)
{
if (this.replaceShader != replaceMat)
{
replaceMat.shader = this.replaceShader;
}
if (this.lastCamEvent != camEvent)
{
if (cmdBuff != null)
{
if (this.lastCamEvent.HasValue) camEventHolder.RemoveCommandBuffer(this.lastCamEvent.Value, cmdBuff);
cmdBuff.name = $"{camEvent} - {this.name}";
if (camEvent.HasValue) camEventHolder.AddCommandBuffer(camEvent.Value, cmdBuff);
}
this.lastCamEvent = camEvent;
}
if (cmdBuff == null)
{
cmdBuff = new CommandBuffer();
cmdBuff.name = $"{this.lastCamEvent} - {this.name}";
if (this.lastCamEvent.HasValue) camEventHolder.AddCommandBuffer(this.lastCamEvent.Value, cmdBuff);
}
cmdBuff.Clear();
if (rt) cmdBuff.SetRenderTarget(rt);
else cmdBuff.SetRenderTarget(camEventHolder.targetTexture);
cmdBuff.ClearRenderTarget(true, true, Color.white);
if (drawType == DrawType.DrawRender)
{
replaceList.Clear();
sourceList.Clear();
// 如果GetRenders处理了:
// - Camera.cullMask 的筛选
// - Renderer.sortingLayerID 的排序
// - Material.queueID 的排序
// - 等等其他相关的排序,那么就和Camera.RenderWithShader或是SetReplacementShader差不多了
GetRenders(this, replaceList, sourceList);
if (drawSrcRender)
{
foreach (var item in sourceList)
{
// 这里面限制太多,想让render指定在对应的camera下渲染都不行
cmdBuff.DrawRenderer(item, item.sharedMaterial);
}
}
foreach (var item in replaceList)
{
cmdBuff.DrawRenderer(item, replaceMat);
}
}
else
{
replaceList_meshFilter.Clear();
sourceList_meshFilter.Clear();
// 如果GetRenders处理了:
// - Camera.cullMask 的筛选
// - Renderer.sortingLayerID 的排序
// - Material.queueID 的排序
// - 等等其他相关的排序,那么就和Camera.RenderWithShader或是SetReplacementShader差不多了
GetMesh(this, replaceList_meshFilter, sourceList_meshFilter);
// GL.GetGPUProjectionMatrix(collectShadowMapCam.projectionMatrix, true); 中的第二个参数要设置为true,因为ShadowCaster2是渲染到RT上的
// 貌似用Camera.Render()的话就没有这些问题,可能内部处理了
var vp = GL.GetGPUProjectionMatrix(renderCam.projectionMatrix, true) * renderCam.worldToCameraMatrix;
cmdBuff.SetGlobalFloat(_InvFar_hash, 1.0f / renderCam.farClipPlane);
if (drawSrcRender)
{
foreach (var item in sourceList_meshFilter)
{
var mMatrix = item.trans.localToWorldMatrix;
// 这里面限制太多,想让render指定在对应的camera下渲染都不行,所以这能自己构建变换矩阵信息传进shader了
item.srcMat.SetMatrix(_MvpMatrix_hash, vp * mMatrix);
item.srcMat.SetMatrix(_MvMatrix_hash, this.renderCam.worldToCameraMatrix * mMatrix);
cmdBuff.DrawMesh(item.mesh, Matrix4x4.identity, item.srcMat);
}
}
foreach (var item in replaceList_meshFilter)
{
var mMatrix = item.trans.localToWorldMatrix;
// 这里面限制太多,想让render指定在对应的camera下渲染都不行,所以这能自己构建变换矩阵信息传进shader了
item.tempReplaceMat.SetMatrix(_MvpMatrix_hash, vp * mMatrix);
item.tempReplaceMat.SetMatrix(_MvMatrix_hash, this.renderCam.worldToCameraMatrix * mMatrix);
cmdBuff.DrawMesh(item.mesh, Matrix4x4.identity, item.tempReplaceMat);
}
}
}
public void Excute()
{
Graphics.ExecuteCommandBuffer(cmdBuff);
}
public void Destroy()
{
if (cmdBuff != null && camEventHolder != null)
{
if (this.lastCamEvent.HasValue) camEventHolder.RemoveCommandBuffer(this.lastCamEvent.Value, cmdBuff);
cmdBuff.Dispose();
cmdBuff = null;
camEventHolder = null;
}
if (replaceList != null)
{
replaceList.Clear();
replaceList = null;
}
if (sourceList != null)
{
sourceList.Clear();
sourceList = null;
}
if (replaceList_meshFilter != null)
{
replaceList_meshFilter.Clear();
replaceList_meshFilter = null;
}
if (sourceList_meshFilter != null)
{
sourceList_meshFilter.Clear();
sourceList_meshFilter = null;
}
}
}
这个类外部调用也是比较简单:
private void Start()
{
shadowCasterRender = new CmdBuff_ReplacementRender(this.cam, null, shadowMapCasterShader, "shadowCasterRender");
shadowCasterRender.replaceType = CmdBuff_ReplacementRender.ReplaceType.CheckTypeAndValue;
shadowCasterRender.replaceTag = "MyShadowMap";
shadowCasterRender.drawSrcRender = false;
shadowCasterRender.drawType = CmdBuff_ReplacementRender.DrawType.DrawMesh;
}
private void OnPreRender()
{
...
var rtSize = (int)resolution;
if (shadowMapRT == null || shadowMapRT.width != rtSize)
{
if (shadowMapRT != null) RenderTexture.ReleaseTemporary(shadowMapRT);
// 注意 RenderTexture.GetTemporary 第三个参数如果使用 CommandBuffer来渲染ShadowCast的话,一般要用深度
// 如果 RenderTexture.GetTemporary 在使用 Camera.Render 来渲染的话,可以不用深度,貌似Camera.Render会使用内置的深度缓存来比较
shadowMapRT = RenderTexture.GetTemporary(rtSize, rtSize, 16, RenderTextureFormat.RG16);
shadowMapRT.name = "_CustomShadowMap";
//collectShadowMapCam.targetTexture = shadowMapRT;
Shader.SetGlobalTexture(_CustomShadowMap_hash, shadowMapRT);
}
//collectShadowMapCam.Render();
...
shadowCasterRender.UpdateVP(collectShadowMapCam);
shadowCasterRender.Update(shadowMapRT);
shadowCasterRender.Excute();
...
}
但是会有很多坑,我已经填了一部分坑了。
我使用了CommandBuffer
来替代Camera.RenderWithShader
或是SetReplacementShader
的方式实现了另一个ShadowMap的版本。
随意列举几个坑,可能有些还遗漏没有想起来的:
坑1// 注意 RenderTexture.GetTemporary 第三个参数如果使用 CommandBuffer来渲染ShadowCast的话,一般要用深度
// 如果 RenderTexture.GetTemporary 在使用 Camera.Render 来渲染的话,可以不用深度,貌似Camera.Render会使用内置的深度缓存来比较
shadowMapRT = RenderTexture.GetTemporary(rtSize, rtSize, 16, RenderTextureFormat.RG16);
坑2
// 在这里设置GLOBAL没有用,因为new Materials为获取到
//cmdBuff.SetGlobalFloat(_InvFar_hash, 1.0f / renderCam.farClipPlane);
这个要结合上下文才知道什么意思。 就是说,如果我先执行了SetGlobalXXX的Uniform,在这之后,我在脚本动态的创建了Material,那么这个Material是没有Global Uniform的信息的。
必须要在new Material之后,去调用SetGlobalXXX,这样才会对刚刚创建的new Material有设置Global uniform。
坑3 private Camera camEventHolder; // camEventHolder 和 renderCam 不一定是同一个,因为camEventHolder是拿来挂载事件的,而renderCam是来用绘制的
[SerializeField] private Camera renderCam; // camEventHolder 和 renderCam 不一定是同一个,因为camEventHolder是拿来挂载事件的,而renderCam是来用绘制的
CommandBuffer封装类中有两个Camera。
前者是用于挂载事件用的。 后者是用于渲染时的变换矩阵的载体,因为比较好控制,所以用了额外的一个相机了操作,与存储View,Project Matrix。
坑4 - 已修正// CommandBuffer.SetViewProjectionMatrices
// https://docs.unity3d.com/ScriptReference/Rendering.CommandBuffer.SetViewProjectionMatrices.html
CommandBuffer.SetViewProjectionMatrices
值对Unity内置的变量赋值。然而我发现,CommandBuffer.DrawMesh
竟然没有提供传入WorldMatrix
或是ModelMatrix
,所以不能用CommandBuffer.SetViewProjectionMatrices
。虽然我继续使用了CommandBuffer.DrawMesh
,但是它的第二个参数我是无视它的,因为我shader中的变换矩阵是外部自己构建,再传入Shader执行的。
修正: CommandBuffer.SetViewProjectionMatrices
是可以设置unity内置的view与projection矩阵变量的,这个之前理解也没有错。 但是我直接理解错的是:CommandBuffer.DrawMesh
中的第二个参数。
这个参数其实就是:ModelMatrix
,或是叫:LocalToWorldMatrix
。我看到注释的时候我还以为是:MVPMatrix
。所以就以为与CommandBuffer.SetViewProjectionMatrices
中的VP有冲突。
之后花了5分钟写了个测试:CommandBuffer.SetViewProjectionMatrixes与CommandBuffer.DrawMesh的测试
坑5 // GL.GetGPUProjectionMatrix(collectShadowMapCam.projectionMatrix, true); 中的第二个参数要设置为true,因为ShadowCaster2是渲染到RT上的
// 貌似用Camera.Render()的话就没有这些问题,可能内部处理了
var vp = GL.GetGPUProjectionMatrix(renderCam.projectionMatrix, true) * renderCam.worldToCameraMatrix;
如注释描述
坑6- 已修正var mMatrix = item.trans.localToWorldMatrix;
// 这里面限制太多,想让render指定在对应的camera下渲染都不行,所以这能自己构建变换矩阵信息传进shader了
item.tempReplaceMat.SetMatrix(_MvpMatrix_hash, vp * mMatrix);
item.tempReplaceMat.SetMatrix(_MvMatrix_hash, this.renderCam.worldToCameraMatrix * mMatrix);
cmdBuff.DrawMesh(item.mesh, Matrix4x4.identity, item.tempReplaceMat);
这里如坑4所说的,调用CommandBuffer.DrawMesh
的第二个参数与CommandBuffer.SetViewProjectionMatrix
API有冲突,所以我就没使用第二个参数了,改用自己定义的矩阵
修正: 正确使用方法:
cmdBuff.SetViewProjectionMatrices(ViewMatrix, ProjectionMatrix);
cmdBuff.DrawMesh(Mesh, LocalToWorldMatrix, Material);
之后花了5分钟写了个测试:CommandBuffer.SetViewProjectionMatrixes与CommandBuffer.DrawMesh的测试
坑7上面坑4和坑6虽然修正了用法,但是_ProjectionParams.w
但不是1/far
的值,我估计是否这样用法的人很少,没人踩过这个坑?
很有可能是因为CommandBuffer.SetViewProjectionMatrixes
并没有对_ProjectionParams
更新,那么其他的变量也是极有可能也是没有更新的,所以还是妥妥的用回自定义的矩阵来处理。
// jave.lin 2020.04.13 - 投射阴影2
Shader "Custom/ShadowMapCaster2" {
CGINCLUDE
#include "UnityCG.cginc"
struct a2v {
float4 vertex : POSITION;
};
struct v2f {
float4 vertex : SV_POSITION;
float depth : TEXCOORD0;
};
// float4x4 vpMatrix;
// float4x4 mMatrix;
// global
float4x4 _MvMatrix;
float _InvFar;
// local
float4x4 _MvpMatrix;
v2f vert (a2v v) {
v2f o;
// o.vertex = UnityObjectToClipPos(v.vertex);
o.vertex = mul(_MvpMatrix, v.vertex);
// Tranforms position from object to camera space
// inline float3 UnityObjectToViewPos( in float3 pos )
// {
// return mul(UNITY_MATRIX_V, mul(unity_ObjectToWorld, float4(pos, 1.0))).xyz;
// }
//#define COMPUTE_DEPTH_01 -(UnityObjectToViewPos( v.vertex ).z * _ProjectionParams.w)
// // x = 1 or -1 (-1 if projection is flipped)
// // y = near plane
// // z = far plane
// // w = 1/far plane
// uniform vec4 _ProjectionParams;
// o.depth = COMPUTE_DEPTH_01;
o.depth = -(mul(_MvMatrix, v.vertex).z * _InvFar);
return o;
}
fixed4 frag (v2f i) : SV_Target {
fixed4 result = fixed4(EncodeFloatRG(i.depth),0,0);
return result;
}
ENDCG
SubShader {
Tags { "MyShadowMap"="1" }
Pass {
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
ENDCG
}
}
}
运行效果是一样的,我就不发图了
尝试给半透明添加接收阴影 虽然可以接收阴影了。 但是半透明的物体,没有投射阴影是很奇怪的。
-
通过类似unity中的 SSSM(Screen Space Shadow Map) 来处理,先收集SSSM(Screen Space Shadow Map)然后其他渲染其他的渲染对象的改动会比较小,只要获取屏幕坐标来采样SSSM就可以了。以后有空再去实现吧。 之前刚学习Shader时,不知道为何Unity透明物体接受不了阴影,我之前还写了一个测试的: Unity Shader - 绘制半透明效果 + 投影 + 遮挡绘制 (半透明物体接收阴影还不知道怎么弄) 现在终于知道为何Unity内置的半透明物体为何接收不了阴影了,因为Unity的阴影是在:SSSM(Screen Space Shadow Map,屏幕空间的阴影)中收集的。 而SSS需要先取屏幕不透明物体的深度纹理,然后对每个像素还原到WorldSpace,再转换到LightSpace处计算是否阴影中。 而我们半透明物体是不写深度的,所以屏幕深度纹理就没有半透明的信息。 这就是原因了。
-
计算PSR(Potential Shadow Receivers:可能会接收阴影对象)来让光源的投影矩阵自动适配大小等参数。这样就不用手动去写死参数了。
-
SDF(Sign Distance Fields)距离场的数据标记,这个以前听说过,没去细究,听说可以改进软阴影,思路是根据光源到接收的片段的距离来处理模糊(起始我很早之前就想到过这些思路,但是一直都没去写阴影相关的内容,所以就没去进一步了解,但是思路都是一样的,细节就需要研究后才知道了,但一直不知道叫:SDF)。
- 后面在学习Raymarching时,按国外的资料抄了一些公式实现了:Unity Shader - Ray Marching - T6 - SoftShadow/PenumbraShadow
-
CSM(这个我就暂时不想去研究,太需要精力了)
- 查看MSDN的:Cascaded Shadow Maps
- Cascaded Shadow Map(CSM)中的一些问题
- Shadow Cascades - Unity 官方文档
具体查看:Unity Shader - Custom SSSM(Screen Space Shadow Map) 自定义屏幕空间阴影图
扩展根据方向光的这个思路,可以实现:SpotLight, PointLight
- SpotLight 如果你的SpotLight是平行光,也么可以直接用回这个DirectionalLight的代码就可以了。如果不是平行光,是类似手电筒这样近距离的带照射角度范围的,就需要将光源空间的投影矩阵改成透视矩阵就好了。
- PointLight 就相当于投射投影的SpotLight 带6个方向的投影,但是投影矩阵的 FOV 必须为90度,否则透视投影不够全面,就会有漏采样,因为上下4面,或是左右4面,想要包含所有面向,就要90度就好了, 4 × 90 = 360 4 \times 90 = 360 4×90=360,这样就够好覆盖到 36 0 o 360^o 360o的范围。然后将收集的阴影深度写入一个CubeMap纹理中。在渲染对象时,根据PointLight到片段坐标方向来作为CubeMap的采样方向向量就可以了。具体可以查看References中的内容,有PointLight的实现参考:Point Shadows。
- 后面可以试试Shadow Volume的方式。
backup :
- UnityShader_CustomShadow_2018.3.0f2
- UnityShader_CustomShadow_includeCmdBuffVersion_2018.3.0f2
- UnityShader_CustomShadow_includeCmdBuffVersion_optimizTransparent_2018.3.0f2
- Unity Shader - Custom Shadow Map (New Version) - 这篇 blog 是我后续重写的 shadow map 基于 view space 下的计算,里头的 Project 是公开到网盘可提供下载的
backup : SDFs.ggb
References- 阴影映射
- Unity实时阴影实现——Shadow Mapping
- OmnidirShadows-whyCaps.pdf 提取码: 8qsn - 全方位阴影知识点算法,有PointLight-ShadowCubeMap, ShadowVolume,右面有空可以看
- Tutorial 43: Multipass Shadow Mapping With Point Lights - 点光源阴影
- Point Shadows - 点光源阴影
- 点光源阴影
- 从0开始的OpenGL学习(三十)-Shadow Map
- UnityShader——阴影源码解析(一) - 写完例子后,发现之前博主也是同样去研究了Unity的阴影。后面可以进一步去学习,参考。里面有Shadow_Bias比较好的方式
- Shadow Map 原理和改进
- Unity SRP自定义渲染管线 – 4.Spotlight Shadows - 后面学习
- 实时阴影技术总结 - PCF,Bias的更好的优化
- Common Techniques to Improve Shadow Depth Maps - MSDN的ShadowMap实现以及优化,有空可以翻译一下,了解各种细节原理,感觉之前翻译的那篇:OpenGL的还不投详细
- Nvidia GPU Gems - Nvidia 的电子书是后面才发现的光影部分介绍(后面全都看看)
- Nvidia GPU Gems I - Chapter 11. Shadow Map Antialiasing
- Nvidia GPU Gems I - Part II: Lighting and Shadows - 完整的光影介绍
- Nvidia GPU Gems II - Part II: Light and Shadows
- Nvidia GPU Gems I - Chapter 11. Shadow Map Antialiasing
- 实时阴影技术总结
- 入门Distance Field Soft Shadows
- penumbra shadows in raymarched SDFs
- Advanced Soft Shadow Mapping Techniques
- Shadow Cascades - Unity 官方文档
- 高精度 高质量 自适应 角色包围盒阴影 Unity ShadowMap - 后续可以扩展使用自适配渲染内容的 camera pos, near, far 的调整