最近接到需求做体积雾,选择使用RayMarching的方式。网上RayMarching相关的资料还是比较充足的。多用于制作体积云、体积雾以及距离场绘制图像。其实现方式大同小异,有兴趣的可以自行查阅相关文献。
网上的资料全部是在Built-in管线下实现,本篇文章将着重讲解如何在URP下实现RayMarching中使用的射线的发射方法。其原理与其他文献一致,将不深入讲解。
由于RayMarching多数情况下都在后处理阶段进行,所以其实现会涉及到后处理脚本及shader的编写。而在Built-in及URP下后处理的实现方式又有较大差异,这就导致在照着网上的资料在URP下实现RayMarching时会遇到一些问题。发射射线就是其中之一。
在网上的资料中,在脚本中算出相机视锥的四条向量TL、TR、BL、BR,同时在相机前绘制一个Quad,自定义其UV及坐标,将vertex.z手动改为射线的序号,再将信息传给shader。在shader的顶点着色器中即可取到正确的射线。
而在URP中,由于不支持OnRenderImage()方法,无法通过GL绘制Quad来自定义UV坐标及vertex.z。只能在RenderFeature中将相机视锥的四条向量传入shader,再在shader中计算出该选择哪条向量作为该点发射的射线。
思想也很简单,在其他文章中通过绘制一个覆盖屏幕的片来确定射线,那么当然也可以直接通过屏幕坐标计算出该选取哪条射线。剩下要做的只是在顶点着色器中计算屏幕坐标。
在RenderFeature中,与Built-in下的脚本十分相似
...
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
source = renderer.cameraColorTarget;
m_Material.SetMatrix("_FrustumCornersES", GetFrustumCorners(renderingData.cameraData.camera));
m_Material.SetMatrix("_CameraInvViewMatrix", renderingData.cameraData.camera.cameraToWorldMatrix);
m_Material.SetMatrix("_DIYCameraInvProj", renderingData.cameraData.GetProjectionMatrix().inverse);
...
}
...
private Matrix4x4 GetFrustumCorners(Camera cam)
{
float camFov = cam.fieldOfView;
float camAspect = cam.aspect;
Matrix4x4 frustumCorners = Matrix4x4.identity;
float fovWHalf = camFov * 0.5f;
float tan_fov = Mathf.Tan(fovWHalf * Mathf.Deg2Rad);
Vector3 toRight = Vector3.right * tan_fov * camAspect;
Vector3 toTop = Vector3.up * tan_fov;
Vector3 topLeft = -Vector3.forward - toRight + toTop;
Vector3 topRight = -Vector3.forward + toRight + toTop;
Vector3 bottomRight = -Vector3.forward + toRight - toTop;
Vector3 bottomLeft = -Vector3.forward - toRight - toTop;
frustumCorners.SetRow(0, topLeft);
frustumCorners.SetRow(1, topRight);
frustumCorners.SetRow(2, bottomRight);
frustumCorners.SetRow(3, bottomLeft);
return frustumCorners;
}
由于后处理脚本中无法获取相机相关的属性,故需要通过脚本手动传入。
在shader的顶点着色器中则通过屏幕坐标来获取射线。
...
float4 sp = ComputeScreenPos(o.posCS);
float2 screenPos = sp.xy / sp.w;
if(screenPos.x > 0.000001 && screenPos.y > 0.000001 )
{
o.ray = _FrustumCornersES[1];
}
else if(screenPos.x > 0.000001 && screenPos.y < 0.000001)
{
o.ray = _FrustumCornersES[2];
}
else if(screenPos.x < 0.000001 && screenPos.y > 0.000001)
{
o.ray = _FrustumCornersES[0];
}
else
{
o.ray = _FrustumCornersES[3];
}
o.ray = mul(_CameraInvViewMatrix, o.ray);
....
这种简易的处理方法得到的结果会有一些瑕疵。如上面两张图所示,上面的图是在Built-in下使用绘制Quad的方法发射的射线,下面的图是在URP下使用本文介绍的方法绘制的射线,可见下图的边界明显,不够平滑。
若发现其他问题或有好的改进方法欢迎在下方讨论。