• 本篇文章侧重 WebGL 基础,非常适合没有相关开发经验的同学。
  • 为了更好地了解一些 WebGL 的基础概念和特性,精心准备了几段代码进行演示,希望大家能有所收获。
  • 文章中会涉及到一些数学公式的推导,不感兴趣可以忽略。

简介

WebGL 是什么

WebGL,全称 Web Graphics Library,是一种用于在网页浏览器中渲染 2D 和 3D 图形的 API。它基于 OpenGL ES 2.0,通过 JavaScript 在不使用插件的情况下,直接利用 GPU 进行硬件加速渲染。

WebGL 是现代浏览器(如 Chrome、Firefox、Safari)支持的标准,能够在 PC、手机等多平台上运行。

WebGL 的应用场景

游戏开发:许多浏览器游戏通过 WebGL 实现复杂的 3D 场景和交互效果。 图形/游戏引擎

数据可视化:使用 WebGL 可以在网页上实现高性能的 3D 数据可视化工具。

交互式 3D 网页:可以实现像 3D 地图、虚拟展览等内容。百度地图

VR/AR:结合 WebGL 和 WebXR API,可以在网页上开发虚拟现实和增强现实应用。VR 体验房间展示

WebGL 的基础概念

OpenGL 与 WebGL 的关系

OpenGL(Open Graphics Library)是一个跨语言、跨平台的图形 API,主要用于2D和3D图形的渲染。它广泛应用于游戏开发、CAD、虚拟现实和可视化等领域。OpenGL ESOpenGL 的一个精简版本,专门为嵌入式系统(如手机、平板)设计。

WebGL (Web Graphics Library)是基于 OpenGL ES 2.0 的一种 API,专门为网页设计,允许开发者在不需要插件的情况下通过 JavaScript 进行编程在浏览器中进行3D图形渲染。WebGL 使得网页应用能够使用 GPU 加速,提供了对图形渲染的高效支持。

着色器编程 (GLSL)

着色器编程是图形编程中的核心部分,使用 GLSL(OpenGL Shading Language)编写。GLSL 是一种高性能的着色器语言,允许开发者在 GPU 上执行自定义的图形计算。

顶点着色器(Vertex Shader),用来描述顶点的特性,如描述顶点的位置。顶点就是指二维或三维空间中的一个坐标。

片元着色器(Fragment Shader),用来描述每个像素的颜色。

着色器程序在 JavaScript 中就是一段字符串,着色器程序的工作流程就是 JavaScript 读取相关着色器信息,并传递给 WebGL 进行使用。

渲染基础

点的绘制

绘制一个点

通过一个绘制一个点来窥探 WebGL 绘图的流程。

在画布中心绘制一个红色的点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 获取 canvas 元素并获取 webgl 的上下文
const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

// 顶点着色器代码
const VERTEX_SHADER_SOURCE = `
void main() {
gl_Position = vec4(0.0, 0.0, 0.0, 1.0);
gl_PointSize = 10.0;
}
`
// gl_Position vec4(0.0,0.0,0.0,1.0) x, y, z, w齐次坐标 (x/w, y/w, z/w)


// 片元着色器代码
const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
`
// gl_FragColor vec4(1.0,0.0,0.0,1.0) r, g, b, a

// 创建着色器
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

// 为着色器指定源码
gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE)
gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE)

// 编译着色器
gl.compileShader(vertexShader)
gl.compileShader(fragmentShader)

// 创建程序对象
const program = gl.createProgram();

// 为程序对象关联着色器
gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)

// 连接着色器程序
gl.linkProgram(program)
// 将连接好的程序设置为当前的活动程序
gl.useProgram(program)
// 执行点的绘制
gl.drawArrays(gl.POINTS, 0, 1);

以上代码实现效果如下:

2.1.2 基础绘制流程

从一个点的绘制中可以看到,WebGL 执行绘制之前需要一系列的着色器创建、关联、编译等初始化步骤,初始化步骤完成之后就可以执行图形的绘制。

初始化的一系列步骤重复且必需,所以对这系列步骤可进行封装复用。封装方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export const initShader = (gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE) => {
const vertexShader = gl.createShader(gl.VERTEX_SHADER);
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);

gl.shaderSource(vertexShader, VERTEX_SHADER_SOURCE) // 指定顶点着色器的源码
gl.shaderSource(fragmentShader, FRAGMENT_SHADER_SOURCE) // 指定片元着色器的源码

gl.compileShader(vertexShader)
gl.compileShader(fragmentShader)

const program = gl.createProgram();

gl.attachShader(program, vertexShader)
gl.attachShader(program, fragmentShader)

gl.linkProgram(program)

gl.useProgram(program)

return program;
}

变量的使用

attribute 变量和 vertexAttrib4f() 同族函数

attribute  是顶点着色器中的一个输入变量,用于接收与每个顶点相关的数据,如位置、颜色等。attribute 只能在顶点着色器中使用,不能用于片元着色器。

vertexAttrib4f 是一个用于设置指定 attribute 的函数,允许将四个浮点数分量传递给顶点着色器。

另外 vertexAttrib1fvertexAttrib2fvertexAttrib3f 分别可以只指定一个、两个、三个分量传递给着色器。

attributevertexAttrib4f 函数的应用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const VERTEX_SHADER_SOURCE = `
// 使用 attribute 变量定义顶点着色器的位置
attribute vec4 aPosition;
void main() {
// 将变量赋值给顶点位置
gl_Position = aPosition;
gl_PointSize = 10.0;
}
`
// 使用 vertexAttrib4f 同族函数为顶点着色器传入位置变量
gl.vertexAttrib1f(aPosition, x)
gl.vertexAttrib2f(aPosition, x, y)
gl.vertexAttrib3f(aPosition, x, y, z)
gl.vertexAttrib4f(aPosition, x, y, z, w)

uniform 变量和 uniform4f() 同族函数介绍

uniform 是一个全局变量,所有的顶点和片元共享同一个值。不同于  attributeattribute 是顶点着色器的输入变量,每个顶点都有自己的值 。

uniform 用于传递不随顶点变化而变化的参数,如变换矩阵、全局颜色等常量数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const FRAGMENT_SHADER_SOURCE = `
// 使用 uniform 变量定义颜色
uniform vec2 uColor;
void main() {
gl_FragColor = uColor
// gl_FragColor = vec4(uColor.r, uColor.g, 0.0,1.0);
}
`;

// 使用 uniform4f 函数传值
gl.uniform1f(uColor, v1)
gl.uniform2f(uColor, v1, v2)
gl.uniform3f(uColor, v1, v2, v3)
gl.uniform4f(uColor, v1, v2, v3, v4)

以下是使用 attributeuniform 变量及其对应赋值函数的应用示例代码。 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
gl_PointSize = 20.0;
}
`

const FRAGMENT_SHADER_SOURCE = `
precision mediump float;
uniform vec4 uColor;
void main() {
gl_FragColor = uColor;
}
`
const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const uColor = gl.getUniformLocation(program, 'uColor')

gl.uniform4f(uColor, 1, 1, 1, 1)

let x = 0
setInterval(() => {
x += 0.05
if (x > 1) x = -1
gl.vertexAttrib1f(aPosition, x)
gl.drawArrays(gl.POINTS, 0, 1)
}, 100)

const colorChangeHandler = (rgb) => {
gl.uniform4f(uColor, ...rgb, 1)
gl.drawArrays(gl.POINTS, 0, 1)
}

document.querySelector('#red-btn').addEventListener('click', () => {
colorChangeHandler([1, 0, 0])
})
document.querySelector('#green-btn').addEventListener('click', () => {
colorChangeHandler([0, 1, 0])
})
document.querySelector('#blue-btn').addEventListener('click', () => {
colorChangeHandler([0, 0, 1])
})

坐标系介绍

WebGL 坐标系与 Canvas 坐标系

**Canvas 坐标系,**原点位于画布的左上角,横坐标和纵坐标向左和向下依次增加,坐标单位为 像素(px)。同 CSS  positiontop、left 计算逻辑类似。如下图所示:

WebGL 坐标系不同于网页布局常规坐标系。

二维 WebGL 坐标系更像是数学中的二维坐标系,原点在画布的正中心,坐标上增下减、右增左减。坐标单位为百分比, 值范围在 -1.0 与 1.0 之间。如下图所示。

三维 WebGL 坐标系则是在二维坐标系基础上,增加一个 Z 轴,Z 轴正方向指向屏幕外,类似 CSS 定位中的 z-indx 。如下图所示:

PS: 上述 WebGL 中的三维坐标系的 Z 轴方向为默认方向,Z 轴方向可设置,感兴趣可自查左右手坐标系。

坐标系的转换

由于 WebGL 和网页事件中的坐标系对于坐标的计算方式不一致。所以如果 WebGL 中元素涉及对鼠标事件进行相应时,需要对鼠标坐标进行转换,转换成在 WebGL 中的坐标以进行位置设置。


已知:

  1. 鼠标在视窗的位置信息,以左上角为顶点,横坐标 e.clientX,  纵坐标 e.clientY,单位为 px
  2. 画布的左上角的位置信息以及宽高,e.target.getBoundingClientRect(), top、left,width、height 单位 px

求: 鼠标所在的位置在 WebGL 坐标系中的坐标

答案如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
canvas.addEventListener('click', (e) => {
// 获取画布的位置信息
const sketch = e.target.getBoundingClientRect()

// 计算鼠标点击位置相对于画布左上角的坐标
const offsetX = e.clientX - sketch.left
const offsetY = e.clientY - sketch.top

const halfWidth = canvas.offsetWidth / 2
const halfHeight = canvas.offsetHeight / 2

// 相对 WebGL 坐标系原点计算偏移量之后进行比例计算
const clickX = (offsetX - halfWidth) / halfWidth
const clickY = (halfHeight - offsetY) / halfHeight

console.log('鼠标点击的位置对应的 WebGL 坐标系中的坐标是')
console.log(`clickX: ${clickX}, clickY: ${clickY}`)
})

实现一个简单的画板

在上一小节中,通过对坐标系进行转换,已经实现了用鼠标控制点绘制的位置。基于此,我们可以重复绘制多个点来实现一个简易 的画板(突出一个简易)。

直接 Show Code。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER_SOURCE = `
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
gl_PointSize = 15.0;
}
`
const FRAGMENT_SHADER_SOURCE = `
precision mediump float;
uniform vec4 uColor;
void main() {
gl_FragColor = uColor;
}
`
const vertexShader = gl.createShader(gl.VERTEX_SHADER)
const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)

const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const uColor = gl.getUniformLocation(program, 'uColor')
gl.uniform4f(uColor, 0, 0, 0, 1)

let points = []
// 多个点绘制方法
const drawPoints = () => {
for(let point of points) {
gl.vertexAttrib2f(aPosition, ...point)
gl.drawArrays(gl.POINTS, 0, 1);
}
}

// 颜色切换方法
const colorChangeHandler = (rgb) => {
gl.uniform4f(uColor, ...rgb, 1)
drawPoints()
}

document.querySelector('#red-btn').addEventListener('click', () => {
colorChangeHandler([1, 0, 0])
})
document.querySelector('#green-btn').addEventListener('click', () => {
colorChangeHandler([0, 1, 0])
})
document.querySelector('#blue-btn').addEventListener('click', () => {
colorChangeHandler([0, 0, 1])
})
document.querySelector('#clear-btn').addEventListener('click', () => {
points = []
gl.drawArrays(gl.POINTS, 0, 0);
})

canvas.addEventListener('click', (e) => {
const position = e.target.getBoundingClientRect()

const offsetX = e.clientX - position.left
const offsetY = e.clientY - position.top

const halfWidth = canvas.offsetWidth / 2
const halfHeight = canvas.offsetHeight / 2

const clickX = (offsetX - halfWidth) / halfWidth
const clickY = (halfHeight - offsetY) / halfHeight

points.push([clickX, clickY])
drawPoints()
})

实现简易版贪吃蛇

通过对以上 WebGL 基础知识的学习,已经可以使用 WebGL 来实现一个小游戏了,以下是一个贪吃蛇的代码,感兴趣同学可以自行研究。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>贪吃蛇</title>
<style>
canvas{
background-color: #f0f0f0;
}
</style>
</head>
<body>
<h3><a href="/">Home</a></h3>
<canvas id="canvas" width="600" height="600"></canvas>
</body>
</html>

<script type="module">

import { initShader } from '../lib/index.js'

const ctx = document.getElementById('canvas')
const gl = ctx.getContext('webgl')

// 创建着色器源码
const VERTEX_SHADER_SOURCE = `
// 只传递顶点数据
attribute vec4 aPosition;
void main() {
gl_Position = aPosition; // vec4(0.0,0.0,0.0,1.0)
gl_PointSize = 15.0;
}
`; // 顶点着色器

const FRAGMENT_SHADER_SOURCE = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`; // 片元着色器

const program = initShader(gl, VERTEX_SHADER_SOURCE, FRAGMENT_SHADER_SOURCE)

const aPosition = gl.getAttribLocation(program, 'aPosition');

// 蛇身的长度
let points = [
{x: 0, y: 0},
]

// 食物的坐标
const random = {
isConnect: true
}

// 移动的速度
let originSpeed = 0.02;

// 行动的速度
let speed = originSpeed;

// 移动的方向
let direction = 'x';

// 允许吃掉食物的误差范围
let base = 1.5;

document.onkeydown = (event) => {
switch (event.keyCode) {
case 37:
direction = 'x';
speed = -originSpeed
break;
case 38:
direction = 'y';
speed = originSpeed;
break;
case 39:
direction = 'x';
speed = originSpeed;
break;
case 40:
direction = 'y';
speed = -originSpeed;
break;
}
}

function createRandom() {
if (random.isConnect) {
random.x = Math.random() * 2 - 1;
random.y = Math.random() * 2 - 1;

random.isConnect = false;
}
}

function draw() {
gl.vertexAttrib3f(aPosition, random.x, random.y, 0.0);
gl.drawArrays(gl.POINTS, 0, 1);

let prex = 0;
let prey = 0;
for (let i = 0; i < points.length; i++) {
if (i === 0) {
prex = points[0].x
prey = points[0].y
points[0][direction] += speed;
} else {
let {x, y} = points[i]
points[i].x = prex
points[i].y = prey

prex = x;
prey = y;
}

gl.vertexAttrib3f(aPosition, points[i].x, points[i].y, 0.0);

gl.drawArrays(gl.POINTS, 0, 1);
}
}

let timer = null
function start() {
timer = setInterval(() => {
// 边界判断
if (
points[0].x > 1.0 ||
points[0].x < -1.0 ||
points[0].y < -1.0 ||
points[0].y > 1.0
) {
alert('游戏结束');
restart();
}

if (
points[0].x > random.x - base * originSpeed &&
points[0].x < random.x + base * originSpeed &&
points[0].y < random.y + base * originSpeed &&
points[0].y > random.y - base * originSpeed
) {
points.push({ x: random.x, y: random.y })
random.isConnect = true;
}

createRandom();
draw();
}, 100)
}

start();

function restart() {
clearInterval(timer)
points = [
{x: 0, y: 0}
]
direction = 'x'
speed = originSpeed
start();
}
</script>

图形绘制和基础动画

缓冲区对象

缓冲区对象介绍

WebGL 中,缓冲区对象(Buffer Object)是用于存储图形数据的内存区域,它们用于将数据传递给 GPU,以便进行绘制和渲染。

WebGL 主要使用以下几种类型的缓冲区对象:

  • 顶点缓冲区对象(gl.ARRAY_BUFFER):用于存储顶点数据(如位置、法线、颜色、纹理坐标等)。
  • 索引缓冲区对象(gl.ELEMENT_ARRAY_BUFFER):用于存储索引数据,以减少顶点数据的冗余。
  • 帧缓冲对象(gl.FRAMBUFFER):用于离屏渲染,可以将渲染结果输出到纹理中。

缓冲区数据的存储和传输有 STATIC_DRAWDYNAMIC_DRAWSTREAM_DRAW 三种方式。

  • STATIC_DRAW:适用于静态数据,数据几乎不变,一次写入后被多次使用。
  • DYNAMIC_DRAW:适用于频繁更新的数据,数据每次更新后可以被多次使用。
  • STREAM_DRAW:适用于每次更新后只使用一次的数据,适合流式更新场景。

以下是三种存储方式的对比:

特性 STATIC_DRAW DYNAMIC_DRAW STREAM_DRAW
数据更新频率 数据一次写入,然后多次使用 数据频繁更新,但每次更新后使用多次 数据频繁更新,每次更新后通常只使用一次
使用场景 静态数据,如固定的顶点、几何图形、背景模型 动态数据,如物理模拟、实时交互、角色动画 实时生成的数据,如粒子系统、摄像机视图变化
性能优化方向 优化为一次传输后高效读取,使用 GPU 内存存储 优化为数据频繁更新且在短时间内重复使用 优化为数据频繁更新,减少 GPU 内存占用和传输
举例 固定模型、场景静态网格 角色动画、实时变形模型 粒子系统、实时变化的图形效果

以下通过使用缓冲区对象来绘制多个点来对缓冲区对象的使用步骤进行介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
gl_PointSize = 15.0;
}
`
const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`
const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)
const aPosition = gl.getAttribLocation(program, 'aPosition')

// 定义缓冲区的数据
const points = new Float32Array([
-0.5, -0.5,
0.5, 0.5,
0.0, 0.5,
])

// 创建缓冲区对象
const buffer = gl.createBuffer()

// 绑定缓冲区对象为顶点缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)

// 将数据填充到缓冲区对象
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)

// 使用 gl.enableVertexAttribArray() 启用顶点属性数组,并指定使用的属性索引:
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

// 绘制图形
gl.drawArrays(gl.POINTS, 0, 3)

多缓冲区和数据偏移

在实际的开发中,往往会使用到多组属性,比如点的绘制中,位置、颜色和大小等信息可能都需要从缓冲区中读取。这时候可以创建多个缓冲区来供 WebGL 绘制使用。

以下是为上一小节的示例增加了点大小的缓冲数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
gl_Position = aPosition;
gl_PointSize = aPointSize;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const aPointSize = gl.getAttribLocation(program, 'aPointSize')

// 定义位置缓冲区的数据
const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

// 定义尺寸的缓冲区数据
const size = new Float32Array([10.0, 20.0, 30.0])
const sizeBuffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, sizeBuffer)
gl.bufferData(gl.ARRAY_BUFFER, size, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPointSize)
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, 0, 0)

// 绘制图形
gl.drawArrays(gl.POINTS, 0, 3)

如果每个元素的每个属性都要创建一个缓冲区,显然对代码的可读性和维护性不是很友好。接下来介绍一个方法 gl.vertexAttribPointer() 。这个方法让多个属性存在一个缓冲区对象里成为了可能。  

1
gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
  • **index:**顶点属性的索引,指定要修改的顶点属性的编号。它通常是通过 gl.getAttribLocation() 获取的。
  • size: 每个顶点属性的组成部分数量,表示每个顶点有多少个数值(即多少个数据分量)。取值范围:1、2、3、4。
  • type: 指定顶点属性的数据类型,表示每个数据的类型。常用的数据类型有:gl.FLOATgl.UNSIGNED_BYTEgl.BYTEgl.UNSIGNED_SHORT 等。
  • normalized: 是否将整数类型的数据归一化为浮点数,如果为 true,则在使用时将该值映射到 [0, 1](无符号)或 [-1, 1](有符号);如果为 false,则直接将数据按原始值传递。对于浮点数类型(如 gl.FLOAT),这个参数可以忽略,通常设为 false
  • stride: 连续顶点属性间的字节偏移量。它用于指定从当前顶点属性到下一个顶点属性之间的间隔。如果所有顶点属性是紧密排列的(没有间隔),则可以设为 0,WebGL 会自动计算步幅。
  • offset: 指定顶点属性数组中第一分量的字节偏移量,即从缓冲区开头到第一个顶点属性的起始位置的偏移量。通常,如果数据从头开始,则偏移量为 0,如果属性数据有其他数据在前面,则需要计算偏移量。

以下是使用 gl.vertexAttribPointer() 方法对以上代码两个缓冲区对象进行的优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
attribute float aPointSize;
void main() {
gl_Position = aPosition;
gl_PointSize = aPointSize;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const aPointSize = gl.getAttribLocation(program, 'aPointSize')

// 定义缓冲区的数据
const points = new Float32Array([
-0.5, -0.5, 10.0,
0.5, -0.5, 20.0,
0.0, 0.5, 30.0,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)

// 获取缓冲区数组每一项的字节数
const BYTES = points.BYTES_PER_ELEMENT

gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, BYTES * 3, 0)

gl.enableVertexAttribArray(aPointSize)
gl.vertexAttribPointer(aPointSize, 1, gl.FLOAT, false, BYTES * 3, BYTES * 2)

// 绘制图形
gl.drawArrays(gl.POINTS, 0, 3)

3.2 多种图形绘制

WebGL 中,支持绘制的图形有 线三角形 三种。

**为什么选择三角形呢?**这是因为任何多边形都可以最终分解为多个三角形,也就是说三角形是多边形的基本单位,并且三角形一定在一个平面上。

下表是所有支持绘制的图形的标识符。

图形 说明
gl.POINTS ⼀系列点
gl.LINES 线段 ⼀系列单独的线段,如果顶点是奇数,最后⼀个会被忽略
gl.LINE_LOOP 闭合线 ⼀系列连接的线段,结束时,会闭合终点和起点
gl.LINE_STRIP 线条 ⼀系列连接的线段,不会闭合终点和起点
gl.TRIANGLES 三角形 ⼀系列单独的三角形
gl.TRIANGLE_STRIP 三角带 ⼀系列条带状的三角形
gl.TRIANGLE_FAN 三角形 飘带状三角形

以下是各图形绘制的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
void main() {
gl_Position = aPosition;
gl_PointSize = 15.0;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
1.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

// 绘制图形
gl.drawArrays(gl.POINTS, 0, 3)
gl.drawArrays(gl.LINES, 0, 2)
gl.drawArrays(gl.LINE_STRIP, 0, 3)
gl.drawArrays(gl.LINE_LOOP, 0, 3)
gl.drawArrays(gl.LINE_LOOP, 0, 3)
gl.drawArrays(gl.TRIANGLES, 0, 3)
gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4)
gl.drawArrays(gl.TRIANGLE_FAN, 0, 4)

图形动画

学习和了解了图形的绘制和顶点着色器位置信息的设置后,借助变量设置的方法,可以对图形设置简单的平移、缩放、旋转等动画效果。

Show Code~

图形平移

图形平移代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
attribute float aTranslate;
void main() {
gl_Position = vec4(aPosition.x + aTranslate, aPosition.y, aPosition.z, 1.0);
gl_PointSize = 15.0;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const aTranslate = gl.getAttribLocation(program, 'aTranslate')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

let x = 0
setInterval(() => {
x += 0.01
if (x > 1) x = -1
gl.vertexAttrib1f(aTranslate, x)
gl.drawArrays(gl.TRIANGLES, 0, 3)
})

图形缩放

图形缩放代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
attribute float aSale;
void main() {
gl_Position = vec4(aPosition.x * aSale, aPosition.y * aSale, aPosition.z * aSale, 1.0);
gl_PointSize = 15.0;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const aSale = gl.getAttribLocation(program, 'aSale')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

let x = 1
setInterval(() => {
x += 0.01
if (x > 3) x = 1
gl.vertexAttrib1f(aSale, x)
gl.drawArrays(gl.TRIANGLES, 0, 3)
})

图形旋转

图形缩放代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
attribute float deg;
void main() {
gl_Position.x = aPosition.x * cos(deg) - aPosition.y * sin(deg);
gl_Position.y = aPosition.x * sin(deg) + aPosition.y * cos(deg);
gl_Position.z = aPosition.z;
gl_Position.w = aPosition.w;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const deg = gl.getAttribLocation(program, 'deg')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

let x = 1
setInterval(() => {
x += 0.01
gl.vertexAttrib1f(deg, x)
gl.drawArrays(gl.TRIANGLES, 0, 3)
})

图形变换矩阵

回顾矩阵知识

矩阵就是纵横排列的数据表格,在接下来 WebGL 图形变换的介绍中,矩阵的作用是把一个点转换到另一个点

行主序与列主序矩阵:

点的映射公式:

  • x’ = x * a1 + y * a2 + z * a3  + w * a4
  • y’ = x * b1 + y * b2 + z * b3  + w * b4
  • z’ = x * c1 + y * c2 + z * c3  + w * c4
  • w’ = x * d1 + y * d2 + z * d3  + w * d4

为什么 WebGL 的矩阵采用列主序?参考 OpenGL 中的转换矩阵

平移矩阵

如上图,一点从(x,  y,  z)移动到(x’, y’, z’),对应每个点的坐标的计算公式为:

  • x’ = x + x1
  • y’ = y + y1
  • z’ = z + z1

如要通过矩阵的运算实现以上坐标的平移计算,则需设法使得:

  • x + x1 =  x * a1 + y * a2 + z * a3  + w * a4
  • y + y1 = x * b1 + y * b2 + z * b3  + w * b4
  • z + z1 = x * c1 + y * c2 + z * c3  + w * c4

其中 w 恒等于 1,所以由以上等式可得出以下结论:

  • 当 a1 = 1,  a2 = a3 = 0, a4 = x1 时,等式  x + x1 =  x * a1 + y * a2 + z * a3  + w * a4 成立
  • 当 b2 = 1, b1 = b3 = 0, b4 = y1 时,等式 y + y1 = x * b1 + y * b2 + z * b3  + w * b4 成立
  • 当 c3 = 1, c1 = c2 = 0, c4 = z1 时,等式 z + z1 = x * c1 + y * c2 + z * c3  + w * c4 成立

由此可得平移矩阵:

推导出平移矩阵后,之后的图形平移变换,都可以使用平移矩阵来进行操作。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
uniform mat4 mat;
void main() {
gl_Position = mat * aPosition;
gl_PointSize = 15.0;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const mat = gl.getUniformLocation(program, 'mat')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

// 平移矩阵
const getTranslateMatrix = (x = 0,y = 0,z = 0) => {
return new Float32Array([
1.0,0.0,0.0,0.0,
0.0,1.0,0.0,0.0,
0.0,0.0,1.0,0.0,
x ,y ,z , 1,
])
}

let x = 0
setInterval(() => {
x += 0.01
if (x > 1) x = -1

const matrix = getTranslateMatrix(x, x)
gl.uniformMatrix4fv(mat, false, matrix)
gl.drawArrays(gl.TRIANGLES, 0, 3)
}, 10)

缩放矩阵

如上图,图形缩放过程中,一点从(x,  y,  z)缩放到(x’, y’, z’),对应每个点的坐标的计算公式为:

  • x’ = x * x1
  • y’ = y * y1
  • z’ = z * z1

同理可推导出以下结论:

  • 当 a1 = x1,  a2 = a3 = a4 = 0 时,等式  x * x1 =  x * a1 + y * a2 + z * a3  + w * a4 成立
  • 当 b2 = y1, b1 = b3 = b4 = 0 时,等式 y * y1 = x * b1 + y * b2 + z * b3  + w * b4 成立
  • 当 c3 = z1, c1 = c2 = c4 = 0 时,等式 z * z1 = x * c1 + y * c2 + z * c3  + w * c4 成立

由此可得缩放矩阵为:(此矩阵为对称矩阵,无需进行行列转换)

使用矩阵进行图形缩放操作的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
uniform mat4 mat;
void main() {
gl_Position = mat * aPosition;
gl_PointSize = 15.0;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const mat = gl.getUniformLocation(program, 'mat')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

// 缩放矩阵
const getScaleMatrix = (x = 1,y = 1,z = 1) => {
return new Float32Array([
x ,0.0,0.0,0.0,
0.0,y ,0.0,0.0,
0.0,0.0,z ,0.0,
0.0,0.0,0.0, 1,
])
}

let x = 1
setInterval(() => {
x += 0.01
if (x > 2) x = 0.1

const matrix = getScaleMatrix(x, x)
gl.uniformMatrix4fv(mat, false, matrix)
gl.drawArrays(gl.TRIANGLES, 0, 3)
}, 10)

旋转矩阵

上图中,A 点由 (x, y, z) 旋转到 (x’, y’, z’) 的 A’ 点,旋转角度为 β,接下来使用点 A 到原点的距离 R 来进行 A’ 坐标的推导。(R = Math.sqrt(x * x + y * y))

推导过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// A 点坐标为
x = R * cos(α)
y = R * sin(α)

// A' 点坐标为
x' = R * cos(α + β)
= R * (cos(α)* cos(β) - sin(α) * sin(β))
= R * cos(α)* cos(β) - R * sin(α) * sin(β)
y' = R * sin(α + β)
= R * (sin(α)* cos(β) + cos(α) * sin(β))
= R * sin(α)* cos(β) + R * cos(α) * sin(β)

// 将 A 点坐标代入到 A' 点
x' = x * cos(β) - y * sin(β)
y' = y * cos(β) + x * sin(β)

由以上 A’ 点坐标公式,结合矩阵乘法公式,可得出以下结论:

  • 当 a1 = cos(β),  a2 = -sin(β),  a3 = a4 = 0 时,等式 x * cos(β) - y * sin(β) =  x * a1 + y * a2 + z * a3  + w * a4 成立
  • 当 b1 = sin(β), b2 = cos(β),  b3 = b4 = 0 时,等式 y * cos(β) + x * sin(β) = x * b1 + y * b2 + z * b3  + w * b4 成立
  • 当 c1 = c2 = c3 = c4 = 0 时,等式 z = x * c1 + y * c2 + z * c3  + w * c4 成立

由此可得旋转矩阵如下:

使用矩阵进行图形旋转操作的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import { initShader } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
uniform mat4 mat;
void main() {
gl_Position = mat * aPosition;
gl_PointSize = 15.0;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const mat = gl.getUniformLocation(program, 'mat')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

// 绕z轴旋转的旋转矩阵
export const getRotateMatrix = (deg) => {
return new Float32Array([
Math.cos(deg) ,Math.sin(deg) ,0.0,0.0,
-Math.sin(deg) ,Math.cos(deg) ,0.0,0.0,
0.0, 0.0, 1.0,0.0,
0.0, 0.0, 0.0, 1,
])
}

let x = 0
setInterval(() => {
x += 0.01

const matrix = getRotateMatrix(x)
gl.uniformMatrix4fv(mat, false, matrix)
gl.drawArrays(gl.TRIANGLES, 0, 3)
}, 10)

组合矩阵

如果一个图形动画中,既有平移,又有缩放,还有旋转,我们使用矩阵可以轻松的同时实现这些动画。复合动画代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import { initShader, getTranslateMatrix, getScaleMatrix, getRotateMatrix } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
uniform mat4 translateMatrix;
uniform mat4 scaleMatrix;
uniform mat4 rotationMatrix;
void main() {
gl_Position = translateMatrix * scaleMatrix * rotationMatrix * aPosition;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const translateMatrix = gl.getUniformLocation(program, 'translateMatrix')
const scaleMatrix = gl.getUniformLocation(program, 'scaleMatrix')
const rotationMatrix = gl.getUniformLocation(program, 'rotationMatrix')

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

let deg = 0;
let translateX = -1;
let scaleX = 0.1;
setInterval(() => {
deg += 0.01;
translateX += 0.01;
scaleX += 0.01;

if (translateX > 1) translateX = -1
if (scaleX > 2) scaleX = 0.1

const translate = getTranslateMatrix(translateX);
const scale = getScaleMatrix(scaleX);
const rotate = getRotateMatrix(deg);
// console.log(translate, scale, rotate)
gl.uniformMatrix4fv(translateMatrix, false, translate);
gl.uniformMatrix4fv(scaleMatrix, false, scale);
gl.uniformMatrix4fv(rotationMatrix, false, rotate);

gl.drawArrays(gl.TRIANGLES, 0, 3)
}, 10)

但是,以上代码中需要为每一种动画都创建一个 uniform 变量,然后在程序执行过程中不断改变变量并注入,这种方式很不友好,且不利于代码维护。  

可以利用矩阵的特性,传入着色器之前,利用矩阵乘法,把多个矩阵混合成一个矩阵传入,在这一个矩阵中包含了所有的动画操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import { initShader, getTranslateMatrix, getScaleMatrix, getRotateMatrix, mixMatrix } from '../lib/index.js'

const canvas = document.getElementById('canvas')
const gl = canvas.getContext('webgl')

const VERTEX_SHADER = `
attribute vec4 aPosition;
uniform mat4 mat;
void main() {
gl_Position = mat * aPosition;
}
`

const FRAGMENT_SHADER = `
void main() {
gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}
`

const program = initShader(gl, VERTEX_SHADER, FRAGMENT_SHADER)

const aPosition = gl.getAttribLocation(program, 'aPosition')
const mat = gl.getUniformLocation(program, 'mat');

const points = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])

const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, points, gl.STATIC_DRAW)
gl.enableVertexAttribArray(aPosition)
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0)

let deg = 0;
let translateX = -1;
let scaleX = 0.1;
setInterval(() => {
deg += 0.01;
translateX += 0.01;
scaleX += 0.01;

if (translateX > 1) translateX = -1
if (scaleX > 2) scaleX = 0.1

const translate = getTranslateMatrix(translateX);
const scale = getScaleMatrix(scaleX);
const rotate = getRotateMatrix(deg);

// 使用混合矩阵方法,将以上三个矩阵混合成一个
const matrix = mixMatrix(mixMatrix(translate, scale), rotate)

gl.uniformMatrix4fv(mat, false, matrix);

gl.drawArrays(gl.TRIANGLES, 0, 3)
}, 10)

小结

在本篇 WebGL 基础篇的博客中,我们探讨了一些 WebGL 的基础概念、API 以及如何利用它创建图形渲染的基础。我们从理解 WebGL 的工作原理开始,看到了 WebGL 的冰山一角。

随着现代网页和应用程序对图形性能和视觉效果的需求不断提升,想要使用 WebGL 实现更复杂的图形效果和动画,需要我们对 WebGL 进行更深一步的学习和实践。

关于本篇博客的内容,如有错误欢迎指正。

下集预告:

  • 颜色和纹理
  • OpenGLES 语言
  • WebGL 三维世界

参考文章:
https://coding.imooc.com/class/622.html
https://zhuanlan.zhihu.com/p/570452494
https://webglfundamentals.org/webgl/lessons/zh_cn/