Unity原生渲染方案

2016年12月05日 14:38 0 点赞 0 评论 更新于 2025-11-21 20:57

一、动机与适用场景

本方案的动机是在原生代码中运用Unity的材质系统进行绘制,同时由原生代码提供绘制数据,以此省去动态模型数据“非托管内存 → 托管内存 → 非托管内存”的传输过程。该方案适用于需要生成大量动态模型数据的场景。需要注意的是,如果不使用Unity的材质系统,则无需采用此方案。此方案是在Miloyip的建议下完成的。

二、目标

在Unity中动态生成三维模型时,需要将数据填入Mesh对象,Unity内部会进行内存分配和数据转换,效率较低。若编写Unity原生插件来生成三维模型,模型数据需经过“非托管内存 → 托管内存 → 非托管内存”的传输过程,这会浪费大量的内存带宽和时间,尤其是对于每帧都需要更新的串流数据。因此,我们期望绕过这一数据传输过程,直接进行原生渲染。本文总结的原生渲染方案,旨在让原生插件继续使用Unity的材质系统,在插件内生成顶点数据并提交Draw - call。

三、方案

3.1 整体思路

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

3.2 进入原生渲染的时机

选项A:GL.IssuePluginEvent

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

选项B:直接调用原生API

另一种方法是在Unity脚本中先设置好材质,然后直接调用原生插件。具体操作如下:在Unity中调用Material.SetPass()设置材质,但该命令不一定会被Unity立即执行。若此时直接进入原生渲染,可能会出现绘制状态不正确的情况。我们找到的可行方法是,在调用SetPass()后紧接着执行DrawMeshNow()绘制一个小网格,此时整个OpenGL的绘制状态会被正确设置,之后再调用原生插件,这样在原生渲染器中查询到的绘制状态就是相应材质对应的正确绘制状态。

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

在不同平台上,利用Unity设置好的绘制状态存在一些区别:

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

3.4 参与Unity的视锥裁剪

为了使原生渲染器能正确参与视锥裁剪,需要在原生渲染器的GameObject上设置与原生几何体相同的包围盒,具体步骤如下:

  1. 在挂载原生渲染器的GameObject上关联Renderer及MeshFilter。
  2. 在MonoBehaviour.Update()时,从原生渲染器中读取原生几何体的包围盒,将一个拥有相同包围盒的Mesh设置到该GameObject的MeshRenderer,Mesh重新计算包围盒会引发MeshRenderer重新计算包围盒。
  3. 在OnWillRenderObject()时,记录GameObject是否通过了视锥裁剪。
  4. 在OnRenderObject()时,根据是否通过裁剪来决定是否调用原生渲染器。

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

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

3.6 绘制次序

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

四、原生渲染器的性能

我们测试了一个包含4W粒子的原生粒子系统,使用的材质为Unity内置的“Particles/Alpha Blended”,但文档中未给出具体测试结果。

五、其他事项

5.1 注意事项

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

5.2 未考虑的事宜

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

5.3 Buffer Object

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

作者信息

孟子菇凉

孟子菇凉

共发布了 3994 篇文章