千家信息网

如何使用Unity 2018深度优化渲染管线

发表于:2025-02-03 作者:千家信息网编辑
千家信息网最后更新 2025年02月03日,这篇文章主要介绍"如何使用Unity 2018深度优化渲染管线",在日常操作中,相信很多人在如何使用Unity 2018深度优化渲染管线问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希
千家信息网最后更新 2025年02月03日如何使用Unity 2018深度优化渲染管线

这篇文章主要介绍"如何使用Unity 2018深度优化渲染管线",在日常操作中,相信很多人在如何使用Unity 2018深度优化渲染管线问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答"如何使用Unity 2018深度优化渲染管线"的疑惑有所帮助!接下来,请跟着小编一起来学习吧!

这段时间一直没有出文章,其实除了学校课程繁忙这些因素,更是把许多时间都放在了整理和优化旧代码的工作上,其中很大一部分包括对内存管理的"苛求",乃至使用一些新特性对渲染管线的在CPU执行的代码进行深度优化。

首先来介绍一下Unity现有的3种编译方法,Mono VM, IL2CPP, Burst。

Mono虚拟机是传统而古老的实现方案,其跨平台的优势使得早年的Unity一直依赖于它,然而Mono本身性能极差,代码执行效率只有Native C++的一半左右,这也使得早年的Unity扣上了"做PC不行""做不了大作""引擎性能差"的帽子,而现在除了Editor中为了开发效率仍在使用Mono VM,PC,主机,手机等平台对其的需求越来越低。

IL2CPP是通过IL作为中间层,将代码编译成Native CPP的办法,在2018.3正式版发布以后,IL2CPP的编译优化有了长足进展,一些古怪的小问题也逐渐减少,官方也大力推荐在高端平台代替Mono使用,这将会是我们目前使用的主要的编译方法。但是这种编译方法也并非没有坑, 相反它的坑要比Mono VM还要多。因为经过IL层的翻译,一些C#高级用法在翻译的过程中极有可能产生额外的性能消耗,所以在开发时我们应该用CPP的思维去考虑开发,毕竟游戏开发与传统.Net开发还是有巨大差距的,当然也不是要求所有的开发者都像我一样代码里指针乱飞,把C#当C++写,但是最起码编程习惯方面的问题还是要留意一下的,该牺牲开发舒适度就要牺牲一下下(笑)。

Burst Compiler是一个专用于科学运算的编译器,之所以说是"专用于科学运算",是因为除此之外它基本没有其他功能,本身不支持托管类型,所以代码中不能有任何访问托管类型的代码,既然托管类型不支持,那么抽象,多态等面向对象就更别想了,可以说就是最原始的C语言编程,Burst的设计是专门用来处理科学运算的,比如处理矩阵,向量等提供了诸如SIMD之类的优化黑科技。官方一直在神化Burst,称其运行性能能远超C++,但是其实在我看来,Burst只在处理个别数据处理类方法时可以派上用场,在处理其他普通代码时并不会比C++快多少,同时只允许使用非托管类型,禁止一切OOP对于项目开发而言许多时候也不切实际(至少在ECS全面普及开来前是这样),所以Burst在目前只会被我们当成开发时佐料。

而我们主要的优化思路基本分为以下几种:

  1. 尽量使用非托管的数据类型,并手动管理内存,做到真正的Runtime 0 GC。

  2. 尽量把C#当成C++来开发,原因上方已经介绍过。

  3. 尽可能多的使用Job System计算逻辑,充分发挥多线程优势。

  4. 尝试使用Burst Compiler编译纯数学运算的代码。

在之前文章中介绍的GPU Driven Pipeline的代码其实有很大的优化空间,因此我们首先处理的就是光照部分,因为我们的管线使用了Tile/Cluster Based Deferred Rendering,所以需要在CPU中准备传入Compute Buffer的数据,同时灯光阴影的运算需要生成视锥体的矩阵,那么这一部分的运算就可以扔到Job System中进行运算,当然这一部分中有不少访问托管类型的部分,而且运算也大多数是逻辑而非运算,所以这里使用传统的IL2CPP编译而非Burst,代码结构大概是这样:

因为整个渲染管线都是单例的存在,因此直接使用static非常安全,另外Job System中是不允许出现实例化的变量,所以这里也只能使用静态,如果不想使用静态,但又一定要使用托管类型,也并非没有办法,我的解决方案如下:

这样的方法可以将托管类型的地址强行赋值到指针中,但是指针并不在GC的管理范围,所以在这样处理时一定要注意,是否应该开启GCHandle保证非托管代码执行的途中该托管类型不会被GC干掉。当然,Unity的Component是受引擎管理的,所以不需要考虑C# GC会作妖。

可以看到,在上方的Job中,处理了包括点光源,聚光源的信息和矩阵,并将运算结果转存到上方的NativeArray和NativeList中,最后再将值直接传入到Compute Buffer中,这个过程的计算一直是一个逻辑重点,比如Cubemap需要计算6个面的View projection Matrix。在将这一部分代码放入到Job中后,在本人的高端PC测试机上,在灯光数量较多的时候,代码执行时间直接减少了0.1ms-0.2ms,这是一个可喜的进步,尤其是项目开发后期主线程的压力可能会成为性能短板,这样的优化是非常有意义的。

除此之外,还有一个运算的大头,就是Cascade Shadowmap的矩阵布置,Cascade Shadowmap的计算方法,这里已经不需多做解释了,其中耗费较高的就是一个通过Frustum Corner获取正方体位置,以及通过ViewProjection的逆矩阵反推世界坐标,进而计算像素的棋盘格,防止因为摄像头移动导致的shadowmap锯齿抖动,而逆矩阵的推导看似其貌不扬,实则运算量颇高,在CPU单核运算能力较弱的平摊上消耗绝对不容小觑,因此我们将其放到Job System中是一个很不错的选择,值得一提的是,这个过程属于纯数值类运算,因此可以使用Burst Compiler:

传入一大堆需要用到的或者返回的属性值,最后在Execute中计算,值得一提的是,Unity现有的GL.GetGPUProjectionMatrix是不支持Burst也不支持在分线程中运算的,这个真的很坑,所以我们只能自己写一个D3D和OpenGL投影矩阵标准转换的函数,同时因为Burst不支持静态变量,所以连isD3D这种flag也需要手动传递(一种隐隐的蛋疼感传来)。

虽然因为Burst的鬼畜限制,代码变得奇丑无比,But it just works well! 计算部分的代码大概如下:

总共4个cascade,因此这段代码将会在4个线程中分别执行,这段代码又为我们省下了主线程的0.1ms。

灯光数据的运算基本是整个渲染管线中消耗最大的一部分了,毕竟SRP已经将更复杂的场景剔除之类逻辑封装好了,所以实际需要我们做的大概就是这些自定义的数据类型,一些自定义的Volume组件的视锥体也是需要我们手动计算的,比如之前Froxel volumetric Rendering中曾经提到的Fog Volume,也就是管理局部雾效的组件,我们当仁不让的也使用了Burst Compiler加速:

其他大大小小的Job还是有不少的,Unity的Job System的原理是将任务累计到一起,通过调用JobHandle.ScheduleBatchedJobs()的方法同时开始计算所有任务,这是因为开启线程本身是一个相对昂贵的步骤,所以我们应该尽量把更多的任务统一堆到Job Queue中,再让执行流程负责启动任务,最后通过Complete保证当前任务执行完毕,保证线程安全。综上所述,Job System的使用本着:"益多,益杂,不益散"的原则,多,是因为每个struct在加入队列的时候需要memcpy,因此体积不应该太大,同时较多的任务也更容易让Unity的Job Threads自行挑选,平衡核心负载,益杂的意思是,许多大大小小的任务,积少成多,应该尽可能的写成Job而不是堆砌在主线程,不益散则映射了刚才说过的ScheduleBatchedJobs的原理,应该尽可能同时启动所有的任务,保证执行的统一性。

除了多线程优化,在内存优化方面我们也是完成了许多的工作,首先值得说的一点就是Unity的Memory Allocator的使用方法,学过C语言的朋友自然知道Malloc和Free的用法,这里就不用多说了,Unity提供了3种Allocator,分别是Temp, TempJob和Persistant。

Temp: 开辟的内存会在帧结束后被释放,适合只在当前帧使用的,同时也是开辟速度最快的,可以说其开辟速度仅次于栈内存stackalloc,如果为了处理帧内传递的数据,应该尽可能使用这种,而且不需要Free,在帧结束后会被Allocator自己回收,没有泄露危险。

TempJob:开辟速度仅次于Temp,可以维持4帧,在4帧后会被自动回收,坦率的讲我个人不是很喜欢这个设定,因为"4帧"这个限制显得有点不伦不类,但是存在即是正义,想必也有其用途所在。

Persistant:永久的开辟内存,开辟速度最慢,必须手动调用Free,否则会泄露,这也是最传统的malloc方法,适合常驻的数据结构使用。

配合C# 7.3给出的unmanaged的泛型限制,可以使用这套内存管理系统写出纯手动管理内存的泛型,真正做到0 GC。然而目前引擎本身还是有一些限制,譬如SRP还不支持获取ECS的Component类型,因此代码还很难做到纯粹的"面向性能编程",总是显得有些不伦不类,还带着许多面向对象的包袱,希望在接下里几年里,Unity能够逐渐完善ECS和SRP,并逐渐彻底抛弃传统面向对象的开发方式。

到此,关于"如何使用Unity 2018深度优化渲染管线"的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注网站,小编会继续努力为大家带来更多实用的文章!

代码 运算 开发 类型 方法 托管 处理 管线 任务 线程 编译 内存 同时 数据 矩阵 管理 就是 性能 支持 深度 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 ADSL用户接入网络技术 那年 欧盟网络安全战略 招软件开发测试 互联网科技目标价 公司网络技术部门管理制度 重庆app软件开发公司 潍坊网络安全工程师最新招聘信息 华为服务器销售案例 网络安全系统脆弱性检测 php上传图片存数据库 涉密信息系统集成软件开发乙级 嘉会医疗软件开发 高校网络安全宣传短视频 软件开发面试问印象深刻的 网络安全标准实践指南原文 无法加入网络安全插画 奥飞数据网络安全 数据库取出的数据产生序号 哈利波特不同服务器可住一个寝吗 南京论之语网络技术怎么样 虹口区会议视频系统服务器 数据库技术之读写分离 我心中的网络安全法 对日软件开发感想 王牌战争早上也可以拆家的服务器 农行乌镇互联网智能科技银行 近两年网络安全大事件 汉语言文学专业与网络技术 美国留学计算机网络技术 数据库高校信息管理系统
0