【URP】stencil实现快速有限局部后处理(一)

2022-4-3 17:04你丧心病狂吧 1005 0

决定写这个方案源于一个奇怪的需求:在某段过场动画时需要一个转场效果,需要把画面中非主角的部分全部变为类似黑白胶片的效果,但是角色本身保持原先渲染不变。

确定方案

接到这个需求的时候第一反应是更改shader,理论上通过在相关shader中设置开关并增加计算步骤的方式可以通过遍历Renderer设置材质球解决问题(就像顶点雾),但是由于在使用情境下需要动态地切换状态,因此这种方式在针对大量渲染对象的情境下操作过于复杂且开销不低。

所以我考虑通过区域后处理的方式来执行类似功能可以在一定程度上降低性能开销。此时的问题变成了,生效区域要如何从屏幕中分离出来?

第一时间想到的方式就是使用mask图。但是这张全屏的mask图开销无疑是巨大的。就算RT只有一半屏幕分辨率,由于这张RT不能进行压缩,其自身内存消耗就很大。同时这张mask图的渲染成本,不论是使用mrt在渲染时多输出一张图还是像实时阴影的算法那样增加pass进行渲染,都在单帧渲染内增加了非常多的计算量。如果需要在较低配置的移动端实现这个效果,这个mask图的来源就只剩下stencil。

方案

使用stencil实现该功能有两个问题需要处理:

  • unity默认设置上深度图在后处理阶段会清空stencil值(目前只在URP实现,其他管线暂时没试过)
  • 由于其他功能会有stencil的使用需要,stencil值不能单独用来处理这个效果,因此需要生效的stencil各不相同

因此我实现了一个通过动态分配stencil实现局部后处理的方案。该方案实现最开始需要的效果只需要增加1个drawcall,而且可以同时叠加多个效果,劣势在于stencil不能偏移读取,因此抠像无法做uv偏移相关的操作。效果如图

这个方案分两个模块,一是通过stencil实现局部后处理,二是实现一个动态分配stencil的系统。

这次先讲怎么通过改管线以实现stencil后处理,找个时间再写我的动态分配方案

让后处理可以获取到stencil

翻看PostProcessPass.cs可以看到URP原生的后处理Blit方式。Blit函数有一个if else的判定,其判定源m_UseDrawProcedrual的值取决于当前相机是否是混合现实渲染的相机。不做混合现实的项目相当于直接使用commandbuffer.blit()函数执行的渲染。

m_UseDrawProcedural   = renderingData.cameraData.xr.enabled;

......


private new void Blit(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, Material material, int passIndex = 0)
{
   cmd.SetGlobalTexture(ShaderPropertyId.sourceTex, source);
   if (m_UseDrawProcedural)
   {
        Vector4 scaleBias = new Vector4(1, 1, 0, 0);
        cmd.SetGlobalVector(ShaderPropertyId.scaleBias, scaleBias);
        cmd.SetRenderTarget(new RenderTargetIdentifier(destination, 0, CubemapFace.Unknown, -1),
            RenderBufferLoadAction.Load, RenderBufferStoreAction.Store, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
        cmd.DrawProcedural(Matrix4x4.identity, material, passIndex, MeshTopology.Quads, 4, 1, null);
   }
  else
  {
        cmd.Blit(source, destination, material, passIndex);
   }
}

该方法在Blit时无法设置RenderTarget,而Unity会在这一步默认不传递depth buffer从而导致stencil在后处理中无法使用。因此需要先改变后处理的渲染方式。我在这里重新定义了一个函数BlitSp(),使用DrawMesh函数配合RenderingUtils.fullscreenMesh达到相同的效果,使得Blit操作可以手动设置RenderTarget以传递depth buffer,把这一步操作放在了所有后处理之前(后续原生后处理仍然不需要传递depth buffer)

void BlitSp(Camera camera, CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier dest,
            RenderTargetIdentifier depth, Material mat, int passIndex, Rect rect, MaterialPropertyBlock mpb = null)
{
     cmd.SetGlobalTexture(ShaderPropertyId.sourceTex, source);
     cmd.SetRenderTarget(dest, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, 
         depth, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
     cmd.ClearRenderTarget(false, false, Color.clear);
     cmd.SetViewProjectionMatrices(Matrix4x4.identity,Matrix4x4.identity);
     cmd.SetViewport(rect);
     cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, mat, 0, passIndex, mpb);
     cmd.SetViewProjectionMatrices(camera.worldToCameraMatrix, camera.projectionMatrix);
}

此时我们需要手动传入正确的depth buffer,实际上PostProcessPass内本身已经定义了m_Depth,只是和上述原因一样只在xr开启。这里只需要传入m_Depth.Identifier()即可获取到正确的rendertarget。以下是一个使用范例(CustomPostProcessAnchor是另外定义的管理自定义后处理的类,可以自由重新实现,这里不放代码了)

注意:PostProcessPass使用Swap()来控制source和dest的切换,实际使用中根据使用情况需要确保增加处理的pass处理结束后最后输出是_CameraColorTexture,否则后续流程可能会造成问题。因此实际使用中需要考虑是否使用、用几次Swap。这里我只用了一次blit,source是_CameraColorTexture,因此只在结束的时候Swap一次。

            if (CustomPostProcessAnchor.passes != null && CustomPostProcessAnchor.customUberMat != null/* && !cameraData.isSceneViewCamera*/)
            {
                BlitSp(cameraData.camera, cmd, GetSource(), BlitDstDiscardContent(cmd, destination), 
                        m_Depth.Identifier(), CustomPostProcessAnchor.customUberMat, CustomPostProcessAnchor.passes[i].index, 
                        cameraData.pixelRect
                    );
                Swap();
                CustomPostProcessAnchor.ClearIndex();
            }

如果有特殊需要,比如需要在原先后处理流程中间的某个阶段使用depth buffer,则需要直接改造原先的Blit()函数,让每次后处理都传递depth buffer

private new void Blit(CommandBuffer cmd, RenderTargetIdentifier source, RenderTargetIdentifier destination, Material material, int passIndex = 0, bool needstencil = false, Camera camera = null)
{
    cmd.SetGlobalTexture(ShaderPropertyId.sourceTex, source);
    if (m_UseDrawProcedural)
    {
        Vector4 scaleBias = new Vector4(1, 1, 0, 0);
        cmd.SetGlobalVector(ShaderPropertyId.scaleBias, scaleBias);
        cmd.SetRenderTarget(new RenderTargetIdentifier(destination, 0, CubemapFace.Unknown, -1),
            RenderBufferLoadAction.Load, RenderBufferStoreAction.Store, RenderBufferLoadAction.Load, RenderBufferStoreAction.Store);
        cmd.DrawProcedural(Matrix4x4.identity, material, passIndex, MeshTopology.Quads, 4, 1, null);
    }
    else
    {
        if (needstencil && camera != null)
        {
            cmd.SetRenderTarget(destination, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store,
                m_Depth.id, RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store);
            cmd.ClearRenderTarget(false, true, Color.clear);
            cmd.SetViewProjectionMatrices(Matrix4x4.identity, Matrix4x4.identity);
            cmd.DrawMesh(RenderingUtils.fullscreenMesh, Matrix4x4.identity, material, 0, passIndex);
            cmd.SetViewProjectionMatrices(camera.worldToCameraMatrix, camera.projectionMatrix);
        }
        else
        {
            cmd.Blit(source, destination, material, passIndex);
        }
    }
}

完成以上设置之后,在渲染场景时对材质球参数设置合适的stencil值,在后处理阶段的shader pass就可以正确读取stencil信息了


鲜花

握手

雷人

路过

鸡蛋

最新评论

QQ|手机版|小黑屋|九艺游戏动画论坛 ( 津ICP备2022000452号-1 )

GMT+8, 2024-3-29 09:34 , Processed in 0.089170 second(s), 18 queries .

Powered by Discuz! X3.4  © 2001-2017 Discuz Team.