《WebGL编程指南》笔记;
之前用 three.js 做了一些效果,有一些地方不是很懂,就想着从头开始学习一下WebGL,也能看看 three.js 到底封装了哪些操作;
本文主要记录了WebGL基础、着色器和缓冲区相关知识;
1、基础
1.1、着色器语言
- WebGL 是从 OpenGL ES 2.0 派生出来的,OpenGL ES 是 OpenGL 的一个特殊版本,移除了 OpenGL 中的一些旧特性;
- OpenGL 从 2.0 版本开始支持可编程着色器方法,编写着色器的语言称为着色器语言(shading language),OpenGL 着色器语言简称 GLSL;
- OpenGL ES 2.0 基于 OpenGL 2.0,所以也实现了其着色器语言,称为 OpenGL ES 着色器语言(GLSL ES);
- WebGL 基于 OpenGL ES 2.0,也使用 GLSL ES 编写着色器;
1.2、着色器
WebGL需要两种着色器,一种是顶点着色器(Vertex shader),一种是片元着色器(Fragment shader);
顶点着色器:用来描述顶点特性的程序,如位置、大小等;
片元着色器:进行逐片元处理过程如光照的程序,片元可以理解为像素,处理颜色(外观);
1.3、着色器代码
着色器代码在 js 中是当做一个字符串来处理的,
顶点着色器代码如下:
void main(){
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
gl_Position: vec4 类型,表示顶点的位置,必须被赋值;
gl_PointSize: float 类型,表示点的尺寸,默认1.0,
vec4 是由四个浮点数组成的矢量,vec4 vec4(x, y, z, w),由四个分量组成的矢量被称为齐次坐标,它能提高处理三维数据的效率,所以在三维图形系统中大量使用;
(x, y, z, w) 等价于三维坐标 (x/w, y/w, z/w),w 为 1.0 时,齐次坐标就可以表示“前三个分量为坐标值”的那个点;w 必须大于等于0,w 趋近于0,那么它表示的点将趋近无穷远;
片元着色器代码如下:
void main(){
gl_FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
gl_FragColor: vec4 类型,rgba格式,四个值范围为 0-1;
1.4、WebGL 绘制操作
gl.clearColor(0,0,0,1) // 绘制背景色,参数为 rgba,值范围为0-1;默认为(0.0, 0.0, 0.0, 0.0),背景色会保留,不管做什么操作,直到再一次调用 clearColor 的时候,颜色才会被重置;
gl.clearDepth(1.0) // 设置深度
gl.clearStencil(0) // 设置模板
g.clear(gl.COLOR_BUFFER_BIT) // 用之前指定的背景色清空绘制区域,可以清空多个,用位操作符 | 隔开;
gl.COLOR_BUFFER_BIT: 指定颜色缓存;
gl.DEPTH_BUFFER_BIT: 指定深度缓冲区
gl.STENCIL_BUFFER_BIT: 指定模板缓冲区
gl.drawArray(mode, first, count); // 执行顶点着色器,以mode参数指定的方式绘制图形;
mode: 绘制的方式,可接受以下常量符号,gl.POINTS、gl.LINES、gl.LINE_STRIP、gl.LINE_LOOP、gl.TRIANGLES、gl.TRIANGLE_STRIP、gl.TRIANGLE_FAN
first: 整型,指定从哪个顶点开始绘制;
count: 整型,指定绘制需要用到多少个顶点
1.5、坐标系
WebGL是右手坐标系,如下图
WebGL的中点 (0,0,0) 在 Canvas 的中心;整个空间宽度为2,即WebGL坐标系中,x、y、z的范围为[-1, 1];
因为 Canvas 坐标系和 WebGL 坐标系不一致,所以在操作中,用到坐标的地方需要将 Canvas 下的坐标转成 WebGL 下的坐标;
比如在点击操作中,Canvas 转 WebGL 代码如下:
var target = ev.target;
var left = target.offsetLeft, top = target.offsetTop;
var x0 = ev.clientX - left, y0 = ev.clientY - top;
var midW = canvas.width / 2, midH = canvas.height / 2;
var x = (x0 - midW) / midW, y = (midH - y0) / midH;
var actual = {x: x, y: y};
2、WebGL 往着色器中传值
使用 attribute 和 uniform 变量传值;attribute只能用在顶点着色器中,传输的是与顶点相关的数据;而uniform变量传输的是那些对于所有顶点都相同(或与顶点无关)的数据;
2.1、attribute 传值
2.1.1、在顶点着色器代码中定义变量
attribute vec4 a_Position;
void main(){
gl_Position = a_Position;
gl_PointSize = 10.0;
}
attribute: 存储限定符,attribute变量必须声明为全局,vec4 是类型;
2.1.2、js 中获取 attribute 变量的存储位置
gl.getAttribLocation(program, name)
program: 指定包含顶点着色器和片元着色器程序对象,暂时不用管;
name: attribute 变量名,比如上面的’a_Position’;
2.1.3、向 attribute 变量赋值
gl.vertexAttrib3f(location, v0, v1, v2)
location: 指定要修改的 attribute 变量的存储位置,2.1.2 中获取的返回值;
v0、v1、v2: 三个分量;
函数的返回值为变量在着色器代码中的顺序(一个数字),但这个顺序有点迷,比如分别按照顺序声明 a_Position1、a_Position0、a_Position 三个变量,并按该顺序使用,结果该方法返回值分别为2、0、1;如果没有找到,返回-1;
gl.vertexAttrib3f 同族函数
3f 中的 3: 参数个数;
3f 中的 f: float,浮点数;i – 整型;
vertexAttrib1i、vertexAttrib2i、vertexAttrib3f、vertexAttrib4f,均可用于 vec4 类型的传值,没传的会设置为默认值;
这些方法也有对应的矢量版本,在函数名最后加一个 “v”
var position = new Float32Array([1.0, 2.0, 3.0, 1.0]);
gl.vertexAttrib4fv(a_Position, position);
2.2、uniform 变量
2.2.1、在片元着色器代码中定义变量
precision mediump float;
uniform vec4 u_FragColor;
void main(){
gl_FragColor = u_FragColor;
}
uniform: 存储限定符,uniform 变量必须声明为全局,vec4 是类型;
precision mediump float; 这一句用精度限定词来指定变量的范围(最大值与最小值)和精度,本例为中等精度;
2.2.2、js 中获取 uniform 变量的存储位置
gl.getUniformLocation(program, name)
program: 指定包含顶点着色器和片元着色器程序对象,暂时不用管;
name: uniform 变量名,比如上面的 ‘u_FragColor’;
2.2.3、向 uniform 变量赋值
gl.uniform4f(location, v0, v1, v2, v3)
location: 指定要修改的 uniform 变量的存储位置,2.2.2 中获取的返回值;
该函数返回值为 uniform 变量的地址(一个对象),如果没找到,返回 null;
gl.uniform4f 和 2.1.3 中的 vertexAttrib3f 类似,也有同族函数
构成三维模型的基本单位是三角形
3、缓冲区对象
用于一次性传入多个点/值,之前的demo中,每次 drawArrays 渲染,都只绘制了一个点,如果绘制几个顶点组成的图形,就需要一次性将多个点传到顶点着色器中;
var arr = [-.5, .5, .3, .2];
var vertices = new Float32Array(arr);
// 创建缓冲区对象
var vertexBuffer = gl.createBuffer();
if (!vertexBuffer) return -1;
// 指定缓冲区对象的处理方法(gl.ARRAY_BUFFER)
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
// 将数据写入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 将缓冲区中的数据分配给变量
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 启用变量
gl.enableVertexAttribArray(a_Position);
0、类型化数组
引入的目的是为了优化性能,因为固定了类型(都是n位整数/无符号整数/单精度浮点数/双精度浮点数等);与普通数组不同,只能通过 new 来创建,支持方法、属性和常量如下:
get(index):获取第 index 个元素;
set(index, value):设置第 index 个元素的值为 value;
set(array, offset):从第offset个元素开始,把offset插入;
length:数组长度;
BYTES_PER_ELEMENT:数组中每个元素所占的字节数;Int8Array->1,Int16Array->2;用类型中的数字除8可得到这个值,32->4,64->8;
var farr = new Float32Array(3); 创建一个长度为3的类型化数组;
1、创建缓冲区对象
var vertexBuffer = gl.createBuffer():创建缓冲区对象
gl.deleteBuffer(vertexBuffer):删除缓冲区对象
2、绑定缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer),为 vertexBuffer 变量指定 webgl 的处理方法,即告诉 webgl 以什么格式(或用什么方法)来处理该缓冲区;
gl.ARRAY_BUFFER:缓冲区对象中包含顶点信息;
gl.ELEMENT_ARRAY_BUFFER:包含顶点的索引值
3、将数据写入缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW),
最后一个参数,gl.STATIC_DRAW 定义了程序将如何使用存储在缓冲区对象中的数据,设置错误不会带来异常,只是降低效率;
gl.STATIC_DRAW:只会向缓冲区对象中写入一次数据,但需要绘制多次;
gl.STREAM_DRAW:只会向缓冲区对象中写入一次数据,然后绘制若干次;
gl.DYNAMIC_DRAW:会向缓冲区对象中多次写入数据,并绘制多次;
4、将缓冲区中的数据分配给变量
gl.vertexAttribPointer(location, size, type, normalize, stride, offset)
location:attribute 变量(的位置);
size:表示传给该变量的分量个数(取值范围1到4),即按几位一组来从数组中取值传给变量;如果位数小于着色器中变量需要的位数,则对应的变量保持默认值;
type:数据格式
- gl.UNSIGNED_BYTE:无符号字节,UInt8Array;
- gl.SHORT:短整型,Int16Array;
- gl.UNSIGNED_SHORT:无符号短整型,UInt16Array;
- gl.INT:整型,Int32Array;
- gl.UNSIGNED_INT:无符号整型,UInt32Array;
- gl.FLOAT:浮点型,Float32Array;
normalize: false/true,是否将非浮点型的数据归化到[0,1]和[-1,0]的区间
stride:相邻两个顶点间的字节数(每组数据的字节大小);
offset:指定缓冲区对象中的偏移量(单位是字节),即在stride的基础上,从哪个字节开始给该变量分配值;如果是起始位置开始的,则应为0;
5、开启 attribute 变量
gl.enableVertexAttribArray(a_Position),为了让顶点着色器能否访问变量对应的缓冲区内的数据,开启后,就无法通过 vertexAttrib3f 这类函数传值了,必须显式关闭该变量才能再次起作用;
gl.disableVertexAttribArray(a_Position) // 关闭 attribute 变量
如果没有关闭就调用 vertexAttrib3f 这类函数,会出现值没有传过去的问题,具体效果可以查看“绘制之后第二次点击位置失效demo”,正常效果的demo在这里,可以对比查看;
6、根据传入的值绘制图形
用 gl.drawArray(mode, first, count) 的 mode 来控制,mode 取值如下:
- gl.POINTS:点,值为0;
- gl.LINES:线段,一系列线段,每两个点绘制一条,如果点的个数是奇数,忽略最后一个点,值为1;
- gl.LINE_STRIP:线条,一条线,按照点的顺序,连接起来,不闭合,连到最后一个点结束,值为3;
- gl.LINE_LOOP:回路,一条线,按照点的顺序,连接起来,闭合,最后一个点会再连接到第一个点,值为2;
- gl.TRIANGLES:三角形,一系列三角形,每3个点绘制一个,如果点的个数不是3的倍数,忽略余数,值为4;
- gl.TRIANGLE_STRIP:三角带,列带状的三角形,(v0, v1, v2) 组成一个三角形,(v2, v1, v3)组成一个三角形,依次类推,(v2, v1, v3)是为了保证第二个三角形也为逆时针方向绘制,值为5;
- gl.TRIANGLE_FAN:三角扇,由起点按顺序连接后边的相邻两个点组成的多个三角形,(v0, v1, v2)、(v0, v2, v3),值为6
即值 0-6 的顺序为:gl.POINTS, gl.LINES, gl.LINE_LOOP, gl.LINE_STRIP, gl.TRIANGLES, gl.TRIANGLE_STRIP, gl.TRIANGLE_FAN;
可以用 gl.TRIANGLE_STRIP 和 gl.TRIANGLE_FAN 来绘制矩形,两种模式下,点的顺序会有所差异,具体实现请 查看demo;
4、单缓冲区单数据给多个变量分配值
看下面的顶点着色器代码
attribute vec4 a_Position;
attribute float a_PointSize;
void main(){
gl_Position = a_Position;
gl_PointSize = a_PointSize;
}
如果绘制三个点,目前的做法是开两个缓冲区,分别传两类值(位置和大小),两个传值各自独立;
其实这种需求可以通过一个缓冲区一个数据集给多个变量分配值;
var pointArr = [
-.3, -.2, 10, // x坐标、y坐标、点的大小
0, -.2, 20,
.3, -.2, 30
];
var step = 3; // 数组中几个值一组来表示一个顶点
var vertices = new Float32Array(pointArr);
var fsize = vertices.BYTES_PER_ELEMENT; // 每个元素所占的字节数
var vertexBuffer = gl.createBuffer(); // 创建缓冲区对象
gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer); // 指定缓冲区对象的处理方法(gl.ARRAY_BUFFER)
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); // 将数据写入缓冲区对象
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, fsize * step, 0); // 将缓冲区中的数据分配给变量
gl.enableVertexAttribArray(a_Position); // 启用变量
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, fsize * step, fsize * 2);
gl.enableVertexAttribArray(a_PointSize);
gl.bindBuffer(gl.ARRAY_BUFFER, null); // 清空缓冲区
gl.uniform4f(u_FragColor, 0, 0, 255, 1); // 设置点的颜色
gl.drawArrays(gl.POINTS, 0, points.length); // 绘制点
上面的代码中,有一个新的点 vertices.BYTES_PER_ELEMENT,这个表示每个元素所占的字节数;其他基本处理逻辑和之前一样,核心代码是这两句,分别给两个变量分配值:
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, fsize * step, 0); // 将缓冲区中的数据分配给变量
gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, fsize * step, fsize * 2);
vertexAttribPointer(location, size, type, normalize, stride, offset) 方法中
参数 stride 中,两句都是 size * step,表示每组数据的字节大小,该例子中,三个数据为一组,每个数据占4个字节;
参数 offset 表示在 stride 的范围内,从哪个字节开始给该变量分配,分配的长度为第二个参数 size,分配字节数的话,要再乘以每个元素所占的字节数;
该例子的详细源代码在这儿。