HuldaGnodim 发表于 2022-5-8 15:33

UnityURP管线实现高质量角色单独投影

这篇文章的初始是因为项目里的角色阴影质量不高,然后搜寻了一些资料后,在游戏葡萄看到了这样一段话,是崩坏中实现的高质量阴影


接下来我们来说一下高质量角色软阴影的实现。
如果我们直接使用unity内置的CSM阴影,在镜头靠近角色的时候阴影品质并不能满足需求,所以我们就为角色单独渲染了一张shadowmap,以确保恒定的阴影品质;为此我们还实现了基于视锥的shadowmap,根据角色的boundingbox和视锥求交集部分,以此作为渲染区域,就可以最大化阴影贴图的使用率,此外还使用了Variance shadow map以及PCSS来减少阴影瑕疵以及获得自然的软阴影效果。发现基于角色包围盒的技术实现起来容易并且时间成本也不高,提升效果显著,便想着手一试。
主要想法是向URP管线传递自定义的包围盒所形成的摄像机参数,来充分利用阴影贴图。
Tips:URPpackage需要复制一份然后进行设置才能在原有基础上修改,可以查询资料如何修改URP



改进前



改进后



不管多远都可以清楚投射

因为阴影人物在阴影贴图上的占比太小,所以会出现何种锯齿,在精细的人物脸部,我们是非常不希望有马赛克的。



改进后的



改进前



原有的固定的摄像机参数,投影贴图利用率与要投影的物体无关

我们可以将光线投影紧密拟合到视图图面会增加阴影贴图覆盖范围,如下图所示:


这样可以极高的提升阴影利用效率。
步骤如下:

1.计算角色的包围盒
2.将角色的包围盒的8个点投射到光源空间。
3.利用包围盒的最大最小值决定投影矩阵。



Tips:URP每次导入会覆盖设置,就需要重新复制一份URPpackage,然后设置,可以参考修改urp的方法。
读了一下urp的代码,经过各种迂回曲折,找到了重要的阴影摄像机参数,在mainLightShadowCasterPass里




这一行中的函数ShadowUtils.ExtractDirectionalLightMatrix()为重中之重。
再继续点进去看,Unity已经封装好了计算阴影贴图矩阵的函数 cullResults.ComputeDirectionalShadowMatricesAndCullingPrimitives。
而这个函数传递的参数为
shadowLightIndex(灯光的index),cascadeIndex(当前的摄像机距离划分的cascade层级), shadowData.mainLightShadowCascadesCount(所有的cascade个数), shadowData.mainLightShadowCascadesSplit(没细看,可能是描述cascade按什么比率划分的,不重要,因为我们不用Cascade), shadowResolution(阴影贴图的), shadowNearPlane(阴影相机的近平面?), out viewMatrix(变换到光源空间的视图矩阵), out projMatrix(摄像机的投影矩阵),out splitData);


而我们想在不新建一个摄像机的情况下,不直接修改主摄像机的参数下直接传递数值给renderer,所以可以利用修改viewMatrix和ProjectionMatrix来自定义阴影相机的参数。
知道要修改什么了以后,便着手计算矩阵即可。


角色包围盒

我们首先来计算包围盒,定义一个HighQualityShadow类,里面定义一些参数:

public class HighQualityShadow : MonoBehaviour
    {
      // Start is called before the first frame update
      Bounds bounds = new Bounds();
      //可以升级为很多个transform
      public Transform shadowCaster;
      public Light mainLight;
      public float shadowClipDistance = 10;
      private Matrix4x4 viewMatrix, projMatrix;
      
      private List<Vector3> vertexPositions = new List<Vector3>();
      private List<MeshRenderer> vertexRenderer = new List<MeshRenderer>();
      private SkinnedMeshRenderer[] skinmeshes;
      
      }
实现AABB包围盒的方式就不细讲,开始对于角色制作了OBB包围盒,但是考虑到角色是Skinmeshrenderer,不清楚角色有动画的话会发生什么
void CalculateAABB(int boundsCount, SkinnedMeshRenderer skinmeshRender)
      {
            if(boundsCount != 0)
            {

                bounds.Encapsulate(skinmeshRender.bounds);
               
            
            }
            else
            {
                bounds = skinmeshRender.bounds;
               
            }
            
            Debug.Log(skinmeshRender.name + " is being encapsulate");
            Debug.Log(boundsCount);
      }对于每一个skinMeshRenderer计算总的包围盒:

void Start()
      {
         

            skinmeshes = shadowCaster.GetComponentsInChildren<SkinnedMeshRenderer>();
            int boundscount = 0;

             for(int i = 0;i <skinmeshes.Length;i++)
            {
               
                CalculateAABB(boundsCount, skinmeshes);
                boundsCount += 1;
            }
}计算出AABB包围盒的每一个顶点的世界坐标,为了方便Debug,这里加了 球体显示:
   void Start()
      {
         

            skinmeshes = shadowCaster.GetComponentsInChildren<SkinnedMeshRenderer>();
            int boundscount = 0;

            for(int i = 0;i <skinmeshes.Length;i++)
            {
               
                CalculateAABB(boundsCount, skinmeshes);
                boundsCount += 1;
            }

            float x = bounds.extents.x;                                       //范围这里是三维向量,分别取得X Y Z
            float y = bounds.extents.y;
            float z = bounds.extents.z;

            vertexPositions.Add(new Vector3(x, y, z));
            vertexPositions.Add(new Vector3(x, -y, z));
            vertexPositions.Add(new Vector3(x, y, -z));
            vertexPositions.Add(new Vector3(x, -y, -z));
            vertexPositions.Add(new Vector3(-x, y, z));
            vertexPositions.Add(new Vector3(-x, -y, z));
            vertexPositions.Add(new Vector3(-x, y, -z));
            vertexPositions.Add(new Vector3(-x, -y, -z));


            for(int i =0;i<vertexPositions.Count;i++)
            {

                vertexRenderer.Add(GameObject.CreatePrimitive(PrimitiveType.Sphere).GetComponent<MeshRenderer>());
               
                vertexRenderer.transform.position = vertexPositions + bounds.center;
                vertexRenderer.material.SetColor("_BaseColor", Color.red);
                vertexRenderer.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
            }
         


      }


ViewMatrix

然后,再定义一个计算光空间内的包围盒的最小最大值,以此确定摄像机的投影矩阵
public void fitToScene()
      {

            float xmin = float.MaxValue, xmax = float.MinValue;
            float ymin = float.MaxValue, ymax = float.MinValue;
            float zmin = float.MaxValue, zmax = float.MinValue;


            foreach(var vertex in vertexPositions)
            {

                Vector3 vertexLS = mainLight.transform.worldToLocalMatrix.MultiplyPoint(vertex);
                xmin = Mathf.Min(xmin, vertexLS.x);
                xmax = Mathf.Max(xmax, vertexLS.x);
                ymin = Mathf.Min(ymin, vertexLS.y);
                ymax = Mathf.Max(ymax, vertexLS.y);
                zmin = Mathf.Min(zmin, vertexLS.z);
                zmax = Mathf.Max(zmax, vertexLS.z);

            }
         
            viewMatrix = mainLight.transform.worldToLocalMatrix;
         

            if (SystemInfo.usesReversedZBuffer)
            {
                viewMatrix.m20 = -viewMatrix.m20;
                viewMatrix.m21 = -viewMatrix.m21;
                viewMatrix.m22 = -viewMatrix.m22;
                viewMatrix.m23 = -viewMatrix.m23;
            }
         

            UniversalRenderPipeline.viewMatrix = viewMatrix;

            

      }注意,光源处的摄像机的position是朝向的相反方向,摄像机朝向为-Z方向,所以要将投影矩阵的后4个参数取负。
ProjectionMatrix

再利用正交投影矩阵(代入包围盒最小最大值)


          zmax += shadowClipDistance * shadowCaster.localScale.x;
            
            Vector4 row0 = new Vector4(2/(xmax - xmin),0, 0,-(xmax+xmin)/(xmax-xmin));
            Vector4 row1 = new Vector4(0, 2 / (ymax - ymin), 0, -(ymax + ymin) / (ymax - ymin));
            Vector4 row2 = new Vector4(0, 0, -2 / (zmax - zmin), -(zmax + zmin) / (zmax - zmin));
            Vector4 row3 = new Vector4(0, 0, 0, 1);
      
            projMatrix.SetRow(0, row0);
            projMatrix.SetRow(1, row1);
            projMatrix.SetRow(2, row2);
            projMatrix.SetRow(3, row3);

            UniversalRenderPipeline.projMatrix = projMatrix;这里用的公式是



在UniversalRenderPipline里面定义两个矩阵




传入参数以后手动设置或者代码设置灯光和阴影投射体:




刘海阴影也可以很清楚
完整代码

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace UnityEngine.Rendering.Universal
{
    public class HighQualityShadow : MonoBehaviour
    {
      // Start is called before the first frame update
      Bounds bounds = new Bounds();
      //可以升级为很多个transform
      public Transform shadowCaster;
      public Light mainLight;
      public float shadowClipDistance = 10;
      private Matrix4x4 viewMatrix, projMatrix;
      
      private List<Vector3> vertexPositions = new List<Vector3>();
      private List<MeshRenderer> vertexRenderer = new List<MeshRenderer>();
      private SkinnedMeshRenderer[] skinmeshes;
      private int boundsCount;
      
      void Start()
      {
         

            skinmeshes = shadowCaster.GetComponentsInChildren<SkinnedMeshRenderer>();
            
            Debug.Log(skinmeshes.Length + "Length");

            for(int i = 0;i < skinmeshes.Length; i++)
            {
               
                CalculateAABB(boundsCount, skinmeshes);
                boundsCount += 1;
            }

            float x = bounds.extents.x;                                       //范围这里是三维向量,分别取得X Y Z
            float y = bounds.extents.y;
            float z = bounds.extents.z;

            vertexPositions.Add(new Vector3(x, y, z));
            vertexPositions.Add(new Vector3(x, -y, z));
            vertexPositions.Add(new Vector3(x, y, -z));
            vertexPositions.Add(new Vector3(x, -y, -z));
            vertexPositions.Add(new Vector3(-x, y, z));
            vertexPositions.Add(new Vector3(-x, -y, z));
            vertexPositions.Add(new Vector3(-x, y, -z));
            vertexPositions.Add(new Vector3(-x, -y, -z));


            for(int i =0;i< vertexPositions.Count;i++)
            {

                vertexRenderer.Add(GameObject.CreatePrimitive(PrimitiveType.Sphere).GetComponent<MeshRenderer>());
               
                vertexRenderer.transform.position = vertexPositions + bounds.center;
                vertexRenderer.material.SetColor("_BaseColor", Color.red);
                vertexRenderer.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
            }
         


      }

      // Update is called once per frame
      void Update()
      {
         

                UpdateAABB();
      
                fitToScene();



      }

      void CalculateAABB(int boundsCount, SkinnedMeshRenderer skinmeshRender)
      {
            if(boundsCount != 0)
            {

                bounds.Encapsulate(skinmeshRender.bounds);
               
            
            }
            else
            {
                bounds = skinmeshRender.bounds;
               
            }
            
            Debug.Log(skinmeshRender.name + " is being encapsulate");
            Debug.Log(boundsCount);
      }

      public void UpdateAABB()
      {


            int boundscount = 0;

            foreach(var skinmesh in skinmeshes) {
                //if(skinmesh.sharedMesh.name == "UpperBody")
                //{
            CalculateAABB(boundscount, skinmesh);
                boundscount += 1;
               // }
                  
            }


            float x = bounds.extents.x;                                       //范围这里是三维向量,分别取得X Y Z
            float y = bounds.extents.y;
            float z = bounds.extents.z;



            vertexPositions = (new Vector3(x, y, z));
            vertexPositions = (new Vector3(x, -y, z));
            vertexPositions = (new Vector3(x, y, -z));
            vertexPositions = (new Vector3(x, -y, -z));
            vertexPositions = (new Vector3(-x, y, z));
            vertexPositions = (new Vector3(-x, -y, z));
            vertexPositions = (new Vector3(-x, y, -z));
            vertexPositions = (new Vector3(-x, -y, -z));


            for (int i = 0; i < vertexPositions.Count; i++)
            {

                //vertexRenderer.Add(GameObject.CreatePrimitive(PrimitiveType.Sphere).GetComponent<MeshRenderer>());
                vertexRenderer.transform.position = vertexPositions + bounds.center;
                vertexRenderer.material.SetColor("_BaseColor", Color.cyan);
                vertexRenderer.transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
                vertexPositions = vertexRenderer.transform.position;
            }
      }
      public void fitToScene()
      {

            float xmin = float.MaxValue, xmax = float.MinValue;
            float ymin = float.MaxValue, ymax = float.MinValue;
            float zmin = float.MaxValue, zmax = float.MinValue;


            foreach(var vertex in vertexPositions)
            {

                Vector3 vertexLS = mainLight.transform.worldToLocalMatrix.MultiplyPoint(vertex);
                xmin = Mathf.Min(xmin, vertexLS.x);
                xmax = Mathf.Max(xmax, vertexLS.x);
                ymin = Mathf.Min(ymin, vertexLS.y);
                ymax = Mathf.Max(ymax, vertexLS.y);
                zmin = Mathf.Min(zmin, vertexLS.z);
                zmax = Mathf.Max(zmax, vertexLS.z);

            }
         
            viewMatrix = mainLight.transform.worldToLocalMatrix;
         

            if (SystemInfo.usesReversedZBuffer)
            {
                viewMatrix.m20 = -viewMatrix.m20;
                viewMatrix.m21 = -viewMatrix.m21;
                viewMatrix.m22 = -viewMatrix.m22;
                viewMatrix.m23 = -viewMatrix.m23;
            }
         

            UniversalRenderPipeline.viewMatrix = viewMatrix;

            zmax += shadowClipDistance * shadowCaster.localScale.x;
            
            Vector4 row0 = new Vector4(2/(xmax - xmin),0, 0,-(xmax+xmin)/(xmax-xmin));
            Vector4 row1 = new Vector4(0, 2 / (ymax - ymin), 0, -(ymax + ymin) / (ymax - ymin));
            Vector4 row2 = new Vector4(0, 0, -2 / (zmax - zmin), -(zmax + zmin) / (zmax - zmin));
            Vector4 row3 = new Vector4(0, 0, 0, 1);
      
            projMatrix.SetRow(0, row0);
            projMatrix.SetRow(1, row1);
            projMatrix.SetRow(2, row2);
            projMatrix.SetRow(3, row3);

            UniversalRenderPipeline.projMatrix = projMatrix;

      }

      public void OnDestroy()
      {
            //foreach (var sphere in vertexRenderer)
            //{
            //    vertexRenderer.Remove(sphere);
            //}
      }
    }
}参考:

米哈游技术总监首次分享:移动端高品质卡通渲染的实现与优化方案 - 知乎 (zhihu.com)
Mathematics for 3D Game Programming and Computer Graphics, Third Edition.pdf (projekti.info)
改进阴影深度映射的常见技术 - Win32 apps | Microsoft Docs
Directional Shadows (catlikecoding.com)
(148条消息) shadow map_jaccen的博客-CSDN博客

Baste 发表于 2022-5-8 15:37

谢谢分享,已经实现~ [耶] 相同分辨率下,人物的阴影明显锐利多了。我上午测时,在URP的 setting 里,忘了把 Cascade 设置为 1,出现了一些奇奇怪怪的阴影绘制。另外,人物的AABB里若有其他物体(脚底下的桶、石头之类的),似乎会把这些其他物体的影子也绘制进去。后期可能还得改改。

Arzie100 发表于 2022-5-8 15:39

其他物体的话是需要设置成不投影子的,因为是针对于人物包围盒绘制的阴影,相当于设置了相机参数呢

xiaozongpeng 发表于 2022-5-8 15:43

谢谢大佬分享, 我尝试复现一下. 不过我头发的投影是用一个pass进行顶点偏移后当作mask输出到一张rt, 然后在面部采样这张rt后作为阴影,(因为我模型的头发做的实在太靠外了,用普通的投影会占据面部很大部分)

HuldaGnodim 发表于 2022-5-8 15:44

这是可以的,根据项目需求来做

xiaozongpeng 发表于 2022-5-8 15:50

作者你好,感谢你的分享,从中学到了很多知识。请问一下UniversalRenderPipeline.viewMatrix = viewMatrix; 这个修改矩阵的代码我这里报错,namespace UnityEngine.Rendering.Universal这个明明空间也加了,但是没有.viewMatrix的这个定义,是什么原因?

JamesB 发表于 2022-5-8 15:57

这个是要自己在pipeline里定义的

super1 发表于 2022-5-8 15:58

哦,知道了,一开始没注意到,还以为自己漏掉了什么。感谢[大笑]

ChuanXin 发表于 2022-5-8 15:59

这样强改,别的物体的影子不就寄了
页: [1]
查看完整版本: UnityURP管线实现高质量角色单独投影