WebGL第四课:画点(二)-构造、传递数据

640 阅读3分钟
本文标题:WebGL第四课:画点(二)-构造、传递数据

本文是接着上文来讲的,如果没有看过上一篇,还是请先看完前一篇为好。

言归正传,一旦我们拥有了纸和笔,下面操作 WebGL 的步骤可以概括成三步:

1. 构造数据
2. 将数据传入 WebGL
3. 告诉 WebGL 画出来
首先第一步:构造数据

我们的目标是在纸(canvas)的中间画一个半径是1的圆,颜色是黑的。根据前面几课的介绍,我们知道,一个圆就是一堆离散的点组成的一个模拟图形而已。所以,我们要将这一堆点的坐标计算出来,点的个数越多,我们的结果呈现就越完美。

好的,现在要做的是,如何找出一个圆的坐标?
先来进行一下需求分析,我们知道,一个圆是 360°,我们只需要在这个圆的圆周上,找到一些点就行了。随便怎么找,反正这些点只要在这个圆周上,而且要尽量的等间隔,为什么,防止稀稀拉拉的不好看呗。看下图:
根据标注,从 x 轴向上转动 α 时,圆周上的点的坐标应该是 xy(cosα, sinα)。 我们不妨让 α 从0开始,每隔 1° ,我们在圆周上取一个点。这样的话,α 的变动区间就是 [0, 359]。正好 360 个点,而且是等间隔的,满足我们对美学的需求~
我们下面的代码就可以算出这 360 个点的坐标:

var pointCount = 360;
var pointData = [];
var loop = 0;
var alpha = 0; // 注意,这里的 α 单位是弧度,关于这个要温习一下初中数学
var step = (2 * Math.pi) / 360; // 每一次增加的弧度 (1°)
var x,y;
for (loop = 0; loop < pointCount; loop++) {
    alpha = loop * step;
    x = Math.cos(alpha);
    y = Math.sin(alpha);
    pointData.push(x);
    pointData.push(y);
    // pointData.push([x,y]); WebGL 不喜欢这种数据格式 
}

这样一来,我们就得到了 360 个坐标。
有小伙伴注意到了,我们的 pointData 是一个平坦的数组,并没有将xy封装起来存成一个元素,而是直接 先x再y,再第二个点的x,再第二个点的y,这样一溜存下去。这是为什么呢,这好像不符合人类去思考吧。
事实上,这是为了下一步做准备,对了,WebGL 他就需要这种平坦的数据格式。

再来第二步:将数据传入 WebGL
这一步将涉及到一些 WebGL api 的使用
拿好我们的笔 pen

回顾一下创建 pen 的代码:
var pen = pointCanvas.getContext('webgl', { preserveDrawingBuffer: true }); // 我们的笔
我们将这个变量起做 pen 是为了类比到绘画,不过通常的做法是将这个变量命名成 gl。像这样:
var gl = pointCanvas.getContext('webgl', { preserveDrawingBuffer: true }); // 我们的笔
从这里开始,gl 这个变量与这张纸canvas保持绑定的关系,所有对这张纸canvas的操作,都要使用 gl 这个变量的方法

关于 gl,这个状态机

也许你在别的教程或者博客里,看见过这个说法。 WebGL 就是一个状态机。但是就算你知道状态机是什么,你也很难从一开始就对这个说法打从心底接受。
一个直接的疑问就是:我为什么要知道 WebGL 是个状态机?
一个过于提前的回答就是:留个心眼,好在挠头的时候有个突破口。

然后就是,我怎么理解 WebGL 是个状态机? 如果你现在不能理解,你又刚好在 Linux 系统里使用过命令行的话,那么:
操作 WebGL ,就是跟命令行的感觉一样。 例如:
ls 这个命令,就是打印出当前目录下的文件名称。
你如果使用 cd 命令切换目录的时候,ls 这个命令的结果就会不一样
这就是状态机的一个表现:

  • 你要时刻记住,WebGL 当前的状态是什么。 关于 WebGL 是状态机这点,还体现在它的 api 风格上:
  • 如果光看某一句api调用,你根本不知道你操作的是什么。 举个例子:
    gl.drawArrays(mode, start, count)
    这句代码实际上就是向 WebGL 发送一个绘制的指令。但是你看,参数:
  • mode 画什么(点 线 三角形)
  • start 整数 从什么地方开始
  • count 整数 画几个 这参数列表里,完全没有提到真正的数据,就是说没有一个参数指向了真正的数据。如果用我们现有的代码思维来设计的话,drawArrays 方法的风格应该是这样:
    gl.drawArrays(data, mode, start, count)
    对吧,至少要传一个 data(我们的圆的坐标) 进去,要不然 gl 哪知道画什么呢。
    而且,上面也没说我们到底用什么颜色来画这个圆的坐标吧。

有人说了,gl 整个是一个大对象,我们预先把这个对象里的一些变量设置好,例如圆的坐标,圆的颜色,等等。然后在画的时候,就不用再脱裤子放屁-多此一举了
哎,说对了,但只是一半。

上述方案无法解决一个问题:如果我有多个想绘制的图形怎么办?
答:将我们想要绘制的所有图形的所有坐标,全部先存进 gl(传到显卡里) 里。然后使用 gl 提供的切换指令,来随时切换图形,然后再调用 draw命令进行绘制

我们上面简单的介绍了 关于 gl 是一个状态机这件小事,确实需要提前说一下,但是就此打住,不再展开,后面的代码会让你更清晰地认识到这一点。

将圆的坐标传进 gl (显卡)

其实代码很简单:

var buffer_id;
buffer_id = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

三个api搞定。 逐一讲解一下:
1)gl.createBuffer
如果你使用过 C 语言的话,我十分推荐你就直接理解成 malloc ,然后返回一个指向新申请的内存的指针(buffer_id)。如果你用 js 或者别的有 new关键字的语言,那么,直接类比成 new是再好不过了。 你可以创建多个,用来存放多个不同的图形的坐标数据。那么 gl 会给你返回多个不同的 buffer_id
2) gl.bindBuffer
上一步我们在显卡里创建了一块存储空间,gl给我们返回了一个 buffer_id。那么根据 gl 是状态机这件小事 这个指导原则,那么只要你的操作是跟 buffer_id 有关的,那么你还就得先执行这一句:
gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id);
别问,问就是 状态机
3) gl.bufferData
观察一下这个方法的参数,data很合理,就是我们的圆的坐标。其他两个参数我们先不解释。最主要的就是,根据 1) 和 2) 所说的,当我们有多个 buffer_id 的时候,怎么来控制,我们到底是往哪个 buffer_id 来传数据呢。答案就是 gl.bindBuffer。只要执行了这一句,传入相应的 buffer_id,那后面的相关操作就是针对这个 buffer_id 的了。

例如:如果我有一个圆,一个正方形,都需要往里面传数据:

var buffer_id_circle; // 圆形
var buffer_id_square; // 方形

buffer_id_circle = gl.createBuffer(); // 创建一块存储用来存圆形的数据
buffer_id_square = gl.createBuffer(); // 创建一块存储用来存方形的数据

gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id_circle); // 告诉gl我要操作的是 buffer_id_circle
gl.bufferData(gl.ARRAY_BUFFER, data_circle, gl.STATIC_DRAW); // 把圆的数据存进去

gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id_square); // 告诉gl我要操作的是 buffer_id_square
gl.bufferData(gl.ARRAY_BUFFER, data_square, gl.STATIC_DRAW); // 把方的数据存进去
gl 关于数据格式的倔强

根据上面所讲的,组合一下,构造数据,传入数据的代码应该长下面这样:

var pointCount = 360;
var pointData = [];
var loop = 0;
var alpha = 0; // 注意,这里的 α 单位是弧度,关于这个要温习一下初中数学
var step = (2 * Math.pi) / 360; // 每一次增加的弧度 (1°)
var x,y;
for (loop = 0; loop < pointCount; loop++) {
    alpha = loop * step;
    x = Math.cos(alpha);
    y = Math.sin(alpha);
    pointData.push(x);
    pointData.push(y);
    // pointData.push([x,y]); WebGL 不喜欢这种数据格式 
}

var buffer_id;
buffer_id = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id);
gl.bufferData(gl.ARRAY_BUFFER, pointData, gl.STATIC_DRAW);

但是这样是不行的,因为 js里的 number 类型实在是不是给底层设计的。原理就不多说了,总之,pointData 不能用。
为了解决这个问题,js提供了 Float32Array 这种对象。从名字看,就是32位浮点数数组。我们来看看怎么从 pointData生成这个东西。

var pointArray = new Float32Array(pointData);

还是很简单的,那么整个代码就变成了:

<!doctype html>
<html>

<head>
    <style>
        canvas {
            border: 1px solid #000000;
        }
    </style>
</head>

<body>
    <canvas id="point" style="width:300px; height:300px">
    </canvas>
    <script>
        var pointCanvas = document.getElementById('point'); // 我们的纸
        var gl = pointCanvas.getContext('webgl', { preserveDrawingBuffer: true }); // 我们的笔
        // 生成 360 个点,来模拟一个圆
        var pointCount = 360;
        var pointData = [];
        var loop = 0;
        var alpha = 0; // 注意,这里的 α 单位是弧度,关于这个要温习一下初中数学
        var step = (2 * Math.pi) / 360; // 每一次增加的弧度 (1°)
        var x, y;
        for (loop = 0; loop < pointCount; loop++) {
            alpha = loop * step;
            x = Math.cos(alpha);
            y = Math.sin(alpha);
            pointData.push(x);
            pointData.push(y);
            // pointData.push([x,y]); WebGL 不喜欢这种数据格式 
        }
        //
        var pointArray = new Float32Array(pointData);
        var buffer_id;
        buffer_id = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id);
        gl.bufferData(gl.ARRAY_BUFFER, pointArray, gl.STATIC_DRAW);
    </script>

</body>

</html>

构造数据以及传入数据已经完成,下一步,绘制,留到下次吧~




本文正文结束,以下是答疑部分
小能能问:我看你上面,还是没有把颜色传进去呢?
  • 答:关于颜色怎么传进去,其实有很多做法。这其实就是他美妙的地方。下次课,我们先用一个简单的方法来传颜色。
小丫丫问:我就是不懂,但是我也不知道我那里不懂,怎么办?
  • 答:学习新知识本身就是这样的,最好的办法就是动脑子,使劲抠每一句话。
小瓜瓜问:上面算圆的坐标,用到了数学相关的知识,我需要去复习吗?
  • 答:对。WebGL 跟数学能力密不可分。这正是他好玩的地方。你可以把 WebGL当做一个数学实验室,学习到了什么数学知识,马上来这里画出来。多么美妙~比如说,画一个圆,画一个抛物线,画一个双曲线,画一个热度图,画一个柱状图。你为什么不用他来搞一个前端图表库呢,小瓜瓜!