zhuanlan.zhihu.com/p/154425898 渲染优化-从GPU的结构谈起 - 知乎 (zhihu.com)
优化方向
- 加载性能优化
- 渲染帧率优化
- 内存管理优化
- 交互操作优化
加载性能优化
- 模型压缩
- gzip
- gltf draco
- 使用 indexedDB
- VAO
- 贴图压缩
- 分包流式加载
- 使用 CDN
渲染帧率优化
- 各种剔除 Culling (视椎体剔除,背面剔除、遮挡剔除)
- 视椎体剔除(算出物体的中心到面的最小距离(带正负方向的)与包围球的半径做比较,如果小于半径,就表示在外面。)
- 背面剔除
- 遮挡剔除(虽然 gpu 有深度测试,会将有遮挡的物体进行剔除,但是我们仍然希望在提交 GPU 之前对遮挡关系进行判断,提前剔除掉一些东西,减少渲染压力。)
- 合批 batch (材质相同的进行 batch)
- instance(共享一份顶点数据)
- LOD
内存管理优化
- buffer数据,在 js 推送完数据之后,将这部分数据从内存中释放掉,从而降低 JS 的内存压力。(BufferAttribute包含一个onUpload回调函数)
- 释放贴图,模型数据
交互操作优化
- gpu 拾取
- 八叉树优化(稀松八叉树)
常用优化手段
- 为啥drawcall越少越好?因为即使渲染一个三角形,
- 在GPU中也要走系列复杂的流程,这系列流程带来的延迟远超过计算一个三角本身,只有同时并行多处理才能发挥GPU的强大并行能力,这也是我们优化的时候要合并渲染的原因,越合并越能最大限度的利用GPU。总之一句话我们拿到的GPU是冲锋枪,冲锋枪最大的优势是连发,不能老用点射把冲锋枪当步枪用。
- 为啥要降低渲染面数?
- 面数越少VS计算使用的线程就越少,顶点计算就越快。
- 为啥要避免在shader中使用if else?
- 因为按照SIMD的执行方式,if else可能会完全不生效,导致两个分支都要走一遍。同样循环中的break也会导致这样的问题。
- 为啥要降低采样次数?
- 因为纹理的读取速度实在是太慢了,读取跟不上运算会导致极大的延迟。
- 一个和多个三角形哪个更快?
- 当然是一个更快了,在覆盖面积相等的情况下顶点多少越好。
- CPU和GPU的交互,CPU和GPU是类似服务端-客户端的模式,他们之间的交互成本也是很高的,GPU的调用指令也是越少越好,最简单的一个就是类似gl里面uniform的设置,uniform设置的数据量都是比较小的,这会导致指令调用成本大于数据传送成本,如果shader中大量的uniform vec3类似的东西还是合并成数组一次送入GPU(当然这会降低程序可读性,需要权衡)。另外一个任何从GPU回读的操作都是相当耗时的,即时是类似gl中获得句柄的操作,比如getUniformLocation,要避免在刷帧中使用。
常用优化2
- 图元合并:应用于大模型结构比较复杂,顶点、面比较数据比较大,可以通过算法根据权重剔除相应的顶点、面。从而到达轻量化模型效果。
- 几何对象构件对象化: 相同形状的几何对象不做多次拷贝,大模型相同几何体只做一个加载,只做相同模型构件移动、旋转、缩放。这样做模型文件和浏览器内存的大小得到明显减少。
- 场景八叉树划分:八叉树可以快速剔除不可见图元,减少进入渲染区域的绘制对象。
- Lod:模型可以设备Lod,根据距离、级别加载不同复杂度结构模型。
- 模型文件压缩:三维模型stl、obj、3ds、obj、json等文件格式算法压缩。
影响性能的因素
- CPU
- 过多的draw call(就是调用了drawElements或者是drawArrays函数,GPU就会根据渲染状态(例如材质、纹理、着色器等)和所有输入的顶点数据来进行计算,最终输出到屏幕)
- 复杂的脚本或是物理模拟
- GPU
-
顶点处理
- 过多的顶点
- 过多的逐顶点计算
-
片元处理
- 过多的片元(可能是由于分辨率造成的,也可能是由于overdraw造成的,同一个像素绘制多遍)
- 过多的逐片元计算
- 带宽
GPU虽然拥有强大的并行能力可以极快数据处理,但GPU储存有限,数据需要从外部传人,可能导致传输数据的时间远大于GPU处理的时间,所以带宽的问题也要注意下。
- 使用了尺寸很大且未压缩的纹理
- 分辨率过高的帧缓存
WebGL的渲染分析工具
- JavaScript Profiler:可以显示JS函数的执行时间
- FPS:查看Frame rate,GPU内存占用情况
- 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
顶点处理优化
- 优化几何体
尽可能减少模型中三角形面片的数目,需要美工人员的帮助。
这种将顶点一分为多的原因主要有两个:一个是为了分离纹理坐标(uv splits),另一个是为了产生平滑的边界(smoothing splits)。它们的本质,其实都是因为对于GPU来说,顶点的每一个属性和顶点之间必须是一对一的关系。而分离纹理坐标,是因为建模时一个顶点的纹理坐标有多个。例如,对于一个立方体,它的6个面之间虽然使用了一些相同的顶点,但在不同面上,同一个顶点的纹理坐标可能并不相同。对于GPU来说,这是不可理解的,因此,它必须把这个顶点拆分成多个具有不同纹理坐标的顶点。而平滑边界也是类似的,不同的是,此时一个顶点可能会对应多个法线信息或切线信息。这通常是因为我们要决定一个边是一条硬边(hard edge)还是一条平滑边。
- 模型的LOD技术(多细节层次)
ThreeJS实现了LOD模型。这种技术的原理是,当一个物体离摄像机很远时,模型上的很多细节是无法被察觉到的。因此,LOD允许当对象逐渐远离摄像机时,减少模型上的面片数量,从而提高性能。
- 面剔除技术
OpenGL允许检查所有正面朝向(Front facing)观察者的面,并渲染它们,而丢弃所有背面朝向(Back facing)的面,这样就节约了我们很多片段着色器的命令(它们很昂贵!)。我们必须告诉OpenGL我们使用的哪个面是正面,哪个面是反面。OpenGL使用一种聪明的手段解决这个问题——分析顶点数据的连接顺序(Winding order)。
默认情况下,逆时针的顶点连接顺序被定义为三角形的正面。 WebGL也是这样,具体设置代码为:
gl.enable(gl.CULL_FACE);
gl.cullFace(gl.FRONT_AND_BACK);复制代码
片元处理优化
减少需要处理的片元数目
- 控制绘制顺序 为了最大限度地避免overdraw,一个重要的优化策略就是控制绘制顺序。由于深度测试的存在,如果我们可以保证物体都是从前往后绘制的,那么就可以很大程度上减少overdraw。这是因为,在后面绘制的物体由于无法通过深度测试,因此,就不会再进行后面的渲染处理。
- 时刻警惕透明物体 由于透明度物体处理时,会导致一些硬件优化策略失效(比如说遮挡剔除,把看不到物体的顶点剔除)。对于透明度混合技术,需要关闭深度写入,所以要保持正确的渲染顺序。透明度测试没有关闭深度测试,但由于它的实现使用了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操作,因为这会影响硬件的某些优化。