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

2022-4-21 21:47沙地网 732 0

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

接下来我们来说一下高质量角色软阴影的实现
如果我们直接使用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 vertexPositions = new List();
        private List vertexRenderer = new List();
        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();
            int boundscount = 0;

             for(int i = 0;i 

计算出AABB包围盒的每一个顶点的世界坐标,为了方便Debug,这里加了 球体显示:

   void Start()
        {
          

            skinmeshes = shadowCaster.GetComponentsInChildren();
            int boundscount = 0;

              for(int i = 0;i ());
               
                vertexRenderer[i].transform.position = vertexPositions[i] + bounds.center;
                vertexRenderer[i].material.SetColor("_BaseColor", Color.red);
                vertexRenderer[i].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 vertexPositions = new List();
        private List vertexRenderer = new List();
        private SkinnedMeshRenderer[] skinmeshes;
        private int boundsCount;
        
        void Start()
        {
          

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

            for(int i = 0;i < skinmeshes.Length; i++)
            {
                
                CalculateAABB(boundsCount, skinmeshes[i]);
                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());
               
                vertexRenderer[i].transform.position = vertexPositions[i] + bounds.center;
                vertexRenderer[i].material.SetColor("_BaseColor", Color.red);
                vertexRenderer[i].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[0] = (new Vector3(x, y, z));
            vertexPositions[1] = (new Vector3(x, -y, z));
            vertexPositions[2] = (new Vector3(x, y, -z));
            vertexPositions[3] = (new Vector3(x, -y, -z));
            vertexPositions[4] = (new Vector3(-x, y, z));
            vertexPositions[5] = (new Vector3(-x, -y, z));
            vertexPositions[6] = (new Vector3(-x, y, -z));
            vertexPositions[7] = (new Vector3(-x, -y, -z));


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

                //  vertexRenderer.Add(GameObject.CreatePrimitive(PrimitiveType.Sphere).GetComponent());
                vertexRenderer[i].transform.position = vertexPositions[i] + bounds.center;
                vertexRenderer[i].material.SetColor("_BaseColor", Color.cyan);
                vertexRenderer[i].transform.localScale = new Vector3(0.1f, 0.1f, 0.1f);
                vertexPositions[i] = vertexRenderer[i].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博客


鲜花

握手

雷人

路过

鸡蛋

最新评论

QQ|手机版|小黑屋|九艺游戏动画论坛 ( 津ICP备2022000452号-1 )

GMT+8, 2024-3-29 06:34 , Processed in 0.045911 second(s), 17 queries .

Powered by Discuz! X3.4  © 2001-2017 Discuz Team.