Notifications
Article
[微软Tech Summit]Unity 2017 XR 开发技巧
Updated a year ago
535
0
微软技术暨生态大会《Unity 2017 XR 开发技巧》的主题演讲整理下。
11月3日,Unity大中华区技术经理鲍健运,在微软技术暨生态大会做了《Unity 2017 XR 开发技巧》的主题演讲。本文整理自演讲中关于Unity XR开发技巧方面的一些知识。
对于许多游戏开发者而言,Unity是非常熟悉的伙伴,藉以它的帮助诸多奇思妙想得以成为现实。作为移动游戏业界的老兵,我事业的发展也是深受Unity影响。非常荣幸,我能成为Unity的一员,为Unity的技术推广贡献一些绵薄之力。

Unity XR 开发技巧

单通道立体渲染 + 实例化

针对Windows Mixed Reality平台,Unity有非常不错的优化功能,比如Single Pass Instanced(单通道立体渲染 + 实例化)功能。这个功能的好处首先要从VR/MR渲染机制说起。

传统双通道模式

上图中所呈现的是,传统的VR渲染模式:先完成左眼的渲染,然后再做右眼的渲染。这种模式虽然能很快速适配VR/MR,但是两眼之间会有一定的时滞与延迟,体验不佳。因为在这种模式下, Unity会为左右眼各分配一个Render Texture做渲染, 目的是和非VR模式下的渲染方式尽可能的兼容。

单通道模式

在Single Pass渲染模式下:
  1. Unity会先向主线程发送一组绘图指令,然后把这组指令转换成渲染线程上两组交叉存取的指令,这样我们就可以把场景中的每一个物体同时渲染到左右眼。
  2. 为了支持上述功能,我们现在的做法是分配一个两倍宽度的Render Texture,这样左眼和右眼会得到这张Render Texture的一半。

实例化

第二个部分就是实例化,而这项功能的基础就是GPU Instancing。上图右边的图示就是一个使用GPU Instancing的案例,其中有4~5千个“包子”,在iPhone SE设备上都可以保证30左右的稳定帧率。GPU Instancing会使用少量绘图调用一次绘制(或渲染)同一个网格的多个副本。 这个针对绘制场景中重复出现的建筑物,树木和草坪等物体或其他物体很有用。它仅使用每个绘图调用渲染相同的网格,但每个实例可以具有不同的参数(例如,颜色或缩放)来添加变体并减少重复的出现。GPU Instancing可以减少每个场景使用的绘制调用次数。 这显着提高了项目的渲染性能。
这是一项Shader层面优化的功能,当Shader中撰写了GPU Instancing命令的调用后,可以在Inspector中直接设置是否启用该项功能,即Enable GPU Instancing的可选项开关。
这是另外一个实际的数据例子。在未启用Instancing时,CPU主线程时滞是17ms,渲染线程时滞是7.2ms;但是启用Instancing后,CPU主线程时滞缩短至11ms,而渲染线程时滞减少到7.2ms。当使用GPU实例时,有以下限制:Unity会自动选择MeshRenderer组件和Graphics.DrawMesh调用实例。 请注意,不支持SkinnedMeshRenderer。Unity仅批量在单个GPU实例绘制调用中共享相同的网格和相同材质的GameObjects,使用少量的网格和材料来更好地实现效率。 必要时,需要创建变体,修改着色器脚本以添加每个实例数据。
大多数内置Shader都可以直接适配这个对于Windows Mixed Reality平台开发提供大大便利的“单通道模式 + 实例化”的功能。但是如果您是自己写的Shader想实现这个功能,就需要添加一些代码实现优化。这里以一个典型的Vertex/Fragment Shader为基础,进行一定的修改,使之能生效Single Pass Instanced (preview)功能。每个在Shader中传递数据的结构体中,必须添加宏参数UNITY_INSTANCE_ID,以辅助实例化处理。在撰写Fragment Shader部分之前,需要在输出的结构体(v2f,即Vertex to Fragment)部分添加宏参数UNITY_VERTEX_OUTPUT_STEREO。并且在Vertex Shader内部必须逐个添加宏方法UNITY_SETUP_INSTANCE_ID(),UNITY_TRANSFER_INSTANCE_ID()和UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(),目的就像这些宏字面所表达的:分配并启用示例ID,将坐标转置进行实例化处理,以及初始化Vertex输出为Single Pass模式。
在后期处理Shader的部分,您将需要在输入纹理声明周围添加UNITY_DECLARE_SCREENSPACE_TEXTURE (tex) 宏,以便2D纹理数组将被正确声明。 然后,与Vertex Shader相近操作的是,在Fragment Shader开头部分添加分配并启用示例ID宏方法UNITY_SETUP_INSTANCE_ID()。接着在取样纹理时候,调用UNITY_SAMPLE_SCREENSPACE_TEXTURE(),以便于在Single Pass模式下给双眼取样。如果大家有兴趣对其他类似的宏深度纹理和屏幕空间阴影贴图的更多信息,可以参阅HLSLSupport.cginc。
这段部分代码的示例,展示了需要实现Single Pass Instanced (preview)功能,在Shader上的一些必要修改。
在C#脚本部分则需要根据实际情况调用这两个方法:Graphics.DrawProceduralIndirect()和CommandBuffer.DrawProceduralIndirect()。计算缓冲区获取所有的参数,因为我们不能轻易地增加实例计数。 所以,您将必须手动加载计算缓冲区中包含的实例数。
该项功能的设置也很方便。我们可以通过Player Settings找到UWP标签页下的XR Settings,在Stereo Rendering Method选项设置Single Pass Instanced (Preivew)

空间映射技术

Unity编辑器拥有收集Mixed Reality项目中环境相关表面(Surface)信息的底层脚本引用API。这些API使您能够最大限度地控制何时查询设备的表面变化,以及何时创建或更新相应的Surface GameObject。空间映射组件(Spatial Mapping)允许您快速启动并运行混合现实,而无需直接使用那些底层API。
大家可以根据实际开发需求,将这些组件一起或单独使用。每个空间映射组件使用自己的表面观察器(Surface Observer)来了解物理世界的变化。 根据组件的配置方式,每个空间映射组件定期查询系统以了解物理空间中发生了哪些更改。 当系统通知组件相关更改时,组件将对各种已更改表面进行烘焙。烘焙过程包括用与物理表面对应的网格生成网格过滤器(Mesh Filter)。 空间映射渲染器(Spatial Mapping Renderer)和空间映射碰撞器(Spatial Mapping Collider)组件以自己的特定方式使用此网格过滤器。
空间映射渲染器(Spatial Mapping Renderer)组件给出了空间映射表面的可视化表示。这对于表面的可视化调试,以及向环境添加视觉效果都非常有用。空间映射渲染器组件定期询问系统物理空间的变化。每当组件被通知这些更改时,它将返回的表面数据烘焙到GameObject中。这些GameObject包含网格过滤器和网格渲染器。渲染器组件管理表面GameObject的生命周期。这意味着空间映射渲染器组件可以处理创建,更新和销毁表面GameObject。该组件提供了一种较简便的方式来动态地更改所有生成的表面上的材质。
它有两种材质类型:
  1. 遮挡材质(Occlusion Material),显示为透明但隐藏全息图。如果你想要一个真实的桌子隐藏在它下面的游戏中的全息对象。
  2. 线框着色器(Wireframe Shader)的材质,适用于所有组件的表面。线框的颜色代表距离。
上图中所展示的就是距离到颜色的映射表格。

细节层次(LOD,Level of Detail)

细节层次属于空间映射组件中的一项重要设置选项,主要是由组件生成的网格的低,中或高质量三种不同品质的设定。默认值为“中”。 质量越高,碰撞器和渲染器的精度越高,但伴随着消耗也越高;质量越低,性能和功率的消耗越低。

性能优化

  1. 请记住,每个空间映射组件都与其他空间映射组件无关。这意味着每个组件都保留自己的表面列表,即使多个组件看到相同的表面。尝试限制您使用多少空间映射组件,从而优化性能。
  2. 如果您期望模拟中的环境是相当静态和不变的(如棋盘游戏),则可以根据需要扫描尽可能多的表面数据,然后将Freeze Updates属性设置为false。这样既提高了部分性能,又降低了功耗。
  3. 移动空间映射组件会产生少量的额外性能消耗。因此,尽量避免移动具有空间映射组件的GameObject。
  4. 在Collider Settings > Level of Detail中,使用Low设置。当计算碰撞交点时,这会提高性能并降低功耗。
  5. 空间映射网格碰撞器在空间映射网格渲染器中的更新时间较短。这意味着Colliders更新速度比Renderers更快。
默认情况下,在Collider Settings > Mesh Layer中,所有游戏对象都分配给默认层。但是,将GameObject分配给特定层是很好的做法。因为Raycasting是计算功耗较高,可能会降低性能。通过使用不同层(Layer),您可以筛选您正在进行的Raycast计算,并优化性能。如果您的默认层上没有很多复杂的网格物体,那么进行Raycast检测可以避免碰撞,因此性能不会太高。但是,最好将GameObject组织到Layer中,以减少在进行碰撞时的Raycast测试的复杂度。
谢谢各位的聆听。

Tags:
Jerry Bao 鲍健运
Product Evangelist - Programmer
6
Comments