您当前的位置: 首页 >  unity

Jave.Lin

暂无认证

  • 3浏览

    0关注

    704博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

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

Jave.Lin 发布时间:2020-04-10 23:24:23 ,浏览量:3

文章目录
  • 思路
  • 实践
    • 在方向光的位置,放一个正交相机
    • 调整光源相机参数
      • 将光源投影空间的正交视锥体画出来
    • 投射阴影
    • 接收阴影
  • 改进
    • 超出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_01EncodeFloatRG 都是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,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 边缘是不处理的。

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

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 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更新,那么其他的变量也是极有可能也是没有更新的,所以还是妥妥的用回自定义的矩阵来处理。

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 的调整
关注
打赏
1664331872
查看更多评论
立即登录/注册

微信扫码登录

0.0469s