最近接到需求做体积雾,选择使用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);
                ....

alt alt 这种简易的处理方法得到的结果会有一些瑕疵。如上面两张图所示,上面的图是在Built-in下使用绘制Quad的方法发射的射线,下面的图是在URP下使用本文介绍的方法绘制的射线,可见下图的边界明显,不够平滑。
若发现其他问题或有好的改进方法欢迎在下方讨论。