说明
【跟月影学可视化】学习笔记。
可视化渲染的性能问题有哪些?
- 渲染效率问题:指的是图形系统在绘图部分所花费的时间
- 计算问题:指绘图之外的其他处理所花费的时间,包括图形数据的计算、正常的程序逻辑处理等等。
在浏览器上渲染动画,每一秒钟最高达到 60 帧左右。1 秒钟内完成 60 次图像的绘制,那么完成一次图像绘制的时间就是 1000/60(1 秒 =1000 毫秒),约等于 16 毫秒。
60fps(即 60 帧每秒,fps 全称是 frame per second,是帧率单位)。
达到比较流畅的动画效果的最低帧率是 24fps,相当于图形系统要在大约 42 毫秒内完成一帧图像的绘制。
影响 Canvas 渲染性能的 2 大要素
影响 Canvas 渲染性能的 2 大要素:
- 绘制图形的数量
- 绘制图形的大小
测试例子:
<!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>canvas性能测试</title> <style> canvas { border: 1px dashed #fa8072; } </style> </head> <body> <canvas width="500" height="500"></canvas> <script> const canvas = document.querySelector("canvas"); const ctx = canvas.getContext("2d"); const WIDTH = canvas.width; const HEIGHT = canvas.height; const COUNT = 500; const RADIUS = 10; function randomColor() { return `hsl(${Math.random() * 360}, 100%, 50%)`; } function drawCircle(context, radius) { const x = Math.random() * WIDTH; const y = Math.random() * HEIGHT; const fillColor = randomColor(); context.fillStyle = fillColor; context.beginPath(); context.arc(x, y, radius, 0, Math.PI * 2); context.fill(); } function draw(context, count = 500, radius = 10) { for (let i = 0; i < count; i++) { drawCircle(context, radius); } } requestAnimationFrame(function update() { ctx.clearRect(0, 0, WIDTH, HEIGHT); draw(ctx, COUNT, RADIUS); requestAnimationFrame(update); }); </script> </body> </html>
在 Canvas 上每一帧绘制 500
个半径为 10
的小圆:
在 Canvas 上每一帧绘制 10000
个半径为 10
的小圆:
在 Canvas 上每一帧绘制 10000
个半径为 100
的小圆:
我们可以看到随着数量的增大,半径的增大 fps 已经降到 24 以下了(还跟个人电脑的 GPU 和显卡有关)。
影响 SVG 性能的 2 大要素
影响 SVG 渲染性能的 2 大要素:
- 绘制图形的数量
- 绘制图形的大小
测试例子:
<!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>svg性能测试</title> <style> svg { border: 1px dashed #fa8072; } </style> </head> <body> <svg xmlns="http://www.w3.org/2000/svg" width="500" height="500"></svg> <script> const root = document.querySelector("svg"); const WIDTH = 500; const HEIGHT = 500; const COUNT = 500; const RADIUS = 10; function randomColor() { return `hsl(${Math.random() * 360}, 100%, 50%)`; } function initCircles(count = COUNT) { for (let i = 0; i < count; i++) { const circle = document.createElementNS( "http://www.w3.org/2000/svg", "circle" ); root.appendChild(circle); } return [...root.querySelectorAll("circle")]; } const circles = initCircles(); function drawCircle(circle, radius = 10) { const x = Math.random() * WIDTH; const y = Math.random() * HEIGHT; const fillColor = randomColor(); circle.setAttribute("cx", x); circle.setAttribute("cy", y); circle.setAttribute("r", radius); circle.setAttribute("fill", fillColor); } function draw() { for (let i = 0; i < COUNT; i++) { drawCircle(circles[i], RADIUS); } requestAnimationFrame(draw); } draw(); </script> </body> </html>
在 SVG 上每一帧绘制 500
个半径为 10
的小圆:
在 SVG 上每一帧绘制 10000
个半径为 10
的小圆:跟 canvas 对比的 SVG 的帧率就要略差一些。
在 SVG 上每一帧绘制 10000
个半径为 100
的小圆:跟 canvas 对比二者差距很大,因为 SVG 是浏览器 DOM 来渲染的,元素个数越多,消耗就越大。
SVG 与 Canvas 不同的是,图形数量增多的时候,SVG 的帧率下降会更明显,因此,一般来说,在图形数量小于 1000 时,我们可以考虑使用 SVG,当图形数量大于 1000 但不超过 3000 时,我们考虑使用 Canvas2D,当图形数量超过 3000 时,用 Canvas2D 也很难达到比较理想的帧率了,这时候,我们就要使用 WebGL 渲染。
影响 WebGL 性能的要素
WebGL 的性能主要有三点决定因素:
- 渲染次数
- 着色器执行的次数:图形增大,片元着色器要执行的次数就会增多,就会增加 GPU 运算的开销。
- 着色器运算的复杂度
另外,元素越多,本身渲染耗费的内存也越多,占用内存太多,渲染效率也会下降。
WebGL 有支持的批量绘制的技术,叫做 InstancedDrawing(实例化渲染),在 OGL 库中,只需要给几何体数据传递带有 instanced 属性的顶点数据,就可以自动使用 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>webgl性能测试</title> <style> canvas { border: 1px dashed #fa8072; } </style> </head> <body> <canvas width="500" height="500"></canvas> <script type="module"> import { Renderer, Program, Geometry, Transform, Mesh, } from "./common/lib/ogl/index.mjs"; const canvas = document.querySelector("canvas"); const renderer = new Renderer({ canvas, antialias: true, width: 500, height: 500, }); const gl = renderer.gl; gl.clearColor(1, 1, 1, 1); // 用来生成指定数量的小球的定点数据 function circleGeometry( gl, radius = 0.002, count = 30000, segments = 20 ) { const tau = Math.PI * 2; const position = new Float32Array(segments * 2 + 2); const index = new Uint16Array(segments * 3); const id = new Uint16Array(count); for (let i = 0; i < segments; i++) { const alpha = (i / segments) * tau; position.set( [radius * Math.cos(alpha), radius * Math.sin(alpha)], i * 2 + 2 ); } for (let i = 0; i < segments; i++) { if (i === segments - 1) { index.set([0, i + 1, 1], i * 3); } else { index.set([0, i + 1, i + 2], i * 3); } } for (let i = 0; i < count; i++) { id.set([i], i); } return new Geometry(gl, { position: { data: position, size: 2, }, index: { data: index, }, id: { instanced: 1, // 通过 instanced:1 的方式告诉 WebGL 这是一个批量绘制的数据 size: 1, data: id, }, }); } const geometry = circleGeometry(gl); // 实现顶点着色器,并且在顶点着色器代码中实现随机位置和随机颜色。 const vertex = ` precision highp float; attribute vec2 position; 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); } // Function from Iñigo Quiles // https://www.shadertoy.com/view/MsS3Wc vec3 hsb2rgb(vec3 c){ vec3 rgb = clamp(abs(mod(c.x*6.0+vec3(0.0,4.0,2.0), 6.0)-3.0)-1.0, 0.0, 1.0); rgb = rgb * rgb * (3.0 - 2.0 * rgb); return c.z * mix(vec3(1.0), rgb, c.y); } varying vec3 vColor; void main() { vec2 offset = vec2( 1.0 - 2.0 * random(vec2(id + uTime, 100000.0)), 1.0 - 2.0 * random(vec2(id + uTime, 200000.0)) ); vec3 color = vec3( random(vec2(id + uTime, 300000.0)), 1.0, 1.0 ); vColor = hsb2rgb(color); gl_Position = vec4(position * 20.0 + offset, 0, 1); } `; const fragment = ` precision highp float; varying vec3 vColor; void main() { gl_FragColor = vec4(vColor, 1); } `; const program = new Program(gl, { vertex, fragment, uniforms: { uTime: { value: 0 }, }, }); const scene = new Transform(); const mesh = new Mesh(gl, { geometry, program }); mesh.setParent(scene); function update(t) { program.uniforms.uTime.value = t / 1000; renderer.render({ scene }); requestAnimationFrame(update); } update(0); </script> </body> </html>
WebGL,绘制 30000
个小球:WebGL 渲染之所以能达到这么高的性能,是因为 WebGL 利用 GPU 并行执行的特性,无论批量绘制多少个小球,都能够同时完成计算并渲染出来。