当前位置: 首页 > news >正文

基于 Dots + GPU Instance 的大规模物体渲染

        之前写的两篇开放世界技术栈都是公司其他同事做的,所以很多细节了解不详细。但这次是全程我自己搭建的轮子,可以讲得稍微详细些。

        之前写的大规模物件渲染的 GPU 版本,虽然渲染量大效率高,但是有个很致命的缺陷:无法与游戏逻辑进行交互。因为主要渲染数据都是放在 GPU 中,为了效率要尽可能减少异步回读,也要尽量减少同步数据量,所以要物体与逻辑交互就基本不可能了。但是使用 Unity 的 Dots 系统再加上 GPU Instance 技术,就可以很好地解决这个问题。
 

0、观前提醒

        这篇文章不是写给初学者的,甚至对于有经验的程序员难度也不小。阅读前确保你已经对 Unity 的 Dots 系统非常熟悉,且对渲染管线有些许了解,更重要的是,要有优秀的抽象能力。

        后续我会把这套系统做成插件包放出来在下面的库中(现在还不是很完善),等我这段时间工作忙完了再来整理这个库。

魔术师Dix / Unity 通用库:紫苑 · GitCodeUnity 的各种通用库 : 紫苑(Aster),基于 ECS 1.0+ 2023.2.5+ 包含方便调用的简化API、数据格式。 会包含我所需要的通用工具、数学计算、编辑器方法、多线程辅助、ECS等; 未来会增加ECS渲染、寻路、BVH、OBB等功能;icon-default.png?t=N7T8https://gitcode.net/cyf649669121/Aster     等我把这个库整理完善之后,再开发一些工具、调试器等供大家使用。我最终的目的,还是希望这一套系统能达到傻瓜也能用的地步。

1、原理解释

        一般来讲,渲染一个物体,需要知道其网格、材质球、位置、旋转、缩放、材质球属性即可。而对于同一类物体(材质球与网格均相同),不同实体的区别也就是位置、旋转、缩放;也即 LTW: Local To World。

        Unity 的 GPU Instance接口: Graphics.DrawMeshInstanced ,其中需要动态改变的参数,大部分情况下只有 Matrix4x4[] ,可能还会有 MaterialPropertyBlock 的修改。


        Unity 的 GPU Instance接口有多个,这里只用 DrawMeshInstanced 举例。使用其他的方法也是可以的,但是使用条件会有所不同(比如需要Shader支持)。

        这里只用 Graphics.DrawMeshInstanced 进行设计,因其适配性最好:只需要材质球能勾上 Enable GPU Instancing 即可。当然,如果条件允许,使用 Graphics.DrawMeshInstancedIndirect 是性能最好的方案。


        因此,在ECS中,将所有待渲染部件的 LTW 记录下来,并计数,然后将数据传给主线程,调用GPU Instance 的API即可完成渲染。

2、部件与渲染数据

        在介绍业务流程之前,需要先了解一些概念。

        对于所有的预制体(Prefab),我按照单个 MeshRender 将其拆分成单个部件,以下图的一个农场模型作为示例:

        这个 Prefab 一共由3个部分组成:地板、风车、房子,也就是图中的3个 MeshRender。我将每一个独立的 MeshRender 的数据收集起来视为一个独立的渲染数据(下文中的RenderData):包含网格、材质球、阴影等配置,放在主线程以备上屏时调用。

        对于这个预制体,其父节点会生成一个空 Entity(Dots中的Entity),每一个子节点生成一个 Entity,然后与父节点关联。这里每一个子节点生成 Entity,就是渲染部件(RenderChild),是渲染的最小单位。

3、渲染流程

        这里要注意一点,在离线时,我会预先将所有的预制烘培成适合 ECS 的数据结构,所有预制都转换成只有一个父节点的层级关系(所有带MeshRender组件的父节点都是预制的根节点),这样就可以不用考虑父子节点的旋转问题了。

        之后根据离线数据和游戏逻辑(例如服务器下发单位),创建与 单位Entity(Rendre Parent Entity,游戏逻辑的最小单位)。之后根据离线烘培的数据,给单位Entity挂载渲染部件。之后经过 System 的逻辑处理,统计处需要上屏的单位,将其部件数据收集在各个渲染数据Entity(与 RenderData 对应)中。

        最终上屏时,按照 Unity 的接口提供对应数据,以 DrawMeshInstanced 为例,我这里直接将 ECS 里的数据拷贝出来了:

//内存拷贝
private static unsafe void CopyTo(NativeArray<float4x4> srcVectors, Matrix4x4[] outVectors)
{fixed (Matrix4x4* dest = outVectors){void* sourceData = srcVectors.GetUnsafeReadOnlyPtr();UnsafeUtility.MemCpy(dest, sourceData, UnsafeUtility.SizeOf<float4x4>() * srcVectors.Length);}
}

        我这么写了还是很简略,毕竟这个不是手把手教程,而且毕竟我有计划写开放库放出源码,所以解释就较为简单。

4、一些细节问题

        最开始这一套是从 SLG 游戏做出来的,可以支持大量的单位渲染(包括下图中的树木、建筑、行军、地面装饰物等)。

  • 如何处理LOD?

        在每一个部件里都存储有 LOD 信息,LOD分级、以及各个LOD对应的部件ID(提前预烘焙好)。对于SLG游戏,一般是固定俯视角,会使用全局LOD,这种也是支持的。
       在 ECS 根据相机距离计算出当前的 LodID ,然后在收集数据的时候收集当前的 LOD 对应的 MeshType 即可。这种做法在 GPU 里也是通用的。LOD 的计算参考:

【Unity】LODGroup 计算公式_unity lodgroup-CSDN博客文章浏览阅读834次。Unity 在配置 LodGroup 时,其分级切换的计算方法是按照物体在相机视野中占据的比例计算的。在运行时,如果相机视野范围(Field of View)没有改变,那么这个值可以直接换算成物体距离相机的距离。这里就讨论下如何计算得到这个距离。_unity lodgrouphttps://blog.csdn.net/cyf649669121/article/details/133308591         也有一种实现方式,是将对应的部件新增一个,当做一个新的 Entity,并在运行时判定是否显示对应的部件。如下图所示,每一个矩形代表一个 Entity,在不同 Lod 的就显示不同的 Entity,其他组件则会隐藏。

        两种方式更建议第一种,但第二种实现难度小。       

  • 如何处理动画?

        如果是 SkinMeshRender ,也就是骨骼动画,可以使用 GpuSkin,网上有很多方案这里不细讲。使用 GpuSkin,因为其本身也是并行的,和 ECS 可以很好地结合起来。但缺点就是动画状态机的控制,一般是很简单的控制,否则在 ECS 里实现很困难。同样的,动画数量也不建议太多,否则需要烘培的动画贴图也会占用很大资源。

        如果是传统 Animation,这种就只有程序写动画了。所以复杂、特殊效果的动画,也不适合用这套系统。

  • 如何设置Shader参数?

        这里以面片树作为例子,所有的树都是用的同一个 Material 和 Mesh,只有贴图的UV不同从而实现不同树木的表现:

        这里我们需要将每一个渲染部件(也就是一个面片树)的Offset、Tiling离线收集起来,然后在收集最终上屏数据的时候,给渲染实体(RenderDataEntity)挂一个额外的 IBufferElementData,之后上屏之前读取出来,通过 MaterialPropertyBlock 进行赋值即可。这样处理仍然可以合批。

  • 如何定制单位渲染流程?

        参考上一个面片树的例子,还有一个问题就是需要对面片树增加一个特殊的 DrawCall 来执行 PreZ,否则面片树在矩阵变换后Z轴重叠导致闪烁。

        因为每一个部件都是单独的一套配置,我在这套配置里增加了一项,即可按照我配置的类型进行特殊预处理,增加一次 PreZ。

  • 单位剔除问题

        单位剔除没有按照部件,而是按照单位进行剔除的。一个单位(例如上面的一个农场)就按照其包围盒进行剔除,且使用 ECS 多线程并行。

【Unity】相机视锥体剔除算法_unity视椎体剔除-CSDN博客文章浏览阅读3.7k次,点赞2次,收藏10次。视锥体剔除是Unity常用的剔除方法,其原理就是通过判定目标包围盒与组成相机视锥体的6个平面进行同侧判定,只要在6个平面之间的包围盒即为可见。本文根据其原理,给出一个视锥体裁剪的剔除算法的实现,并兼容ECS。_unity视椎体剔除https://blog.csdn.net/cyf649669121/article/details/125779899        对于 SLG 游戏这种固定俯视角的,还可以使用更简单的剔除方式:

【Unity】俯视角相机地面视野范围的计算_unity相机视野范围-CSDN博客文章浏览阅读3.9k次,点赞4次,收藏16次。在SLG等游戏中,相机总是固定为俯视角(上帝视角)。为了更好地管理游戏数据,需要对地图进行分块,只处理视野内的部分。判定某个单位是否在视野内有很多方法了,但是要么不够精确,要么性能不够,要么无法与AOI配合。 一个可行的方案就是将相机在地面上的视野计算出一个AABB 2D 包围盒,然后基于此包围盒来计算 AOI、显隐等。这个方案效率够高,而且对俯视角适配较好。_unity相机视野范围https://blog.csdn.net/cyf649669121/article/details/127529668

  • 如果渲染单位超过1024个了怎么办?

        这里的处理方案就是在 RenderDataEntity 上挂载一个 ComponentData ,标记另一个同样类型的 RenderDataEntity ,形成一个类似链表的数据结构。如果自身需要渲染的部件数量超过设定值(例如1024),就切换到下一个渲染实体。

        在实际开发过程中,超过 1024 的情况还是很少的,更多的根据项目实测,为了节约内存会限定单个 RenderDataEntity 的最大渲染数量(例如128)。毕竟,记录各个部件的 Transform 信息的 Buffer 需要一开始就初始化好,自然能省一点是一点。

  • 如何将预制体转换成所需要的数据?

        我自己是写了一个工具进行转换(具体参考工程里的代码),在离线情况下收集所有需要渲染的预制体,然后烘培成纯数据。虽然理论上可以在线试试转换,但我不建议这样做。

        实际上转换后,原有的预制体就不需要了。所以后面我的做法,都是在专门的美术工程里进行数据整理、烘焙,正式工程里只有纯数据。

5、优势与限制

优势:

  1. 完美的合批!(除了 DrawMeshInstanced 有 1024 个的限制外,能完美合批);
  2. 高性能,支持大批量物件渲染;
  3. 能与主线程进行逻辑交互;
  4. 可以在 ECS 系统里可定制化一些特殊功能;

劣势:

  1. 技术水平要求较高,需要熟练掌握 Dots ECS 体系才能良好维护。
  2. 无法使用动画(只能程序实现一些简单效果)
  3. 不支持多材质球、多Mesh的模型(需要拆分)

除此之外,还有以下情况不建议使用这套系统:

  1. 独特物体:一般来讲,单个物体会有独特的模型、效果和控制方式,而在ECS控制起来较为困难,且没有什么效率提升。
  2. 静态物体:静态海量物体(不会移动、形变等)建议使用下面的方案使用GPU来处理,效率更高。GPU驱动的大规模静态物件渲染-CSDN博客文章浏览阅读745次,点赞9次,收藏18次。GPU Driven 的静态物件渲染,听起来很高级,其实具体操作很简单,基础就是直接调用 Graphics.DrawMeshInstancedIndirect 这个 Unity 内置接口就可以了。但我们项目对这个流程做了一些优化,使得支持的实体数量有大幅提升。这套系统主要也是公司的 TA 实现的,这里我也只简明扼要地介绍一下原理。https://blog.csdn.net/cyf649669121/article/details/141222437

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • appium学习记录
  • ISP代理与住宅代理:主要区别?
  • ECMAScript 6 基础
  • Golang基础1
  • 【蓝桥杯集训100题】scratch游泳时长 蓝桥杯scratch比赛专项预测编程题 集训模拟练习题第27题
  • Windows电脑本地安装跨平台文生音乐AI应用MusicGPT详细教程
  • TCP/IP和SNMP
  • Python学习-打工人挣钱
  • 韩顺平Java-第二十六章:正则表达式
  • 游戏开发设计模式之中介者模式
  • echarts-简单温度热力图
  • 设计模式 3 单例模式
  • 力扣最热一百题——6.三数之和
  • vue part4
  • HashMap源码剖析
  • Apache的80端口被占用以及访问时报错403
  • canvas 五子棋游戏
  • CSS居中完全指南——构建CSS居中决策树
  • JavaScript函数式编程(一)
  • javascript面向对象之创建对象
  • js数组之filter
  • Lucene解析 - 基本概念
  • PHP 的 SAPI 是个什么东西
  • Python学习之路16-使用API
  • Vue--数据传输
  • vue--为什么data属性必须是一个函数
  • 精益 React 学习指南 (Lean React)- 1.5 React 与 DOM
  • 前端攻城师
  • 如何正确配置 Ubuntu 14.04 服务器?
  • 微信公众号开发小记——5.python微信红包
  • 用jquery写贪吃蛇
  • AI又要和人类“对打”,Deepmind宣布《星战Ⅱ》即将开始 ...
  • 数据可视化之下发图实践
  • ​业务双活的数据切换思路设计(下)
  • ## 基础知识
  • #define
  • #每日一题合集#牛客JZ23-JZ33
  • #在 README.md 中生成项目目录结构
  • $.ajax,axios,fetch三种ajax请求的区别
  • (01)ORB-SLAM2源码无死角解析-(56) 闭环线程→计算Sim3:理论推导(1)求解s,t
  • (2)(2.4) TerraRanger Tower/Tower EVO(360度)
  • (LeetCode 49)Anagrams
  • (Windows环境)FFMPEG编译,包含编译x264以及x265
  • (附源码)计算机毕业设计ssm本地美食推荐平台
  • (力扣记录)235. 二叉搜索树的最近公共祖先
  • (三) diretfbrc详解
  • (四)软件性能测试
  • (已解决)vue+element-ui实现个人中心,仿照原神
  • (转)德国人的记事本
  • (转载)从 Java 代码到 Java 堆
  • .bat批处理(十):从路径字符串中截取盘符、文件名、后缀名等信息
  • .NET Framework杂记
  • .NET 设计模式初探
  • .ui文件相关
  • @JoinTable会自动删除关联表的数据