Unity原生渲染方案

时间:2022-04-18 02:29:44

Unity原生渲染方案

作者:3dimensions

three_dimensions@live.cn

本文为原创内容,转载请注明出处。

  做这个的动机是想在原生代码中使用Unity的材质系统绘制,同时由原生代码提供绘制数据,省掉Unity内部的分配内存及数据转换,以及动态模型数据“非托管内存→ 托管内存→ 非托管内存”的传输过程。适用于有大量动态模型数据生成的情况,测试结果在PC和移动平台上均有数倍的性能提升。注意,如果不使用Unity的材质系统,并不需要按这个方案做。方案是我在Miloyip的建议下完成的。

一、目标

  在Unity中,动态生成三维模型需要把数据填入Mesh对象中,当中Unity内部需要分配内存及做数据转换,效率不佳。而且如果要编写Unity原生插件去生成三维模型,模型数据需要 经“非托管内存→ 托管内存→ 非托管内存”的传输过程,浪费很多内存频宽及时间,特别是每 帧都需要更新的串流数据。所以,我们希望能绕过这数据传输过程,直接进行原生渲染。本文总结的原生渲染方案,目标是令原生插件继续使用Unity的材质系统,然后在插件内生成顶点数据并提交Draw-call。

二、方案

  大致的想法是希望让Unity设置好OpenGL绘制状态后,在原生插件中获取这些状态并 加以利用。此方案暂不考虑多线程渲染及多步骤材质(Multi-pass material),并针对Open GL (Windows)及OpenGL ES(iOS和Android)进行测试。

2.1 进入原生渲染的时机──选项A:GL.IssuePluginEvent

  首先我们考虑使用Unity的GL.IssuePluginEvent,但这个方法还存在问题,在PC上测试 的结果显示,通过GL.IssuePluginEvent进入原生渲染后,一些绘制资源(如纹理)已经被解 绑,Unity中设置好的绘制状态被破坏了。所以此方案不可行。

2.2 进入原生渲染的时机──选项B:直接调用原生API

  另一个方法是在Unity脚本中先设置好材质,然后直接调用原生插件。首先,在Unity中调 用Material.SetPass()设置材质,但这个命令并不一定会被Unity立即执行,此时若直接进入 原生渲染,可能会发现绘制状态不正确。我们找到一个可行的方法是,在调用SetPass()后紧 随执行DrawMeshNow()去绘制一个小网格,此时整个OpenGL的绘制状态已经被正确设置,然 后才调用原生插件,在原生渲染器中查询到的绘制状态,便是相应材质对应的正确绘制状态。

2.3 如何在Native plugin中利用Unity的绘制状态

  如何利用Unity设置好的绘制状态,对于PC和Android有一些区别:

  1. 在PC上,Unity进入原生渲染之后,查询到的当前着色器名字为0,但这并不意味着绘制 状态被破坏,仍然可以绘制出正确的结果。我们怀疑DrawMeshNow()选择了设置材质到固 定管线,在PC原生渲染中只能利用这一固定管线。OpenGL固定管线的顶点属性是有语 义的,在原生渲染中调用gl*Pointer接口提供顶点数据就可以提交draw-call。
  2. 在Android上,OpenGL ES 2.0以上版本只能使用可编程管线,且移除了顶点属性的语义, 所有顶点属性变成了Generic。为了给当前绘制状态提供顶点数据,需要在原生渲染器中 查询当前着色器名称,并解析着色器的接口信息。Unity对顶点属性有比较固定的命名格 式,可以根据这些属性字符串从当前着色器中查询到Location信息。在原生渲染器中调 用glVertexAttribPointer提供顶点数据到相应Location,然后再提交Draw-call。

2.4 参与Unity的视锥裁剪

  为了使原生渲染器正确的参与视锥裁剪,我们需要在原生渲染器的GameObject设置和原生 几何体相同的包围盒。 具体方法为如下。首先,在挂载原生渲染器的GameObject上关联Renderer及MeshFilter。 然后,在MonoBehaviour.Update()时,从原生渲染器中读取原生几何体的包围盒,再把一个 拥有相同包围盒的Mesh设置到这个GameObject的MeshRenderer,然后Mesh重新计算包围盒引 发MeshRenderer重新计算包围盒。最后,在OnWillRenderObject()时,记录GameObject是否 通过了视锥裁剪。而在OnRenderObject()时,根据是否通过了裁剪来决定是否调用原生渲染 器.

2.5 多个摄像机及多个原生调用

  我们测试了多个摄像机的情况,以及在Material.SetPass()后紧随多次原生调用的情况, 原生渲染器均能正确绘制。

2.6 绘制次序

  原生渲染器的绘制次序通过Unity的层(Layer)控制,原生渲染器对应不同于场景其他物 体的Layer,原生渲染器的层对应独立的摄像机。摄像机之间依据其Depth属性决定绘制顺序。

三、原生渲染器的性能

  测试了一个4W粒子的原生粒子系统,材质为Unity内置的“Particles/Alpha Blended”。测试结果:

Unity原生渲染方案

四、其他事项

4.1 注意事项

  在PC上运行Unity时要给予命令行参数-force-opengl, 强制在PC上选择OpenGL作为绘 制API。内置材质“Particles/Multipy”在PC上效果不正确,原因未明。 OpenGL状态是全局的,在原生渲染器中对OpenGL状态的改变,除原生渲染器申请的自 有资源不需要释放,其他改变如bind/unbind必须在返回Unity时还原回进入原生渲染器时的初 始状态,否则可能导致崩溃。 测试过程中遇到一些问题,如Nexus 10(Mali T604)的驱动在执行Draw-call后会造成内 存泄漏,运行一段时间后驱动便会占用超过1GB内存,令malloc分配新空间时出现内存不足。 在别的机型上没有遇到这种情况,应属于驱动问题。

4.2 未考虑的事宜

  1. 本文总结的方案未考虑多步骤材质(Multi-pass material),多步骤的材质情况比较复杂, 暂时未考虑它们参与原生渲染器的方法。
  2. 本文方案未将遮挡剔除考虑在内。
  3. 开启多线程渲染后,本文方法可能不能得到正确效果。
  4. Unity调用DrawMeshNow()绘制的网络需要被隐藏起来。

4.3 Buffer Object

  把数据上传到GPU有一些需要注意的地方,如果上传新数据时Buffer object仍然被绘制 占用,可能会引发隐式同步(Implicit synchronization),等待绘制完成。通过使用多个Buffer object,在帧之间循环使用不同Buffer object可以降低这方面的开销。在GPU负载较大时会有一些性能提升,缺点是可能会增加了驱动的内存占用。