【性能篇】30 # 怎么给WebGL绘制加速?

简介: 【性能篇】30 # 怎么给WebGL绘制加速?

说明

【跟月影学可视化】学习笔记。




常规绘图方式的性能瓶颈

例子:在一个画布上渲染 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


cfa4a150b09744829f68126eb3cddc97.png



减少 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 引擎的运算速度很快,感觉将顶点计算放到顶点着色器中进行了,性能差别也很微小。

1ca64c738345442da374b71f30b2b6a9.png



静态批量绘制(多实例绘制)

重复图形的批量绘制,在 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 倍。

d61e5a3113454849ad7651ee9d5e1d85.png

动态批量绘制


如果是绘制不同的几何图形,只要它们使用同样的着色器程序,而且没有改变 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

6e860aa4d9f840cdaa6963db9768e05b.png

<!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>


采用动态批量绘制之后:

2fb13b6a1fda4ed6b4e0f5fd6aeb1df6.png



透明度与反锯齿


透明度

在 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 的优势。

核心原则有两个:


  1. 尽量减少 CPU 计算次数
  2. 减少每一帧的绘制次数



目录
相关文章
|
7月前
|
前端开发 JavaScript 数据可视化
WebGL 入门:开启三维网页图形的新篇章(上)
WebGL 入门:开启三维网页图形的新篇章(上)
|
7月前
|
运维 小程序 vr&ar
6个维度分析实时渲染和Webgl技术异同
虽然二者均为B/S技术架构路线,但webgl对本地电脑性能还是有些要求,因为webgl的程序有些数据是需要下载到本地,借助本地电脑的显卡和CPU来完成的,不算完全的B/S架构。 而实时渲染技术是完全使用的服务器显卡和CPU等资源,是纯B/S技术架构方案,用户侧的终端只是程序指令的接收和执行,只要能看1080P的视频即可。
115 0
|
2月前
|
测试技术 异构计算
|
2月前
|
缓存 算法 测试技术
|
4月前
|
算法 异构计算
第2章-图形渲染管线-2.1-架构
第2章-图形渲染管线-2.1-架构
29 0
|
JavaScript 前端开发 数据可视化
6 个用于 3D 网页图形渲染的最佳 WebGL 库
现代前端、游戏和Web开发正是WebGL可以转化为数字杰作的东西。使用GPU绘制在浏览器屏幕上生成的矢量元素,WebGL创建交互式Web图形,从而获得用户体验。视觉元素的质量和复杂性使该工具在HTML或CSS等其他方法中脱颖而出。
571 0
|
7月前
|
存储 缓存 数据可视化
WebGL 入门:开启三维网页图形的新篇章(下)
WebGL 入门:开启三维网页图形的新篇章(下)
WebGL 入门:开启三维网页图形的新篇章(下)
|
7月前
|
人工智能 安全 API
Unity优化——加速物理引擎1
Unity优化——加速物理引擎1
149 0
|
JavaScript 前端开发 API
使用three.js与WebGL相比有什么优势?
简单的说Three.js是WebGL的框架。封装和简化了WebGL的方法。three.js在它的基础上进行了进一步的封装和简化开发开发过程,个人认为类似于jQuery对原生js的关系。下面我们一点一点来了解下。
306 0
使用three.js与WebGL相比有什么优势?
|
图形学
unity如何计算drawcall
unity如何计算drawcall
189 0