mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-10-22 01:43:44 +08:00
mod
This commit is contained in:
304
极客时间专栏/跟月影学可视化/性能篇/28 | Canvas、SVG与WebGL在性能上的优势与劣势.md
Normal file
304
极客时间专栏/跟月影学可视化/性能篇/28 | Canvas、SVG与WebGL在性能上的优势与劣势.md
Normal file
@@ -0,0 +1,304 @@
|
||||
<audio id="audio" title="28 | Canvas、SVG与WebGL在性能上的优势与劣势" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/a9/27/a9e59409a006e3d5a01d8f8cd43f8027.mp3"></audio>
|
||||
|
||||
你好,我是月影。
|
||||
|
||||
性能优化,一直以来都是前端开发的难点。
|
||||
|
||||
我们知道,前端性能是一块比较复杂的内容,由许多因素决定,比如,网页内容和资源文件的大小、请求数、域名、服务器配置、CDN等等。如果你能把性能优化好,就能极大地增强用户体验。
|
||||
|
||||
在可视化领域也一样,可视化因为要突出数据表达的内容,经常需要设计一些有视觉震撼力的图形效果,比如,复杂的粒子效果和大量元素的动态效果。想要实现这些效果,图形系统的渲染性能就必须非常好,能够在用户的浏览器上稳定流畅地渲染出想要的视觉效果。
|
||||
|
||||
那么针对可视化渲染,我们都要解决哪些性能问题呢?
|
||||
|
||||
## 可视化渲染的性能问题有哪些?
|
||||
|
||||
由于前端的可视化也是在Web上展现的,因此像网页大小这些因素也会影响它的性能。而且,无论是可视化还是普通Web前端,针对这些因素进行性能优化的原理和手段都一样。
|
||||
|
||||
所以我今天想和你聊的是,可视化方面特殊的性能问题。它们在我们熟悉的Web前端工作中并不常见,通常只在可视化中绘制复杂图形的时候,我们才需要重点考虑。这些问题大体上可以分为两类,一类是**渲染效率问题,<strong>另一类是**计算问题</strong>。
|
||||
|
||||
**我们先来看它们的定义,渲染效率问题指的是图形系统在绘图部分所花费的时间,而计算问题则是指绘图之外的其他处理所花费的时间,包括图形数据的计算、正常的程序逻辑处理等等**。
|
||||
|
||||
我们知道,在浏览器上渲染动画,每一秒钟最高达到60帧左右。也就是说,我们可以在1秒钟内完成60次图像的绘制,那么完成一次图像绘制的时间就是1000/60(1秒=1000毫秒),约等于16毫秒。
|
||||
|
||||
换句话说,如果我们能在16毫秒内完成图像的计算与渲染过程,那视觉呈现就可以达到完美的60fps(即60帧每秒,fps全称是frame per second,是帧率单位)。但是,在复杂的图形渲染时,我们的帧率很可能达不到60fps。
|
||||
|
||||
所以,我们只能退而求其次,最低可以选择24fps,就相当于图形系统要在大约42毫秒内完成一帧图像的绘制。这是在我们的感知里,达到比较流畅的动画效果的最低帧率了。要保证这个帧率,我们就必须保证计算加上渲染的时间不能超过42毫秒。
|
||||
|
||||
因为计算问题与数据和算法有关,所以我们后面会专门讨论。这里,我们先关注渲染效率的问题,这个问题和图形系统息息相关。
|
||||
|
||||
我们知道,Canvas2D、SVG和WebGL等图形系统各自的特点不同,所以它们在绘制不同图形时的性能影响也不同,会表现出不同的性能瓶颈。其实,通过基础篇的学习,我们也大体上知道了这些图形系统的区别和优劣。那今天,我们就在此基础上,深入讨论一下影响它们各自性能的关键因素,理解了这些要素,我们针对不同图形系统,就能快速找到需要进行性能优化的点了。
|
||||
|
||||
## 影响Canvas渲染性能的2大要素
|
||||
|
||||
我们知道,Canvas是指令式绘图系统,它通过绘图指令来完成图形的绘制。那么我们很容易就会想到2个影响因素,首先绘制图形的数量越多,我们需要的绘图指令就越多,花费的渲染时间也会越多。其次,画布上绘制的图形越大,绘图指令执行的时间也会增多,那么花费的渲染时间也会越多。
|
||||
|
||||
这些其实都是我们现阶段得出的假设,而实践是检验真理的唯一标准,所以我们一起做个实验,来证明我们刚才的假设吧。
|
||||
|
||||
```
|
||||
const canvas = document.querySelector('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const WIDTH = canvas.width;
|
||||
const HEIGHT = canvas.height;
|
||||
|
||||
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);
|
||||
requestAnimationFrame(update);
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
如上面代码所示,我们在Canvas上每一帧绘制500个半径为10的小圆,效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/b2/e3/b278a4f98413b9029dfa914ab4b88be3.jpg" alt="" title="500个小球,半径10">
|
||||
|
||||
注意,为了方便查看帧率的变化,我们在浏览器中开启了帧率检测。Chrome开发者工具自带这个功能,我们在开发者工具的Rendering标签页中,勾选FPS Meter就可以开启这个功能查看帧率了。
|
||||
|
||||
我们现在看到,即使每帧渲染500个位置和颜色都随机的小圆形,Canvas渲染的帧率依然能达到60fps。
|
||||
|
||||
接着,我们增加小球的数量,把它增加到1000个。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/8c/f9/8cbe753219d08a1b84753fe5f89518f9.jpg" alt="" title="1000个小球,半径10">
|
||||
|
||||
这时你可以看到,因为小球数量增加一倍,所以帧率掉到了50fps左右,现在下降得还不算太多。而如果我们把小球的数量设置成3000,你就能看到明显的差别了。
|
||||
|
||||
那如果我们把小球的数量保持在500,把半径增大到很大,如200,也会看到帧率有明显下降。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ee/81/ee69d57ce2b1214f55650f8c3f8d9681.jpg" alt="" title="500个小球,半径200">
|
||||
|
||||
但是,单从上图的实验来看,图形大小对帧率的影响也不是很大。因为我们把小球的半径增加了20倍,帧率也就下降到33fps。当然这也是因为画圆比较简单,如果我们绘制的图形更复杂一些,那么大小的影响会相对显著一些。
|
||||
|
||||
通过这个实验,我们能得出,影响Canvas的渲染性能的主要因素有两点,一是**绘制图形的数量**,二是**绘制图形的大小。**这正好验证了我们开头的结论。
|
||||
|
||||
总的来说,Canvas2D绘制图形的性能还是比较高的。在普通的个人电脑上,我们要绘制的图形不太大时,只要不超过500个都可以达到60fps,1000个左右其实也能达到50fps,就算要绘制大约3000个图形,也能够保持在可以接受的24fps以上。
|
||||
|
||||
因此,在不做特殊优化的前提下,如果我们使用Canvas2D来绘图,那么3000个左右元素是一般的应用的极限,除非这个应用运行在比个人电脑的GPU和显卡更好的机器上,或者采用特殊的优化手段。那具体怎么优化,我会在下节课详细来说。
|
||||
|
||||
## 影响SVG性能的2大要素
|
||||
|
||||
讲完了Canvas接下来我们看一下SVG。
|
||||
|
||||
我们用SVG实现同样的绘制随机圆形的例子,代码如下:
|
||||
|
||||
```
|
||||
function randomColor() {
|
||||
return `hsl(${Math.random() * 360}, 100%, 50%)`;
|
||||
}
|
||||
|
||||
const root = document.querySelector('svg');
|
||||
const COUNT = 500;
|
||||
const WIDTH = 500;
|
||||
const HEIGHT = 500;
|
||||
|
||||
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]);
|
||||
}
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
draw();
|
||||
|
||||
|
||||
```
|
||||
|
||||
在我的电脑上(一台普通的MacBook Pro,内存8GB,独立显卡)绘制了500个半径为10的小球时,SVG的帧率接近60fps,会比Canvas稍慢,但是差别不是太大。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/01/fa/01f6d0d37a0aeabd8449dyyecfc4e2fa.jpg" alt="" title="SVG绘制500个小球,半径10">
|
||||
|
||||
当我们将小球数量增加到1000个时,SVG的帧率就要略差一些,大概45fps左右。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4e/b6/4efb6930462da79f36e913546f5eb1b6.jpg" alt="" title="SVG绘制1000个小球,半径10">
|
||||
|
||||
乍一看,似乎SVG和Canvas2D的性能差别也不是很大。不过,随着小球数量的增加,两者的差别会越来越大。比如说,当我们将小球的个数增加到3000个左右的时候,Canvas2D渲染的帧率依然保持在30fps以上,而SVG渲染帧率大约只有15fps,差距会特别明显。
|
||||
|
||||
之所以在小球个数较多的时候,二者差距很大,因为SVG是浏览器DOM来渲染的,元素个数越多,消耗就越大。
|
||||
|
||||
如果我们保证小球个数在一个小数值,然后增大每个小球的半径,那么与Canvas一样,SVG的渲染效率也会明显下降。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/19/ea/19d991c1ee547d1f98fe2f504eaba1ea.jpg" alt="" title="SVG绘制500个小球,半径200">
|
||||
|
||||
如上图所示,当渲染500个小球时,我们把半径增加到200,帧率下降到不到20fps。
|
||||
|
||||
最终,我们能得到的结论与Canvas类似,影响SVG的性能因素也是相同的两点,一是**绘制图形的数量**,二是**绘制图形的大小**。但与Canvas不同的是,图形数量增多的时候,SVG的帧率下降会更明显,因此,一般来说,在图形数量小于1000时,我们可以考虑使用SVG,当图形数量大于1000但不超过3000时,我们考虑使用Canvas2D。
|
||||
|
||||
那么当图形数量超过3000时,用Canvas2D也很难达到比较理想的帧率了,这时候,我们就要使用WebGL渲染。
|
||||
|
||||
## 影响WebGL性能的要素
|
||||
|
||||
用WebGL渲染上面的例子,我们不需要一个一个小球去渲染,利用GPU的并行处理能力,我们可以一次完成渲染。
|
||||
|
||||
因为我们要渲染的小球形状相同,所以它们的顶点数据是可以共享的。在这里我们采用一种WebGL支持的批量绘制技术,叫做**InstancedDrawing(实例化渲染)**。在OGL库中,我们只需要给几何体数据传递带有instanced属性的顶点数据,就可以自动使用instanced drawing技术来批量绘制图形。具体的操作代码如下:
|
||||
|
||||
```
|
||||
function circleGeometry(gl, radius = 0.04, 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,
|
||||
size: 1,
|
||||
data: id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们实现一个circleGeometry函数,用来生成指定数量的小球的定点数据。这里我们使用批量绘制的技术,一下子绘制了30000个小球。与绘制单个小球一样,我们计算小球的position数据和index数据,然后我们设置一个id数据,这个数据等于每个小球的下标。
|
||||
|
||||
我们通过instanced:1的方式告诉WebGL这是一个批量绘制的数据,让每一个值作用于一个几何体。这样我们就能区分不同的几何体,而WebGL在绘制的时候会根据id数据的个数来绘制相应多个几何体。
|
||||
|
||||
接着,我们实现顶点着色器,并且在顶点着色器代码中实现随机位置和随机颜色。
|
||||
|
||||
```
|
||||
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);
|
||||
}
|
||||
|
||||
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 + offset, 0, 1);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
上面的代码中的random函数和hsb2rgb函数,我们都学过了,整体逻辑也并不复杂,相信你应该能看明白。
|
||||
|
||||
最后,我们将uTime作为uniform传进去,结合id和uTime,用随机数就可以渲染出与前面Canvas和SVG例子一样的效果。
|
||||
|
||||
这个WebGL渲染的例子的性能非常高,我们将小球的个数设置为30000个,依然可以轻松达到60fps的帧率。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/84/6f/84be3a259d9d7dc0572cf8044029536f.jpg" alt="" title="WebGL,绘制30000个小球,半径10">
|
||||
|
||||
WebGL渲染之所以能达到这么高的性能,是因为WebGL利用GPU并行执行的特性,无论我们批量绘制多少个小球,都能够同时完成计算并渲染出来。
|
||||
|
||||
如果我们增大小球的半径,那么帧率也会明显下降,这一点和Canvas2D与SVG一样。当我们将小球半径增加到0.8(相当于Canvas2D中的200),那么可以流畅渲染的数量就无法达到这么多,大约渲染3000个左右可以保持在30fps以上,这个效率仍比Canvas2D有着5倍以上的提升。小球半径增加导致帧率下降,是因为图形增大,片元着色器要执行的次数就会增多,就会增加GPU运算的开销。
|
||||
|
||||
好了,那我们来总结一下WebGL性能的要素。WebGL情况比较复杂,上面的例子其实不能涵盖所有的情况,不过不要紧,我这里先说一下结论,你先记下来,我们之后还会专门讨论WebGL的性能优化方法。
|
||||
|
||||
首先,WebGL和Canvas2D与SVG不同,它的性能并不直接与渲染元素的数量相关,而是取决于WebGL的渲染次数。有的时候,图形元素虽然很多,但是WebGL可以批量渲染,就像前面的例子中,虽然有上万个小球,但是通过WebGL的instanced drawing技术,可以批量完成渲染,那样它的性能就会很高。当然,元素的数量多,WebGL渲染效率也会逐渐降低,这是因为,元素越多,本身渲染耗费的内存也越多,占用内存太多,渲染效率也会下降。
|
||||
|
||||
其次,在渲染次数相同的情况下,WebGL的效率取决于着色器中的计算复杂度和执行次数。图形顶点越多,顶点着色器的执行次数越多,图形越大,片元着色器的执行次数越多,虽然是并行执行,但执行次数多依然会有更大的性能开销。最后,如果每次执行着色器中的计算越复杂,WebGL渲染的性能开销自然也会越大。
|
||||
|
||||
总的来说,WebGL的性能主要有三点决定因素,**一是渲染次数,二是着色器执行的次数,三是着色器运算的复杂度。**当然,数据的大小也会决定内存的消耗,因此也会对性能有所影响,只不过影响没有前面三点那么明显。
|
||||
|
||||
## 要点总结
|
||||
|
||||
要针对可视化的渲染效率进行性能优化,我们就要先搞清影响图形系统渲染性能的主要因素。
|
||||
|
||||
对于Canvas和SVG来说,影响渲染性能的主要是绘制元素的数量和元素的大小。一般来说,Canvas和SVG绘制的元素越多,性能消耗越大,绘制的图形越大,性能消耗也越大。相比较而言,Canvas的整体性能要优于SVG,尤其是图形越多,二者的性能差异越大。
|
||||
|
||||
WebGL要复杂一些,它的渲染性能主要取决于三点。
|
||||
|
||||
第一点是渲染次数,渲染次数越多,性能损耗就越大。需注意,要绘制的元素个数多,不一定渲染次数就多,因为WebGL支持批量渲染。
|
||||
|
||||
第二点是着色器执行的次数,这里包括顶点着色器和片元着色器,前者的执行次数和几何图形的顶点数有关,后者的执行次数和图形的大小有关。
|
||||
|
||||
第三点是着色器运算的复杂度,复杂度和glsl代码的具体实现有关,越复杂的处理逻辑,性能的消耗就会越大。
|
||||
|
||||
最后,数据的大小会影响内存消耗,所以也会对WebGL的渲染性能有所影响,不过没有前面三点的影响大。
|
||||
|
||||
## 小试牛刀
|
||||
|
||||
<li>
|
||||
刚才我们用SVG、Canvas和WebGL分别实现了随机小球,由此比较了三种图形系统的性能。但是我们并没说HTML/CSS,你能用HTML/CSS来实现这个例子吗?用HTML/CSS来实现,在性能方面与SVG、Canvas和WebGL有什么区别呢?从中,你能得出影响HTML/CSS渲染性能的要素吗?
|
||||
</li>
|
||||
<li>
|
||||
在WebGL的例子中,我们采用了批量绘制的技术。实际上我们也可以不采用这个技术,给每个小球生成一个mesh对象,然后让Ogl来渲染。你可以试着用Ogl不采用批量渲染来实现随机小球,然后对比它们之间的渲染方案,得出性能方面的差异吗?
|
||||
</li>
|
||||
|
||||
## 源码
|
||||
|
||||
[课程中详细示例代码](https://github.com/akira-cn/graphics/tree/master/performance-basic)
|
415
极客时间专栏/跟月影学可视化/性能篇/29 | 怎么给Canvas绘制加速?.md
Normal file
415
极客时间专栏/跟月影学可视化/性能篇/29 | 怎么给Canvas绘制加速?.md
Normal file
@@ -0,0 +1,415 @@
|
||||
<audio id="audio" title="29 | 怎么给Canvas绘制加速?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/ed/33/ed06f10b056dca8af21f2f7dd8494433.mp3"></audio>
|
||||
|
||||
你好,我是月影。
|
||||
|
||||
上节课,我们从宏观上了解了各个图形系统在性能方面的优劣,以及影响性能的要素。实际上,想要解决性能问题,我们就必须要知道真正消耗性能的点,从而结合项目需求进行有针对的处理,否则性能优化就是纸上谈兵、空中楼阁。
|
||||
|
||||
所以这节课,我们就深入讨论一下影响Canvas绘图性能的因素,一起来分析几个不同类型的Canvas项目,找到的性能瓶颈以及对应的解决办法,从而学会对大部分Canvas项目进行性能优化。
|
||||
|
||||
我们知道,Canvas是指令式绘图系统,它有状态设置指令、绘图指令以及真正的绘图方法(fill和stroke)等各类API。通常情况下利用Canvas绘图,我们要先调用状态设置指令设置绘图状态,然后用绘图指令决定要绘制的图形,最后调用真正的fill()或stroke()方法将内容输出到画布上。
|
||||
|
||||
那结合上节课的实验我们知道,影响Canvas性能的两大因素分别是图形的数量和图形的大小。它们都会直接影响绘图指令,一个决定了绘图指令的多少,另一个决定了绘图指令的执行时间。通常来说,绘图指令越多、执行时间越长,渲染效率就越低,性能也就越差。
|
||||
|
||||
因此,我们想要对Canvas性能进行优化,最重要的就是优化渲染效率。常用的手段有5种,分别是优化Canvas指令、使用缓存、分层渲染、局部重绘和优化滤镜。此外,还有一种手段叫做**多线程渲染**,是用来优化非渲染的计算和交互方面导致的性能问题。
|
||||
|
||||
首先,我们来说说优化Canvas指令。
|
||||
|
||||
## 手段一:优化Canvas指令
|
||||
|
||||
刚刚我们说了,Canvas执行的绘图指令越多,性能的消耗就越大。那如果希望Canvas绘图达到更好的性能,我们要尽可能减少绘图指令的数量。这就是“优化Canvas指令”要做的事情。
|
||||
|
||||
那具体怎么做呢?我们看一个例子。
|
||||
|
||||
假设,我们要在一个600 X 600的画布上,实现一些位置随机的多边形,并且不断刷新这些图形的形状和位置,效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4e/68/4e226d7385aa48c23049b451a4525468.gif" alt="">
|
||||
|
||||
结合我们之前学过的知识,这个效果其实并不难实现,可以分为4步,分别是创建多边形的顶点,根据顶点绘制图形,生成随机多边形,执行绘制。
|
||||
|
||||
具体的实现代码如下:
|
||||
|
||||
```
|
||||
const canvas = document.querySelector('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// 创建正多边形,返回顶点
|
||||
function regularShape(x, y, r, edges = 3) {
|
||||
const points = [];
|
||||
const delta = 2 * Math.PI / edges;
|
||||
for(let i = 0; i < edges; i++) {
|
||||
const theta = i * delta;
|
||||
points.push([x + r * Math.sin(theta), y + r * Math.cos(theta)]);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// 根据顶点绘制图形
|
||||
function drawShape(context, points) {
|
||||
context.fillStyle = 'red';
|
||||
context.strokeStyle = 'black';
|
||||
context.lineWidth = 2;
|
||||
context.beginPath();
|
||||
context.moveTo(...points[0]);
|
||||
for(let i = 1; i < points.length; i++) {
|
||||
context.lineTo(...points[i]);
|
||||
}
|
||||
context.closePath();
|
||||
context.stroke();
|
||||
context.fill();
|
||||
}
|
||||
|
||||
// 多边形类型,包括正三角形、正四边形、正五边形、正六边形和正100边形
|
||||
const shapeTypes = [3, 4, 5, 6, 100];
|
||||
const COUNT = 1000;
|
||||
|
||||
// 执行绘制
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for(let i = 0; i < COUNT; i++) {
|
||||
const type = shapeTypes[Math.floor(Math.random() * shapeTypes.length)];
|
||||
const points = regularShape(Math.random() * canvas.width,
|
||||
Math.random() * canvas.height, 10, type);
|
||||
drawShape(ctx, points);
|
||||
}
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
draw();
|
||||
|
||||
|
||||
```
|
||||
|
||||
这个效果实现起来虽然不难,但性能却不是很好,因为它在我的Macbook Pro电脑上只有不到30fps的帧率。那问题出在哪呢?我们还是要回到代码中。
|
||||
|
||||
我们注意到drawShape函数里的for循环,它是根据顶点来绘制图形的,一个点对应一条绘图指令。而在我们绘制的随机图形里,有3、4、5、6边形和100边形。对于一个100边形来说,它的顶点数量非常多,所以Canvas需要执行的绘图指令也会非常多,那绘制很多个100边形自然会造成性能问题了。因此,如何减少绘制100边形的绘图指令的数量,才是我们要优化的重点。具体该怎么做呢?
|
||||
|
||||
我们知道,对于半径为10的小图形来说,正100边形已经完全是正圆形了,所以我们可以用arc指令来替代for循环。
|
||||
|
||||
我们修改shapeTypes和draw函数,用-1代替正100边形,然后判断type是否大于0,如果是就用之前的方式绘制正多边形,否则用arc指令来画圆。这么做了之后,整个效果的帧率就会从30fps提升到40fps,效果还是比较明显的。
|
||||
|
||||
```
|
||||
const shapeTypes = [3, 4, 5, 6, -1];
|
||||
const COUNT = 1000;
|
||||
const TAU = Math.PI * 2;
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for(let i = 0; i < COUNT; i++) {
|
||||
const type = shapeTypes[Math.floor(Math.random() * shapeTypes.length)];
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
if(type > 0) {
|
||||
// 画正多边形
|
||||
const points = regularShape(x, y, 10, type);
|
||||
drawShape(ctx, points);
|
||||
} else {
|
||||
// 画圆
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, 10, 0, TAU);
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
到这里,你会发现,我们讲的其实是个特例,那在实际工作中,我们是需要针对特例来优化的。我希望我讲完今天的内容你能够做到举一反三。
|
||||
|
||||
## 手段二:使用缓存
|
||||
|
||||
在上面的方法中,我们优化了绘图指令,让渲染性能有了比较明显的提升。不过,因为这个绘图任务的图形数量和状态都是有限的,我们还有更好的优化方法,那就是**使用缓存**。
|
||||
|
||||
因为Canvas的性能瓶颈主要在绘图指令方面,如果我们能将图形缓存下来,保存到离屏的Canvas(offscreen Canvas)中,然后在绘制的时候作为图像来渲染,那我们就可以将绘制顶点的绘图指令变成直接通过drawImage指令来绘制图像,而且也不需要fill()方法来填充图形,这样性能就会有大幅度的提升。
|
||||
|
||||
具体的做法,是我们先实现一个创建缓存的函数。代码如下:
|
||||
|
||||
```
|
||||
function createCache() {
|
||||
const ret = [];
|
||||
for(let i = 0; i < shapeTypes.length; i++) {
|
||||
// 创建离屏Canvas缓存图形
|
||||
const cacheCanvas = new OffscreenCanvas(20, 20);
|
||||
// 将图形绘制到离屏Canvas对象上
|
||||
const type = shapeTypes[i];
|
||||
const context = cacheCanvas.getContext('2d');
|
||||
context.fillStyle = 'red';
|
||||
context.strokeStyle = 'black';
|
||||
if(type > 0) {
|
||||
const points = regularShape(10, 10, 10, type);
|
||||
drawShape(context, points);
|
||||
} else {
|
||||
context.beginPath();
|
||||
context.arc(10, 10, 10, 0, TAU);
|
||||
context.stroke();
|
||||
context.fill();
|
||||
}
|
||||
ret.push(cacheCanvas);
|
||||
}
|
||||
// 将离屏Canvas数组(缓存对象)返回
|
||||
return ret;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
然后,我们一次性创建缓存,直接通过缓存来绘图。
|
||||
|
||||
```
|
||||
const shapes = createCache();
|
||||
const COUNT = 1000;
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for(let i = 0; i < COUNT; i++) {
|
||||
const shape = shapes[Math.floor(Math.random() * shapeTypes.length)];
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
ctx.drawImage(shape, x, y);
|
||||
}
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这样,我们就通过缓存渲染,把原本数量非常多的绘图指令优化成了只有drawImage的一条指令,让渲染帧率达到了60fps,从而大大提升了性能。
|
||||
|
||||
### 缓存的局限性
|
||||
|
||||
不过,虽然使用缓存能够显著降低Canvas的性能消耗,但是缓存的使用也有局限性。
|
||||
|
||||
首先,因为缓存是通过创建离屏Canvas对象实现的,如果我们要绘制的图形状态(指不同形状、颜色等)非常多的话,那将它们都缓存起来,就需要创建大量的离屏Canvas对象。这本身对内存消耗就非常大,有可能反而降低了性能。
|
||||
|
||||
其次,缓存适用于图形状态本身不变的图形元素,如固定的几何图形,它们每次刷新只需要更新它的transform,这样的图形比较适合用缓存。如果是经常发生状态改变的图形元素,那么缓存就必须一直更新,缓存更新本身也是绘图过程。因此,这种情况下,采用缓存根本起不到减少绘图指令的作用,反而因为增加了一条drawImage指令产生了更大的开销。
|
||||
|
||||
第三,严格上来说,从缓存绘制和直接用绘图指令绘制还是有区别的,尤其是在fillText渲染文字或者我们绘制一个图形有较大缩放(scale)的时候。因为不使用缓存直接绘制的是矢量图,而通过缓存drawImage绘制出的则是位图,所以缓存绘制的图形,在清晰度上可能不是很好。
|
||||
|
||||
但是总体来说,缓存的应用还是非常多的,我们应该要掌握它的用法,学会在合适的时候运用缓存来提升Canvas的渲染性能。
|
||||
|
||||
## 手段三:分层渲染
|
||||
|
||||
前面两种手段是操作Canvas上所有元素来优化性能的,但有的时候,我们要绘制的元素很多,其中大部分元素状态是不变的,只有一小部分有变化。这个时候,我们又该如何进行优化呢?
|
||||
|
||||
我们知道,Canvas是将上一次绘制的内容擦除,然后绘制新的内容来实现状态变化的。利用这一特点,我们就可以将变化的元素和不变的元素进行分层处理。也就是说,我们可以用两个Canvas叠在一起,将不变的元素绘制在一个Canvas中,变化的元素绘制在另一个Canvas中。
|
||||
|
||||
我们还是来看一个例子。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/4f/e9/4f0c60c3810d372d41ee14d90efb23e9.gif" alt="">
|
||||
|
||||
假设,我们要实现一个如上图的效果。这个效果的特点是,画面上有一个飞机在运动,运动的物体比较少,而其他静止不动的图形很多(如背景中的上千个三角形)。
|
||||
|
||||
在绘制的时候,我们如果将运动的物体和其他物体都绘制在同一个Canvas画布中,要改变飞机的运动状态,我们就要重新绘制所有的物体,这会非常浪费性能。因此,更好的做法是我们使用两层画布,一层Canvas作为背景,来绘制静态的图形,就是这个例子里的上千个小三角形,而另一层Canvas作为前景,用来绘制运动的物体就是运动的飞机。
|
||||
|
||||
这样的话,我们只需要一次绘制就能得到背景层Canvas,并且不管飞机的状态怎么改变,我们都不需要重绘,而前景的飞机可以每一帧重绘,也就大大减少了图形绘制的数量,并且提升了性能。
|
||||
|
||||
下面我列出具体的代码,虽然很长但逻辑并不复杂,核心就是用两个Canvas元素来分别绘制,你可以看一下。
|
||||
|
||||
```
|
||||
function drawRandomTriangle(path, context) {
|
||||
const {width, height} = context.canvas;
|
||||
context.save();
|
||||
context.translate(Math.random() * width, Math.random() * height);
|
||||
context.fill(path);
|
||||
context.restore();
|
||||
}
|
||||
|
||||
function drawBackground(context, count = 2000) {
|
||||
context.fillStyle = '#ed7';
|
||||
const d = 'M0,0L0,10L8.66, 5z';
|
||||
const p = new Path2D(d);
|
||||
for(let i = 0; i < count; i++) {
|
||||
drawRandomTriangle(p, context);
|
||||
}
|
||||
}
|
||||
|
||||
function loadImage(src) {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
return new Promise((resolve) => {
|
||||
img.onload = resolve(img);
|
||||
img.src = src;
|
||||
});
|
||||
}
|
||||
|
||||
async function drawForeground(context) {
|
||||
const img = await loadImage('http://p3.qhimg.com/t015b85b72445154fe0.png');
|
||||
const {width, height} = context.canvas;
|
||||
function update(t) {
|
||||
context.clearRect(0, 0, width, height);
|
||||
context.save();
|
||||
context.translate(0, 0.5 * height);
|
||||
const p = (t % 3000) / 3000;
|
||||
const x = width * p;
|
||||
const y = 0.1 * height * Math.sin(3 * Math.PI * p);
|
||||
context.drawImage(img, x, y);
|
||||
context.restore();
|
||||
requestAnimationFrame(update);
|
||||
}
|
||||
update(0);
|
||||
}
|
||||
|
||||
const bgcanvas = document.querySelector('#bg');
|
||||
const fgcanvas = document.querySelector('#fg');
|
||||
drawBackground(bgcanvas.getContext('2d'));
|
||||
drawForeground(fgcanvas.getContext('2d'));
|
||||
|
||||
```
|
||||
|
||||
## 手段四:局部重绘
|
||||
|
||||
但是,我们用分层渲染解决性能问题的时候,所绘制的图形必须满足两个条件:一是有大量静态的图形元素不需要重新绘制,二是动态和静态图形元素绘制顺序是固定的,先绘制完静态元素再绘制动态元素。如果元素都有可能运动,或者动态元素和静态元素的绘制顺序是交错的,比如先绘制几个静态元素,再绘制几个动态元素,然后再绘制静态元素,这样交替进行,那么分层渲染就不好实现了。这时候,我们还有另外一种优化手段,它叫做局部重绘。
|
||||
|
||||
**局部重绘顾名思义,就是不需要清空Canvas的全局区域,而是根据运动的元素的范围来清空部分区域。**在很大一部分可视化大屏项目中,我们不会让整个屏幕的所有元素都不断改变,而是只有一些固定的区域改变,所以我们直接刷新那部分区域,重绘区域中的元素就可以了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/ff/16/ff209e67ac077c1c43782e0291500e16.jpeg" alt="" title="大屏的动态区与静态区">
|
||||
|
||||
如上图所示,一个可视化大屏只有2块动态区域需要不断重绘,那我们用Canvas上下文的**clearRect方法控制要刷新的动态区域**,只对这些区域进行擦除然后重绘。
|
||||
|
||||
要注意的是,动态区重绘的时候,区域内的静态元素也需要跟着重绘。如果有静态元素跨越了动态和静态区域范围,那在重绘时,我们自然不希望破坏了静态区的图形。这时候,我们可以使用Canvas上下文的clip方法,它是一种特殊的绘图指令,可以设定一个绘图区,让图形的绘制限制在这个绘图区内部。这样的话,图形中超过clip范围的部分,浏览器就不会把它渲染到Canvas上。
|
||||
|
||||
这种固定区域的局部重绘使用起来不难,但有时候我们不知道具体的动态区域究竟多大。这个时候,我们可以使用动态计算要重绘区域的技术,它也被称为**脏区检测**。它的基本原理是根据动态元素的**包围盒**,动态算出需要重绘的范围。
|
||||
|
||||
那什么是包围盒呢?
|
||||
|
||||
我们知道,多边形由顶点构成,包围盒就是指能包含多边形所有顶点,并且与坐标轴平行的最小矩形。
|
||||
|
||||
[<img src="https://static001.geekbang.org/resource/image/f2/55/f26c205843dc9479c9ce9ebd7d762855.jpeg" alt="" title="多边形包围盒">](https://www.pngsucai.com/png/950314.html)
|
||||
|
||||
在Canvas平面直角坐标系下,求包围盒并不复杂,只要分别找到所有顶点坐标中x的最大、最小值xmin和xmax,以及y的最大、最小值ymin和ymax,那么包围盒就是矩形[(xmin, ymin), (xmin, ymax), (xmax, ymax), (xmax, ymin)]。
|
||||
|
||||
对所有的动态元素计算出包围盒,我们就能知道局部刷新的范围了。不过在实际操作的时候,我们经常会遇到各种复杂的细节问题需要解决。因为涉及的细节比较多,我没法全都讲到,所以,如果你遇到了问题,可以看看蚂蚁金服AntV团队的[Canvas局部渲染优化总结](https://juejin.im/post/5e7c13b351882535ea43e1a2)这篇文章。
|
||||
|
||||
## 手段五:优化滤镜
|
||||
|
||||
实际上,分层渲染和局部重绘解决的都是图形重绘的问题。那除了重绘,影响渲染效率的还有Canvas滤镜。
|
||||
|
||||
我们知道,滤镜是一种对图形像素进行处理的方法,Canvas支持许多常用的滤镜。不过Canvas渲染滤镜的性能开销比较大。到底有多大呢?我们还是用前面绘制随机图形的例子来体验一下。
|
||||
|
||||
这次我们用缓存优化版本的代码,这一版代码的性能最高。在绘制前,我们给Canvas设置一个blur滤镜。代码如下:
|
||||
|
||||
```
|
||||
ctx.filter = 'blur(5px)';
|
||||
|
||||
```
|
||||
|
||||
这样呢,我们让Canvas绘制出来的图形有了模糊的效果。但是这么设置了之后,你会发现原本60fps的帧率直接掉到2fps,画面看上去一顿一顿的,卡得惨不忍睹。这就是因为滤镜对渲染性能的开销实在太大了。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/1f/7b/1f236cbdb1eb16a3faf9f7e6c18fa27b.gif" alt="">
|
||||
|
||||
那这种情况下,其实我们也有优化手段。针对这个场,我们实际上是对Canvas应用一个全局的blur滤镜,把绘制的所有元素都变得模糊,所以,我们完全没必要对每个元素应用滤镜,而是可以采用类似后期处理通道的做法,先将图形以不使用滤镜的方式绘制到一个离屏的Canvas上,然后直接将这个离屏Canvas以图片方式绘制到要显示的画布上,在这次绘制的时候采用滤镜。这样,我们就把大量滤镜绘制的过程缩减为对一张图片使用一次滤镜了。大大减少了处理滤镜的次数之后,效果立竿见影,帧率立即回到了60fps。
|
||||
|
||||
那么具体实现的代码和效果我也列出来,你可以看一下。
|
||||
|
||||
```
|
||||
ctx.filter = 'blur(5px)';
|
||||
|
||||
// 创建离屏的 Canvas
|
||||
const ofc = new OffscreenCanvas(canvas.width, canvas.height);
|
||||
const octx = ofc.getContext('2d');
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
octx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
// 将图形不应用滤镜,绘制到离屏Canvas上
|
||||
for(let i = 0; i < COUNT; i++) {
|
||||
const shape = shapes[Math.floor(Math.random() * shapeTypes.length)];
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
octx.drawImage(shape, x, y);
|
||||
}
|
||||
// 再将离屏Canvas图像绘制到画布上,这一次绘制采用了滤镜
|
||||
ctx.drawImage(ofc, 0, 0);
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
|
||||
draw();
|
||||
|
||||
```
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/63/5b/63dcf4f363561cb745yy7075b418985b.gif" alt="">
|
||||
|
||||
当然这种优化滤镜的方式,只有当我们要对画布上绘制的所有图形,都采用同一种滤镜的时候才有效。不过,如果有部分图形采用相同的滤镜,而且它们是连续绘制的,我们也可以采用类似的办法,把这部分图形绘制到离屏Canvas上,之后再将图像应用滤镜并绘制回画布。这样也能够减少滤镜的处理次数,明显提升性能。总之,想要达到比较好的性能,我们要记住一个原则,尽量合并图形应用相同滤镜的过程。
|
||||
|
||||
## 手段六:多线程渲染
|
||||
|
||||
到这里,我们说完了几种提升渲染性能的常见手段。不过,影响用户体验的不仅仅是渲染性能,有时候,我们还要对绘制的内容进行交互,而如果渲染过程消耗了大量的时间,它也可能会阻塞其他的操作,比如对事件的响应。
|
||||
|
||||
遇到这种问题的时候,以前我们会比较头疼,甚至不得不降低渲染性能,以减少CPU资源占用,从而让交互行为不被阻塞。不过现在,浏览器支持的Canvas可以在WebWorker中以单独的线程来渲染,这样就可以避免对主线程的阻塞,也不会影响用户交互行为了。
|
||||
|
||||
那么具体怎么才能在Worker中绘制呢?其实也很简单。我们在浏览器主线程中创建Worker,然后将Canvas对象通过 transferControlToOffscreen 转成离屏Canvas对象发送给Worker线程去处理。
|
||||
|
||||
```
|
||||
const canvas = document.querySelector('canvas');
|
||||
|
||||
const worker = new Worker('./random_shapes_worker.js');
|
||||
const ofc = canvas.transferControlToOffscreen();
|
||||
worker.postMessage({
|
||||
canvas: ofc,
|
||||
type: 'init',
|
||||
}, [ofc]);
|
||||
|
||||
```
|
||||
|
||||
这样,从使用上来说,无论在Worker线程中还是在主线程中操作都没有太大的区别,还能不阻塞浏览器主线程的任何操作。
|
||||
|
||||
我这里列出一部分核心代码,完整的代码我放在GitHub仓库里,你可以试着运行一下,看看效果。
|
||||
|
||||
```
|
||||
function draw(ctx, shapes) {
|
||||
const canvas = ctx.canvas;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
for(let i = 0; i < COUNT; i++) {
|
||||
const shape = shapes[Math.floor(Math.random() * shapeTypes.length)];
|
||||
const x = Math.random() * canvas.width;
|
||||
const y = Math.random() * canvas.height;
|
||||
ctx.drawImage(shape, x, y);
|
||||
}
|
||||
requestAnimationFrame(draw.bind(null, ctx, shapes));
|
||||
}
|
||||
|
||||
self.addEventListener('message', (evt) => {
|
||||
if(evt.data.type === 'init') {
|
||||
const canvas = evt.data.canvas;
|
||||
if(canvas) {
|
||||
const ctx = canvas.getContext('2d');
|
||||
const shapes = createCache();
|
||||
draw(ctx, shapes);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
## 要点总结
|
||||
|
||||
这节课我们讲了Canvas性能优化的6种手段,其中前5种是针对渲染效率进行优化,分别是优化Canvas指令、使用缓存、分层渲染、局部重绘,以及针对滤镜的优化。最后一种是通过多线程来优化计算的性能,让计算过程能够并行执行不会阻塞浏览器的UI。下面,我再带你一起梳理一下性能优化的原则。
|
||||
|
||||
首先,我们在绘制图形时,用越简单的绘图指令来绘制,渲染的效率就越高。所以,我们要想办法减少Canvas绘图指令的数量,比如,用arc指令画圆来代替绘制边数很多的正多边形。
|
||||
|
||||
然后,当我们大批量绘制有限的几种形状的图形时,可以采用缓存将图形一次绘制后保存在离屏的Canvas中,下一次绘制的时候,我们直接绘制缓存的图片来取代原始的绘图指令,也能大大提升性能。
|
||||
|
||||
可如果我们绘制的元素中只有一部分元素发生改变,我们就可以采用分层渲染,将变化的元素绘制在一个图层,剩下的元素绘制在另一个图层。这样每次只需要重新绘制变化元素所在的图层,大大减少绘制的图形数,从而显著提升了性能。
|
||||
|
||||
还有一种情况是,如果Canvas只有部分区域发生变化,那我们只需要刷新局部区域,不需要刷新整个Canvas,这样能显著降低消耗、提升性能。
|
||||
|
||||
还要注意的是,一些Canvas滤镜渲染起来非常耗费性能,所以我们可以对滤镜进行合并,让多个元素只应用一次滤镜,从而减少滤镜对性能的消耗。
|
||||
|
||||
最后,除了优化渲染性能外,我们还可以通过WebWork以多线程的手段优化计算性能,以达到渲染不阻塞UI操作的目的。
|
||||
|
||||
## 小试牛刀
|
||||
|
||||
学会了使用多种优化手段之后,我们来尝试实现一个粒子效果吧!
|
||||
|
||||
具体效果是,我们要让小三角形以不同的角度和速度,由画布中心点向四周运动,同时小三角形自身也以随机的角速度旋转。
|
||||
|
||||
你可以尝试用两种方式来实现这个效果,分别是使用性能优化和不使用性能优化。在这两种情况下,你的电脑最多能支持同时绘制多少个小三角形?
|
||||
|
||||
我们今天学的这6种性能优化手段,对你的工作是不是很有帮助?那不妨就把这节课分享出去吧!我们下节课再见!
|
||||
|
||||
## 源码
|
||||
|
||||
[课程中详细示例代码GitHub仓库](https://github.com/akira-cn/graphics/tree/master/performance_canvas)
|
||||
|
||||
## 推荐阅读
|
||||
|
||||
[1] [AntV Canvas 局部渲染总结](https://juejin.im/post/5e7c13b351882535ea43e1a2)<br>
|
||||
[2] [Speed up Your Canvas Operations with a Web Worker,WebWorker和OffscreenCanvas使用参考文档](https://developers.google.com/web/updates/2018/08/offscreen-canvas)
|
488
极客时间专栏/跟月影学可视化/性能篇/30|怎么给WebGL绘制加速?.md
Normal file
488
极客时间专栏/跟月影学可视化/性能篇/30|怎么给WebGL绘制加速?.md
Normal file
@@ -0,0 +1,488 @@
|
||||
<audio id="audio" title="30|怎么给WebGL绘制加速?" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d5/12/d51b62b019a47160fyya1e4f35039312.mp3"></audio>
|
||||
|
||||
你好,我是月影。这节课,我们一起来讨论WebGL的性能优化。
|
||||
|
||||
WebGL因为能够直接操作GPU,性能在各个图形系统中是最好的,尤其是当渲染的元素特别多的时候,WebGL的性能优势越明显。但是,WebGL整体性能好,并不意味着我们用WebGL就能写出高性能代码来。如果使用方式不当,也不能充分发挥WebGL在性能方面的优势。
|
||||
|
||||
这节课,我们就重点来说说,怎么充分发挥WebGL的优势,让它保持高性能。
|
||||
|
||||
## 尽量发挥GPU的优势
|
||||
|
||||
首先,我们来想一个问题,WebGL的优势是什么?没错,我强调过很多遍,就是直接操作GPU。因此,我们只有尽量发挥出GPU的优势,才能让WebGL保持高性能。但这一点是很多习惯用SVG、Canvas的WebGL初学者,最容易忽视的。
|
||||
|
||||
为了让你体会到发挥GPU优势的重要性,我们先来看一个没有进行任何优化的绘图例子,再对它进行优化。
|
||||
|
||||
### 常规绘图方式的性能瓶颈
|
||||
|
||||
假设,我们要在一个画布上渲染3000个不同颜色的、位置随机的三角形,并且让每个三角形的旋转角度也随机。
|
||||
|
||||
常规的实现方法当然是用JavaScript来创建随机三角形的顶点,然后依次渲染。我在创建随机三角形顶点的时候,是使用向量角度旋转的方法创建了正三角形,我想这个方法你应该也不会陌生。
|
||||
|
||||
```
|
||||
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)],
|
||||
];
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
然后,我在下面代码的for循环中依次渲染每个三角形。
|
||||
|
||||
```
|
||||
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();
|
||||
|
||||
```
|
||||
|
||||
这里,我们只给着色器传入了一个颜色参数,其他的运算都是在JavaScript中完成的,所以对应的着色器代码非常简单。代码如下:
|
||||
|
||||
```
|
||||
// 顶点着色器
|
||||
attribute vec2 a_vertexPosition;
|
||||
|
||||
void main() {
|
||||
gl_Position = vec4(a_vertexPosition, 1, 1);
|
||||
}
|
||||
|
||||
// 片元着色器
|
||||
#ifdef GL_ES
|
||||
precision highp float;
|
||||
#endif
|
||||
|
||||
uniform vec4 u_color;
|
||||
|
||||
void main() {
|
||||
gl_FragColor = u_color;
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这样,我们就完成了渲染3000个随机三角形的功能,效果如下:
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/3a/d3/3a67da60549daebb2e460d2dc267efd3.gif" alt="">
|
||||
|
||||
你会发现,这样实现的图形性能很一般,因为3000个三角形渲染在普通笔记本电脑上只有20fps,这大概和Canvas2D渲染出来的性能差不多,可以说完全没能发挥出WebGL应有的优势。
|
||||
|
||||
那我们应该对哪些点进行优化,从而**尽量发挥出GPU的优势呢**?
|
||||
|
||||
### 减少CPU计算次数
|
||||
|
||||
首先,我们可以不用生成这么多个三角形。根据前面学过的知识,我们可以创建一个正三角形,然后通过视图矩阵的变化来实现绘制多个三角形,而视图矩阵可以放在顶点着色器中计算。这样,我们就只要在渲染每个三角形的时候更新视图矩阵就行了。
|
||||
|
||||
具体来说就是,我们直接生成一个正三角形顶点,并设置数据到缓冲区。
|
||||
|
||||
```
|
||||
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)],
|
||||
],
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
然后,我们用随机坐标和角度更新每个三角形的modelMatrix数据。
|
||||
|
||||
```
|
||||
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.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();
|
||||
|
||||
```
|
||||
|
||||
而位置和角度的计算,我们放到顶点着色器内完成,代码如下:
|
||||
|
||||
```
|
||||
attribute vec2 a_vertexPosition;
|
||||
|
||||
uniform mat3 modelMatrix;
|
||||
|
||||
void main() {
|
||||
vec3 pos = modelMatrix * vec3(a_vertexPosition, 1);
|
||||
gl_Position = vec4(pos, 1);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
这么做了之后,三角形渲染的fps会略有提升,因为我们通过在顶点着色器中并行矩阵运算减少了顶点计算的次数。不过,这个性能提升在最新的chrome浏览器下可能并不明显,因为现在浏览器的JavaScript引擎的运算速度很快,尽管将顶点计算放到顶点着色器中进行了,性能差别也很微小。但不管怎么样,这种方法依然是可以提升性能的。
|
||||
|
||||
### 静态批量绘制(多实例绘制)
|
||||
|
||||
那有没有办法更大程度地提升性能呢?当然是有的。实际上,对于需要重复绘制的图形,最好的办法是使用批量绘制。重复图形的批量绘制,在WebGL中也叫做**多实例绘制**(Instanced Drawing),它是一种减少绘制次数的技术。
|
||||
|
||||
在WebGL中,一个几何图形一般需要一次渲染,如果我们要绘制多个图形的话,因为每个图形的顶点、颜色、位置等属性都不一样,所以我们只能一一渲染,不能一起渲染。但是,如果几何图形的顶点数据都相同,颜色、位置等属性就都可以在着色器计算,那么我们就可以使用WebGL支持的多实例绘制方式,一次性地把所有的图形都渲染出来。
|
||||
|
||||
多实例绘制的代码,其实我们在第28课里已经见过了。这里,我们再看一个例子,帮你加深印象。
|
||||
|
||||
首先,我们也是创建三角形顶点数据,然后使用多实例绘制的方式传入数据。因为gl-renderer中已经封装好了多实例绘制的方法,我们只需要传入instanceCount表示要绘制的图形数量即可。在原生的WebGL中使用多实例绘制会稍微复杂一点,我们一般不会这么做,但如果你想要尝试一下,可以参考[这篇文章](https://www.jianshu.com/p/d40a8b38adfe)。
|
||||
|
||||
使用多实例绘制的代码如下:
|
||||
|
||||
```
|
||||
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},
|
||||
},
|
||||
});
|
||||
|
||||
```
|
||||
|
||||
这样,我们就只需要每帧渲染一次就可以了。为了能在顶点着色器中完成图形的位置和颜色计算,我们传入了时间uTime参数。代码如下:
|
||||
|
||||
```
|
||||
function render(t) {
|
||||
renderer.uniforms.uTime = t;
|
||||
renderer.render();
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
render(0);
|
||||
|
||||
```
|
||||
|
||||
对应的顶点着色器如下:
|
||||
|
||||
```
|
||||
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);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们这么做了之后,每一帧的实际渲染次数(即WebGL执行drawElements的次数)从原来的3000减少到了只有1次,而且计算都放到着色器里,利用GPU并行处理了,因此性能提升了3000倍。而且,现在不要说3000,哪怕是6000个三角形,帧率都可以轻松达到60fps了,是不是很厉害?
|
||||
|
||||
### 动态批量绘制
|
||||
|
||||
可是,我又要给你泼一盆冷水了。虽然在绘制大量图形的时候,使用多实例绘制是一种非常好的方式,但是多实例渲染也有局限性,那就是只能在绘制相同的图形时使用。
|
||||
|
||||
不过,如果是绘制不同的几何图形,只要它们使用同样的着色器程序,而且没有改变uniform变量,我们也还是可以将顶点数据先合并再渲染,以减少渲染次数。
|
||||
|
||||
这么说你可能还不太理解,我们一起来看一个例子。假设,我们现在不只显示正三角形,而是显示随机的正三角形、正方形和正五边形。最常规的实现方式和前面显示随机正三角形的例子类似,我们只要修改一下顶点生成的函数,根据不同的边数生成对应的正多边形就可以了。代码如下:
|
||||
|
||||
```
|
||||
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 {positions, cells} = randomShape(x, y, 3 + Math.floor(4 * Math.random()), rotation);
|
||||
renderer.setMeshData([{
|
||||
positions,
|
||||
cells,
|
||||
}]);
|
||||
|
||||
```
|
||||
|
||||
不过这个例子的性能就更差了,渲染完3000个图形之后只有大概5fps。当然这是正常的,因为正四边形、正五边形、正六边形每个分别要用2、3、4个三角形,所以虽然要绘制3000个图形,但我们实际绘制的三角形数量要远多于3000个。
|
||||
|
||||
而且,因为这些图形的形状不同,所以我们就不能使用多实例绘制的方式了。这个时候,我们又该如何优化呢?
|
||||
|
||||
我们依然可以将顶点合并起来绘制。因为每个图形都是由顶点(positions)和索引(cells)构成的,所以我们可以批量创建图形,将这些图形的顶点和索引全部合并起来。
|
||||
|
||||
```
|
||||
function createShapes(count) {
|
||||
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};
|
||||
}
|
||||
|
||||
|
||||
```
|
||||
|
||||
如上面代码所示,我们首先创建两个类型数组positions和cells,我们可以假定所有的图形都是正六边形,算出要创建的类型数组的总长度。注意,这里我们用的是三维顶点而不是二维顶点,这并不是说我们要绘制的图形是3D图形,而是我们使用z轴来保存当前图形的id,提供给着色器中的伪随机函数使用。
|
||||
|
||||
计算顶点的方式和前面一样,都用的是向量旋转的方法。值得注意的是,在计算索引的时候,我们只要将之前已经算过的几何图形顶点总数记录下来,保存到offset变量里,从offset值开始计算就可以了。
|
||||
|
||||
最终,createShapes函数会返回一个包含几万个顶点和索引的几何体数据,然后我们将它一次性渲染出来就行了。
|
||||
|
||||
```
|
||||
const {positions, cells} = createShapes(COUNT);
|
||||
|
||||
renderer.setMeshData([{
|
||||
positions,
|
||||
cells,
|
||||
}]);
|
||||
|
||||
function render(t) {
|
||||
renderer.uniforms.uTime = t;
|
||||
renderer.render();
|
||||
requestAnimationFrame(render);
|
||||
}
|
||||
|
||||
render(0);
|
||||
|
||||
```
|
||||
|
||||
因为,对应的顶点着色器代码,与我们前面用多实例绘制的三角形例子差不多,只有一些微小的改动,所以你可以对比着看一下,加深理解。
|
||||
|
||||
```
|
||||
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);
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
采用动态批量绘制之后,之前不到5fps的帧率,就被我们轻松提升到了60fps。
|
||||
|
||||
<img src="https://static001.geekbang.org/resource/image/31/d5/31c369d5441acb07c3724b1d0daa91d5.jpg" alt="">
|
||||
|
||||
批量渲染几乎是WebGL绘制最大的优化手段,因为它充分发挥了GPU的优势,所以能极大地提升性能。因此,在实际的WebGL项目中,如果我们遇到性能瓶颈,第一步就是要看看绘制的几何图形有哪些是可以批量渲染的,如果能批量渲染的,要尽量采用批量渲染,以减少一帧中的绘制次数。
|
||||
|
||||
不过批量渲染也有局限性,如果我们绘制的图形必须要用到不同的WebGLProgram,或者每个图形要用到不同的uniform变量,那么它们就无法合并渲染。因此,我们在设计程序的时候,要尽量避免WebGLProgram切换,以及uniform的修改。
|
||||
|
||||
另外,在前面两个例子中,我们将id传入着色器,然后根据id在着色器中用伪随机函数计算位置和颜色。这样的好处自然是渲染起来特别快,但坏处是这些数据是在着色器中计算出来的,如果我们想从JavaScript中拿到一些有用信息,比如,图形的位置、颜色等等,就很难拿到了。
|
||||
|
||||
因此,如果业务中需要用到这些信息,我们就不能将它们放在着色器中计算。当然,我们可以通过JavaScript来计算位置和颜色信息,然后把它们写到attribute中。不过这样的话,我们使用的内存消耗就会增加一些,而且用JavaScript计算这些值的过程会比在着色器中略慢。当然这也是因为项目需求不得不做出的选择。
|
||||
|
||||
## 其他优化手段
|
||||
|
||||
好了,对性能影响最大的批量绘制我们讲完了。其实,还有两个因素对性能也有影响,分别是透明与反锯齿和Shader效率。下面,我也简单介绍一下。由于这些因素影响性能的原理相对比较简单,我就不举例来说了,你可以自己实践一下来加深理解。
|
||||
|
||||
### 透明度与反锯齿
|
||||
|
||||
首先,是透明与反锯齿。在WebGL中,我们要处理半透明图形,可以开启混合模式(Blending Mode)让透明度生效。只有这样,WebGL才会根据Alpha通道值和图形的层叠关系正确渲染并合成出叠加的颜色值。开启混合模式的代码如下:
|
||||
|
||||
```
|
||||
gl.enable(gl.BLEND);
|
||||
|
||||
```
|
||||
|
||||
不过,混合颜色本身有计算量,所以开启混合模式会造成一定的性能开销。因此,如果不需要处理半透明图形,我们尽量不开启混合模式,这样性能好就会更好一些。
|
||||
|
||||
此外,WebGL本身对图形有反锯齿的优化,反锯齿可以避免图形边缘在绘制时出现锯齿,当然反锯齿本身也会带来性能开销。因此,如果对反锯齿的要求不高,我们在获取WebGL上下文时,关闭反锯齿设置也能减少开销、提升渲染性能。
|
||||
|
||||
```
|
||||
const gl = canvas.getContext('webgl', {antiAlias: false}); //不消除反锯齿
|
||||
|
||||
```
|
||||
|
||||
### Shader的效率
|
||||
|
||||
最后,Shader的效率也是我们在使用WebGL时需要注意的。我们前面说过,为了尽可能合并数据,动态批量绘制图形,我们要求图形尽量使用同一个WebGLProgram,并且避免在绘制过程中切换WebGLProgram。
|
||||
|
||||
但如果不同图形的绘制都使用同一个WebGLProgram,这也会造成着色器本身的代码逻辑复杂,从而影响Shder的效率。最好的解决办法就是尽可能拆分不同的着色器代码,然后在绘制过程中根据不同元素进行切换。所以,批量绘制和简化WebGLProgram是一对矛盾,我们只能对两者进行取舍,尽可能让性能达到最优。
|
||||
|
||||
另外,shader代码不同于常规的JavaScript代码,它最大的特性是并行计算,因此处理逻辑的过程与普通的代码不同。
|
||||
|
||||
那不同在哪儿呢?我们先来看一个常规的JavaScript代码。
|
||||
|
||||
```
|
||||
if(Math.random() > 0.5) {
|
||||
do something
|
||||
} else {
|
||||
do somthing else
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
我们都知道,如果if语句中的条件值为true,那么第一个分支被执行,否则第二个分支被执行,这两个分支是不能同时被执行的。
|
||||
|
||||
但如果是Shader中的代码,情况就完全不同了。
|
||||
|
||||
```
|
||||
if(random(st) > 0.5) {
|
||||
gl_FragColor = vec4(1)
|
||||
} else {
|
||||
gl_FragColor = vec4(0)
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
无论是if还是else分支,在glsl中都会被执行,最终的值则根据条件表达式结果不同取不同分支计算的结果。
|
||||
|
||||
之所以会这样,就是因为GPU是并行计算的,也就是说并行执行大量glsl程序,但是每个子程序并不知道其他子程序的执行结果,所以最优的办法就是事先计算好if和else分支中的结果,再根据不同子程序的条件返回对应的结果。因此,if语句必然要同时执行两个分支,但这样就会造成性能上一定的损耗,解决这个问题的办法是尽可能不用if语句。比如,对上面的代码,我们不用if语句,而是用step函数来解决问题,这样性能就会好一些。代码如下:
|
||||
|
||||
```
|
||||
gl_FragColor = vec4(1) * step(random(st), 0.5);
|
||||
|
||||
```
|
||||
|
||||
此外,一些耗时的计算,比如开平方、反正切、反余弦等等,我们的优化原则也是能避免就尽可能避免,多使用简单的加法和乘法,这样就能保证着色器的高效率运行了。
|
||||
|
||||
## 要点总结
|
||||
|
||||
今天,我们重点讲了优化WebGL绘制性能的核心原则。
|
||||
|
||||
虽然WebGL是图形系统中渲染性能最高的,但如果我们不够了解GPU,不对它进行有效的优化,就不能很好地发挥出WebGL的高性能优势。
|
||||
|
||||
用一句话总结,WebGL的性能优化原则就是尽量发挥出GPU的优势。核心原则有两个:首先,我们尽量减少CPU计算次数,把能放在GPU中计算的部分放在GPU中并行计算;其次,也是更重要的,我们应该减少每一帧的绘制次数。
|
||||
|
||||
对应的优化方法也有两个:一是如果我们要绘制大量相同的图形,可以利用多实例渲染来实现静态批量绘制;二是如果绘制的图形不同,但是采用的WebGL程序相同、以及uniform的值没有改变,那我们可以人为合并顶点并进行渲染。减少绘制次数一般来说对性能会有比较明显的提升。
|
||||
|
||||
除此之外,我们还可以在不需要处理透明度的时候不启用混合模式,在不需要抗锯齿的时候关闭抗锯齿功能,它们都能减少性能开销。以及,我们还要注意Shader的效率,尽量用函数代替分支,避免一些耗时的计算,多使用简单的加法和乘法,这样能够保证着色器高效运行。
|
||||
|
||||
总的来说,性能优化是一个非常复杂的问题,我们应该结合实际项目的需求、数据的特征、技术方案等等综合考虑,最终才能得出最适合的方案。在实际项目中,无论你是直接用原生的WebGL,还是使用OGL、SpriteJS或者ThreeJS,大体的优化思路肯定离不开我前面总结的这些点。但怎么既恰到好处的优化,又保持性能与产品功能、开发效率以及扩展性的平衡,就需要我们通不断积累项目经验,才能慢慢做到最好啦。
|
||||
|
||||
## 小试牛刀
|
||||
|
||||
在前面的例子中,我们把位置和颜色的计算都放在了着色器中。这有利有弊,如果让你来重构代码,你能做到既兼顾性能,又能满足我们从JavaScript中拿到几何体位置和颜色的需求吗?如果可以,就快把你的解决方案写好分享出来吧。
|
||||
|
||||
欢迎在留言区和我讨论,分享你的答案和思考,也欢迎你把这节课分享给你的朋友,我们下节课见!
|
||||
|
||||
## 源码
|
||||
|
||||
[课程中完整示例代码](https://github.com/akira-cn/graphics/tree/master/performance-webgl)
|
||||
|
||||
## 推荐阅读
|
||||
|
||||
[WebGL2系列之实例数组(Instanced Arrays)](https://www.jianshu.com/p/d40a8b38adfe)
|
Reference in New Issue
Block a user