WebGL从零开始(三)—— 平移、旋转、缩放与矩阵

3/10/2026 WebGL

# 一、前置准备

平移、旋转、缩放是空间上面的常规操作,我们将以一个立方体来进行演练展示。

# 1. 数据准备

webgl的代码准备就不多说了,就是获取canvas->创建webgl对象->初始化着色程序->渲染绘制,可以参考 WebGL从零开始(一)——认识WebGL并绘制一个点,完整代码后续会附上。

构建一个立方体,以(0,0)点作为第一个点,以 WebGL 的坐标系的 0.5 作为其边长,则其

# 1.1 顶点坐标

const verticesLib = [
  0.0, 0.0, 0.0,
  0.0, 0.5, 0.0,
  0.5, 0.5, 0.0,
  0.5, 0.0, 0.0, // z轴为0 的四个顶点 

  0.0, 0.0, 0.5,
  0.0, 0.5, 0.5,
  0.5, 0.5, 0.5,
  0.5, 0.0, 0.5, // z轴为0.5 的四个顶点
]
1
2
3
4
5
6
7
8
9
10
11

顶点之后就是线,线用的是线段 LINES,两个点作为连接,一个立方体是12条边,那么就需要12对顶点坐标。

# 1.2 12条边的顶点索引

然后根据边的顶点索引 创建绘制边所需的顶点坐标

const indices = [
  0,1,
  1,2,
  2,3,
  3,0, // 底四边
  
  0,4,
  1,5,
  2,6,
  3,7, // 四个柱边
  
  4,5,
  5,6,
  6,7,
  7,4, // 上四边
];
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

# 顶点索引

顶点索引的概念就是,顶点索引集合(上面的indices)中的每个数字,如 0 对应顶点坐标集合(verticesLib)中的第一个点的坐标,也就是(0.0, 0.0, 0.0)。

# 1.3 绘制边的顶点坐标

通过遍历顶点索引,生成新的顶点坐标集合

const borderVertices = []; // 边所需的顶点坐标
indices.forEach(item => {
    const i = item * 3; // 对应顶点的 x 坐标
    borderVertices.push(
        verticesLib[i], 
        verticesLib[i + 1], 
        verticesLib[i + 2]
    )
});
1
2
3
4
5
6
7
8
9

# 通过WebGL api实现根据顶点索引自动处理顶点坐标

const verticesLib = new Float32Array([
  0.0, 0.0, 0.0,
  0.0, 0.5, 0.0,
  0.5, 0.5, 0.0,
  0.5, 0.0, 0.0, // z轴为0 的四个顶点 

  0.0, 0.0, 0.5,
  0.0, 0.5, 0.5,
  0.5, 0.5, 0.5,
  0.5, 0.0, 0.5, // z轴为0.5 的四个顶点
])
const indices = new Unit8Array([
  0,1,
  1,2,
  2,3,
  3,0, // 底四边
  
  0,4,
  1,5,
  2,6,
  3,7, // 四个柱边
  
  4,5,
  5,6,
  6,7,
  7,4, // 上四边
]);
// 后面写入缓冲数据的时候就不用 new Float32Array(borderVertices),而直接先用 verticesLib
// gl.bufferData(gl.ARRAY_BUFFER, verticesLib, gl.STATIC_DRAW);
// 然后再新增顶点索引缓冲区,并绑定顶点索引缓冲区,注意这里的类型用的是 gl.ELEMNT_ARRAY_BUFFER
const indicesBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMNT_ARRAY_BUFFER, indicesBuffer)
gl.bufferData(gl.ELEMNT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)

// 绘图的时候就不用 drawArrays 方法了,改为 drawElemnts(mode, count, type, offset)
gl.drawElemnts(gl.LINES, indices.length, gl.UNSIGNED_BYTE, 0);
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

# 2. 渲染

# 2.1 通过缓冲数据进行写入渲染

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
// 变量名
const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
// 顶点属性
gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(a_Position);
// 写入数据
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(borderVertices), gl.STATIC_DRAW);
// 修改顶点数据
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

gl.clearColor(0, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.LINES, 0, indices.length);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 2.2 效果图:

# 2.3 完整代码

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>动态绘制点</title>
</head>

<body>
  <canvas id='webgl' width='500' height='500'></canvas>

  <script id="vertex-shader-source" type="notjs">
    attribute vec4 a_Position;
    void main() {
      gl_Position = a_Position;
      gl_PointSize = 10.0;
    }
  </script>
  <script id="fragment-shader-source" type="notjs">
    precision mediump float;
    void main() {
      gl_FragColor = vec4(1, 0, 0.5, 1); 
    }
  </script>

  <script>
    let canvas = document.getElementById('webgl');
    let gl = canvas.getContext('webgl');
    if (!gl) {
      alert('您的浏览器不支持WebGL!');
      // …… 其他操作
    }
    const vsSource = document.querySelector('#vertex-shader-source').text
    const fsSource = document.querySelector('#fragment-shader-source').text
    initProgram(gl, vsSource, fsSource);
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const verticesLib = [
      0.0, 0.0, 0.0,
      0.0, 0.5, 0.0,
      0.5, 0.5, 0.0,
      0.5, 0.0, 0.0, // z轴为0 的四个顶点 

      0.0, 0.0, 0.5,
      0.0, 0.5, 0.5,
      0.5, 0.5, 0.5,
      0.5, 0.0, 0.5, // z轴为0.5 的四个顶点
    ];

    const indices = [
      0, 1,
      1, 2,
      2, 3,
      3, 0, // 底四边

      0, 4,
      1, 5,
      2, 6,
      3, 7, // 四个柱边

      4, 5,
      5, 6,
      6, 7,
      7, 4, // 上四边
    ];

    const borderVertices = []; // 边所需的顶点坐标
    indices.forEach(item => {
      const i = item * 3; // 对应顶点的 x 坐标
      borderVertices.push(
        verticesLib[i],
        verticesLib[i + 1],
        verticesLib[i + 2]
      )
    });

    // 缓冲区 及绑定
    const pointBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pointBuffer);
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    gl.vertexAttribPointer(a_Position, 3, gl.FLOAT, false, 0, 0);
    // 开启对应属性
    gl.enableVertexAttribArray(a_Position);
    // 写入数据
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(borderVertices), gl.STATIC_DRAW);

    gl.drawArrays(gl.LINES, 0, indices.length);


    // 初始化着色程序
    function initProgram(gl, vertexShaderSource, fragmentShaderSource) {
      const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
      const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource)
      var program = gl.createProgram();
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);
      gl.linkProgram(program);
      var success = gl.getProgramParameter(program, gl.LINK_STATUS);
      if (success) {
        gl.useProgram(program);
        gl.program = program; // 挂在到gl对象上,便于后续使用
        return program;
      }

      console.log(gl.getProgramInfoLog(program));
      gl.deleteProgram(program);
    }

    // 创建Shader
    function createShader(gl, type, source) {
      const shader = gl.createShader(type); // 创建着色器对象
      gl.shaderSource(shader, source); // 提供数据源
      gl.compileShader(shader); // 编译 -> 生成着色器
      const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
      if (success) {
        return shader;
      }

      console.log(gl.getShaderInfoLog(shader));
      gl.deleteShader(shader);
    }
  </script>
</body>

</html>
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

# 2.4 为什么看到的是平面?

这是因为本篇使用的是默认的正交投影,顶点的坐标没有任何投影矩阵和视图矩阵(gl_Position = a_Position;),正交投影是从 z 轴“往下”看,并且没有近大远小,所以我们只能看到上四边,下面的棱柱、底边都被遮住了。

# 二、平移

# 1. 平移的原理

平移在数学上极其简单: 如果要把物体沿 X 轴移动 Tx,沿 Y 轴移动 Ty,沿 Z 轴移动 Tz,那么对于顶点 (x, y, z),它的新位置就是:

新X = x + Tx 新Y = y + Ty 新Z = z + Tz

在三维空间中,物体的每一个顶点都可以看作一个位置向量 $\vec{P}$=(x,y,z)。平移就是要把它变成一个新的位置向量 $\vec{P′}$。

平移的数学公式极其简洁,就是向量加法:

​ $\vec{P'}$ = $\vec{P}$ + $\vec{T}$

其中,$\vec{T}$*=(*Tx,Ty,Tz) 就是平移向量(位移向量)。

# 2. GLSL代码实现

那么对应的就是我们将 gl_Position = a_Position; 的 a_Position 加一个偏移量,这里使用 gl 中提供的 vec* 方法来创建偏移量:

attribute vec4 a_Position;
vec3 u_Translation=vec3(-0.1,0.1,0.0); // 这里z设为0 暂时不进行偏移,因为目前还是正交投影图
void main() {
    // 平移就是 向量加法
    gl_Position = a_Position + vec4(u_Translation, 0.0); // vec4(u_Translation, 0.0) 就是我们的平移向量,这里的齐次坐标的第四个值 w 为 0.0 是可以的,后续用矩阵的话就不能是 0.0了
}
1
2
3
4
5
6

# 3. JS控制平移

我们可以将GLSL中的平移向量通过 属性 暴露出去,然后通过js动态设置。

attribute vec4 a_Position;
attribute vec4 u_Translation; // 使用 attribute 暴露 或者使用 uniform 暴露
void main() {
    // 平移就是 向量加法
    gl_Position = a_Position + u_Translation; 
}
1
2
3
4
5
6

然后使用gl.vertexAttrib3f更改u_Translation来更新顶点坐标。

// 通过 gl.getAttribLocation 来获取 u_Translation 变量对象
const u_Translation = gl.getAttribLocation(gl.program, 'u_Translation');

// 通过 requestAnimationFrame 来改变平移位置
let deltaY = 0.01;
!(function ani(){
    deltaY += 0.01;
    if(deltaY > 1.0) {
        deltaY = -1.0;
    }
    gl.vertexAttrib4f(u_Translation, 0.0, deltaY, 0.0, 0.0); // vertexAttrib4f 之前有讲过,第一个参数是 Attribute变量对象,后续参数为方法名后面的数量,如这里的4f,指的是 4个浮点参数
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.LINES, 0, indices.length);
    requestAnimationFrame(ani);
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 三、旋转

# 1. 旋转的原理

旋转在数学上需要借助三角函数(初中数学)

首先要知道旋转轴,例如绕 z 轴旋转(z坐标不变)

然后是旋转方向:顺时针(旋转角度是正的)、逆时针(旋转角度是负的)

最后是旋转角度:如旋转 θ 角度

以点 A(x,y,z) 为例,以原点(0,0,0) 为旋转点,绕z轴旋转 θ 角度后的 B(x', y', z) 点,计算公式:

Bx‘ = Ax × cosθ − Ay × sinθ

By'= Ax × sin⁡θ + Ay × cos⁡θ

图解:

用到了三角函数的基本定义:直角三角形中 sinA = $\frac{对边}{斜边}$ cosA = $\frac{邻边}{斜边}$ tanA = $\frac{对边}{邻边}$

以及三角函数的两角和差公式:

sin⁡(α + β)= sin⁡α cos⁡β + cos⁡α sin⁡β

cos⁡(α + β)= cos⁡α cos⁡β − sin⁡α sin⁡β

# 2. GLSL代码实现

  1. 旋转轴; 2. 旋转方向; 3. 旋转角度; 绕z轴旋转30° 向量实现旋转:(是以(0.0)点进行旋转) 目标的x = 初始点的xcos(角度) - 初始点的ysin(角度) 目标的y = 初始点ycos(角度) + 初始点xsin(角度) 目标的z = 初始点的z (二维)
// 顶点着色器
attribute vec4 a_Position;
float a_angle=radians(90.0);
float sinB = sin(a_angle);
float cosB = cos(a_angle);

void mian () {
  gl_Position = vec4(
    a_Position.x*cosB - a_Position.y*sinB,
    a_Position.y*cosB + a_Position.x*sinB,
    a_Position.z, a_Position.w
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13

或者用 二阶旋转矩阵来乘以a_Position的x,y:

// 顶点着色器
attribute vec4 a_Position;
float a_angle=radians(90.0);
float sinB = sin(a_angle);
float cosB = cos(a_angle);
// webgl 中是列矩阵
mat2 rotate = mat2(
  cosB, sinB,
  -sinB, cosB
);

void mian () {
  gl_Position = vec4(
    rotate*vec2(a_Position),
    a_Position.z, a_Position.w
  );
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 3. JS控制旋转

我们可以把 a_angle 通过 attribute 变量暴露出去,然后通过 gl.getAttribLocation 获取,再通过 gl.vertexAttrib1f 来修改弧度值。

// 通过 gl.getAttribLocation 来获取 a_angle 变量对象
const a_angle = gl.getAttribLocation(gl.program, 'a_angle');

// 通过 requestAnimationFrame 来改变旋转的角度
let deltaY = 0.01;
!(function ani(){
    deltaY += 0.01;
    if(deltaY > 1.0) {
        deltaY = -1.0;
    }
    gl.vertexAttrib1f(a_angle, deltaY); 
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.LINES, 0, indices.length);
    requestAnimationFrame(ani);
})()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

# 四、缩放

# 1.缩放的原理

模型缩放的原理起始很简单,就是模型顶点坐标 x,y,z 乘以一个缩放系数。

# 2.GLSL代码实现

// 顶点着色器
attribute vec4 a_Position;
float scale = 1.5;
void main () {
  gl_Position = vec4(
    scale*vec3(a_Position),
    a_Position.w
  );
}
1
2
3
4
5
6
7
8
9

# 五、矩阵

从旋转那儿可以看出,我们对 a_Position 的改变,可以用矩阵来实现,一个 二维矩阵*二维向量 得到新的二维向量,同样的一个 四维矩阵*四维向量得到新的四维向量。

# 1.矩阵平移

数学中的矩阵是行矩阵,矩阵*向量(同阶) = 矩阵每一行*向量对应的列。如项目的单位矩阵和向量P(1,3,5) $$\left(\begin{array}{ccc} 1 & 0 & 0 \ 0 & 1 & 0 \ 0 & 0 & 1 \end{array}\right)$$ 第一行矩阵元素乘以向量第一列得到 x = 1*1 + 0*1 + 0*1 = 1 第二行矩阵元素乘以向量第二列得到 y = 0*3 + 1*3 + 0*3 = 3 第三行矩阵元素乘以向量第三列得到 z = 0*5 + 0*5 + 1*5 = 5 最终得到新向量 (1,3,5)

WebGL的矩阵是列矩阵,矩阵*向量(同阶) = 矩阵每一列*向量对应的列。 步骤类似行矩阵。

那么平移的矩阵在WebGL中就可以写成:

attribute vec4 a_Position;
float tx = 0.5;
float ty = 0.5;
float tz = 0.5;
mat4 translate = mat4(
  1.0, 0.0, 0.0, 0.0,
  0.0, 1.0, 0.0, 0.0,
  0.0, 0.0, 1.0, 0.0,
  tx,  ty,  tz, 1.0
);
void main() {
  // 矩阵 乘以 顶点坐标(向量)
  gl_Position = translate*a_Position;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 2.矩阵旋转

attribute vec4 a_Position;
float a_angle=radians(90.0);
float sinB = sin(a_angle);
float cosB = cos(a_angle);
mat4 translate = mat4(
  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
);
void main() {
  // 矩阵 乘以 顶点坐标(向量)
  gl_Position = translate*a_Position;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 3.矩阵缩放

attribute vec4 a_Position;
float sx = 1.5;
float sy = 1.5;
float sz = 1.5;
mat4 translate = mat4(
  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
);
void main() {
  // 矩阵 乘以 顶点坐标(向量)
  gl_Position = translate*a_Position;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 4.复合矩阵

平移可以和缩放或旋转合在一个矩阵中,缩放和旋转矩阵需要乘以后才行

attribute vec4 a_Position;
float tx = 0.5;
float ty = 0.5;
float tz = 0.5;

float sx = 0.5;
float sy = 0.5;
float sz = 0.5;

float a_angle=radians(0.0);
float sinB = sin(a_angle);
float cosB = cos(a_angle);
// 平移和旋转合在一个矩阵中
mat4 translate = mat4(
  cosB,  sinB, 0.0, 0.0,
  -sinB, cosB, 0.0, 0.0,
  0.0,   0.0,  sz,  0.0,
  tx,    ty,   tz,  1.0
);
// 缩放矩阵
mat4 scale = mat4(
  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
);
// 最终变化矩阵 = 平移旋转矩阵*缩放矩阵
mat4 matrix = translate*scale;
void main() {
  // 最终变化矩阵 乘以 顶点坐标(向量)
  gl_Position = matrix*a_Position;
}
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

# 六、本篇涉及的WebGL-API

本篇主要讲顶点的位置变换实现,通过绘制一个立方体来直观感受模型的平移、旋转和缩放。 绘制立方体使用了缓冲区,并且介绍了顶点索引,以及通过WebGL的api来实现根据顶点索引自动计算顶点坐标的方式。 顶点索引缓冲区的类型不同于顶点缓冲区,使用的是 gl.ELEMENT_ARRAY_BUFFER。 使用顶点索引缓冲区来绘制图形:drawElemnts(mode, count, type, offset)

  • mode:绘图方式,同 drawArrays 一样
  • count:元素数量
  • type:缓冲区数据的类型
  • offset:当前系列的字节索引位
Last Updated: 9/24/2026, 8:10:57 AM