WebGL笔记(一)着色器和缓冲区

《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 类似,也有同族函数


demo地址源代码地址


构成三维模型的基本单位是三角形

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,分配字节数的话,要再乘以每个元素所占的字节数;

该例子的详细源代码在这儿

如果这篇文章对你有用,可以点击下面的按钮告诉我

2

发表回复