您当前的位置: 首页 >  unity

Unity Shader - Custom DirectionalLight ShadowMap 自定义方向光的ShadowMap

发布时间:2020-04-10 23:24:23 ,浏览量:7

文章目录
  • 思路
  • 实践
    • 在方向光的位置,放一个正交相机
    • 调整光源相机参数
      • 将光源投影空间的正交视锥体画出来
    • 投射阴影
    • 接收阴影
  • 改进
    • 超出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<Camera>(); ... 
调整光源相机参数
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的默认为光照

在这里插入图片描述

屏幕像素,对应超出了:光源投影视锥体范围的,都默认成有阴影了。 我们想要的是超出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 || uv.y < 0) return 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 if (fragDepth > 1) return 1; } 

留意:

// clamp to edge color : 1 if (uv.x > 1 || uv.y > 1 || uv.x < 0 || uv.y < 0) return 1; ... if (fragDepth > 1) return 1; 

运行效果: 在这里插入图片描述

添加光照处理

在这里插入图片描述

添加PCF柔滑整体边缘
// 获取光照衰减系数 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 || uv.y < 0) return 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 atten = 1; if (_CustomShadowSmoothType == 0) {// hard float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv).xy); if (fragDepth < 1 && fragDepth - _CustomShadowBias > 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 < 2; ++i) { for(int j = -1; j < 2; ++j) { offset = float2(i, j) * _CustomShadowMap_TexelSize.xy * _CustomShadowPCFSpread; float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv + offset).xy); if (fragDepth - _CustomShadowBias > shadowMapDepth) { atten -= minus_step; } } } } return atten; } 

运行效果 在这里插入图片描述

上面的PCF效果我是故意将效果调整大一些,方便查看效果。

添加SDFs_Like效果

首先为何有个"Like",意思就是模拟,但不是的意思。

SDFs,我粗略看到了,需要对观察者,有尺寸,如,点光源,是没有尺寸的,而SDFs模拟的是类似光源时有大小,体积的,如:太阳,就大的很,它利用光源大小中,计算出光源重叠的区域,然后根据区域来制作边缘淡入淡出。

我用GGB话一个动态图,就很好理解 在这里插入图片描述

但我这个方法不是按这种思路制作的,还没有详细的去了解过,就先不制作了,这里只制作一个"Like"的效果。

下面这个SDFs_Like这个是我自己总结的方法,没有参考别的资料,单凭想象力,但是有可能别人也早有这样的思路。

就是根据阴影遮挡深度(shadow map的深度)与当前绘制片段的深度,之间的距离,假设为:Distance;

将这个Distaince作为控制PCF的扩散程度系数,就可以达到类似效果;

核心Shader 逻辑

} else if (_CustomShadowSmoothType == 2) {// SDFs_Like float center_shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv).xy); float distance = 1; if (fragDepth - _CustomShadowBias > 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 < 3; ++i) { for(j = -2; j < 3; ++j) { idx = (j+2) + (i+2) * 5; w = _CustomShadowBlurWeight[idx]; w *= strengthen; offset = float2(i, j) * _CustomShadowMap_TexelSize.xy * _CustomShadowPCFSpread * distance; float shadowMapDepth = DecodeFloatRG(tex2D(_CustomShadowMap, uv + offset).xy); if (fragDepth - _CustomShadowBias > shadowMapDepth) { atten -= w; } } } } 

_CustomShadowBlurDistance和_CustomShadowBlurWeight是我从CSharp层传到Shader层的global uniform CSharp 层:

private static readonly int _CustomShadowBlurDistance_hash = Shader.PropertyToID("_CustomShadowBlurDistance"); private static readonly int _CustomShadowBlurWeight_hash = Shader.PropertyToID("_CustomShadowBlurWeight"); public enum ShadowType // shadow map 边缘的平滑类型 { Hard = 0, // 硬边 PCFs_Like, // 类似PCFs,柔化整体边缘,类似均值模糊 SDFs_Like, // 类似SDFs,根据与遮挡体距离做模拟散射采样 } // 软阴影类型 public ShadowType softType = ShadowType.Hard; [Range(0, 10)] public float pcfSpread = 1; [Range(0.01f, 1)] public float sdfBurDistance = 1; private void Start() { ... blurWeight = new float[25] { 0.0030f, 0.0133f, 0.0219f, 0.0133f, 0.0030f, 0.0133f, 0.0596f, 0.0983f, 0.0596f, 0.0133f, 0.0219f, 0.0983f, 0.1621f, 0.0983f, 0.0219f, 0.0133f, 0.0596f, 0.0983f, 0.0596f, 0.0133f, 0.0030f, 0.0133f, 0.0219f, 0.0133f, 0.0030f, }; Shader.SetGlobalFloatArray(_CustomShadowBlurWeight_hash, blurWeight); } private void OnPreRender() { Shader.SetGlobalInt(_CustomLightEnable_hash, lightingEnable ? 1 : 0); Shader.SetGlobalInt(_CustomLightHalfLambert_hash, halfLambert ? 1 : 0); Shader.SetGlobalInt(_CustomShadowSmoothType_hash, (int)softType); Shader.SetGlobalFloat(_CustomShadowPCFSpread_hash, pcfSpread); Shader.SetGlobalFloat(_CustomShadowBlurDistance_hash, sdfBurDistance); ... } 

运行效果: 在这里插入图片描述

GIF动态图看看 在这里插入图片描述

添加柔和阴影边缘衰减到光照 MixToLight

CSharp

private static readonly int _CustomShadowMixToLight_hash = Shader.PropertyToID("_CustomShadowMixToLight"); // 阴影衰减混合到光照 public bool mixToLight = true; private void OnPreRender() { ... Shader.SetGlobalInt(_CustomShadowMixToLight_hash, mixToLight ? 1 : 0); ... } 

Shader

fixed4 shading(v2f i, float atten) { if (_CustomLightEnable) { // code here: // ambient // diffuse // specular // etc ... // albedo fixed4 albedo = tex2D(_MainTex, i.uv); i.worldNormal = normalize(i.worldNormal); //viewDir后面高光用 half3 viewDir = normalize(_WorldSpaceCameraPos.xyz - i.worldPos); half3 lightDir = normalize(_WorldSpaceLightPos0.xyz); // ambient fixed3 ambient = UNITY_LIGHTMODEL_AMBIENT.rgb * albedo.rgb; // diffuse fixed LdotN = dot(lightDir, i.worldNormal); fixed diffuse = _CustomLightHalfLambert ? LdotN * 0.5 + 0.5 : max(0, LdotN); // specular fixed specular = 0; bool appliedAtten = appliedAtten = atten == 1; // Hard if (_CustomShadowMixToLight) { // PCFs_Like || SDFs_Like if (_CustomShadowSmoothType == 1 || _CustomShadowSmoothType == 2) { appliedAtten = true; // 意味着模糊边缘的话,specular会乘上atten来衰减 } } if (LdotN > 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边缘是不处理的。

效果: 在这里插入图片描述

CommandBuffer版

主要我封装了一个测试用的类

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<Renderer> replaceList, List<Renderer> sourceList) { var checkTag = replacement.replaceTag; var checkValue = replacement.replaceType == ReplaceType.CheckTypeAndValue ? replacement.replaceMat.GetTag(checkTag, false) : string.Empty; var renderArr = GameObject.FindObjectsOfType<Renderer>(); 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<MeshInfo> replaceList, List<MeshInfo> sourceList) { var checkTag = replacement.replaceTag; var checkValue = replacement.replaceType == ReplaceType.CheckTypeAndValue ? replacement.replaceMat.GetTag(checkTag, false) : string.Empty; var renderArr = GameObject.FindObjectsOfType<Renderer>(); if (replacement.replaceType == ReplaceType.IngoreType_ForAll) { foreach (var item in renderArr) { replaceList.Add(new MeshInfo { mesh = item.gameObject.GetComponent<MeshFilter>().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<MeshFilter>().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<MeshFilter>().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<Renderer> replaceList = new List<Renderer>(); [SerializeField] private List<Renderer> sourceList = new List<Renderer>(); [SerializeField] private List<MeshInfo> replaceList_meshFilter = new List<MeshInfo>(); [SerializeField] private List<MeshInfo> sourceList_meshFilter = new List<MeshInfo>(); [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.SetViewProjectionMatrixAPI有冲突,所以我就没使用第二个参数了,改用自己定义的矩阵

修正: 正确使用方法:

cmdBuff.SetViewProjectionMatrices(ViewMatrix, ProjectionMatrix); cmdBuff.DrawMesh(Mesh, LocalToWorldMatrix, Material); 

之后花了5分钟写了个测试:CommandBuffer.SetViewProjectionMatrixes与CommandBuffer.DrawMesh的测试

坑7

上面坑4和坑6虽然修正了用法,但是_ProjectionParams.w但不是1/far的值,我估计是否这样用法的人很少,没人踩过这个坑?

很有可能是因为CommandBuffer.SetViewProjectionMatrixes并没有对_ProjectionParams更新,那么其他的变量也是极有可能也是没有更新的,所以还是妥妥的用回自定义的矩阵来处理。

Shader
// 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 官方文档
已实现 SSSM

具体查看: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的方式。
Project

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 是公开到网盘可提供下载的
GGB

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
  • 实时阴影技术总结
  • 入门Distance Field Soft Shadows
  • penumbra shadows in raymarched SDFs
  • Advanced Soft Shadow Mapping Techniques
  • Shadow Cascades - Unity 官方文档
  • 高精度 高质量 自适应 角色包围盒阴影 Unity ShadowMap - 后续可以扩展使用自适配渲染内容的 camera pos, near, far 的调整
关注
打赏
1688896170
查看更多评论

暂无认证

  • 7浏览

    0关注

    115984博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文
立即登录/注册

微信扫码登录

0.0864s