说明
【跟月影学可视化】学习笔记。
常规绘图方式的性能瓶颈
例子:在一个画布上渲染 3000 个不同颜色的、位置随机的三角形,并且让每个三角形的旋转角度也随机。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>常规绘图方式的性能瓶颈</title> <style> canvas { border: 1px dashed #fa8072; } </style> </head> <body> <canvas width="500" height="500"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const vertex = ` attribute vec2 a_vertexPosition; void main() { gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif uniform vec4 u_color; void main() { gl_FragColor = u_color; } `; const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); // 创建随机三角形的顶点 function randomTriangle( x = 0, y = 0, rotation = 0.0, radius = 0.1 ) { const a = rotation, b = a + (2 * Math.PI) / 3, c = a + (4 * Math.PI) / 3; return [ [x + radius * Math.sin(a), y + radius * Math.cos(a)], [x + radius * Math.sin(b), y + radius * Math.cos(b)], [x + radius * Math.sin(c), y + radius * Math.cos(c)], ]; } const COUNT = 3000; // 依次渲染每个三角形 function render() { for (let i = 0; i < COUNT; i++) { const x = 2 * Math.random() - 1; const y = 2 * Math.random() - 1; const rotation = 2 * Math.PI * Math.random(); renderer.uniforms.u_color = [ Math.random(), Math.random(), Math.random(), 1, ]; const positions = randomTriangle(x, y, rotation); renderer.setMeshData([ { positions, }, ]); renderer._draw(); } requestAnimationFrame(render); } render(); </script> </body> </html>
我这台电脑渲染出来只有 4.2 fps
减少 CPU 计算次数
可以创建一个正三角形,然后通过视图矩阵的变化来实现绘制多个三角形,而视图矩阵可以放在顶点着色器中计算。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>减少 CPU 计算次数</title> <style> canvas { border: 1px dashed #fa8072; } </style> </head> <body> <canvas width="500" height="500"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); // 利用顶点着色器内完成位置和角度的计算 const vertex = ` attribute vec2 a_vertexPosition; uniform mat3 modelMatrix; void main() { vec3 pos = modelMatrix * vec3(a_vertexPosition, 1); gl_Position = vec4(pos, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif uniform vec4 u_color; void main() { gl_FragColor = u_color; } `; const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); // 生成一个正三角形顶点,并设置数据到缓冲区 const alpha = 2 * Math.PI / 3; const beta = 2 * alpha; renderer.setMeshData({ positions: [ [0, 0.1], [0.1 * Math.sin(alpha), 0.1 * Math.cos(alpha)], [0.1 * Math.sin(beta), 0.1 * Math.cos(beta)], ], }); const COUNT = 3000; // 依次渲染每个三角形 function render() { for (let i = 0; i < COUNT; i++) { const x = 2 * Math.random() - 1; const y = 2 * Math.random() - 1; const rotation = 2 * Math.PI * Math.random(); // 用随机坐标和角度更新每个三角形的 modelMatrix 数据 renderer.uniforms.modelMatrix = [ Math.cos(rotation), -Math.sin(rotation), 0, Math.sin(rotation), Math.cos(rotation), 0, x, y, 1 ]; renderer.uniforms.u_color = [ Math.random(), Math.random(), Math.random(), 1, ]; renderer._draw(); } requestAnimationFrame(render); } render(); </script> </body> </html>
也是 4.2 fps
,由于浏览器的 JavaScript 引擎的运算速度很快,感觉将顶点计算放到顶点着色器中进行了,性能差别也很微小。
静态批量绘制(多实例绘制)
重复图形的批量绘制,在 WebGL 中也叫做多实例绘制(Instanced Drawing)
,它是一种减少绘制次数的技术。多实例渲染的局限性:只能在绘制相同的图形时使用。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>静态批量绘制(多实例绘制)</title> <style> canvas { border: 1px dashed #fa8072; } </style> </head> <body> <canvas width="500" height="500"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const canvas = document.querySelector('canvas'); const renderer = new GlRenderer(canvas); const vertex = ` attribute vec2 a_vertexPosition; attribute float id; uniform float uTime; highp float random(vec2 co) { highp float a = 12.9898; highp float b = 78.233; highp float c = 43758.5453; highp float dt= dot(co.xy ,vec2(a,b)); highp float sn= mod(dt,3.14); return fract(sin(sn) * c); } varying vec3 vColor; void main() { float t = id / 10000.0; float alpha = 6.28 * random(vec2(uTime, 2.0 + t)); float c = cos(alpha); float s = sin(alpha); mat3 modelMatrix = mat3( c, -s, 0, s, c, 0, 2.0 * random(vec2(uTime, t)) - 1.0, 2.0 * random(vec2(uTime, 1.0 + t)) - 1.0, 1 ); vec3 pos = modelMatrix * vec3(a_vertexPosition, 1); vColor = vec3( random(vec2(uTime, 4.0 + t)), random(vec2(uTime, 5.0 + t)), random(vec2(uTime, 6.0 + t)) ); gl_Position = vec4(pos, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif varying vec3 vColor; void main() { gl_FragColor.rgb = vColor; gl_FragColor.a = 1.0; } `; const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); const alpha = (2 * Math.PI) / 3; const beta = 2 * alpha; const COUNT = 3000; renderer.setMeshData({ positions: [ [0, 0.1], [0.1 * Math.sin(alpha), 0.1 * Math.cos(alpha)], [0.1 * Math.sin(beta), 0.1 * Math.cos(beta)], ], instanceCount: COUNT, attributes: { id: { data: [...new Array(COUNT).keys()], divisor: 1 }, }, }); function render(t) { renderer.uniforms.uTime = t / 1e6; renderer.render(); requestAnimationFrame(render); } render(0); </script> </body> </html>
效果如下:每一帧的实际渲染次数(即 WebGL 执行 drawElements 的次数)从原来的 3000 减少到了只有 1 次,而且计算都放到着色器里,利用 GPU 并行处理了,因此性能提升了 3000 倍。
动态批量绘制
如果是绘制不同的几何图形,只要它们使用同样的着色器程序,而且没有改变 uniform 变量,可以将顶点数据先合并再渲染,以减少渲染次数。
例子:将上面常规的代码改成随机的正三角形、正方形和正五边形
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>动态批量绘制</title> <style> canvas { border: 1px dashed #fa8072; } </style> </head> <body> <canvas width="500" height="500"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const vertex = ` attribute vec2 a_vertexPosition; void main() { gl_Position = vec4(a_vertexPosition, 1, 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif uniform vec4 u_color; void main() { gl_FragColor = u_color; } `; const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); // 创建随机的正三角形、正方形和正五边形 function randomShape(x = 0, y = 0, edges = 3, rotation = 0.0, radius = 0.1) { const a0 = rotation; const delta = 2 * Math.PI / edges; const positions = []; const cells = []; for(let i = 0; i < edges; i++) { const angle = a0 + i * delta; positions.push([x + radius * Math.sin(angle), y + radius * Math.cos(angle)]); if(i > 0 && i < edges - 1) { cells.push([0, i, i + 1]); } } return { positions, cells }; } const COUNT = 3000; // 依次渲染每个三角形 function render() { for (let i = 0; i < COUNT; i++) { const x = 2 * Math.random() - 1; const y = 2 * Math.random() - 1; const rotation = 2 * Math.PI * Math.random(); renderer.uniforms.u_color = [ Math.random(), Math.random(), Math.random(), 1, ]; // 随机生成三、四、五、六边形 const {positions, cells} = randomShape(x, y, 3 + Math.floor(4 * Math.random()), rotation); renderer.setMeshData([{ positions, cells, }]); renderer._draw(); } requestAnimationFrame(render); } render(); </script> </body> </html>
正四边形、正五边形、正六边形每个分别要用 2、3、4 个三角形去绘制,现在只有 3fps
了
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>动态批量绘制2</title> <style> canvas { border: 1px dashed #fa8072; } </style> </head> <body> <canvas width="500" height="500"></canvas> <script src="./common/lib/gl-renderer.js"></script> <script> const canvas = document.querySelector("canvas"); const renderer = new GlRenderer(canvas); const vertex = ` attribute vec3 a_vertexPosition; uniform float uTime; highp float random(vec2 co) { highp float a = 12.9898; highp float b = 78.233; highp float c = 43758.5453; highp float dt= dot(co.xy ,vec2(a,b)); highp float sn= mod(dt,3.14); return fract(sin(sn) * c); } varying vec3 vColor; void main() { vec2 pos = a_vertexPosition.xy; float t = a_vertexPosition.z / 10000.0; float alpha = 6.28 * random(vec2(uTime, 2.0 + t)); float c = cos(alpha); float s = sin(alpha); mat3 modelMatrix = mat3( c, -s, 0, s, c, 0, 2.0 * random(vec2(uTime, t)) - 1.0, 2.0 * random(vec2(uTime, 1.0 + t)) - 1.0, 1 ); vColor = vec3( random(vec2(uTime, 4.0 + t)), random(vec2(uTime, 5.0 + t)), random(vec2(uTime, 6.0 + t)) ); gl_Position = vec4(modelMatrix * vec3(pos, 1), 1); } `; const fragment = ` #ifdef GL_ES precision highp float; #endif varying vec3 vColor; void main() { gl_FragColor.rgb = vColor; gl_FragColor.a = 1.0; } `; const program = renderer.compileSync(fragment, vertex); renderer.useProgram(program); // 将图形的顶点和索引全部合并起来批量创建图形 function createShapes(count) { // 创建两个类型数组 positions 和 cells const positions = new Float32Array(count * 6 * 3); // 最多6边形 const cells = new Int16Array(count * 4 * 3); // 索引数等于3倍顶点数-2 let offset = 0; let cellsOffset = 0; for(let i = 0; i < count; i++) { const edges = 3 + Math.floor(4 * Math.random()); const delta = 2 * Math.PI / edges; for(let j = 0; j < edges; j++) { const angle = j * delta; positions.set([0.1 * Math.sin(angle), 0.1 * Math.cos(angle), i], (offset + j) * 3); if(j > 0 && j < edges - 1) { cells.set([offset, offset + j, offset + j + 1], cellsOffset); cellsOffset += 3; } } offset += edges; } return { positions, cells }; } // 一次性渲染出来 const COUNT = 3000; const { positions, cells } = createShapes(COUNT); renderer.setMeshData([{ positions, cells, }]); function render(t) { renderer.uniforms.uTime = t; renderer.render(); requestAnimationFrame(render); } render(0); </script> </body> </html>
采用动态批量绘制之后:
透明度与反锯齿
透明度
在 WebGL 中,我们要处理半透明图形,可以开启混合模式(Blending Mode)让透明度生效。
gl.enable(gl.BLEND);
如果不需要处理半透明图形,尽量不开启混合模式,混合颜色本身有计算量,开启混合模式会造成一定的性能开销。
反锯齿
反锯齿(英语:anti-aliasing,简称AA),也译为抗锯齿或边缘柔化、消除混叠、抗图像折叠有损等。它是一种消除显示器输出的画面中图物边缘出现凹凸锯齿的技术,那些凹凸的锯齿通常因为高分辨率的信号以低分辨率表示或无法准确运算出3D图形坐标定位时所导致的图形混叠(aliasing)而产生的,反锯齿技术能有效地解决这些问题。它通常被用在数字信号处理、数字摄影、电脑绘图与数码音效等方面,柔化被混叠的数字信号。
在获取 WebGL 上下文时,关闭反锯齿设置也能减少开销、提升渲染性能。
const gl = canvas.getContext('webgl', { antiAlias: false }); // 不消除反锯齿
Shader 的效率
为了尽可能合并数据,动态批量绘制图形,要尽量使用同一个 WebGLProgram,并且避免在绘制过程中切换 WebGLProgram。但这也会造成着色器本身的代码逻辑复杂,从而影响 Shder 的效率。
最好的解决办法就是尽可能拆分不同的着色器代码,然后在绘制过程中根据不同元素进行切换。
另外,shader 代码不同于常规的 JavaScript 代码,它最大的特性是并行计算,因此处理逻辑的过程与普通的代码不同。
比如下面 shader 代码,无论是 if 还是 else 分支,在 glsl 中都会被执行,最终的值则根据条件表达式结果不同取不同分支计算的结果。
if(Math.random() > 0.5) { ... } else { ... }
因为 GPU 是并行计算的,也就是说并行执行大量 glsl 程序,但是每个子程序并不知道其他子程序的执行结果,所以最优的办法就是事先计算好 if 和 else 分支中的结果,再根据不同子程序的条件返回对应的结果。
上面的代码可以使用 step 函数来解决问题,这样性能就会好一些。代码如下:
gl_FragColor = vec4(1) * step(random(st), 0.5);
总结
WebGL 的性能优化原则就是尽量发挥出 GPU 的优势。
核心原则有两个:
- 尽量减少 CPU 计算次数
- 减少每一帧的绘制次数