本文标题: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当做一个数学实验室,学习到了什么数学知识,马上来这里画出来。多么美妙~比如说,画一个圆,画一个抛物线,画一个双曲线,画一个热度图,画一个柱状图。你为什么不用他来搞一个
前端图表库
呢,小瓜瓜!