这篇文章的初始是因为项目里的角色阴影质量不高,然后搜寻了一些资料后,在游戏葡萄看到了这样一段话,是崩坏中实现的高质量阴影 接下来我们来说一下高质量角色软阴影的实现。 如果我们直接使用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博客 |