本文标题:WebGL第五课:画点(三)-绘制
在前面的文章里,我们成功地把一个圆的坐标数据传到了 gl 里,那么这次课,我们就要真正做到绘制出来。
节奏必须慢,因为我们还有没解决的事情,现在不能贸然地将 shader 祭出来。如果你着急,直接翻到后面将代码一考,运行即可。
第一个问题,如果我们想改变圆的半径,用现有的知识怎么做
- 重新进行数学运算
- 重新算出一堆坐标
- 重新创建
gl buffer - 重新将坐标数据传给
gl buffer对吧,上面就是正规正常的做法,但是这个做法有缺陷: - 首先,
gl 的 api调用可不是没有消耗的,要尽量少调用gl 的 api。 - 其次,如果你用来模拟圆的坐标数量很大,比如说10000个点,那么数学运算那一步也将卡你半天。
- 最后,我要搞一个动画呢,圆的半径从小到大,然后从大到小,一秒30帧。这恐怕卡爆了。
此时,我才会告诉你,用 vertex shader 这玩意就可以。
vertex shader 驾到
肯定要先逐个解释一下单词的含义,vertex 顶点, shader着色器。
连起来:顶点着色器。
这东西就是一个运行于显卡中的一段程序,需要用户编写传入显卡,我们可以在这个程序里玩一玩花样,比如说,改变圆的半径。
下面给出这个程序的基本骨架:
// Vertex Shader
precision mediump int;
precision mediump float;
attribute vec2 a_PointVertex;
void main() {
gl_Position = vec4(a_PointVertex, 1.0, 1.0);
}
一切从main函数开始讲起:
gl_Position 这个变量显得格外令人讨厌,因为没有声明定义,怎么就出来一个这个变量了?
attribute vec2 a_PointVertex; 这又是啥?
我决定从 a_PointVertex 这个变量讲起:
从名字可以看出,他就是我们传给 gl 的 坐标点数据。然后它的变量类型是 vec2,这是个拥有两个元素的向量类型,因为我们当时传进来的 坐标数据就是xy的一个数组,其中每一个点都有两个数据x y。所以这里要用 vec2 这个变量来代表一个坐标点。
那么这个 a_PointVertex 到底是哪个点呢,是第一个点,还是最后一个点?我们有360个点,对吧。
实际上,显卡会将圆的所有坐标点全部传给这个程序,然后都要执行这个 main 函数,然后都会算出一个独立的 gl_Position。
你可以认为显卡就是在循环迭代每一个坐标点,然后每一个坐标点都要算出一个变量gl_Position。
而且,最好玩的就是,这个迭代的过程,显卡是并行的哟。几乎是每个点同时进行运算,这就是为啥在显卡里算这东西要比cpu快。
然后 gl_Position 这个变量,我们看出类型是 vec4 ,也就是拥有四个元素的向量。这东西为什么要有四个元素,后面的课程自会讲解。我们现在只需要知道,这东西的前两个变量分别代表,一个点经过运算之后真真正正的即将在 canvas 所要出现的位置。
例如:如果 gl_Position 是 vec4(0.3, 0.2, 0.0, 1.0), 那么就是说,一个坐标点数据,经过运算之后,出现在 canvas 的位置就是 xy(0.3, 0.2)。
这里就很奇怪,为什么要经过这个 vertex shader 运算一下呢?我本身的点的坐标就是好好的了,我不需要运算。
好的,如果你的点的坐标不需要运算,就直接:
gl_Position = vec4(a_PointVertex, 0.0, 1.0);
这样就好了。
如果我要把这个圆的半径变成0.5,那么 vertex shader 应该这样写:
// Vertex Shader
precision mediump int;
precision mediump float;
attribute vec2 a_PointVertex;
void main() {
gl_Position = vec4(a_PointVertex.x * 0.5, a_PointVertex.y * 0.5, 1.0, 1.0);
}
发现了吗,每个坐标点进去迭代运算的时候,我们都把 x 和 y 变成了原来的一半。那么可想而知,所有的点的 x 和 y 坐标都变成一半了,很明显,圆的半径也就变成一半了。
颜色到底在哪控制: fragment shader
先逐个解释一下单词的含义,fragment 片段, shader着色器。
连起来:片段着色器。
那么什么叫做片段(fragment)?
一句话描述:跟一个像素点相联系的所有数据,我们称之为一个 fragment。例如,xy坐标,颜色,等等。
从上面可以看出,这个东西就是用来最终产生颜色的地方了。先来看看这个东西长啥样:
// Fragment shader
precision mediump int;
precision mediump float;
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
跟 vertex shader差不多,不过这次,奇怪的变量叫做gl_FragColor。
这个也是一个 vec4类型的变量,前三个元素分别就是 R G B, 例如
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
这句话就是 RGB 全部都是0,也就是黑色。
gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
这句话就是 RGB 全部都是1,也就是白色。
以此类推。 第四个元素先忽略。写成1.0就好。
那么同样的,fragment shader也是要迭代每一个坐标数据的。但是我们上面的 fragment shader太简陋了,看不出运算的过程。我们只是写了一个固定的黑色进去。后面我们会讲解 fragment shader的高级用法,现在不急,我们就要画黑色,上面的程序已经满足了。
简易版 shader 流程图
相信经过上面的介绍,小伙伴们对 vertex shader 和 fragment shader 有了及其模糊的认识。
没事,请看下图,让你对整个过程,一目了然。
buffer_id 对应了显卡里的一个数据,就是我们存起来的那360个坐标,720个数据。
然后,我们人为地把每两个数据(一个坐标)画到一起,我们称之为一个vertex。
然后每个vertex都要经过vertex_shader 和 fragment_shader经过运算。
最后,得出的结果,就会绘制到我们的canvas里了。
上面的图画得还不够完整,但是对于在canvas上画一堆点来说,这张图够用了。
完整的程序,直接跑起来!
本课程中给出的代码都没有正确的错误处理,只是用来学习,请勿用于生产环境。
<!doctype html>
<html>
<head>
<style>
canvas {
border: 1px solid #000000;
}
</style>
</head>
<body>
<canvas id="point" style="width:300px; height:300px">
</canvas>
<script id="vertex_shader" type="myshader">
// Vertex Shader
precision mediump int;
precision mediump float;
attribute vec2 a_PointVertex;
void main() {
gl_Position = vec4(a_PointVertex, 0.0, 1.0);
gl_PointSize = 3.0;
}
</script>
<script id="fragment_shader" type="myshader">
// Fragment shader
precision mediump int;
precision mediump float;
void main() {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
</script>
<script type="text/javascript">
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) / pointCount; // 每一次增加的弧度 (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);
//
var vertex_shader_code = document.getElementById('vertex_shader').textContent;
console.log(vertex_shader_code);
var vertex_shader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertex_shader, vertex_shader_code);
gl.compileShader(vertex_shader);
//
var fragment_shader_code = document.getElementById('fragment_shader').textContent;
var fragment_shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragment_shader, fragment_shader_code);
gl.compileShader(fragment_shader);
//
var program = gl.createProgram();
gl.attachShader(program, vertex_shader);
gl.attachShader(program, fragment_shader);
gl.linkProgram(program);
gl.useProgram(program);
//
var a_PointVertex = gl.getAttribLocation(program, 'a_PointVertex');
gl.vertexAttribPointer(a_PointVertex, 2, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_PointVertex);
//
gl.drawArrays(gl.POINTS, 0, pointCount);
</script>
</body>
</html>
运行如下图:
代码中重要的概念讲解
program : 简单的理解就是,vertex_shader 和 fragment_shader组成了一个program。这两个部分缺一不可。
gl.drawArrays : 绘制。具体就是,让显卡将 buffer 中的每一个 坐标点 全部经过当前 program运算之后,得到 最终坐标和最终颜色。然后绘制到 canvas 上。
当前 program : gl 里可以同时存储多个 program,你想用哪一个 program对数据进行运算(绘制),就必须先 gl.useProgram。这又是状态机的思维。
vertex : 其实每一个坐标到这里可以称之为顶点了。这是个惯例,我们以后也用顶点这个名称。
attribute : 只能出现在vertex shader里面的变量种类。拥有这个修饰的变量,指的是存储在显卡中的顶点数据。这种数据有一个特点,就是以数组的形式存放在显卡中,vertex shader会对每一个数据进行计算。
gl.vertexAttribPointer : 这个要重点讲解一下。我们回顾一下,我们用来存储圆顶点的 buffer,长什么样:
所以每个顶点数据,在 vertex_shader里,我们用attribute vec2 a_PointVertex来接收。因为是两个数据,所以用 vec2。
但是真正告诉vertex shader,一个顶点数据是两个数据的,并不是这里的vec2,而是:
gl.vertexAttribPointer(a_PointVertex, 2, gl.FLOAT, false, 0, 0);
我们假设 gl 是一个公司的管理人员,vertex shader 是这家公司的员工,那么上面的代码,拟人化如下:
gl:
嘿,当前的 program 里面的 vertex shader啊,记住了,当前的buffer里面,数据是两个为一个整体的,你到时候迭代的时候别搞错了!!一定要用 vec2 去接收,千万不要用 vec3 或者 vec4!!!!
vertex shader:
好的,我知道用 vec2 去接收了,但是,当前的buffer是哪个啊?
gl:
你这个偷懒鬼,gl.bindBuffer(gl.ARRAY_BUFFER, buffer_id),这一句已经告诉你了!!给我记住,我是状态机!!
vertex shader:
哦,明白了。。
一个简单的办法来控制颜色
这个办法可以让我们控制每一个点的颜色,都不一样。
首先,我们头脑风暴一下:
- 我们将每个坐标的颜色就存到buffer里,用额外的三个数据RGB。
- 我们告诉
vertex shader, buffer 里的数据是五个一组的,并且 前两个是 xy 坐标,后三个是 RGB 颜色。 vertex shader用两个 attribute 来分别代表着两部分数据。vertex shader计算出 最终的坐标(gl_Position), 同时将 RGB 传给fragment shader。fragment shader利用 gl_Position 和 RGB 数据 画出最终的点。
完美~!~好像不太简单。。这么些个步骤。。。。。
先给出最终的代码:
<!doctype html>
<html>
<head>
<style>
canvas {
border: 1px solid #000000;
}
</style>
</head>
<body>
<canvas id="point" style="width:300px; height:300px">
</canvas>
<script id="vertex_shader" type="myshader">
// Vertex Shader
precision mediump int;
precision mediump float;
attribute vec2 a_PointVertex;
attribute vec3 a_PointColor;
varying vec3 color;
void main() {
gl_Position = vec4(a_PointVertex, 0.0, 1.0);
gl_PointSize = 2.0;
color = a_PointColor;
}
</script>
<script id="fragment_shader" type="myshader">
// Fragment shader
precision mediump int;
precision mediump float;
varying vec3 color;
void main() {
gl_FragColor = vec4(color, 1.0);
}
</script>
<script type="text/javascript">
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) / pointCount; // 每一次增加的弧度 (1°)
var x, y, _R, _G, _B;
for (loop = 0; loop < pointCount; loop++) {
alpha = loop * step;
x = Math.cos(alpha);
y = Math.sin(alpha);
pointData.push(x);
pointData.push(y);
_R = Math.random(); // 随机一个
_G = Math.random(); // 随机一个
_B = Math.random(); // 随机一个
pointData.push(_R);
pointData.push(_G);
pointData.push(_B);
}
//
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);
//
var vertex_shader_code = document.getElementById('vertex_shader').textContent;
console.log(vertex_shader_code);
var vertex_shader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertex_shader, vertex_shader_code);
gl.compileShader(vertex_shader);
//
var fragment_shader_code = document.getElementById('fragment_shader').textContent;
var fragment_shader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragment_shader, fragment_shader_code);
gl.compileShader(fragment_shader);
//
var program = gl.createProgram();
gl.attachShader(program, vertex_shader);
gl.attachShader(program, fragment_shader);
gl.linkProgram(program);
gl.useProgram(program);
//
var a_PointVertex = gl.getAttribLocation(program, 'a_PointVertex');
console.log('a_PointVertex', a_PointVertex);
gl.vertexAttribPointer(a_PointVertex, 2, gl.FLOAT, false, 5 * 4, 0);
gl.enableVertexAttribArray(a_PointVertex);
var a_PointColor = gl.getAttribLocation(program, 'a_PointColor');
console.log('a_PointColor', a_PointColor);
gl.vertexAttribPointer(a_PointColor, 3, gl.FLOAT, false, 5 * 4, 2 * 4);
gl.enableVertexAttribArray(a_PointColor);
//
gl.drawArrays(gl.POINTS, 0, pointCount);
</script>
</body>
</html>
这个运行之后:
刷新网页,每一次都不一样,因为我们 RGB 颜色是用的 Math.random() 生成的。
小伙伴们,使劲研究一下代码,看一下,到底 buffer 和 attribute 是怎么对应和控制的。
我这里更新一下以上代码的 buffer 图:
正文结束,下面是答疑