笔记本: UnityShader入门精要thdjqm
最常见的优化技术是批处理。
批处理的实现原理是减少每帧所需的绘制调用数。 为了在屏幕上渲染每个对象,CPU必须检查哪些光源影响了对象,绑定sahder并设置参数,然后将渲染命令传递给GPU。 如果场景中包含许多对象,则这些操作非常耗时。
作为一个极端的示例,如果需要渲染1000个三角形网格,则每1000个单独的网格进行渲染将消耗比包含1000个三角形网格的渲染多得多的世界。 在这两种情况下,GPU的性能消耗实际上没有很大的差异,但CPU的DrawCall数会成为性能瓶颈。 因此,批处理的想法很简单。 每次调用drawCall时处理尽可能多的物体。
批处理——只有使用相同材质的物体才能一起处理。 这是因为使用了相同材质的物体。 他们的区别只是顶点数据的差异,通过将这些顶点数据汇总发送到GPU,可以一次完成批处理。
Unity支持两种批处理方法:动态批处理和静态批处理。 动态批处理的优点是所有处理都是由Unity自动完成的,我们什么都不需要做,并且物体可以移动。 缺点是限制很多。 不小心可能会破坏此机制,导致Unity无法动态批处理使用相同材质的对象。 对于静态批处理,他的优点是自由度高、限制少,但缺点是可能占用更多内存,所有静态批处理的物体都无法移动。
动态批处理:
如果场景中的某些模型共享材质并满足某些条件,则Unity可以自动对它们进行批处理,并在一个绘制调用中渲染所有模型。 动态批处理的基本原理是将可以在每一帧进行批处理的模型网格合并,将合并的模型数据传递给GPU,并使用相同的材质进行渲染。 除了实现方便之外,动态批处理的另一个优点是批处理对象可以移动。 这是因为Unity会逐帧重新组合网格。
Unity动态批处理不需要额外的工作,但只对满足条件的模型和材质进行动态批处理。
限制动态批处理的主要条件:
可动态批处理的网格顶点属性规模小于900。 例如,如果材质球需要使用三个顶点属性:顶点位置、法线和纹理坐标,或者要允许动态批出模型,则他的顶点数不得超过300个。 请注意,这个数字将来可能会发生变化,不要依赖于这个数据。
一般来说,所有对象都必须使用相同的缩放大小。 有例外。 如果所有物体使用不同的比例,他们也会动态批处理。 (Unity5取消了此限制)
使用灯光纹理的物体需要慎重处理。 这些对象需要其他渲染参数,如灯光纹理的索引、偏移和比例信息。 因此,为了能够动态批处理这些物体,需要确保指向灯光纹理的同一位置;
多路径着色器中断批处理。 对于正向渲染,可能需要使用其他过程向模型中添加更多照明效果,但这将避免动态批处理模型。
如果需要应用多种光线,则具有多个通过的着色器破坏了动态批处理机制,并且Unity不能对这些物体执行动态批处理。 平行光和点光源需要分别影响4个物体,因此需要2*4个批处理操作。 请注意,只有在点光源的影响范围内存在物体时,Unity才会调用其他Pass来处理他。 因此,即使场景内的物体离开点光源,也能够自动地进行批处理。
动态批处理的条件限制比较多,我们的模型数量往往超过900个顶点属性限制,在这种情况下依赖动态批处理减少drawcall神勇的葡萄柚已经不能满足我们的需要。 此时,静态批处理变为可用。
静态批处理:
静态批处理使用任意大小的几何模型。 他的实现原理是,只要有运行开始阶段,就将需要静态批处理的模型集成到新的网格结构中,在运行时不能移动这些模型。 但是,因为他只需要进行一次合并操作,所以比动态批处理更有效率; 静态批处理的另一个缺点是占用大量内存来存储合并的几何结构。 如果在静态批处理之前有几个物体共享同一网格,则内存中的每个物体都对应于该网格的副本。 也就是说,一个网格会变成多个网格发送到GPU。
例如,如果在使用1000个网格的同一树模型的森林中使用静态批处理,则内存消耗将增加1000倍,对内存的影响将变得严重。 在这种情况下,您可能需要容忍牺牲内存以换取性能的方法,或者使用动态批处理技术(但必须减少顶点数)而不是静态批处理,或者自己创建批处理。
静态批处理的方法非常容易实现。 只需选中物体面板上的静态复选框。 (实际上,只需选中batching static即可。)
查看每个模型在运行时使用的网格时,它名为Combined Mesh,这是由Unity将所有标识为静态的物体合并而成的。 该合并网格实际上包含单个物体的子网格。 对于合并的网格,Uniy确定其中使用了相同材质的子网格,并对其进行批处理。
在内部实现中,Unity首先将这些静态物体转换为世界空间,然后为他们构建更大的顶点和索引缓存。对于使用统一材质的对象,Unity只需要调用一个DrawCall即可全部实现
的物体。而对于使用了不同材质的物体,静态批处理同样可以提升渲染性能,机关这些物体仍然需要调用多个drawcall,但是静态批处理可以减少这些draw call之间的状态切换。切换状态往往是耗时的操作;
在Unity的分析器中观察到在应用静态批处理前后VBO Total的变化。VBO(Vertex Buffer Object)顶点缓冲对象的数目变大了;这是因为静态批处理会占用更多的内存。静态批处理需要占用更多的内存来存储合并后的几何结构体,如果一些物体共享了相同的网格,那么内存中每一个物体都会对应个该网格的复制品。
共享材质:
无论是动态批处理还是静态批处理,都要求模型之间需要共享同一个材质。但是不同的模型之间总会需要不同的渲染属性,例如使用不同的纹理、颜色等。所以需要一些策略尽可能的合并材质;
如果两个材质之间只有使用的纹理不同,我们可以把这些纹理合并到一张更大的纹理中,这张更大的纹理统一被称为图集atlas。一旦使用了同一张纹理,我就可以使用同一个材质,在使用不同的采样坐标对纹理采样即可;
但是有时候,除了纹理不同以外,不同的物体在材质上还有一些微小的参数变化,例如,颜色不同,某些浮点属性不同。但是不管是动态批处理还是静态批处理,他们的前提是都要使用同一个材质,是同一个,而不是使用了同一种shader材质。也就是说他们指向的材质必须是同一个实体。这意味着,只要调整了参数,就会影响到所有使用这个材质的对象,那么想要微小的调整,常用的方法就是使用网格的顶点数据(最常见的就是顶点颜色的数据)来存储这些参数;
经过批处理后的物体会被处理成更大的VBO发送给GPU,VBO的数据可以作为输入传递给顶点着色器,因此,可以巧妙的对VBO中的数据进行控制,从而达到不同的效果的目的。一个例子是,森林场景中所有的树木都是用了同一种材质,希望他们可以通过批处理来减少drawcall,但不同的树木的颜色可能不同,这时候就可以利用网格的顶点的颜色数据来调整;
需要注意的是,如果需要在脚本中访问共享材质。应该使用Renderer.shareMaterial来保证修改的是和其他物体共享的材质,这是这意味着修改会应用到所有该材质的物体上。另一个类似的API是Renderer.Material是来修改材质,Unity会创建一个该材质的复制品,从而破坏批处理在改物体上的应用,
批处理的使用注意事项:
在选择使用动态批处理还是静态批处理的时候一些建议:
1、尽可能渲染静态批处理,但是得时刻注意对内存的消耗,并且记住经过静态批处理的物体不可以在移动;
2、如果无法进行静态批处理,而要选择动态批处理的话。那么请小心不要打破动态批处理的限制。例如:尽可能让这样的物体变少,并且尽可能让这些物体包含少量的顶点属性和顶点数目。
3、对于游戏中的小道具,例如可以捡起的金币等,可以使用动态批处理。
4、对于包含动画的这类物体,无法全部使用静态批处理,但是其中如果有不动的部分,可以把这部分表示成Static。
除了以上的提示之外,在使用批处理的时候还有一些需要注意的地方,由于批处理需要把多个模型变换到世界空间下在合并他们,如果在shader中存在一些基于模型空间下的坐标计算,那么往往可能得到一个错误的效果。解决办法就是:在sahder中使用DisableBatching标签来强制使用该shader的材质不会被批处理;另一个注意事项:使用半透明材质的物体通常需要使用严格的从后往前的绘制顺序来保证透明的混合的正确性。对于这些物体,Unity会首先会保证他们的绘制顺序,在尝试对他们进行批处理。这意味着当绘制顺序无法满足的时候,批处理无法在这些物体上被应用。