WebAssembly之二维绘图测试

2,445 阅读3分钟

前言

最近看boss直聘上,WebAssembly 的需求挺多的,主要涉及音视频处理、图形图像、2D、3D、游戏开发等项目。

而最近我正好遇到一个二维图形优化的需求,所以就想看看WebAssembly 能否提高项目的运行速度。

1-WebAssembly 简介

WebAssembly简称wasm,是一个低级编程语言。

WebAssembly是便携式的抽象语法树,被设计来提供比JavaScript更快速的编译及执行。

WebAssembly可以让C/C++程序在浏览器内运行。

WebAssembly具有以下限制:

  • 通常,WebAssembly 不允许与DOM直接交互。所有交互都必须通过 JavaScript 互操作进行。
  • 没有垃圾收集(garbage collection)机制。
  • 安全问题,比如容易隐藏恶意代码。

2-WebAssembly+canvas 2d 绘图测试

接下来我会通过Google的 canvaskit 工具来辅助测试WebAssembly+canvas 2d 的运行速度。

CanvasKit是以WASM为编译目标的Web平台图形绘制接口,其目标是将Skia的图形API导出到Web平台。所以我们可以直接用WebAssembly 开发web端的图形项目。

1.在自己的项目中安装canvaskit-wasm

npm install canvaskit-wasm

2.建立一个测试文件。

  • 01-wasm绘图测试.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>test</title>
        <style>
            #foo {
                border: 1px solid #ddd;
            }
        </style>
    </head>

    <body>
        <canvas id="foo" width="700" height="600"></canvas>

        <script src="/node_modules/canvaskit-wasm/bin/canvaskit.js"></script>
        <script>
            const { width, height } = document.getElementById('foo')
            CanvasKitInit({
                locateFile: (file) => '/node_modules/canvaskit-wasm/bin/' + file,
            }).then((CanvasKit) => {
                class Rect {
                    constructor(x = 0, y = 0, w = 100, h = 60) {
                        this.x = x
                        this.y = y
                        this.w = w
                        this.h = h
                        this.speed = 3
                        this.dirX = 1
                        this.dirY = 1
                    }
                    ani(width = 700, height = 600) {
                        const { x, y, w, h, speed } = this
                        if (x < 0 || x + w > width) {
                            this.dirX *= -1
                        }
                        if (y < 0 || y + h > height) {
                            this.dirY *= -1
                        }
                        this.x += this.dirX * speed
                        this.y += this.dirY * speed
                    }
                    draw(canvas, paint) {
                        const { x, y, w, h } = this
                        const rr = CanvasKit.XYWHRect(this.x, this.y, w, h)
                        canvas.drawRect(rr, paint)
                    }
                }

                const rects = []
                for (let i = 0; i < 50000; i++) {
                    const rect = new Rect(Math.random() * 500, Math.random() * 500)
                    rect.speed = Math.random() * 20 + 1
                    rects[i] = rect
                }

                const surface = CanvasKit.MakeCanvasSurface('foo')
                const paint = new CanvasKit.Paint()
                paint.setColor(CanvasKit.Color4f(0, 0, 0, 0.01))
                paint.setStyle(CanvasKit.PaintStyle.Stroke)
                // paint.setAntiAlias(true)

                let time = new Date()
                function drawFrame(canvas) {
                    const newTime = new Date()
                    console.log(newTime - time)
                    time = newTime
                    canvas.clear(CanvasKit.WHITE)
                    rects.forEach((rect) => {
                        rect.ani(width, height)
                        rect.draw(canvas, paint)
                    })
                    surface.requestAnimationFrame(drawFrame)
                }
                surface.requestAnimationFrame(drawFrame)
            })
        </script>
    </body>
</html>

在上面的代码中,我画了50000个透明的矩形,并且通过请求动画帧来驱动动画,效果如下:

下载

console.log(newTime - time) 打印结果如下:

image-20230310112634586

初次绘图用时206,之后的刷新频率在170-190之间,平均值可以取180。

接下来我们对比原生的canvas2d 看看。

2-原生canvas2d 绘图测试

接下来我依旧会画50000个透明的矩形。

  • 02-canvas2d绘图测试.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>canvas2d绘图测试</title>
        <style>
            #foo {
                border: 1px solid #ddd;
            }
        </style>
    </head>

    <body>
        <canvas id="foo" width="700" height="600"></canvas>

        <script>
            class Rect {
                constructor(x = 0, y = 0, w = 100, h = 60) {
                    this.x = x
                    this.y = y
                    this.w = w
                    this.h = h
                    this.speed = 3
                    this.dirX = 1
                    this.dirY = 1
                }
                ani(width = 700, height = 600) {
                    const { x, y, w, h, speed } = this
                    if (x < 0 || x + w > width) {
                        this.dirX *= -1
                    }
                    if (y < 0 || y + h > height) {
                        this.dirY *= -1
                    }
                    // move
                    this.x += this.dirX * speed
                    this.y += this.dirY * speed
                }
                draw(ctx) {
                    const { x, y, w, h } = this
                    ctx.strokeStyle = 'rgba(0,0,0,0.01)'
                    ctx.beginPath()
                    ctx.moveTo(x, y)
                    ctx.lineTo(x + w, y)
                    ctx.lineTo(x + w, y + h)
                    ctx.lineTo(x, y + h)
                    ctx.closePath()
                    ctx.stroke()
                }
            }

            const rects = []
            const rect = new Rect()
            for (let i = 0; i < 50000; i++) {
                const rect = new Rect(Math.random() * 500, Math.random() * 500)
                rect.speed = Math.random() * 20 + 1
                rects[i] = rect
            }

            const canvas = document.getElementById('foo')
            const { width, height } = canvas
            const ctx = canvas.getContext('2d')

            let time = new Date()
            !(function drawFrame() {
                const newTime = new Date()
                console.log(newTime - time)
                time = newTime

                ctx.clearRect(0, 0, width, height)
                rects.forEach((rect) => {
                    rect.ani(width, height)
                    rect.draw(ctx)
                })

                requestAnimationFrame(drawFrame)
            })()
        </script>
    </body>
</html>

效果和之前一样:

下载

console.log(newTime - time) 打印结果如下:

image-20230310112959174

初次绘图用时92,之后的刷新频率在80-90之间,平均值可以取85。

当前原生canvas2d 竟然比wasm快了一倍,我们暂且不做分析,再看一下WebGL的绘图速度。

3-原生WebGL 绘图测试

接下来我依旧会画50000个透明的矩形。

  • 03-WebGL绘图测试.html
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <title>test</title>
        <style>
            #foo {
                border: 1px solid #ddd;
            }
        </style>
    </head>

    <body>
        <canvas id="foo" width="700" height="600"></canvas>
        <script id="vertexShader" type="x-shader/x-vertex">
            attribute vec2 a_Position;
            void main(){
                 gl_Position=vec4(a_Position,0,1.);
                 gl_PointSize=20.0;
            }
        </script>
        <script id="fragmentShader" type="x-shader/x-fragment">
            void main(){
              gl_FragColor=vec4(0,0,0,0.01);
            }
        </script>
        <script type="module">
            class Rect {
                constructor(gl, x = 0, y = 0, w = 0.2, h = 0.2) {
                    this.gl = gl
                    this.x = x
                    this.y = y
                    this.w = w
                    this.h = h
                    this.speed = 3
                    this.dirX = 1
                    this.dirY = 1
                }
                update() {
                    const { x, y, w, h, speed, gl } = this
                    const vertexBuffer = gl.createBuffer()
                    gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
                    gl.bufferData(
                        gl.ARRAY_BUFFER,
                        new Float32Array([x, y, x + w, y, x + w, y + w, x, y + w]),
                        gl.STATIC_DRAW
                    )
                    const a_Position = gl.getAttribLocation(gl.program, 'a_Position')
                    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)
                    gl.enableVertexAttribArray(a_Position)
                }
                ani() {
                    const { x, y, w, h, speed } = this
                    if (x < -1 || x + w > 1) {
                        this.dirX *= -1
                    }
                    if (y < -1 || y + h > 1) {
                        this.dirY *= -1
                    }
                    this.x += this.dirX * speed
                    this.y += this.dirY * speed
                }
                draw() {
                    this.update()
                    this.gl.drawArrays(gl.LINE_LOOP, 0, 4)
                }
            }

            const canvas = document.querySelector('#foo')
            const vsSource = document.querySelector('#vertexShader').innerText
            const fsSource = document.querySelector('#fragmentShader').innerText

            const gl = canvas.getContext('webgl')
            gl.enable(gl.BLEND)
            gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA, gl.ONE, gl.ONE)
            gl.clearColor(1, 1, 1, 1)
            initShaders(gl, vsSource, fsSource)

            const rects = []
            for (let i = 0; i < 50000; i++) {
                const rect = new Rect(
                    gl,
                    Math.random() * 1.6 - 0.8,
                    Math.random() * 1.6 - 0.8
                )
                rect.speed = Math.random() * 0.01 + 0.001
                rects[i] = rect
            }

            let time = new Date()
            !(function render() {
                const newTime = new Date()
                console.log(newTime - time)
                time = newTime
                gl.clear(gl.COLOR_BUFFER_BIT)
                rects.forEach((rect) => {
                    rect.ani()
                    rect.draw()
                })
                requestAnimationFrame(render)
            })()

            function initShaders(gl, vsSource, fsSource) {
                const program = gl.createProgram()
                const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
                const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
                gl.attachShader(program, vertexShader)
                gl.attachShader(program, fragmentShader)
                gl.linkProgram(program)
                gl.useProgram(program)
                gl.program = program
                return true
            }
            function createProgram(gl, vsSource, fsSource) {
                const program = gl.createProgram()
                const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource)
                const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource)
                gl.attachShader(program, vertexShader)
                gl.attachShader(program, fragmentShader)
                gl.linkProgram(program)
                return program
            }

            function loadShader(gl, type, source) {
                const shader = gl.createShader(type)
                gl.shaderSource(shader, source)
                gl.compileShader(shader)
                return shader
            }
        </script>
    </body>
</html>

效果如下:

image-20230310120246447

console.log(newTime - time) 打印结果如下:

image-20230310115707726

初次绘图用时4903,之后的刷新频率在7185-12630之间,平均值可以取10000。

4-结果分析

下图是我们之前的测试结果:

test.png

由此可知:

  • canvas2d 在绘制简单图形的时候,速度最快。
  • wasm 要比canvas2d 慢一倍。
  • WebGL 最慢,且极慢。

对于这个结果,我考虑到以下可能原因:

  • canvas2d之所以最快,是它在绘制简单的二维图形,或者开发简单的二维项目的时候,具备主场优势,因为canvas2d 本身就是为二维而生的。
  • wasm 的编译和执行速度快,我是深信不疑的,毕竟是Google出品。但它之所以比原生canvas2d 慢,我想它应该还没有发挥出自己的优势,至于如何在二维项目中发挥其最大优势,还有待研究。
  • WebGL 最慢的原因是它擅长GPU并行渲染,而在渲染简单图形的时候,因为它要走一些复杂逻辑,比如着色器与js间的数据传输,反而渲染速度要比原生canvas 慢很多很多。

总结

最后,我个人认为,若只是开发简单的二维项目,不涉及复杂的光栅化,或者大量的逐像素操作,直接使用原生canvas2d 开发即可,因为适合的才是最好的。

下一章,我会在逐像素操作方面,对比一下canvas2d、wasm和WebGL的渲染速度。