WebGL绘制如何进行优化?

2,859 阅读7分钟

很久之前写的笔记才贴出来,在某次面试之后,面试官问: 100万个模型绘制 ,很卡,你会怎么优化? 我查询的大部分资料都是OpenGL的(太久了,没留下地址),所以我会根据自己的经验,说明下WebGL怎么做。

影响性能的因素

  1. CPU
  • 过多的draw call(就是调用了drawElements或者是drawArrays函数,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出到屏幕)
  • 复杂的脚本或是物理模拟
  1. GPU
  • 顶点处理
    • 过多的顶点
    • 过多的逐顶点计算
  • 片元处理
    • 过多的片元(可能是由于分辨率造成的,也可能是由于overdraw造成的,同一个像素绘制多遍)
    • 过多的逐片元计算
  1. 带宽 GPU虽然拥有强大的并行能力可以极快数据处理,但GPU储存有限,数据需要从外部传人,可能导致传输数据的时间远大于GPU处理的时间,所以带宽的问题也要注意下。
  • 使用了尺寸很大且未压缩的纹理
  • 分辨率过高的帧缓存

WebGL的渲染分析工具

  1. JavaScript Profiler:可以显示JS函数的执行时间
  2. FPS:查看Frame rate,GPU内存占用情况
  3. Performance:统计某一时间段的操作情况,录制各种性能指标(FPS,CPU,NET,重排重绘等)。可以通过统计报表中的Call tree找到对于的文件链接,点进去排查代码。

优化技术

1. CPU优化

  • 减少draw call 使用批处理,简单理解就是将使用同一个材质的物体一起处理,因为他们之间的不同就是顶点数据的差别。如果使用不同材质,但要使用批处理,也可以将这些纹理合并到同一张大纹理称为图集,再使用不同的采样坐标对纹理采样即可。如果需要微小的不同,使用顶点颜色数据来储存。批处理是OpenGL的概念,WebGL有实例绘制,告诉GPU使用共享模型来绘制每一个实例,也是一次绘制多个物体。

在WebGL2.0绘制方法是:

 gl.drawArraysInstanced(mode,first,count,instanceCount);
 gl.drawElementsInstanced(mode, count, type, offset, instanceCount);

下面这个方法调整 实例位置

gl.vertexAttribDivisor(index,divisor);

WebGL1.0可以使用扩展:

var ext = gl.getExtension('ANGLE_instanced_arrays');
ext.drawArraysInstancedANGLE();
ext.drawElementsInstancedANGLE();

在ThreeJS中使用InstanceMesh或InstanceGeometry。

2. GPU

顶点处理优化

1. 优化几何体

尽可能减少模型中三角形面片的数目,需要美工人员的帮助。

这种将顶点一分为多的原因主要有两个:一个是为了分离纹理坐标(uv splits),另一个是为了产生平滑的边界(smoothing splits)。它们的本质,其实都是因为对于GPU来说,顶点的每一个属性和顶点之间必须是一对一的关系。而分离纹理坐标,是因为建模时一个顶点的纹理坐标有多个。例如,对于一个立方体,它的6个面之间虽然使用了一些相同的顶点,但在不同面上,同一个顶点的纹理坐标可能并不相同。对于GPU来说,这是不可理解的,因此,它必须把这个顶点拆分成多个具有不同纹理坐标的顶点。而平滑边界也是类似的,不同的是,此时一个顶点可能会对应多个法线信息或切线信息。这通常是因为我们要决定一个边是一条硬边(hard edge)还是一条平滑边。

2. 模型的LOD技术(多细节层次)

ThreeJS实现了LOD模型。这种技术的原理是,当一个物体离摄像机很远时,模型上的很多细节是无法被察觉到的。因此,LOD允许当对象逐渐远离摄像机时,减少模型上的面片数量,从而提高性能。

3. 面剔除技术

OpenGL允许检查所有正面朝向(Front facing)观察者的面,并渲染它们,而丢弃所有背面朝向(Back facing)的面,这样就节约了我们很多片段着色器的命令(它们很昂贵!)。我们必须告诉OpenGL我们使用的哪个面是正面,哪个面是反面。OpenGL使用一种聪明的手段解决这个问题——分析顶点数据的连接顺序(Winding order)。

默认情况下,逆时针的顶点连接顺序被定义为三角形的正面。 WebGL也是这样,具体设置代码为:

gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT_AND_BACK);

片元处理优化

减少需要处理的片元数目
  1. 控制绘制顺序 为了最大限度地避免overdraw,一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,那么就可以很大程度上减少overdraw。这是因为,在后面绘制的物体由于无法通过深度测试,因此,就不会再进行后面的渲染处理。

  2. 时刻警惕透明物体 由于透明度物体处理时,会导致一些硬件优化策略失效(比如说遮挡剔除,把看不到物体的顶点剔除)。对于透明度混合技术,需要关闭深度写入,所以要保持正确的渲染顺序。透明度测试没有关闭深度测试,但由于它的实现使用了discard或clip操作。也就是说,只要在执行了所有的片元着色器后,GPU才知道哪些片元会被真正渲染到屏幕上,这样,原先那些可以减少overdraw的优化就都无效了。

这里可以去查看下Threejs的源码,大概是先对不透明物体从前往后排序后绘制,再绘制透明物体,进行混合。

减少逐片元计算

减少实时光照和阴影,可以使用光照贴图代替实时计算。

3. 带宽

  • 使用了mipmaps(多级渐远纹理) WebGL API的WebGLRenderingContext.generateMipmap()方法为对象生成一组mipmap 。
  • 纹理压缩(美工实现)
  • 采用VBO(Vertex Array Object顶点数组对象)的方式可以降低数据传输,ThreeJS默认采用的就是这种方式,立即渲染物体除外。
  • 使用分辨率缩放解决分辨率过高的问题。 WebGL API的WebGLRenderingContext.viewport()方法设置视口,该视口指定x和y从标准化设备坐标到窗口坐标的仿射变换。

4. 减少计算复杂度

  • 可能地把计算放在每个对象或逐顶点上 通常来讲,游戏需要计算的对象、顶点和像素的数目排序是对象数 < 顶点数 < 像素数。 实现高斯模糊和边缘检测时,我们把采样坐标的计算放在了顶点着色器中,这样的做法远好于把它们放在片元着色器中。

  • 尽可能使用低精度的浮点值进行运算

  • 在使用插值寄存器把数据从顶点着色器传递给下一个阶段时,我们应该使用尽可能少的插值变量。比如,把两个纹理坐标打包在同一个四维变量中。

  • 尽可能不要使用全屏的屏幕后处理效果。如果美术风格实在是需要使用类似Bloom、热扰动这样的屏幕特效,我们应该尽量使用fixed/lowp进行低精度运算(纹理坐标除外,可以使用half/mediump)。那些高精度的运算可以使用查找表(LUT)或者转移到顶点着色器中进行处理。

  • 尽量把多个特效合并到一个Shader中。例如,我们可以把颜色校正和添加噪声等屏幕特效在Bloom特效的最后一个Pass中进行合成。

  • 代码优化规则

    尽可能不要使用分支语句和循环语句。

    尽可能避免使用类似sin、tan、pow、log等较为复杂的数学运算。我们可以使用查找表来作为替代。

    尽可能不要使用discard操作,因为这会影响硬件的某些优化。