高冷的 WebGL

在上一篇文章中,我给大家分享了,如何能快速入门Threejs。Threejs是一个用于在浏览器中绘制3D图形的JS库,其底层实际是对浏览器提供的WebGL Api进行了封装。作为一个好奇宝宝,看到了Threejs那些神奇的绘制3D图形Api,又怎么能抑制住想要钻进去一探究竟的冲动呢?所以今天的文章,就来给大家分享一下WebGL本身。

WebGL(全写Web Graphics Library)是一种3D绘图标准,这种绘图技术标准允许把JavaScript和OpenGL ES 2.0结合在一起,通过增加OpenGL ES 2.0的一个JavaScript绑定,WebGL可以为HTML5 Canvas提供硬件3D加速渲染,这样Web开发人员就可以借助系统显卡来在浏览器里更流畅地展示3D场景和模型了,还能创建复杂的导航和数据视觉化。

以上是WebGL在百科上的一段介绍,说白了,就是通过浏览器提供的接口,我们能直接和底层的OpenGL库打交道。由于能直接调用底层接口,并且有硬件加速,因此WebGL要比普通的Canvas 2D Api性能要高出不少。这里有一个对WebGL和Canvas 2D Api的性能对比实验https://developer.tizen.org/dev-guide/web/2.3.0/org.tizen.mobile.web.appprogramming/html/guide/w3c_guide/graphics_guide/performance_comparison.htm。在实验中,通过加载一幅图片并随机显示在canvas中的某个位置,通过requestAnimationFrame定时修改图片的颜色,并记录页面的FPS。

从结果中可见,当需要执行大量绘制任务时,WebGL的性能远远超越了Canvas 2D Api,达到了后者的3~5倍。

即然WebGL性能这么高,为什么没有看到在日常开发中有大规模的应用呢(好吧,可能是我写的代码太少了)。 我想至少有以下两个原因。第一,由于WebGL是直接调用底层的OpenGL,这使得WebGL的接口十分晦涩,对于一般的Web开发人员来说,门槛比较高。第二,WebGL的兼容性并不好,从caniuse上,我们可以看到:

只有edge和chrome对WebGL有比较好的支持,safari则要到8.0后的版本才支持,firefox则只是部分支持。因此,一般的情况,我们都会对浏览器做feature detection,如果浏览器不支持WebGL,就需要有一个Canvas 2D Api的降级方案,而Threejs就是这么处理的,在Threejs里,除了有一个WebGLRenderer,还有一个CanvasRenderer,以备不时之需。

接下来,我们就通过代码,直接感受一下WebGL的高冷。为了能让大家有一个直观的感受,我同时使用Canvas 2D Api和WebGL,在canvas上绘制一个红色的矩形:


<div class="km_insert_code">

    var canvas = document.getElementById('canvas');
    var context = canvas.getContext('2d');

    context.clearRect(0, 0, canvas.width, canvas.height);

    context.fillStyle = '#ff0000';
    context.fillRect(100, 100, 100, 100);

</div>

上面这段代码,我们应该比较熟悉,Canvas 2D Api给我们提供了非常直观的接口,直接就可以在canvas中绘制。显示的效果如下:

接下来我们再来看看WebGL的版本:

<div class="km_insert_code">

    var canvas = document.getElementById('canvas');
    var gl = canvas.getContext('experimental-webgl');

    var VSHADER_SOURCE = `
        attribute vec4 a_Position;

        void main() {
            gl_Position = a_Position;
        }
    `;

    var FSHADER_SOURCE = `
        precision mediump float;

        void main() {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `;

    var vertexShader = gl.createShader(gl.VERTEX_SHADER);

    gl.shaderSource(vertexShader, VSHADER_SOURCE);
    gl.compileShader(vertexShader);

    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

    gl.shaderSource(fragmentShader, FSHADER_SOURCE);
    gl.compileShader(fragmentShader);

    var program = gl.createProgram();

    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);

    gl.linkProgram(program);
    gl.useProgram(program);

    var vertices = new Float32Array([
        -0.3,  0.3,  0.0,
         0.3,  0.3,  0.0,
        -0.3, -0.3,  0.0,
         0.3, -0.3,  0.0
    ]);

    var buffer = gl.createBuffer();

    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

    var a_Position = gl.getAttribLocation(program, 'a_Position');

    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
    gl.enableVertexAttribArray(a_Position);

    gl.clearColor(1.0, 1.0, 1.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);

</div>

就是这么酸爽(┬_┬)。即使是绘制一个矩形这么简单的任务,WebGL都不能让你省心,就更别说要在WebGL里绘制3D图像了。但希望各位小伙伴不要被上面这堆东西吓唬到。让我来带这大家一步一步的解读上面的代码。

要解读上面这段代码,我们首先要重新包装一下,把那些细枝末节先隐藏起来,毕竟裸露不一定就代表性感。通过函数的抽象,上面的代码可以写成下面的样子:

<div class="km_insert_code">

    var canvas = document.getElementById('canvas');

    // 获取WebGL上下文
    var gl = getWebGLContext(canvas);

    // 编译着色器代码
    initShader(gl, VSHADER_SOURCE, FSHADER_SOURCE);

    // 往顶点数据缓存冲写入数据
    initVertexBuffer(gl, vertices);

    // 使着色器代码中的a_Position变量,指向顶点数据缓冲区
    setAttributeFromBuffer(gl, 'a_Position', 3, 0, 0);

    // 清除颜色缓冲区中数据
    clear(gl, 1.0, 1.0, 1.0, 1.0);

    // 根据着色器代码,绘制图像
    draw(gl, 4);

</div>

是不是觉得没那么心塞?简化了代码后,我们就一步一步来解读。首先明确一点,WebGL也是基于canvas标签的,只是获取的上下文不一样而已,在WebGL中我们获取的上下文对象是webgl,但由于大部分浏览器并没有全面支持WebGL,而是通过experimental-webgl这样一个带前缀的上下文来提供实验性质的WebGL功能。

有了WebGL的上下文,我们就可以开始调用WebGL为我们提供的接口。不过WebGL和Canvas 2D Api不同,并没有直接可以绘制图像的接口,而是需要我们一笔一划的告诉它如何绘制图像。因此,你首先得教会WebGL要如何绘制,而WebGL中表示如何绘制的方式称为着色器。

着色器并不是直接由js来编写,而是用一种叫做GLSL ES的语言来编写。该语言与c语言很接近,但内置了一些方便计算机绘图的工具方法,具体可看https://www.opengl.org/documentation/glsl/这个地址,这里我就不详细说明了。

<div class="km_insert_code">

    // 顶点着色器
    var VSHADER_SOURCE = `
        attribute vec4 a_Position;
        void main() {
            gl_Position = a_Position;
        }
    `;

    // 片元着色器
    var FSHADER_SOURCE = `
        precision mediump float;
        void main() {
            gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
        }
    `;

</div>

在WebGL中着色器分为两种,一种叫顶点着色器(vertex shader),WebGL会根据你提供的图形顶点数据,逐个顶点的执行顶点着色器来组装图形。另外一种叫做片元着色器(fragment shader),WebGL利用顶点着色器组装好图形后,就会进行图像栅格化,图像栅格化后,你就得到了对应的片元,你可以想象成屏幕上的像素,然后WebGL就会逐个片元的执行片元着色器来给图像上颜色,最终把绘制好的图像传给颜色缓冲区显示在屏幕上:

通过initShader方法,我们已经教会了WebGL如何绘制图像。接下来,我们就要给告诉WebGL,你要绘制的是什么,也只是说,用于控制图形的顶点数据。然而要和WebGL的着色器沟通,我们并不能直接向着色器传入数据(其实也是可以的,不过比较低效),我们需要先在内存里开辟一块缓冲区,然后通过WebGL提供的接口,把数据写入缓冲区,这就是initVertexBuffer方法的功能。

内存中有了数据后,我们就可以通过调用setAttributeFromBuffer方法把着色器里的变量指向该块内存,这样当WebGL逐个顶点的执行顶点着色器时,就可以从对应的内存分块中读取到顶点数据。

一切准备就绪,我们终于可以开始绘制图像了,在绘制之前先调用clear方法,清除颜色缓冲区中的数据(类似Canvas 2D Api中的clearRect)最后调用draw方法,真正绘制出图像。终于松一口气。

通过上面的这个例子,我们明白了,要在WebGL中绘制图像,首先得教会WebGL如何绘制(编写着色器),然后告诉WebGL要绘制什么(创建缓存区,写入顶点数据,并关联到着色器变量上),最后清理一下之前绘制的东西,把准备好的图像绘制到屏幕上。

最后,我把上面用到的每一个方法补上:


<div class="km_insert_code">

    function getWebGLContext(canvas) {
        return canvas.getContext('experimental-webgl');
    }

    function initShader(gl, vertexShaderSource, fragmentShaderSource) {
        var vertexShader = gl.createShader(gl.VERTEX_SHADER);

        gl.shaderSource(vertexShader, vertexShaderSource);
        gl.compileShader(vertexShader);

        var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

        gl.shaderSource(fragmentShader, fragmentShaderSource);
        gl.compileShader(fragmentShader);

        var program = gl.createProgram();

        gl.attachShader(program, vertexShader);
        gl.attachShader(program, fragmentShader);

        gl.linkProgram(program);
        gl.useProgram(program);

        gl.program = program;
    }

    function initVertexBuffer(gl, vertices) {
        var buffer = gl.createBuffer();

        gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
    }

    function setAttributeFromBuffer(gl, name, size, stride, offset) {
        var attribute = gl.getAttribLocation(gl.program, name);

        gl.vertexAttribPointer(attribute, size, gl.FLOAT, false, stride, offset);
        gl.enableVertexAttribArray(attribute);
    }

    function clear(gl, r, g, b, a) {
        gl.clearColor(r, g, b, a);
        gl.clear(gl.COLOR_BUFFER_BIT);
    }

    function draw(gl, size) {
        gl.drawArrays(gl.TRIANGLE_STRIP, 0, size);
    }

</div>

这就是我今天要给大家介绍的WebGL基础,以上!

原创声明,本文系作者授权云+社区-专栏发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Golang语言社区

Golang语言实现猜数字小游戏的方法

随机生成一个数字,输入一个数字看是否匹对,匹配则结速,反之提示是大了还是小了 package main import ( "bufio" "f...

3419
来自专栏Golang语言社区

Golang语言社区--标准库分析之strconv包

大家好,我是Golang语言社区主编彬哥,这篇是给大家转载的关于Go语言的strconv 包相关的知识。

7547
来自专栏税国龙的专栏

我们来用 D3.js 画个饼图

最近在做腾讯云大数据可视化项目,天天跟各种柱状图、饼图、面积图等打交道。 饼图能直观的表现一堆数据中各项所占比例,是非常常见的图表之一。本文尝试来讲讲如何在浏览...

7867
来自专栏kalifaの日々

opencv grabCap python实现

处理后模特的蓝眼珠被涂黑了,看久了简直是精神污染。解决办法是去mask里改动眼珠位置的值,设成确定的前景。

1042
来自专栏大数据文摘

手把手|如何用Python绘制JS地图?

29413
来自专栏Android常用基础

自定义View(四)-动画- Interpolator与Evaluator

Interpolator插值器之前我们已经接触过了,而Evaluator好像我们还没有将,这是属性动画中俩个比较中的两个知识点,弄清楚它们有助于我们更好的使用与...

792
来自专栏向治洪

andriod游戏音效

同学们在玩游戏的时候应该都会发现游戏中会有两种形式来播放音乐 ,一般设置选项中会明确标明 设置游戏音乐 与设置游戏音效。 客观的分析一下这两种形式的音乐,游戏背...

1986
来自专栏青蛙要fly的专栏

图片操作系列 —(1)手势缩放图片功能

项目开发中,大家APP开发一般都会用到上传图片,比如是上传了自己的生活照,然后在某个界面处查看上传的图片,这时候一般在这个查看详情的界面,会有手势放大缩小功能,...

701
来自专栏吴老师移动开发

【iOS开发】UITableView优化

移动开发中,任何一个应用都或多或少的有列表的存在,列表的上下滑动直接关系到用户体验。如果处理不好,就会使得列表滑动起来有明显的卡顿效果。所以对列表的优化,让它更...

621
来自专栏wym

俄罗斯方块

//*********************************************// //**************  头文件  *****...

551

扫码关注云+社区