《WebGL权威指南》学习笔记-基础

/post/webgl-basic article cover image

概述

WebGL(Web Graphic Library的缩写)是一项结合HTML5和JavaScript在网页上绘制和渲染复杂三维图形,并允许用户与之进行交互的技术。WebGL完全集成到浏览器的所有网页标准中,可将影像处理和效果的GPU加速使用方式当做网页Canvas的一部分。WebGL元素可以加入其他HTML元素之中并与网页或网页背景的其他部分混合。

优势

  • 跨平台性(支持web项目的设备均可以)
  • 开发便捷(只需要编辑器和浏览器)
  • 可以充分利用浏览器的功能
  • 互联网拥有大量县城资料帮助学习WebGL

起源

个人计算机使用最广泛的两种三维图形渲染技术是Direct3D(微软控制的编程接口,主要用于Windows)和OpenGL(开源且免费,可在不同系统设备上使用,Windows也对OpenGL提供了良好的支持,开发者可以用它来替换Direct3D)。

OpenGL最初由SGI(Silicon Graphics Inc)开发,并于1992年发布为开源标准。WebGL虽然根植于OpenGL但是实际上是从OpenGL的一个特殊版本OpenGL ES中派生出来的.OpenGL ES成功被这些设备采用的部分原因是,它在添加新特性的同时从OpenGL中移除了许多陈旧无用的旧特性,这使得保持轻量级的同时,仍具有足够的能力来渲染出精美的三维图形。

从2.0版本开始,OpenGL支持了一项非常重要的特性,即可编程着色器方法(programmable shader functions)。该特性被OpenGL ES 2.0继承,并成为了WebGL1.0标准的核心部分。

webgl程序结构

除了传统的HTML、JAVASCRIPT外WebGL页面增加了GLSL ES,因为通常GLSL ES是以字符串的形式在js文件中编写的,实际上WebGL程序也只需要用到HTML文件和JS文件

基本用法

WebGL程序绘图必须使用着色器(Shader)是用来替代固定渲染管线的可编辑程序用以实现图像渲染。在WebGL中着色器程序是以字符串的形式"潜入"在JavaScript文件中的

js
// 顶点着色器程序
const VSHADER_SOURCE = `
  void main() {
    gl_Position = vec4(0.0, 0.0, 0.0, 1.0); // 设置坐标
    gl_pointSize = 10.0; // 设置尺寸
  }
`

// 片元着色器程序
const FSHADER_SOURCE = `
  viod main() {
    gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0); // 设置颜色
  }
`

着色器

WebGL中主要有顶点着色器(Vertex Shader)和片元着色器(Fragment Shader):

  • 顶点着色器: 是用来描述顶点特性(如位置、颜色等)的程序。顶点(vertex)是指二维或三维空间中的一个点,比如三维或三维图形的端点或交点。
    • vec4 gl_Position: 表示顶点位置
    • float gl_PointSize: 表示点的尺寸
  • 片元着色器: 进行逐片元处理过程如光照的程序。片元(fragment)是一个WebGL术语,可以理解为像素(片元包括这个像素的位置、颜色和其他信息)。
    • vec4 gl_FragColor: 指定片元颜色

vec4由4个分量组成的矢量被称为 齐次坐标 (x,y,z,w)等价于三维坐标(x/w,y/w,z/w)

WebGL程序的执行流程:

webgl-process

内置常量

可通过WebGL上下文访问

绘制方式:

  • PINTS
  • LINES
  • LINE_STRIP
  • LINE_LOOP
  • TRIANGLES
  • TRIANGLE_STRIP
  • TRIANGLE_FAN

缓冲区相关:

  • COLOR_BUFFER_BIT 指定颜色缓存
  • DEPTH_BUFFER_BIT 指定深度缓冲区
  • STENCIL_BUFFER_BIT 指定模版缓冲区

坐标系统

三维坐标系统(笛卡尔坐标系)具有X(水平:正方向为右)、Y(垂直:正方向为下)、Z轴(垂直与屏幕:正方向为外),这套坐标系又被称为右手坐标系(right-handed coordinate system)

WebGL的坐标系和canvas绘图去的坐标系不同,需要将前者映射到后者。

  • canvas中心点(0.0, 0.0, 0.0)
  • canvas的上边缘和下边缘: (0.0, 1.0, 0.0)和(0.0, -1.0, 0.0)
  • canvas的左边缘和右边缘: (1.0, 0.0, 0.0)和(-1.0, 0.0, 0.0)

信息传递

从javascript程序中传递参数给顶点着色器,attribute(与顶点相关的数据)对应顶点着色器和uniform(与顶点无关对于所有顶点都相同的数据)对应片元着色器

顶点着色器信息传递使用getAttribLocation获取位置存储位置,使用attrib3f系列函数设置位置参数

片元着色器信息传递使用getUniformLocation获取位置存储位置,使用uniform3f系列函数设置颜色参数

js
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' + // attribute variable
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  gl_PointSize = 10.0;\n' +
  '}\n';

var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform vec4 u_FragColor;\n' +  // uniform変数
  'void main() {\n' +
  '  gl_FragColor = u_FragColor;\n' +
  '}\n';

// 获取顶点位置属性存储地址
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
if (a_Position < 0) {
  console.log('Failed to get the storage location of a_Position');
  return;
}

// 获取颜色属性存储地址
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor');
if (!u_FragColor) {
  console.log('Failed to get the storage location of u_FragColor');
  return;
}

// 设置位置存储地址变量, 当类型为vec4时会自动补1.0
gl.vertexAttrib3f(a_Position, 0.0, 0.5, 0.0)

// 设置颜色存储地址变量,当类型为vec4时会自动补1.o
gl.uniform3f(u_FragColor, 0.0, 0.0, 1.0)

// ...

attribute变量由三部分组成:存储限定符[attribute]类型[vec4]、变量名[a_Position],WebGL函数gl.vertexAttrib3f()命名规范b3f代表vertexAttrib(基础函数)3(参数个数)f(参数类型)

uniform变量由三部分组成:存储限定符[uniform]类型[vec4]、变量名[u_FragColor],WebGL函数gl.uniform3f()命名规范代表uniform3f(基础函数)3(参数个数)f(参数类型)

其系列同族函数如下:

js
gl.vertexAttrib1f(location, v0)
gl.vertexAttrib2f(location, v0, v1)
gl.vertexAttrib3f(location, v0, v1, v2)
gl.vertexAttrib4f(location, v0, v1, v2. v3)

var positions = new Float32Array([1.0, 2.0, 3.0, 1.0])
gl.vertexAttrib3f(a_Poisition, positions)

gl.uniform1f(location, v0)
gl.uniform2f(location, v0, v1)
gl.uniform3f(location, v0, v1, v2)
gl.uniform4f(location, v0, v1, v2. v3)

var colorFa = new Float32Array([0.0, 0.0, 1.0, 1.0])
gl.uniform3f(u_FragColor, colorFa)

<Callout>当绘制点之后,颜色缓冲区会被WebGL重置,所以在每次绘制之前都要调用gl.clear(gl.COLOR_BUFFER_BIT)来指定的背景色清空</Callout>

绘制图形

缓冲区buffer-object

缓冲区对象是WebGL系统中的一块内存区域用以一次性向内存区对象中填充大量的顶点数据,保存其中供顶点着色器使用

js
function initVertexBuffers(gl) {
	// 类型化数组提升运行效率,通常用来从存储顶点坐标或颜色数据
  var vertices = new Float32Array([
    0.0, 0.5, -0.5, -0.5, 0.5, 0.5
  ])
  var n = 3;

  // 绑定缓冲区对象到目标(向顶点着色器提供传给attribute变量的数据)
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer)
  // 向缓冲区对象写入数据(不能直接向缓冲区写数据只能向绑定缓冲区的目标写入)
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW)

  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position')
    return -1
  }

  // 将缓冲区对象赋值给attribute位置变量
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0)

  // 连接顶点位置变量和分配给它的缓冲区对象
  gl.enableVertexAttribArray(a_Position)

  return n;
}

缓冲区操作大致分为:

  1. 使用createBuffer创建缓冲区
  2. 使用bindBuffer绑定缓冲区对象到目标
  3. 使用bufferData向缓冲区对象写入数据.
  4. 使用vertexAttribPointer将缓冲区对象分配给attribute变量
  5. 使用enableVertexAttribArray激活attribute变量,使缓冲区对象和顶点变量建立连接
js
var gl = getWebGLContext(canvas)

// 创建缓冲区
var vertexBuffer = gl.createBuffer()
if (!vertexBuffer) {
	console.log('Failed to create the buffer object')
  return -1
}

var n = initVertexBuffers(gl)

// 绘制三个点
gl.drawArrays(gl.POINTS, 0, n)

<Callout>虽然函数名称是用来处理顶点数组的,但实际上处理的对象是缓冲区</Callout>

基本图形

WebGL的基本图形可以通过drawArrays(mode)来设置且只有三种: 点、线段和三角形,从球体到立方体再到游戏中的三位角色都可以由小的三角形组成

  • gl.Points 点
  • gl.LINES 线段
  • gl.LINE_STRIP 线条
  • gl.LINE_LOOP 回路
  • gl.TRIANGLES 三角形
  • gl.TRIANGLE_STRIP 三角带
  • gl.TRIANGLE_FAN 三角扇

图形绘制顺序示意图:

shape-connect

js
//绘制矩形
function initVertexBuffers() {
	var vertices = new Float32Array([
    -0.5, 0.5, -0.5, -0.5,  0.5, 0.5, 0.5, -0.5
  ]);
  var n = 4;
	// ...
  gl.drawArrays(gl.TRIANGLES, 0, 4)
  //通过图形连接次序的变化形状
  gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)
  return n;
}

移动旋转和缩放

移动:通过将两个vec4类型的变量(a_Position+u_Translation)相加得到最终齐坐标矢量(顶点位置),移动是逐顶点操作(per-vertex operation)而非片元操作,发生在顶点着色器.

js
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform vec4 u_Translation;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position + u_Translation;\n' +
  '}\n';

// Fragment shader program
var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
  '}\n';
var Tx = 0.5, Ty = 0.5, Tz = 0.0;
var gl = getWebGLContext(canvas)
// ...
var n = initVertexBuffers(gl);

// 将平移距离传输给顶点着色器
var u_Translation = gl.getUniformLocation(gl.program, 'u_Translation');
// ...
gl.uniform4f(u_Translation, Tx, Ty, Tz, 0.0);
// ...
// 绘制三个点
gl.drawArrays(gl.TRIANGLES, 0, n)

function initVertexBuffers(gl) {
	var vertices = new Float32Array([
    0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ]);
  var n = 3; // The number of vertices
  // ...
  return n;
}

<Callout type="info">vec4 a_Position + vec4 u_Translation = x1+x2, y1+y2, z1+z2, w1+w2; 移动后还是一个点位置坐标所以w1+w2必须为1.0</Callout>

旋转:描述一个旋转需要指明:

  • 旋转轴(图形将围绕旋转轴旋转)
  • 旋转方向(方向:顺时针或逆时针)
  • 旋转角度(图形旋转经过的角度)

围绕z轴逆时针旋转,z值不变x和y相应改变(ANGLE正值为逆时针,负值为顺时针):

js
var VSHADER_SOURCE =
  // x' = x cosβ - y sinβ
  // y' = x sinβ + y cosβ Equation 3.3
  // z' = z
  'attribute vec4 a_Position;\n' +
  'uniform float u_CosB, u_SinB;\n' +
  'void main() {\n' +
  '  gl_Position.x = a_Position.x * u_CosB - a_Position.y * u_SinB;\n' +
  '  gl_Position.y = a_Position.x * u_SinB + a_Position.y * u_CosB;\n' +
  '  gl_Position.z = a_Position.z;\n' +
  '  gl_Position.w = 1.0;\n' +
  '}\n';
// ...iniVertexBuffers
var ANGLE = 90.0;
var radian = Math.PI * ANGLE / 180.0; // Convert to radians
var cosB = Math.cos(radian);
var sinB = Math.sin(radian);
var u_CosB = gl.getUniformLocation(gl.program, 'u_CosB');
var u_SinB = gl.getUniformLocation(gl.program, 'u_SinB');
if (!u_CosB || !u_SinB) {
  console.log('Failed to get the storage location of u_CosB or u_SinB');
  return;
}
gl.uniform1f(u_CosB, cosB);
gl.uniform1f(u_SinB, sinB);

gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLES, 0, n);

<Callout type="info">事先计算出90度的正弦值和余弦值,再传给顶点着色器的两个uniform变量最终通过公式计算出旋转角度</Callout>

变换矩阵(Transformation matrix):矩阵 * 矢量 = 新的矢量,只有在矩阵的列数与矢量的行数相等时,才可以将两者相乘.

text
x' a b c x
y' d e f y
z' g h i z

x' = ax + by + cz
y' = dx + ey + fz
z' = gx + hy + iz

和公式相等
x' = ax + by +cz
x' = x cosβ - y sinβ
y' = dx + ey + fz
y' = x sinβ + y cosβ

旋转变换
x' = cosβx -sinβy 0
y' = sinβx cosβy 0
z' = 0 0 1

使用旋转矩阵改写:

js
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform mat4 u_xformMatrix;\n' +
  'void main() {\n' +
  '  gl_Position = u_xformMatrix * a_Position;\n' +
  '}\n';

// Create a rotation matrix
var ANGLE = 90;
var radian = Math.PI * ANGLE / 180.0; // Convert to radians
var cosB = Math.cos(radian), sinB = Math.sin(radian);

// Note: WebGL is column major order
var xformMatrix = new Float32Array([
   cosB, sinB, 0.0, 0.0,
  -sinB, cosB, 0.0, 0.0,
    0.0,  0.0, 1.0, 0.0,
    0.0,  0.0, 0.0, 1.0
]);

// Pass the rotation matrix to the vertex shader
var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
if (!u_xformMatrix) {
  console.log('Failed to get the storage location of u_xformMatrix');
  return;
}
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);

缩放:使用矩阵乘以矢量(缩放因子)的形式进行缩放

js
//...
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform mat4 u_xformMatrix;\n' +
  'void main() {\n' +
  '  gl_Position = u_xformMatrix * a_Position;\n' +
  '}\n';

// Fragment shader program
var FSHADER_SOURCE =
  'void main() {\n' +
  '  gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n' +
  '}\n';

var Sx = 1.0, Sy = 1.5, Sz = 1.0;
var xformMatrix = new Float32Array([
  Sx,   0.0,  0.0,  0.0,
  0.0,  Sy,   0.0,  0.0,
  0.0,  0.0,  Sz,   0.0,
  0.0,  0.0,  0.0,  1.0
]);
var u_xformMatrix = gl.getUniformLocation(gl.program, 'u_xformMatrix');
gl.uniformMatrix4fv(u_xformMatrix, false, xformMatrix);
//...

总结:在坐标中的变换都是通过更改顶点坐标来达到的,这章一共介绍了4种方式:

  1. 使用逐顶点操作(per-vertex operation):矢量分别相加常量x' y' z' > x + Tx y + Ty z + Tz
  2. 使用数学表达式通过角度计算移动后的坐标:x' = x * cos(β) - y * sin(β)y' = x * sin(β) + y * cos(β),在复杂情形下相对繁琐
  3. 使用变换矩阵(旋转矩阵):矩阵乘以矢量x' = ax + by + cz再通过公式转换为x' = x * cosβ - y * sinβ,利用着色器支持矩阵矢量相乘的功能进行公式套用转换更为灵活方便
  4. 使用4x4的旋转矩阵:<新坐标> = <变换矩阵> * <旧坐标>的形式gl_Position = u_xformMatrix * a_Position注意webgl矩阵是按列主序存储

高级变换

主要对图形的变换操作通过matrix封装库来完成,cuon-matrix.js主要包括两类方法带有set前缀的和没前缀的其主要区别在于set函数直接计算出变换矩阵并写入到自身中,而非set函数除了计算变换矩阵外还与存储到Matrix中的矩阵相乘并写入(例如复合矩阵的场景)

模型矩阵

通过公式变换计算出新矩阵的形式被称为模型变换或建模变换(model transformation)模型变换(modeling transformation)的矩阵被称为模型矩阵(model matrix),常用的复合坐标方程式演变如下:

  1. <平移后的坐标> = <平移矩阵> x <原始坐标>
  2. <平移并旋转后的坐标> = <旋转矩阵> x <平移后坐标>
  3. <平移并旋转后的坐标> = <旋转矩阵> x (<平移矩阵> x <原始坐标>)
  4. <平移并旋转后的坐标> = (<旋转矩阵> x <平移矩阵>) x <原始坐标>
js
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'uniform mat4 u_ModelMatrix;\n' +
  'void main() {\n' +
  '  gl_Position = u_ModelMatrix * a_Position;\n' +
  '}\n';

var modelMatrix = new Matrix4();
var ANGLE = 60.0;
var Tx = 0.5;

// 设置模型矩阵为旋转矩阵
modelMatrix.setRotate(ANGLE, 0, 0, 1);
// 将模型矩阵乘以平移矩阵
modelMatrix.translate(Tx, 0, 0);

// 调换执行顺序时效果不同
// modelMatrix.setTranslate(Tx, 0, 0)
// modelMatrix.rotate(ANGLE, 0, 0, 1)

var u_ModelMatrix = gl.getUniformLocation(gl.program, 'u_ModelMatrix');
gl.uniformMatrix4fv(u_ModelMatrix, false, modelMatrix.elements);

颜色与纹理

创建多缓存区

通过为顶点的每种数据建立一个缓冲器,然后分配给对应的attribute变量,就可以向顶点着色器传递多份逐顶点的数据信息了.

js
function initVertexBuffers(gl) {
  var vertices = new Float32Array([
    0.0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ]);
  var n = 3;

  var sizes = new Float32Array([
    10.0, 20.0, 30.0  // Point sizes
  ]);

  // Create a buffer object
  var vertexBuffer = gl.createBuffer();
  var sizeBuffer = gl.createBuffer();

  // Write vertex coordinates to the buffer object and enable it
  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);

  // Bind the point size buffer object to target
  gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STATIC_DRAW);
  var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');

  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(a_PointSize);

  // Unbind the buffer object
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

这种形式比较适合数据量不大的情况,对于大量顶点的场景时可以通过将集中逐顶点的数据(坐标和尺寸)交叉存储在一个数组中的交错组织(interleaving),并将数组写入一个缓冲区对象,WebGL就需要有差别地从缓冲区中获取某种特定数据(坐标或尺寸),就需要使用gl.wertexAttribPointer()函数的第5个参数stride和第6个参数offset

js
function initVertexBuffers(gl) {
  var n = 3;
  var verticesSizes = new Float32Array([
     0.0,  0.5,  10.0,  // the 1st point
    -0.5, -0.5,  20.0,  // the 2nd point
     0.5, -0.5,  30.0   // the 3rd point
  ]);

  var vertexSizeBuffer = gl.createBuffer();

  // Bind the buffer object to target
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexSizeBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, verticesSizes, gl.STATIC_DRAW);

  var FSIZE = verticesSizes.BYTES_PER_ELEMENT;

  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 3, 0);
  gl.enableVertexAttribArray(a_Position);

  var a_PointSize = gl.getAttribLocation(gl.program, 'a_PointSize');
  gl.vertexAttribPointer(a_PointSize, 1, gl.FLOAT, false, FSIZE * 3, FSIZE * 2)
  gl.enableVertexAttribArray(a_PointSize);
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

gl.vertexAttribPointer(location, size, type, normalize, stride, offset): 其中stride代表两个顶点间的距离(3 * 类型数组中每个字节的长度),offset代表当前数据项距离首个元素的距离,以此区分当前赋值顶点操作是坐标还是尺寸

图形装配和光栅化

在顶点着色器和片元着色器之间还存在以下两个步骤:

  • 图形装配过程: 将孤立的顶点坐标装配成几何图形,几何图形的类别由gl.drawArrays()函数的第一个参数决定
  • 光栅化过程: 将装配好的几何图形转化为片元

几何图形装配过程又被成为图元装配过程(primitive assembly process),因为被装配出的基本图形(点、线、面)又被成为图元(primitive),从几何图形装配到图元装配的过程被称为光栅化(rasterization)

片元的颜色取决于他的坐标位置,所以片元颜色会随着片元位置逐渐变化,三角形呈现平滑的颜色渐变效果

js
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '}\n';
// Fragment shader program
var FSHADER_SOURCE =
  'precision mediump float;\n' +
  'uniform float u_Width;\n' +
  'uniform float u_Height;\n' +
  'void main() {\n' +
  '  gl_FragColor = vec4(gl_FragCoord.x/u_Width, 0.0, gl_FragCoord.y/u_Height, 1.0);\n' +
  '}\n';

function initVertexBuffers(gl) {
  var vertices = new Float32Array([
    0, 0.5,   -0.5, -0.5,   0.5, -0.5
  ]);
  var n = 3; // The number of vertices

  // Create a buffer object
  var vertexBuffer = gl.createBuffer();
  if (!vertexBuffer) {
    console.log('Failed to create the buffer object');
    return -1;
  }

  // Bind the buffer object to target
  gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);
  // Write date into the buffer object
  gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

  // Pass the position of a point to a_Position variable
  var a_Position = gl.getAttribLocation(gl.program, 'a_Position');
  if (a_Position < 0) {
    console.log('Failed to get the storage location of a_Position');
    return -1;
  }
  gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

  var u_Width = gl.getUniformLocation(gl.program, 'u_Width');
  if (!u_Width) {
    console.log('Failed to get the storage location of u_Width');
    return;
  }

  var u_Height = gl.getUniformLocation(gl.program, 'u_Height');
  if (!u_Height) {
    console.log('Failed to get the storage location of u_Height');
    return;
  }

  // Pass the width and hight of the <canvas>
  gl.uniform1f(u_Width, gl.drawingBufferWidth);
  gl.uniform1f(u_Height, gl.drawingBufferHeight);

  // Enable the generic vertex attribute array
  gl.enableVertexAttribArray(a_Position);

  // Unbind the buffer object
  gl.bindBuffer(gl.ARRAY_BUFFER, null);

  return n;
}

图形装配过程是指根据gl.drawArray(type, 0, n)第三个参数n来决定传递几个顶点数据到缓冲区,等所有顶点装载完毕时再将图形(点、线、面)转化为片元这个过程就是光栅化

根据几何图形类型的不同片元产生的数量也不同,填充片元的过程中又根据其坐标位置发生了内插过程(interpolation process)比如RGBA中的R值从1.0降至0.0从原点到终点的颜色值都会被恰当计算出来并传给片元着色器,故而三角形会呈现出平滑渐变的效果

纹理映射

将一张图像映射到一个几何图形表面上又称为贴图,这张图片又称为纹理图像(texture image)纹理(texture),为之前光栅化后的每一片元涂上合适的颜色组成纹理图像的像素又被称为纹素(texels, texture elements),每个纹素的颜色使用RGBRGBA格式

纹理映射步骤如下:

  1. 准备好映射到几何图形上的纹理图像
  2. 为几何图形配置纹理映射方式
  3. 加载纹理图像,对其进行一些配置,以在WebGL中使用它
  4. 在片元着色器中将对应的纹素从纹理中抽取出来,并将纹素的颜色赋给片元
js
var verticesTexCoords = new Float32Array([
  // Vertex coordinates, texture coordinate
  -0.5,  0.5,   0.0, 1.0,
  -0.5, -0.5,   0.0, 0.0,
  0.5,  0.5,   1.0, 1.0,
  0.5, -0.5,   1.0, 0.0,
]);
var n = 4; // The number of vertices

// Create the buffer object
var vertexTexCoordBuffer = gl.createBuffer();

// Bind the buffer object to target
gl.bindBuffer(gl.ARRAY_BUFFER, vertexTexCoordBuffer);
gl.bufferData(gl.ARRAY_BUFFER, verticesTexCoords, gl.STATIC_DRAW);

var FSIZE = verticesTexCoords.BYTES_PER_ELEMENT;
//Get the storage location of a_Position, assign and enable buffer
var a_Position = gl.getAttribLocation(gl.program, 'a_Position');

gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, FSIZE * 4, 0);
gl.enableVertexAttribArray(a_Position);  // Enable the assignment of the buffer object

// Get the storage location of a_TexCoord
var a_TexCoord = gl.getAttribLocation(gl.program, 'a_TexCoord');

// Assign the buffer object to a_TexCoord variable
gl.vertexAttribPointer(a_TexCoord, 2, gl.FLOAT, false, FSIZE * 4, FSIZE * 2);
gl.enableVertexAttribArray(a_TexCoord);  // Enable the assignment of the buffer object

return n;

通过顶点坐标到纹理坐标的映射: 类型数组的前后两位相互对应,顶点坐标-0.5. 0.5 > 纹理坐标 0.0, 1.0一次类推,此时纹理坐标的四个角位置就会按照顶点坐标展示出来

因为图片坐标系统WebGL纹理坐标系统方向相反,坐标传递后还需要对纹理进行配置:

js
// 设置Y轴反转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
// 激活纹理单元TEXTURE0-7,这里只有一个
gl.activeTexture(gl.TEXTURE0);
// 绑定纹理对象2d还是3d(TEXTURE_2D、TEXTURE_CUBE_MAP)
gl.bindTexture(gl.TEXTURE_2D, texture);

// 配置纹理对象的参数,第二个值为填充方式
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
// 将纹理图像分配给纹理对象
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);

// 将纹理单元传递给片元着色器
gl.uniform1i(u_Sampler, 0);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);

从顶点着色器通过varying变量向片元着色器传输纹理坐标,并使用texture2D(纹理单元编号,纹理坐标)来抽取纹素颜色

js
var VSHADER_SOURCE =
  'attribute vec4 a_Position;\n' +
  'attribute vec2 a_TexCoord;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main() {\n' +
  '  gl_Position = a_Position;\n' +
  '  v_TexCoord = a_TexCoord;\n' +
  '}\n';

var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'uniform sampler2D u_Sampler;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main() {\n' +
  '  gl_FragColor = texture2D(u_Sampler, v_TexCoord);\n' +
  '}\n';

多个纹素的情况类似

js
//...
var FSHADER_SOURCE =
  '#ifdef GL_ES\n' +
  'precision mediump float;\n' +
  '#endif\n' +
  'uniform sampler2D u_Sampler0;\n' +
  'uniform sampler2D u_Sampler1;\n' +
  'varying vec2 v_TexCoord;\n' +
  'void main() {\n' +
  '  vec4 color0 = texture2D(u_Sampler0, v_TexCoord);\n' +
  '  vec4 color1 = texture2D(u_Sampler1, v_TexCoord);\n' +
  '  gl_FragColor = color0 * color1;\n' +
  '}\n';

function initVertexBuffers(gl) {
 var texture0 = gl.createTexture();
  var texture1 = gl.createTexture();
  if (!texture0 || !texture1) {
    console.log('Failed to create the texture object');
    return false;
  }

  var u_Sampler0 = gl.getUniformLocation(gl.program, 'u_Sampler0');
  var u_Sampler1 = gl.getUniformLocation(gl.program, 'u_Sampler1');
  if (!u_Sampler0 || !u_Sampler1) {
    console.log('Failed to get the storage location of u_Sampler');
    return false;
  }

  var image0 = new Image();
  var image1 = new Image();
  if (!image0 || !image1) {
    console.log('Failed to create the image object');
    return false;
  }
  image0.onload = function(){ loadTexture(gl, n, texture0, u_Sampler0, image0, 0); };
  image1.onload = function(){ loadTexture(gl, n, texture1, u_Sampler1, image1, 1); };

  image0.src = '../resources/sky.jpg';
  image1.src = '../resources/circle.gif';

  return true;
}

var g_texUnit0 = false, g_texUnit1 = false;
function loadTexture(gl, n, texture, u_Sampler, image, texUnit) {
  //...
  if (texUnit == 0) {
    gl.activeTexture(gl.TEXTURE0);
    g_texUnit0 = true;
  } else {
    gl.activeTexture(gl.TEXTURE1);
    g_texUnit1 = true;
  }
  // Bind the texture object to the target
  gl.bindTexture(gl.TEXTURE_2D, texture);

  // ...
  gl.uniform1i(u_Sampler, texUnit);
  gl.clear(gl.COLOR_BUFFER_BIT);

  if (g_texUnit0 && g_texUnit1) {
    gl.drawArrays(gl.TRIANGLE_STRIP, 0, n);   // Draw the rectangle
  }
}
//...