热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【Unity3D】URP下的GrabPass方案

  GrabPass和AlphaBlend都有渲染物体包含背景物体颜色的特点,不同的是,AlphaBlend渲染像素时,只能基于该像素的前一次DrawCall结果来混合,而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();
        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();
            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 shaderTagIdList = new List();
        //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 shaderTagIdList = new List();

        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方法传递材质时,总会出现各种各样的错误。


推荐阅读
  • Iamtryingtomakeaclassthatwillreadatextfileofnamesintoanarray,thenreturnthatarra ... [详细]
  • 开发笔记:加密&json&StringIO模块&BytesIO模块
    篇首语:本文由编程笔记#小编为大家整理,主要介绍了加密&json&StringIO模块&BytesIO模块相关的知识,希望对你有一定的参考价值。一、加密加密 ... [详细]
  • ZSI.generate.Wsdl2PythonError: unsupported local simpleType restriction ... [详细]
  • Spring常用注解(绝对经典),全靠这份Java知识点PDF大全
    本文介绍了Spring常用注解和注入bean的注解,包括@Bean、@Autowired、@Inject等,同时提供了一个Java知识点PDF大全的资源链接。其中详细介绍了ColorFactoryBean的使用,以及@Autowired和@Inject的区别和用法。此外,还提到了@Required属性的配置和使用。 ... [详细]
  • 本文介绍了RxJava在Android开发中的广泛应用以及其在事件总线(Event Bus)实现中的使用方法。RxJava是一种基于观察者模式的异步java库,可以提高开发效率、降低维护成本。通过RxJava,开发者可以实现事件的异步处理和链式操作。对于已经具备RxJava基础的开发者来说,本文将详细介绍如何利用RxJava实现事件总线,并提供了使用建议。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • 阿,里,云,物,联网,net,core,客户端,czgl,aliiotclient, ... [详细]
  • 使用Ubuntu中的Python获取浏览器历史记录原文: ... [详细]
  • 计算机存储系统的层次结构及其优势
    本文介绍了计算机存储系统的层次结构,包括高速缓存、主存储器和辅助存储器三个层次。通过分层存储数据可以提高程序的执行效率。计算机存储系统的层次结构将各种不同存储容量、存取速度和价格的存储器有机组合成整体,形成可寻址存储空间比主存储器空间大得多的存储整体。由于辅助存储器容量大、价格低,使得整体存储系统的平均价格降低。同时,高速缓存的存取速度可以和CPU的工作速度相匹配,进一步提高程序执行效率。 ... [详细]
  • 解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法
    本文介绍了解决nginx启动报错epoll_wait() reported that client prematurely closed connection的方法,包括检查location配置是否正确、pass_proxy是否需要加“/”等。同时,还介绍了修改nginx的error.log日志级别为debug,以便查看详细日志信息。 ... [详细]
  • 背景应用安全领域,各类攻击长久以来都危害着互联网上的应用,在web应用安全风险中,各类注入、跨站等攻击仍然占据着较前的位置。WAF(Web应用防火墙)正是为防御和阻断这类攻击而存在 ... [详细]
  • macOS Big Sur全新设计大版本更新,10+个值得关注的新功能
    本文介绍了Apple发布的新一代操作系统macOS Big Sur,该系统采用全新的界面设计,包括图标、应用界面、程序坞和菜单栏等方面的变化。新系统还增加了通知中心、桌面小组件、强化的Safari浏览器以及隐私保护等多项功能。文章指出,macOS Big Sur的设计与iPadOS越来越接近,结合了去年iPadOS对鼠标的完善等功能。 ... [详细]
  • 本文整理了315道Python基础题目及答案,帮助读者检验学习成果。文章介绍了学习Python的途径、Python与其他编程语言的对比、解释型和编译型编程语言的简述、Python解释器的种类和特点、位和字节的关系、以及至少5个PEP8规范。对于想要检验自己学习成果的读者,这些题目将是一个不错的选择。请注意,答案在视频中,本文不提供答案。 ... [详细]
  • 数据结构与算法的重要性及基本概念、存储结构和算法分析
    数据结构与算法在编程领域中的重要性不可忽视,无论从事何种岗位,都需要掌握数据结构和算法。本文介绍了数据结构与算法的基本概念、存储结构和算法分析。其中包括线性结构、树结构、图结构、栈、队列、串、查找、排序等内容。此外,还介绍了图论算法、贪婪算法、分治算法、动态规划、随机化算法和回溯算法等高级数据结构和算法。掌握这些知识对于提高编程能力、解决问题具有重要意义。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
author-avatar
手机用户2502883383
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有