离屏粒子优化

一、GPU Gems3 Chapter 23:高速的离屏粒子

原文:Chapter 23. High-Speed, Off-Screen Particles

粒子特效一直是一个游戏开发中非常吃性能的点,特点就在于①数量不固定,在极端情况下同时存在的特效数量特别多,不且好合批;②其往往都为半透明物体,混合方式各有不同,同一个 pixel 可能会叠好几层特效。最后带来的结果就是:①大量的 DC,CPU 端优化不下去;②过多的 overdraw 也会带来 GPU 上的压力

而一种比较传统的优化方式就是离屏渲染:即将所有的粒子渲染到一张更低分辨率的 RT 上,并在后处理前把它混合回主渲染目标上,这可有效缓解②带来的性能问题

1.1 2007 年 GPU Gems3 中关于离屏粒子流程的简单介绍

这可以说是离屏粒子的起源,绝大多数的方案思路根源也都是这篇文章,尽管思路其实并不复杂,大致流程如下:

  1. 正常渲染场景中的不透明物体并开启深度写入
  2. 将当前深度缓存进行降采样到一张低分辨率的 RT 中
  3. 渲染粒子特效到一张离屏的 RT 中,其 RT 分辨率和前者深度 RT 一致
  4. 混合粒子的 RT 到主渲染目标,其中需要对粒子 RT 进行升采样

其中书中主要对如下的几个重点进行了详细介绍

  1. 一是粒子绘制到 RT 时使用的混合方式,以及最后与主渲染目标混合时,对应的混合公式(由于你粒子是离屏渲染的,渲染目标底色是黑色,因此在绘制粒子时,没有办法拿到当前 backbuffer 的颜色,无脑 alpha-blend 并不能得到正确的最终颜色,这里涉及了一些简单的数学公式,后面会有详细解释)
  2. 不同分辨率的 RT 混合可能会导致常规物体与粒子特效的交界处出现锯齿,需要探讨如何缓解这一部分的问题,书中的主要思路为边缘检测 + 修正
  3. 性能分析

1.2 现在离屏粒子应该怎么做

07年的渲染技术文章,放在现在来看确实有点“古文献”的感觉,特别是其中测试的显卡 GeForce8800 更是一个上古时代的老卡了,因此无脑造搬思路并不是一个好选择

考虑现在的硬件性能和主流的管线,我们或许可以少做一些事情……

1.2.1 深度信息考量

原文需要在绘制粒子之前,对当前的深度进行一次降采样拷贝

但是现在的主流管线中,无论是前向还是延迟,都可以在这个阶段直接拿到一张全分辨率的深度图,尽管这张图也不是白拿的也需要一个 blit 的成本,但是绝大多数情况下为了其它的效果,我们已经有了这张深度图了,就没必要降分辨率再拿一张,除非是另有用途

至少在绘制粒子的时候,可以不需要

1.2.2 是否可以接受的锯齿

文章中提供了一种边缘检测思路去解决低分辨率粒子升采样后出现的锯齿问题

图片来源:GPU Gems3 Chapter 23. High-Speed, Off-Screen Particles

可以优化最终的效果但是会有额外成本,尽管在低分辨率下做这件事成本也不会太高,但是肯定能不做就不做,先测试下现在 PC 1920P 的分辨率下粒子边缘的锯齿情况:你甚至看不出来哪张是降了分辨率渲染的(其中一张图的分辨率长宽为另一张的 1/2)

移动平台由于屏幕不大,更不会出现什么问题,再考虑拿实际游戏中真实的场景(国战 20PVP,全部玩家同时放技能)进行测试、以及经过美术同学的评估,得出的最终结论是:完全可以接受的结果,相对于其它部分,没有必要做这一部分抗锯齿的优化

1.2.3 场景中的其它半透明物体,也需要离屏渲染嘛?

原文并没有考虑过场景中的其它半透明物体,可能是那时的设备,基本都会避免除粒子外的半透明物体的渲染,诸如酒瓶、玻璃这类的物品,都是通过 SSS 或全透明做的假半透效果

直接上一个结论:

  1. 如果场景中的半透明物体与特效之间发生穿插(特效 | 半透物件 | 特效),那么只对特效进行离屏渲染,得到的最终混合结果不可能完全正确,也做不到完全正确
  2. 如果场景中的半透明物体是最先被渲染的,即在所有的特效的后面(半透物件 | 特效 A | 特效 B),此时只对特效进行离屏渲染,可以得到正确的结果
  3. 如果场景中的半透明物体是最后被渲染的,即在所有的特效的最前面(特效 A | 特效 B | 半透物件),那么只对特效进行离屏渲染,需要保证渲染顺序为 不透明物体 -> 粒子离屏渲染 -> 粒子 RT 升采样并与主屏幕进行混合 -> 渲染半透明物体,才可得到正确的结果

整合而言就是:需要对所有的半透明物体都进行离屏渲染,才能保证最后混合结果完全正确,因此,与其说是粒子的离屏渲染,我们真正想做的是:所有常规半透明物体的离屏渲染,在这个方案下其中一个半透物体可以考虑在外,那就是水面,它可以被视为前面②中的情况

当然还有一个策略就是:只做离屏幕近的特效的离屏渲染,该策略有两个好处:

  1. 半透物体和特效物体混合问题发生概率大幅降低,只离屏绘制贴脸特效基本上只会出现上面②的情况
  2. 真正产生大量 overdraw,大量 frag 绘制的特效正是那种贴脸特效,其一个特效就占据了屏幕中的绝大部分面积,而许多离摄像机较远的特效其实是不会浪费太多 frag 绘制时间的,往往 drawcall 会先是瓶颈,离屏渲染优化这部分特效并没有什么收益

二、一个粒子离屏优化案例

写在最前面的注意事项:

  1. 本方案实现于 Unity URP,版本 2020+,需对 URP 源码做出略微修改,方式不唯一
  2. 所有离屏的粒子特效,只考虑 AlphaBlend 即 Addtive 两种主流混合方式

2.1 绘制特效的 RenderPass

创建一张低分辨率的粒子 RT,并且把需要绘制的物体筛选出来,非常简单的逻辑

筛选要绘制的物体有很多种方式,比如说指定 layer、或指定 Tag、按照渲染队列筛选也可以,案例中的策略是使用一个自己定义的 lightmode

public OffScreenParticlePass(OffScreenParticleSettings setting)
{
    this.setting = setting;
    particleLowResRT.Init("_ParticleLowResRT");

    shaderTagIdList.Add(new ShaderTagId("OffScreenForward"));
    filteringSettings = new FilteringSettings(RenderQueueRange.transparent, LayerMask.NameToLayer("Everything"));
}

如果没有开启离屏渲染:修改原先半透物体渲染的 RenderPass,添加对应 ShaderTag,此时对应 Lightmode 的物件也会按照原先流程正常绘制

此操作需要修改 URP 源码:DrawObjectPass,也是唯一需要修改源码的地方,之所以改源码,而不是在原先的 shader 中多添加一个 subshader 或者 shaderpass,是为了避免变体数量增多,操作不当的话对应的 shader 会有原先两倍的变体

foreach (ShaderTagId sid in shaderTagIds)
    m_ShaderTagIdList.Add(sid);
if (!UniversalRenderPipeline.assetRuntimeParams.offscreenRender)
    m_ShaderTagIdList.Add(new ShaderTagId("OffScreenForward"));

当然方案不唯一,这只是一个例子

2.2 离屏绘制混合方式

不考虑预乘,这里参考 GPU Gems3 中提供的方案,无需美术参与,通过简单修改混合模式解决

举一个例子:假设当前同一个 pixel 上有三个特效需要依次绘制,三个特效对应的 color 分别为 c_1, c_2, c_3,对应的 alpha 值为 a_1, a_2, a_3d 为特效还没绘制任何特效时,当前颜色缓冲区的源颜色

2.2.1 仅考虑 alpha-blend 的混合

先只考虑 alpha-blend 的混合方式,对于正常绘制的情况,绘制第一个特效后,其 pixel 的颜色为

p_1=d(1-a_1)+c_1 a_1

绘制完第二个特效后,其 pixel 的颜色为

p_2=p_1(1-a_2)+c_2 a_2=(d(1-a_1)+c_1 a_1)(1-a_2)+c_2 a_2

绘制完最后一个(第三个)特效后,其 pixel 的最终颜色应为

p_3=((d(1-a_1)+c_1 a_1)(1-a_2)+c_2 a_2)(1-a_3)+c_3 a_3

那么离屏渲染粒子并最后混合回主屏幕的颜色,也应该是上面的 p_3。不同于正常绘制,离屏渲染在绘制这些粒子时并不能拿到当前 d 的信息,也无法正常混合,只有在最后 merge 的时候,渲染目标才有 d 的信息,因此需要一些小小的操作,才能得到正确的结果


那么该如何操作呢?

绘制粒子时,由于没有 d 的信息,对上面的三个公式,把 d 作为未知参数,移项:

\begin{aligned} & p_1=d(1-a_1)+c_1 a_1 \\ & p_2=d(1-a_1)(1-a_2)+c_1 a_1(1-a_2)+c_2 a_2 \\ & p_3=d(1-a_1)(1-a_2)(1-a_3)+c_1 a_1(1-a_2)(1-a_3) + c_2 a_2(1-a_3)+c_3 a_3 \end{aligned}

可以看到,要想最后能正确混合粒子 RT 及屏幕 RT:就需要在绘制粒子的时候,存储

  1. P_{A}=(1-a_1)(1-a_2)(1-a_3)
  2. P_{RGB}=c_1 a_1(1-a_2)(1-a_3) + c_2 a_2(1-a_3)+c_3 a_3

正好,①只和 alpha 有关,可以存储在粒子 RT 的 alpha 中,②存储在粒子 RT 的 RGB 通道中

很明显,alpha 通道就是所有粒子的 alpha 值拿一减去后连续相乘,因此离屏绘制粒子时,其 alpha 通道需要设置单独的混合模式:即 Zero OneMinusSrcAlpha,而对于正常 RGB 混合模式,由于 d 项为零并不影响最终公式的结果,因此仍然为 SrcAlpha OneMinusSrcAlpha

Blend SrcAlpha OneMinusSrcAlpha, Zero OneMinusSrcAlpha

正确设置了如上 blend mode 后,渲染完所有粒子,粒子 RT 的 RGB 通道存储值就为如上的 P_{RGB},A 通道存储的值为如上的 P_{A},当然你还需要确保粒子 RT 在没有绘制任何物体前,其 buffer 要初始化为 (0, 0, 0, 1)(确保 d 项为零),即 Color.black

public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
    ConfigureTarget(new RenderTargetIdentifier(particleLowResRT.id));
    ConfigureClear(ClearFlag.All, Color.black);
}

2.2.2 同时考虑 alpha-blend 及 addtive 粒子的混搭混合

再考虑夹带 addtive 粒子的混合方式,原文中大致意思是 addtive 和 alpha-blend 要分开处理,但事实上,它们也只有 blendmode 的不同而已,完全可以混搭绘制到一张离屏 RT 中,不用独立处理,下面给出同时绘制 addtive 及 alpha-blend 粒子的混合方式及公式

一样假设当前同一个 pixel 上有三个特效需要依次绘制,三个特效对应的 color 分别为 c_1, c_2, c_3,对应的 alpha 值为 a_1, a_2, a_3d 为特效还没绘制任何特效时,当前颜色缓冲区的源颜色,唯一的区别就是:第二个粒子的混合方式为 addtive

对于正常绘制的情况,绘制第一个特效后,其 pixel 的颜色为

p_1=d(1-a_1)+c_1 a_1

绘制完第二个特效后,其 pixel 的颜色为

p_2=p_1+c_2 a_2=d(1-a_1)+c_1 a_1+c_2 a_2

绘制完最后一个(第三个)特效后,其 pixel 的最终颜色应为

p_3=(d(1-a_1)+c_1 a_1+c_2 a_2)(1-a_3)+c_3 a_3

同样对 p_3 进行移项,得到 p_3=d(1-a_1)(1-a_3)+(c_1 a_1+c_2 a_2)(1-a_3)+c_3 a_3,可以看到,其本质就是 1-a_2=0 的一个特例

此时要想最后能正确混合粒子 RT 及屏幕 RT:就需要在绘制粒子的时候,存储

  1. P_{A}=(1-a_1)(1-a_3)
  2. P_{RGB}=(c_1 a_1+c_2 a_2)(1-a_3)+c_3 a_3

很明显,当前和前者只绘制 alpha-blend 物体不同的是:在绘制 addtive 物体时,并不需要对当前 alpha 通道做任何处理,其对应的 alpha blend-mode 就为 Zero One,同理 addtive 的 color blend mode 不变,仍然为 SrcAlpha One,除此之外原先 alpha-blend 的特效,和前者 2.2.1 混合方式一致

Blend SrcAlpha One, Zero One

如果你的特效 shader 为 Ubershader,blend-mode 不写死通过参数控制,那么其最后离屏渲染修改后的特效 shader blend-mode 就应如下:

Blend [_Src] [_Dst], Zero [_Dst]

2.3 离屏粒子 RT 混合回主渲染目标

这一步在后处理之前做,将粒子 RT 混合回主目标

也就在此时,渲染目标有前面的 d 信息,粒子 RT 存储的值为 ({P_{RGB}, \ P_{A}}),需要得到的最终混合结果 p_3=dP_{A}+P_{RGB}

这个公式显而易见(如果不明白是怎么来的建议再看一次 2.2 的所有推导),这次 merge 的混合模式就也显而易见了,必然是 One SrcAlpha

Merge shader 也非常简单明了:

Blend One SrcAlpha
#pragma vertex vert
#pragma fragment frag
            
v2f vert(appdata v)
{
    v2f o;
    o.uv = v.texcoord;
}

float4 frag(v2f i) : SV_Target
{
    float2 uv = i.uv;
#ifdef DEPTH_RESOLVE
    //如果你需要做边缘抗锯齿,可以在这里处理升采样
#endif
    float4 particleColor = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv);
    return particleColor;
}

搞定!这只需要一次 cmd.blit 的操作与开销

2.4 特效与物体接壤处锯齿优化

不做,带来额外的性能负担且效果不明显,这边只丢几个参考文章:

  1. https://zhuanlan.zhihu.com/p/681262305
  2. https://zhuanlan.zhihu.com/p/24801448(类似于 VSM 的思路)

2.5 实机性能分析

该方案仅能优化离屏物体带来GPU Frag 计算瓶颈

以下是一个 GPU 瓶颈优化案例:使用的设备为 Mi6(骁龙835),其中画面为游戏实机画面加上大量特效的结合,即除了主体大量的特效以外,还包含其它场景物件,包括但不限于 UI、水体、地形、大量人物角色等等,因此相对极端的测试案例可能优化有限

出于信息保密,画面内容及具体性能数据无法公开,只能贴下 profile,见谅

不开启离屏渲染
开启离屏渲染,其离屏 RT 分辨率为原先的 1/2

从上可见优化明显,CPU 等待 GPU 时间缩短

开启离屏渲染,其离屏 RT 分辨率为原先的 1/4

相对于前者,几乎就不再有优化了,因为此时已经是其它瓶颈了,再缩小分辨率并无收益

极端案例:所有半透物体、特效全部离屏渲染,分辨率为原来的 1/2

包括场景中的水在内,所有半透物体及特效全部离屏渲染结果,有明显的性能优化,但是游戏画质明显下降:原因是高频内容并不适合降分辨率渲染

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/678187.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第二证券炒股知识:短线炒股技巧?

在股票商场上,出资者分为长线和短线这两大类,其中短线炒股存在以下技巧: 1、早盘集合竞价时刻上的技巧 早上集合竞价对短线出资者来说比较重要,其中早上集合竞价期间9:15-9:20之间出资者可以进行撤单操作&#xff0c…

Qt开发技术:Q3D图表开发笔记(四):Q3DSurface三维曲面图颜色样式详解、Demo以及代码详解

若该文为原创文章,转载请注明原文出处 本文章博客地址:https://hpzwl.blog.csdn.net/article/details/139424086 各位读者,知识无穷而人力有穷,要么改需求,要么找专业人士,要么自己研究 红胖子网络科技博…

中学生学人工智能系列:如何用AI学生物

经常有读者朋友给公众号《人工智能怎么学》留言咨询如何使用人工智能学习语文、数学、英语、化学等科目。这些都是中学教师、中学生朋友及其家长们普遍关注的问题。仅仅使用留言回复的方式,不可能对这些问题做出具体和透彻的解答,因此本公众号近期将推出…

【Python】 探索Python中的2D数组峰值检测

基本原理 在Python编程中,经常会遇到需要处理多维数组的场景。2D数组,也就是二维数组,是数组的一种形式,它由多个一维数组组成,可以想象成一个矩阵。峰值检测是数据分析中的一项常见任务,特别是在信号处理…

航空交流电源车:高品质电源,保障飞机正常运行

航空交流插电式电源车作为一种专为航空飞机提供稳定交流电源的地面支持设备。它能够满足航空器在地面运行过程中的电力需求,如维护、试验和充电等。这种电源车采用电能作为动力来源,具有环保、节能、安全、可靠等特点。航空交流插电式电源车作为一种创新…

Elastic Connectors:增量同步对性能的影响

作者:Artem Shelkovnikov Elastic 连接器是一种 Elastic 集成,可将数据从原始数据源同步到 Elasticsearch 索引。连接器使你能够创建可搜索的只读数据源副本。 有许多连接器支持各种第三方,例如: MongoDB各种 SQL DBMS&#xff…

AMD提前发布新AI芯片,硬刚英伟达!Zen 5架构性能提高一倍

眼看着英伟达要打破摩尔定律,开启一年一更的新时代;搭载高通骁龙新芯片的设备,也将于数日后上市。AMD这坐不住啊:这风头怎么都被别人抢了? 于是,在周一的COMPUTEX(台北国际电脑展)上…

借助调试工具理解BLE协议_2.BLE协议栈

名词解释: BT SIG英文全称为Bluetooth Special Interest Group(蓝牙特别兴趣组),网址为 www.Bluetooth.com。 Bluetooth Technology Website SIG成立于1998年,是一个全球技术交流组织,拥有超过36000家公…

centos7下卸载MySQL,Oracle数据库

📑打牌 : da pai ge的个人主页 🌤️个人专栏 : da pai ge的博客专栏 ☁️宝剑锋从磨砺出,梅花香自苦寒来 操作系统版本为CentOS 7 使⽤ MySQ…

Linux云计算架构师涨薪班课程内容包含哪些?

第一阶段:Linux云计算运维初级工程师 目标 云计算工程师,Linux运维工程师都必须掌握Linux的基本功,这是一切的根本,必须全部掌握,非常重要,有了这些基础,学习上层业务和云计算等都非常快&#x…

常见的多态面试题

多态的概念及其构成条件 多态概念:对不同的对象会有不同的实现方法,即为多种形态。 构成条件: 派生类要进行虚函数的重写(父子类虚函数需要三同,三同指函数名、参数、返回值)要用父类的指针或引用去调用虚…

黄仁勋的AI时代:英伟达GPU革命的狂欢与挑战

在最近的COMPUTEX 2024大会上,英伟达创始人黄仁勋发布了最新的Blackwell GPU。这次发布不仅标志着英伟达在AI领域的又一次飞跃,也展示了其对未来技术发展的战略规划。本文将详细解析英伟达最新技术的亮点,探讨其在AI时代的市场地位和未来挑战…

Transformer学习(2)

这是Transformer的第二篇文章,上篇文章中我们了解了分词算法BPE,本文我们继续了解Transformer中的位置编码和核心模块——多头注意力。下篇文章就可以实现完整的Transformer架构。 位置编码 我们首先根据BPE算法得到文本切分后的子词标记,然…

baremaps 部署

参考:https://baremaps.apache.org/documentation/ 一、基础环境 1、安装 JDK 版本需要至少 Java 17 下载:https://www.oracle.com/cn/java/technologies/downloads/ tar -zxf jdk-17_linux-x64_bin.tar.gz -C /usr/local cd /usr/local mv jdk-17.…

centos安装vscode的教程

centos安装vscode的教程 步骤一:打开vscode官网找到历史版本 历史版本链接 步骤二:找到文件下载的位置 在命令行中输入(稍等片刻即可打开): /usr/share/code/bin/code关闭vscode后,可在应用程序----编程…

商品最大价值-第13届蓝桥杯选拔赛Python真题精选

[导读]:超平老师的Scratch蓝桥杯真题解读系列在推出之后,受到了广大老师和家长的好评,非常感谢各位的认可和厚爱。作为回馈,超平老师计划推出《Python蓝桥杯真题解析100讲》,这是解读系列的第77讲。 商品最大价值&…

在windows操作系统上安装MariaDB

最近收到关于数据库在哪里看的评论,所以就一不做二不休,把安装数据库的步骤写一篇文章吧。 这篇文章介绍如何在windows上完成MariaDB-10.6.5版本的安装,对应MySQL-8.x版本。 第一步:下载安装包 通过以下网盘链接下载MariaDB-10.6…

免杀基本知识,shellcode混淆免杀

一、shellcode分析及免杀的必要性 shellcode是一段十六进制的机器码,插入内存后会被翻译成为CPU的指令,用于执行相关操作。渗透中的shellcode的主要功能就是反弹shell。将shellcode编译成为exe文件后,执行文件主要进行以下三个操作&#xff…

若依:mybatis查询的结果未映射到实体类报null

开启驼峰命名转换: mapUnderscoreToCamelCase: true 我的是mtybatis配置开启驼峰命名转换不生效,还需要在MyBatisConfig中配置 // 配置mybatis自动转驼峰 生效 sessionFactory.getObject().getConfiguration().setMapUnderscoreToCamelCase(true)&#x…

2041:【例5.9】新矩阵

#include <iostream> using namespace std; int main(){const int N 21;//几行几列 int g[N][N] {};int n 0;cin >> n;for (int i 1; i < n; i){for (int j 1; j < n; j){// 输入到几行几列 cin >> g[i][j];if (i j || i j n 1){//如果是这种…