最近将GPU Instancing应用在游戏中,遇到了一些坑和大家分享一下,我们用的版本是unity2017。前面说了GPU Instancing目前适合游戏中的草、石头等元素较多的地方。
首先需要开发一个刷地表的编辑器,试想一下如果场景上的草每个都要美术手动的去摆这得多麻烦啊,如下图所示,根据网格和材质动态的在地形上刷地表。
刷地表的代码如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
void OnSceneGUI() { Event e = Event.current; if (e == null) { return; } HandleUtility.AddDefaultControl (GUIUtility.GetControlID (FocusType.Passive)); Ray worldray = HandleUtility.GUIPointToWorldRay (e.mousePosition); if (e.type == EventType.MouseDown && !e.alt && e.button == 0) { //点击种草 } if (e.type == EventType.MouseDrag && !e.alt && e.button == 0) { //拖动种草 } if (Physics.Raycast (worldray, out m_Hit,500f,1 << LayerMask.NameToLayer("GInstances"))) { //种草椭圆区域 Handles.color = new Color (1f, 1f, 1f, 0.5f); Handles.DrawSolidDisc (m_Hit.point, m_Hit.normal, m_Radiua.floatValue); Handles.color = Color.red; Handles.DrawWireDisc (m_Hit.point, m_Hit.normal, m_Radiua.floatValue); SceneView.RepaintAll (); } } |
刷出来的元素就是每个不同的游戏对象,编辑模式下可以还可以单独的调整它们。不过这又带来另一个问题,如果美术刷了几万个草元素,总不能运行时也管理这些游戏对象吧。上篇文章我们也讲过,GPU Instancing使用游戏对象的方式效率是最低的,所以我们需要使用Graphics.DrawMeshInstanced()一次性将元素画出来,不需要游戏对象。
所以在编辑模式下我们还需要一个保存的功能,就是将美术刷出来的草元素每个的位置、旋转、缩放、顶点色、序列化在本地,我是将游戏对象的矩阵序列化在本地的。如下图所示,运行时就不需要游戏对象了,使用Graphics.DrawMeshInstanced()一次画出来,只占用一个drawcall。
Graphics.DrawMeshInstanced()这方法还有两问题
1.一次最多画1023个元素,如果超出就会报错,所以需要将草进行分类管理。
2.它不提供裁切的功能,也就是说摄像机看不到的地方,这些草是不会被剔除掉的,依然会被渲染。
解决这个问题,为了避免运行时暴力的for循环来判断是否在视野内,我采取的方法是预先将场景分成20X20若干个格子(可根据游戏的可视范围而定)根据玩家的位置,始终只渲染周围9个格子内的草元素,这样将大幅度减少运行时for循环的次数。
如果每个草的顶点色是不一样的怎么办呢?接着我们看看C#这边如何将参数传到shader中,如下代码所示,创建MaterialPropertyBlock以后就可以将参数以及对应的值传递给shader中了。
1 2 3 4 5 6 7 8 9 10 |
for (int i = 0; i < m_Matrixs.Count; i++) { MaterialPropertyBlock prop = new MaterialPropertyBlock(); prop.SetVectorArray("_LightMapUV", m_Lightmaps[i]); prop.SetColor("_Color", Color.white); var item = m_Matrixs[i]; Graphics.DrawMeshInstanced(m_InstanceMesh, 0, m_InstanceMaterial, item, prop, ShadowCastingMode.On, true); } |
Shader中可以接受这些值,在vs和ps中处理 。
1 2 3 4 5 6 |
UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float4, _Color) UNITY_DEFINE_INSTANCED_PROP(float4, _LightMapUV) UNITY_INSTANCING_BUFFER_END(Props) |
毕竟opengl es2.0的手机是不支持的,所以我们还需要做个容错机制。 可以判断出来手机是否支持 SystemInfo.supportsInstancing
最后如果有什么建议或者意见欢迎在下面留言!
- 本文固定链接: https://www.xuanyusong.com/archives/4504
- 转载请注明: 雨松MOMO 于 雨松MOMO程序研究院 发表
如果按照你说的这样子,为什么竟然分格子,不直接预先把一个格子整成一个批次(相当于静态合并批次,但是u3d的静态合并批次,并不是将全部相同物体整成一个批次,他只是减少setpass,有些才合并了一个,但是你上面那种例子,肯定是适合提前整一个格子整成一个批次),上一篇讲这个文章,没用DrawMeshInstanced让它自动instancing,帧数那么低其实有一个小概率可能,就是并没有真正instancing,因为u3d代码很多情况会break,我这边遇到一个很奇葩的现象,一些低端机(当然是支持instancing的),有一些截帧工具发现它并没有instancing,换是分开绘制,但是里面的shader已经编译为带instancing标识的,同时supportsInstancing是true。当然我用高端一些的机器就没有这个问题,不知道你有没有遇到过这样的问题,就是在联发科的cpu,机器大概才500块
当gameobject不连续时,渲染列队被打断,会导致分成多个批次。比如有多个相同的角色,每个角色都拿着相同的武器,由于武器是挂接在每个角色上的,会导致本应该相同批次的物体没有连续,无法在同一批次完成。这种情况可以自己调DrawMeshInstanced,或者用srp自己控制列队。参见这个例子:https://github.com/hanchong22/SRPTest
也就是说,GPU Instancing 需要相同VBO,相同的着色器Pass, 并且绘制区别仅限于值类型参数的数值区别,GPU Instancing才会生效,是这样吗?
是的
gpu instancing的意义不是在于相同的材质使用不同的属性 但仍然可以合批吗…
准确的说是 相同材质 相同shader 相同mesh