|
这篇文章的初始是因为项目里的角色阴影质量不高,然后搜寻了一些资料后,在游戏葡萄看到了这样一段话,是崩坏中实现的高质量阴影
接下来我们来说一下高质量角色软阴影的实现。
如果我们直接使用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 + &#34; is being encapsulate&#34;);
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(&#34;_BaseColor&#34;, 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 + &#34; Length&#34;);
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(&#34;_BaseColor&#34;, 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 + &#34; is being encapsulate&#34;);
Debug.Log(boundsCount);
}
public void UpdateAABB()
{
int boundscount = 0;
foreach(var skinmesh in skinmeshes) {
//if(skinmesh.sharedMesh.name == &#34;UpperBody&#34;)
//{
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<MeshRenderer>());
vertexRenderer.transform.position = vertexPositions + bounds.center;
vertexRenderer.material.SetColor(&#34;_BaseColor&#34;, 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博客 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|