【Unity3D】URP下的GrabPass方案

  GrabPass和AlphaBlend都有渲染物体包含背景物体颜色的特点,不同的是,AlphaBlend渲染像素时,只能基于该像素的前一次DrawCall结果来混合,而GrabPass渲染此像素时,可以得到其他像素颜色,能做出扰动、折射之类的效果。
  然而URP管线并不支持GrabPass,虽然官方事例中提到可以用OpaqueTexture代替功能,但顾名思义,OpaqueTexture是在不透明物体渲染之后截取的一张RT,其中不包含半透明物体,因此我们需要手动实现GrabTexture功能。
  注:OpaqueTexture实际是在半透明物体渲染之前,也就是说是在天空盒渲染之后,甚至在BeforeRenderTransparent之后CopyColor。
  如果想要完全和GrabPass一致,可能是做不到的,Unity自带的GrabPass,能在此物体DrawCall的前一次DrawCall来Blit一张图片,而我们是无法打开一个渲染队列的,例如半透明物体整体在一个队列中,我们无法在其中插一个Blit。
  OnRenderImage、OnPerPost之类的方法也不推荐研究,不说URP是否支持,就算支持,他们对渲染位置的控制也有限。
  比他们控制能力更高一层的是CommandBuffer,CommandBuffer的控制精度在渲染队列之间,例如能在半透明渲染之后、或后处理之前插上几个渲染命令。

曾经Unity2018的替换方案

  我在2018BuildIn管线下替代GrabPass(原因是当时说GrabPass性能消耗高)就用的这种方法,在每个需要Grab的物体上加个脚本,脚本将自己注册到一个全局的单例管理器中,同时关闭粒子的Renderer组件,单例管理器组织一个CommandBuffer,先Blit一张图片,然后渲染需要GrabTexture的物体,这样的缺点是,当渲染物体为半透明物体(实际上需要Grab的物体大多是放在半透明渲染队列中,并且基本是粒子)时,半透明物体的排序不受Unity控制,可能需要花心思重新排序。场景切换时,需要花心思维护管理器,维护管理器中的Renderer(物体增减)、Camera(多相机)、CommandBuffer(重新排列)等。

2019URP下的GrabPass方案

  我一开始也沿用上述的方案,只不过URP下Camera.AddCommandBuffer无法正确将CommandBuffer加到渲染队列中,我采用了RenderFeature来载入CommandBuffer在渲染队列之间的位置。
  不幸的是,需要Grab的特效依旧没有被渲染出来,经过排查,CommandBuffer.DrawRenderer方法无法渲染粒子的ParticleSystemRenderer,在google上查,发现别人也出现了类似的问题,MeshRenderer以及SpriteRenderer可以正常渲染,经过对Unity官方人员的询问得知,这是Unity一直以来的Bug,在2018.4中被修复,但因为未来可能需要重新用DOTS实现,所以2019暂时不去修复。
  Unity人员建议用多相机解决这个问题,这和我们想法一致。

多相机解决方案

  这个方案就是为每个需要渲染GrabTexture特效、物体的相机,增加一个特效相机,这个特效相机除了cullingmask外,参数和生成它的主要相机完全一致。
  利用layer,让主要相机不去渲染这个物体,在后处理前用CommandBuffer Blit一张纹理,设置特效相机的cullingmask只渲染这个需要GrabTexture的物体。
  这里注意下URP下的多相机流程,和BuildIn下不同,BuildIn下,多相机是用相机深度来控制相机渲染次序,而URP下,是利用相机的Camera Stack,将主要相机类型设置为Base,其他在此之后渲染的相机设为Overlay,将Overlay相机加入到Base Camera的Camera Stack中。
当时我的脚本是这样的:

[RequireComponent(typeof(Camera))]
public class AddEffectCamera : MonoBehaviour
{
    public LayerMask EffectLayer;

    private Camera selfCamera;
    private Camera effectCamera;

    private LayerMask originSelfCameraLayerMask;

    private bool AttachToMainCamera = false;
    private bool Attached = false;
    private Scene preScene;

    private void Awake()
    {
        selfCamera = GetComponent<Camera>();
        var selfCameraData = selfCamera.GetUniversalAdditionalCameraData();
        originSelfCameraLayerMask = selfCamera.cullingMask;
        selfCamera.cullingMask &= (~EffectLayer);

        if (effectCamera == null)
        {
            GameObject go = new GameObject(name + "'s Effect Camera");
            go.transform.parent = transform;
            go.transform.localPosition = Vector3.zero;
            go.transform.localRotation = Quaternion.identity;
            go.transform.localScale = Vector3.zero;
            effectCamera = go.AddComponent<Camera>();
            var cameraData = effectCamera.GetUniversalAdditionalCameraData();
            cameraData.renderType = CameraRenderType.Overlay;
            cameraData.renderShadows = false;
            cameraData.clearDepth = false; //readonly, 要改URP源码,多加一个set属性

            effectCamera.cullingMask = EffectLayer;
            effectCamera.orthographic = selfCamera.orthographic;
            effectCamera.useOcclusionCulling = false;

            if (selfCameraData.renderType == CameraRenderType.Base)//主相机的特效相机,直接挂在最前边
                selfCameraData.cameraStack.Insert(0, effectCamera);
            else//其他相机可能需要考虑场景切换,Update时检查
                AttachToMainCamera = true;
        }

        preScene = SceneManager.GetActiveScene();
    }

    private void Update()
    {
//如果切换场景,则重新设置给主相机
        Scene currScene = SceneManager.GetActiveScene();
        if (currScene != preScene)
        {
            preScene = currScene;
            Attached = false;
        }
        if(AttachToMainCamera && !Attached && Camera.main != null)
        {
            var mainCameraData = Camera.main.GetUniversalAdditionalCameraData();
            int parentIndex = mainCameraData.cameraStack.FindIndex((Camera camera) => camera == selfCamera);
            if(parentIndex != -1)
            {
                mainCameraData.cameraStack.Insert(parentIndex + 1, effectCamera);
                Attached = true;
            }
        }

        effectCamera.fieldOfView = selfCamera.fieldOfView;
        effectCamera.nearClipPlane = selfCamera.nearClipPlane;
        effectCamera.farClipPlane = selfCamera.farClipPlane;
        effectCamera.orthographicSize = selfCamera.orthographicSize;

        selfCamera.cullingMask = (selfCamera.cullingMask & ~EffectLayer);
        effectCamera.cullingMask = EffectLayer;
    }

    public void SetEffectLayerMask(LayerMask effectLayer)
    {
        EffectLayer = effectLayer;
    }
}

  除此之外还需要写一个RenderFeature,这个Feature在每个相机后处理前截一张RT,因为这个RT我们是交给外面的粒子用,所以Execute最后不释放,等下一次执行时再释放再重新申请:

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomGrabPassFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public class Setting
    {
        public string textureName = "_GrabTexture";
        [Range(0, 1)]public float sampleDown = 0.5f;
        public RenderPassEvent passEvent = RenderPassEvent.AfterRenderingPostProcessing;
        public bool useBlitMat = true;
    }
    public Setting settings = new Setting();

    class BlitPass : ScriptableRenderPass
    {
        Setting setting;
        Material BlitMat;
        //RenderTexture rt;
        //RenderTargetIdentifier colorBuffer;

        public BlitPass(Setting setting)
        {
            this.setting = setting;
            BlitMat = new Material(Shader.Find("Hidden/Universal Render Pipeline/Blit"));
        }

        ///public void Setup(RenderTargetIdentifier colorBuffer) => this.colorBuffer = colorBuffer;

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {
            if (renderingData.cameraData.camera.cameraType == CameraType.SceneView)
                return;

            CommandBuffer cmd = CommandBufferPool.Get("Grab " + setting.textureName);

            RenderTextureDescriptor desc = renderingData.cameraData.cameraTargetDescriptor;
            int width = (int)(desc.width * setting.sampleDown);
            int height = (int)(desc.height * setting.sampleDown);

            int textureId = Shader.PropertyToID(setting.textureName);
            //cmd.GetTemporaryRT(textureId, desc);

            cmd.ReleaseTemporaryRT(textureId);
            cmd.GetTemporaryRT(textureId, width, height, 0, FilterMode.Bilinear, RenderTextureFormat.ARGB32);

            
            if(setting.useBlitMat)
                cmd.Blit(renderingData.cameraData.targetTexture, textureId, BlitMat, 0);
            else
                cmd.Blit(renderingData.cameraData.targetTexture, textureId);

            //Debug.LogWarning("Blit Camera: " + renderingData.cameraData.camera.name +  " to " + setting.textureName);

            context.ExecuteCommandBuffer(cmd);

            //cmd.ReleaseTemporaryRT(textureId);
            CommandBufferPool.Release(cmd);
        }

        
    }

    BlitPass mBlitPass;

    public override void Create()
    {
        mBlitPass = new BlitPass(settings) { renderPassEvent = settings.passEvent};
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //mBlitPass.Setup(renderer.cameraColorTarget);
        renderer.EnqueuePass(mBlitPass);
    }

}

  这里说几个坑,官方文档提到:在照相机渲染完成后,将删除所有未明确释放的临时纹理,但我们需要的是跨相机渲染,这个临时申请的RT我们还可以拿到,并且内容没有被清空或覆盖。
  在只有Game窗口时可以正常挨个执行,但假如中间穿插了SceneView相机,可能就会渲染出问题。
  其次CommandBuffer.Blit截取RT时,如果RT尺寸不同,例如复制到的RT时复制源相机的1/4,那么截到的画面只能是左上角的1/4。如果用CommandBuffer.Blit传入纹理的重载,则不会出现这个问题,但Blit是否传入纹理还会影响Blit的调用时机,所以不建议降采样。
  这个方法的好处是,将渲染粒子完全交给了Unity,我们无需考虑排序,只要考虑在什么地方截帧就好了。
  缺点是增加了相机管理的复杂度,因为几乎每个相机(除了阴影相机外)都会被传入RenderFeature,所以每增加一个其他用途的相机都要小心翼翼,如果出错就要重新排查。

ScriptableRenderContext.DrawRenderers解决方案

  上面说用多相机的原因,是因为CommandBuffer无法渲染粒子系统。不过URP不可能渲染不出粒子,经过实验,ScriptableRenderContext,也就是RenderFeature.Execute传进来的context参数,通过调用DrawRenderers方法,可以渲染粒子系统。这和上述的多相机方案差不多,都是利用Unity本身的机制渲染物体,Unity可以帮助我们排序、剪裁。

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class CustomGrabPassFeature : ScriptableRendererFeature
{
    [System.Serializable]
    public class Setting
    {
        public string textureName = "_ScreenGrabTexture";
        //[Range(0, 1)]public float sampleDown = 0.5f;
        public RenderPassEvent passEvent = RenderPassEvent.AfterRenderingTransparents;
        //Shader的Tag->LightMode
        public List<string> shaderTagIdList = new List<string>();
        //public bool useBlitMat = true;
    }
    public Setting settings = new Setting();

    class BlitPass : ScriptableRenderPass
    {
        Setting setting;
        //Material BlitMat;
        RenderTargetHandle mRT = RenderTargetHandle.CameraTarget;

        RenderStateBlock renderStateBlock;
        FilteringSettings mFilteringSettings;

        public List<ShaderTagId> shaderTagIdList = new List<ShaderTagId>();

        public BlitPass(Setting setting)
        {
            this.setting = setting;
            //BlitMat = new Material(Shader.Find("Hidden/Universal Render Pipeline/Blit"));
            mRT.Init(setting.textureName);

            renderStateBlock = new RenderStateBlock(RenderStateMask.Nothing);
            mFilteringSettings = new FilteringSettings(RenderQueueRange.transparent);

            foreach (string tag in setting.shaderTagIdList)
            {
                shaderTagIdList.Add(new ShaderTagId(tag));
            }
        }

        public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
        {
            cameraTextureDescriptor.depthBufferBits = 0;
            //cameraTextureDescriptor.width = (int)(cameraTextureDescriptor.width * setting.sampleDown);
            //cameraTextureDescriptor.height = (int)(cameraTextureDescriptor.height * setting.sampleDown);
            cameraTextureDescriptor.width = (int)(cameraTextureDescriptor.width);
            cameraTextureDescriptor.height = (int)(cameraTextureDescriptor.height);
            //ARGB32格式的取值再0-1中,无法进行Bloom等后效处理,所以沿用BackBuffer的RGB111110Float格式
            //cameraTextureDescriptor.colorFormat = RenderTextureFormat.ARGB32;

            cmd.GetTemporaryRT(mRT.id, cameraTextureDescriptor, FilterMode.Bilinear);
            //if(setting.useBlitMat)
            //{
            //    cmd.Blit(colorAttachment, mRT.Identifier(), BlitMat, 0);
            //}
            //else
            cmd.Blit(colorAttachment, mRT.Identifier());
            
            cmd.SetGlobalTexture(setting.textureName, mRT.Identifier());
        }

        public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
        {

            CommandBuffer drawCMD = CommandBufferPool.Get("Draw " + setting.textureName);
            using (new ProfilingSample(drawCMD, "Draw " + setting.textureName))
            {
                context.ExecuteCommandBuffer(drawCMD);
                drawCMD.Clear();
                drawCMD.SetRenderTarget(colorAttachment);

                var drawSettings = CreateDrawingSettings(shaderTagIdList, ref renderingData, SortingCriteria.CommonTransparent);
                //mFilteringSettings.layerMask = renderingData.cameraData.camera.cullingMask;

                context.DrawRenderers(renderingData.cullResults, ref drawSettings, ref mFilteringSettings, ref renderStateBlock);

            }
            context.ExecuteCommandBuffer(drawCMD);
            CommandBufferPool.Release(drawCMD);
        }

        public override void FrameCleanup(CommandBuffer cmd)
        {
            cmd.ReleaseTemporaryRT(mRT.id);
        }
    }



    BlitPass mBlitPass;

    public override void Create()
    {
        mBlitPass = new BlitPass(settings) { renderPassEvent = settings.passEvent};
    }

    public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
    {
        //不检查的话,当shaderTagIdList为空时,不会断Warning
        if (settings.shaderTagIdList == null || settings.shaderTagIdList.Count == 0)
            return;
        renderer.EnqueuePass(mBlitPass);
    }
}

  只有shader的tag中的LightMode属性在这个Feature的shaderTagIdList中才会被渲染出来,此外DrawRenderers还能控制队列、排序方法等。
  比起2018手动组织渲染队列,以及多相机方案,这个方法的好处是复杂度很低,无需考虑渲染排序(管理中的Grab物体无需考虑,管理之外的半透物体依旧需要考虑),无需考虑多场景等等。
  如果说有什么劣势,就是对抓屏纹理降采样,至少我做不到,当我试图给Blit方法传递材质时,总会出现各种各样的错误。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容