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 + &#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 = (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(&#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博客 谢谢分享,已经实现~ [耶] 相同分辨率下,人物的阴影明显锐利多了。我上午测时,在URP的 setting 里,忘了把 Cascade 设置为 1,出现了一些奇奇怪怪的阴影绘制。另外,人物的AABB里若有其他物体(脚底下的桶、石头之类的),似乎会把这些其他物体的影子也绘制进去。后期可能还得改改。 其他物体的话是需要设置成不投影子的,因为是针对于人物包围盒绘制的阴影,相当于设置了相机参数呢 谢谢大佬分享, 我尝试复现一下. 不过我头发的投影是用一个pass进行顶点偏移后当作mask输出到一张rt, 然后在面部采样这张rt后作为阴影,(因为我模型的头发做的实在太靠外了,用普通的投影会占据面部很大部分) 这是可以的,根据项目需求来做 作者你好,感谢你的分享,从中学到了很多知识。请问一下UniversalRenderPipeline.viewMatrix = viewMatrix; 这个修改矩阵的代码我这里报错,namespace UnityEngine.Rendering.Universal这个明明空间也加了,但是没有.viewMatrix的这个定义,是什么原因? 这个是要自己在pipeline里定义的 哦,知道了,一开始没注意到,还以为自己漏掉了什么。感谢[大笑] 这样强改,别的物体的影子不就寄了
页:
[1]