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

Posted 凯小默

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了性能篇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

减少 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 

以上是关于性能篇30 # 怎么给WebGL绘制加速?的主要内容,如果未能解决你的问题,请参考以下文章

性能篇29 # 怎么给Canvas绘制加速?

性能篇29 # 怎么给Canvas绘制加速?

性能篇28 # CanvasSVG与WebGL在性能上的优势与劣势

性能篇28 # CanvasSVG与WebGL在性能上的优势与劣势

WebGL简易教程:颜色

视觉高级篇20 # 如何用WebGL绘制3D物体?