WebGL从零开始(二)

2/23/2026 WebGL

# 前言

本篇将完成几个内容: 1.了解缓冲、属性、全局变量 2.通过JS更改顶点坐标 3.绘制线段

# 一、什么是缓冲、属性、全局变量

我们通过gl.drawArrays将着色器方法在GPU中运行,进行渲染绘制,如果想要更改渲染的数据肯定要把数据发送到GPU(缓冲),那么应该如何做呢?WebGL提供了4种方法,这里先介绍两种,分别是属性(Attributes)和全局变量(Uniforms)。

缓冲(Buffer):GPU 内存中的一块区域。用于存储顶点数据(位置、颜色等)的数组。它位于 GPU 端,但由 JavaScript 通过 gl.bufferData 填充。它存储原始字节/数字。

属性(Attributes):从缓冲区获取数据到顶点着色器的“指针”或“读取器”。它告诉 GPU 如何从缓冲区中提取数据(例如,从偏移量 0 开始,每次取 3 个浮点数)。Attribute 本身不是数据;它是缓冲区的视图/解析方式,仅在顶点着色器执行期间对每个顶点有效。

全局变量( Uniforms):着色器代码中的全局范围变量,在绘制调用期间保持不变。它不在缓冲区中(或者更确切地说,在单独的常量内存中),由 JavaScript 通过 gl.uniform* 直接设置,而不是通过缓冲区流式传输。

先看看这些名词的意义,后续还是通过实例来看看到底如何做的。

# 二、动态改变顶点位置

要实现此效果,我们需要借助gl.getAttribLocation获取顶点着色器中定义的变量,并在后续进行赋值,从而达到动态改变的效果。

# 1.顶点着色器

顶点着色器需要小小修改一下,添加属性变量a_Position,并将a_Position赋值给gl_Position

attribute vec4 a_Position;
void main() {
  gl_Position = a_Position;
  gl_PointSize = 10.0;  
}
1
2
3
4
5

# 2.片段着色器

片段着色器不进行更改:

precision mediump float;

void main(){
  gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
1
2
3
4
5

# 3.创建WebGL对象

首先还是建立canvas标签

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

然后在js中通过 document.getElementById 获取到canvas对象;再通过canvas来获取WebGL对象。

let canvas = document.getElementById('webgl');
let gl = canvas.getContext('webgl'); 
1
2

# 4.创建着色器对象并编译

// 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
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);
}

const vertexShaderSource = document.querySelector("#vertex-shader-source").text;
const fragmentShaderSource = document.querySelector("#fragment-shader-source").text;
 
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 5.链接到着色程序

// 创建program的方法
function createProgram(gl, vertexShader, fragmentShader) {
  const program = gl.createProgram();
  gl.attachShader(program, vertexShader);
  gl.attachShader(program, fragmentShader);
  gl.linkProgram(program);
  var success = gl.getProgramParameter(program, gl.LINK_STATUS);
  if (success) {
    return program;
  }
 
  console.log(gl.getProgramInfoLog(program));
  gl.deleteProgram(program);
}

// 得到创建的 program 便于后面使用
const program = createProgram(gl, vertexShader, fragmentShader);
gl.useProgram(program);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 6.获取属性变量并更改

我们在顶点着色器中定义了属性变量a_Position,现在来获取 attribute 变量 a_Position:

const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0);
// 清空画布
gl.clearColor(0.0, 0.0, 0.0, 1.0);
// 绘制渲染
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, 1);
1
2
3
4
5
6
7

这里通过 getAttribLocation 获取,需要两个参数,一个是 着色程序对象,一个是 attribute 变量名称;得到属性变量对象

通过 vertexAttrib3f 更改顶点数据,第一个参数是属性变量对象,后面参数依次是x坐标,y坐标,z坐标

gl.vertexAttrib* 是一系列方法,有 vertexAttrib1f,更改x坐标;vertexAttrib2f,更改x,y坐标,和这里用的 vertexAttrib3f,更改x,y,z坐标。

最后绘制渲染,可以看到,点的位置从中心(对比上一节)变到了右上。

最终结果如图:

# 7.通过鼠标点击动态更改

要实现鼠标点击动态更改点位置,我们需要前提条件,鼠标点击的坐标(视口坐标)-> canvas画布坐标 -> WebGL坐标,然将得到的WebGL坐标赋值给gl,最后重新渲染绘制。

// 为 canvas 绑定监听事件
canvas.addEventListener('click', (e) => {
    // 视口坐标系:浏览器可视窗口的左上角为原点
    const clientX = e.clientX;
    const clientY = e.clientY;
    // canvas.getBoundingClientRect
    // e.target.getBoundingClientRect()  被点击元素相对于视口的边界矩形(位置和尺寸),是一个包含 left、top、right、bottom、width、height 属性的 DOMRect 对象,由于这里只有一个canvas容器并且没有对其进行位置偏移,所以 left、top 为 0
    const rect = canvas.getBoundingClientRect();
    // 1.鼠标点击的坐标在 canvas 坐标系中的对应坐标
    const [canvasX, canvasY] = [clientX - rect.left, clientY - rect.top]
    // 2.webgl原点在canvas的中心点,也就是 canvas.width / 2, canvas.height / 2
    const [glBaseX, glBaseY] = [rect.width/2, rect.height/2];
    // 3.将 canvas 坐标 转为 webgl 坐标
    const x0 = (canvasX - glBaseX) / glBaseX;
    // 4. y轴方向,与 canvas 轴方向相反
    const y0 = -(canvasY - glBaseY) / glBaseY;

    // 等效于下面 
    const x1 = ((clientX - rect.left) / canvas.width) * 2 - 1;
    const y1 = -((clientY - rect.top) / canvas.height) * 2 + 1;
    gl.vertexAttrib2f(a_Position, x0, y0);
    gl.clear(gl.COLOR_BUFFER_BIT);
    // drawArrays 异步调用 会导致 WebGL 重新渲染,之前的都没了
    gl.drawArrays(gl.POINTS, 0, 1);
});
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

# 5.完整代码

<!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!');
      // …… 其他操作
    }

    // 创建着色器方法,输入参数:渲染上下文,着色器类型,数据源
    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);
    }

    const vertexShaderSource = document.querySelector("#vertex-shader-source").text;
    const fragmentShaderSource = document.querySelector("#fragment-shader-source").text;

    const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
    const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);

    // 创建program的方法
    function createProgram(gl, vertexShader, fragmentShader) {
      const program = gl.createProgram();
      gl.attachShader(program, vertexShader);
      gl.attachShader(program, fragmentShader);
      gl.linkProgram(program);
      var success = gl.getProgramParameter(program, gl.LINK_STATUS);
      if (success) {
        return program;
      }

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

    // 得到创建的 program 便于后面使用
    const program = createProgram(gl, vertexShader, fragmentShader);
    gl.useProgram(program);
    
    // 这里通过 getAttribLocation 获取,需要两个参数,一个是 着色程序对象,一个是 attribute 变量名称
    const a_Position = gl.getAttribLocation(program, 'a_Position');
    gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0);
    // 清空画布
    gl.clearColor(0.0, 0.0, 0.0, 1.0);
    // 绘制渲染
    gl.clear(gl.COLOR_BUFFER_BIT);
    gl.drawArrays(gl.POINTS, 0, 1);

    canvas.addEventListener('click', (e) => {
      // 视口坐标系:浏览器可视窗口的左上角为原点
      const clientX = e.clientX;
      const clientY = e.clientY;
      // canvas.getBoundingClientRect
      // e.target.getBoundingClientRect()  被点击元素相对于视口的边界矩形(位置和尺寸),是一个包含 left、top、right、bottom、width、height 属性的 DOMRect 对象,由于这里只有一个canvas容器并且没有对其进行位置偏移,所以 left、top 为 0
      const rect = canvas.getBoundingClientRect();
      // 1.鼠标点击的坐标在 canvas 坐标系中的对应坐标
      const [canvasX, canvasY] = [clientX - rect.left, clientY - rect.top]
      // 2.webgl原点在canvas的中心点,也就是 canvas.width / 2, canvas.height / 2
      const [glBaseX, glBaseY] = [rect.width/2, rect.height/2];
      // 3.将 canvas 坐标 转为 webgl 坐标
      const x0 = (canvasX - glBaseX) / glBaseX;
      // 4. y轴方向,与 canvas 轴方向相反
      const y0 = -(canvasY - glBaseY) / glBaseY;

      // 等效于下面 
      const x1 = ((clientX - rect.left) / canvas.width) * 2 - 1;
      const y1 = -((clientY - rect.top) / canvas.height) * 2 + 1;
      gl.vertexAttrib2f(a_Position, x0, y0);
      gl.clear(gl.COLOR_BUFFER_BIT);
      // drawArrays 异步调用 会导致 WebGL 重新渲染,之前的都没了
      gl.drawArrays(gl.POINTS, 0, 1);
    });
  </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

# 6.效果图

# 三、连续绘制点

# 1. 封装初始化方法

这里的示例都是按照上面的顺序进行,我们可以将创建着色器对象、编译着色器、连接着色器程序封装成一个方法,便于使用。

// ../utils/utils.js
// 初始化着色程序
export const 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);
}
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

好了,继续。

# 2. 连续绘制多点

# 2.1 JS数组遍历的方式进行

也就是上面动态绘制点案例,改造一下,提供一个 points 数组变量,存储每次点击的坐标,然后遍历渲染。

let points = [];
// 遍历渲染方法
function drawRender() {
    // webgl 底色处理,渲染一帧必须调用
    gl.clear(gl.COLOR_BUFFER_BIT);
    points.map(item => {
        const { x, y, size } = item;
        // 修改着色器中的变量值,准备下一次渲染
        gl.vertexAttrib2f(a_Position, x, y);
        // 同步调用drawArrays方法绘制点
        gl.drawArrays(gl.POINTS, 0, 1);
    });
}
canvas.addEventListener('click', (e) => {
    const clientX = e.clientX;
    const clientY = e.clientY;
    const rect = canvas.getBoundingClientRect();
    const [canvasX, canvasY] = [clientX - rect.left, clientY - rect.top]
    const [glBaseX, glBaseY] = [rect.width/2, rect.height/2];
    const x0 = (canvasX - glBaseX) / glBaseX;
    const y0 = -(canvasY - glBaseY) / glBaseY;

    // 将 webgl 坐标添加到 绘制点的集合中
    points.push({x0, y0});    
    drawRender();
});

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

这里通过数组遍历调用 gl.drawArrays本质上属于分批绘制,在WebGL 1.0中,这是渲染多个不同物体的标准做法。但存在性能代价,每一次循环都意味着一次CPU到GPU的通信,如果循环次数很多,CPU开销会急剧增加,会成为性能瓶颈。

# 2.2 缓冲多点绘制★★★

缓冲的定义开篇已经讲了,就是一个数据缓冲区域,我们可以将数据传入到这个缓冲区域中,调用一次gl.drawArrays进行绘制,降低CPU的开销。

# 1.建立缓冲区并绑定

// ...初始化着色程序后

// 缓冲区 及绑定
const pointBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, pointBuffer);
1
2
3
4
5

gl.createBuffer:创建缓冲对象的方法

gl.bindBuffer(target, buffer):将buffer绑定到目标上;

  • target:绑定的目标,可以是:
    • gl.ARRAY_BUFFER: 包含顶点属性的 Buffer,如顶点坐标,纹理坐标数据或顶点颜色数据。
    • gl.ELEMENT_ARRAY_BUFFER: 用于元素索引的 Buffer。
  • buffer:创建的缓冲对象

# 2. 写入数据

gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
1

gl.bufferData有多个重载方法,[参考](WebGLRenderingContext.bufferData() - Web API | MDN (opens new window))

这里用的是gl.bufferData(target, srcData, usage)

  • target和gl.bindBuffer的一样;

  • srcData:二进制数据

  • usage指定数据存储区的使用方法,

    • gl.STATIC_DRAW: 缓冲区的内容可能经常使用,而不会经常更改。内容被写入缓冲区,但不被读取。

# 3.指定写入的变量

const a_Position = gl.getAttribLocation(program, 'a_Position');
gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
// 开启对应属性
gl.enableVertexAttribArray(a_Position);
1
2
3
4

gl.vertexAttribPointer(index, size, type, normalized, stride, offset) 告诉GPU从当前绑定的缓冲区(bindBuffer() 指定的缓冲区)中读取顶点数据。

  • index:指定要修改的顶点属性
  • size:指定每个顶点属性的组成数量,必须是 1,2,3 或 4。
  • type:指定数组中每个元素的数据类型,可以是:
    • gl.BYTE:有符号的 8 位字节
    • gl.FLOAT:32 位 IEEE 标准的浮点数
  • normalized:当转换为浮点数时是否应该将整数数值归一化到特定的范围。 对于类型gl.BYTEgl.SHORT,如果是 true 则将值归一化为 [-1, 1]。 对于类型gl.FLOATgl.HALF_FLOAT,此参数无效。
  • stride:以字节为单位指定连续顶点属性开始之间的偏移量 (即数组中一行长度)。不能大于 255。如果 stride 为 0,则假定该属性是紧密打包的,即不交错属性,每个属性在一个单独的块中,下一个顶点的属性紧跟当前顶点之后。
  • offset:指定顶点属性数组中第一部分的字节偏移量。必须是类型的字节长度的倍数。

gl.enableVertexAttribArray(index)打开属性数组列表中指定索引处的通用顶点属性数组。不被激活的属性是不会被使用的。

# 4.渲染

gl.clearColor(0,0,0,1);
gl.clear(gl.COLOR_BUFFER_BIT);
gl.drawArrays(gl.POINTS, 0, points.length/2);
1
2
3

# 5.动态绘制

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <canvas id="canvas" width="500" height="500"></canvas>
  <script id="vsSource" type="notjs">
    attribute vec4 a_Position;
    void main() {
      gl_Position=a_Position;
      gl_PointSize=20.0;
    }
  </script>
  <script id="fsSource" type="notjs">
    void main() {
      gl_FragColor=vec4(1,0,0.5,1);
    }
  </script>
  <script type="module">
    import { initProgram } from '../utils/utils.js'
    const canvas = document.querySelector('#canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const gl = canvas.getContext('webgl');
    const vsSource = document.querySelector('#vsSource').text;
    const fsSource = document.querySelector('#fsSource').text;
    initProgram(gl, vsSource, fsSource);
    gl.clearColor(0,0,0,1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    let points = [];
    // 缓冲区 及绑定
    const pointBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pointBuffer);
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    // 开启对应属性
    gl.enableVertexAttribArray(a_Position);
    
    canvas.addEventListener('click', (e) => {
        const clientX = e.clientX;
        const clientY = e.clientY;
        const rect = canvas.getBoundingClientRect();
        const [canvasX, canvasY] = [clientX - rect.left, clientY - rect.top]
        const [glBaseX, glBaseY] = [rect.width/2, rect.height/2];
        const x0 = (canvasX - glBaseX) / glBaseX;
        const y0 = -(canvasY - glBaseY) / glBaseY;

        // 将 webgl 坐标添加到 绘制点的集合中
        points.push(x0);
        points.push(y0);
        drawRender();
    });
    
    function drawRender(){
        // 写入数据
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW); 
        // 修改顶点数据
        gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
        
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.drawArrays(gl.POINTS, 0, points.length/2);
    }
    
  </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

# 2.3 效果图

# 四、线

了解了点,再了解线。在上一篇中,gl.drawArrays方法的第一个参数绘制类型,提及了点、线(线条、线断、闭合线),依次来看看,我们先准备一些顶点数据(四个顶点,x,y 的一维数组,并且直接转换成了32位浮点数)。

const vertices = new Float32Array([
    0,   0, 
    0.5, 0,
    0.5, 0.5,
    0,   0.5
]);
1
2
3
4
5
6

后续线的展示就以此为顶点数据进行展示。然后我们使用buffer进行写入数据

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <canvas id="canvas" width="500" height="500"></canvas>
  <script id="vsSource" type="notjs">
    attribute vec4 a_Position;
    void main() {
      gl_Position=a_Position;
      gl_PointSize=20.0;
    }
  </script>
  <script id="fsSource" type="notjs">
    void main() {
      gl_FragColor=vec4(1,0,0.5,1);
    }
  </script>
  <script type="module">
    import { initProgram } from '../utils/utils.js'
    const canvas = document.querySelector('#canvas');
    canvas.width = window.innerWidth;
    canvas.height = window.innerHeight;
    const gl = canvas.getContext('webgl');
    const vsSource = document.querySelector('#vsSource').text;
    const fsSource = document.querySelector('#fsSource').text;
    initProgram(gl, vsSource, fsSource);
    gl.clearColor(0,0,0,1);
    gl.clear(gl.COLOR_BUFFER_BIT);

    const vertices = new Float32Array([
        0,   0,   // 第一个点 x,y
        0.5, 0,   // 第二个点
        0.5, 0.5, // 第三个点
        0,   0.5  // 第四个点
    ]);
    // 缓冲区 及绑定
    const pointBuffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, pointBuffer);
    const a_Position = gl.getAttribLocation(gl.program, 'a_Position');
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    // 开启对应属性
    gl.enableVertexAttribArray(a_Position);
    // 写入数据
    gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
    // 修改顶点数据
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);
    
	// 渲染绘制
    // ...
    
  </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

# 1. gl.LINE_STRIP

这个线类型是比较常见的类型,上一个点连接到下一个点,依次进行。

 gl.drawArrays(gl.LINE_STRIP, 0, 4); // 总共 4 个点,count即为 4 
1

左下角为第一个点,也就是 (0, 0) 点,然后右下角是第二个点 (0.5, 0) 点,右上角是第三个点 (0.5, 0.5) ,左上角是第四个点 (0, 0.5)。

# 2. gl.LINE_LOOP

这就是闭合的线,最后一个和第一个点进行连线。

 gl.drawArrays(gl.LINE_LOOP, 0, 4);
1

# 3. gl.LINES

绘制一系列单独线段。每两个点作为端点,线段之间不连接。

 gl.drawArrays(gl.LINES, 0, 4);
1

可以看见,第一个点连接第二个点后,第二个没有去连接第三个点,以此类推。

# 4. 动态绘制线

思路:同动态绘制点一样,通过Canvas绑定点击事件,求出鼠标对应的webgl坐标,然后将点加入到线的顶点集合里面。

// 注意下面这点,进行bufferData时再将webgl坐标转为32位浮点数
const points = [];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW);
// ...

// 绑定点击事件
canvas.addEventListener('click', (e) => {
    const clientX = e.clientX;
    const clientY = e.clientY;
    const rect = canvas.getBoundingClientRect();
    const [canvasX, canvasY] = [clientX - rect.left, clientY - rect.top]
    const [glBaseX, glBaseY] = [rect.width/2, rect.height/2];
    const x0 = (canvasX - glBaseX) / glBaseX;
    const y0 = -(canvasY - glBaseY) / glBaseY;

    // 将 webgl 坐标添加到 绘制点的集合中
    points.push(x0);
    points.push(y0);
    drawRender();
});
    
function drawRender(){
    // 写入数据
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(points), gl.STATIC_DRAW); 
    // 修改顶点数据
    gl.vertexAttribPointer(a_Position, 2, gl.FLOAT, false, 0, 0);

    gl.clear(gl.COLOR_BUFFER_BIT);
    // 这里 count 个数 为 points.length/2 因为 points里面一个点只有 x,y。后续还会有其他值,再说……
    gl.drawArrays(gl.LINE_STRIP, 0, points.length/2);
}
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

# 五、总结

本篇介绍了缓冲、属性、全局变量这些gl内置定义,并以动态绘制点、连续绘制点为例,学习如何使用缓冲和属性。并了解了线的类型、动态绘制线等内容。

其中动态绘制,通过绑定 canvas 事件来获取鼠标的视图坐标,再通过视图坐标转为 canvas 坐标,最后通过canvas坐标转为webgl坐标。

# 六、本篇涉及的WebGL-API

  1. gl.getAttribLocation(program, name);

    定义:改方法用于获取着色器程序对象中指定属性变量的属性对象。

    参数

    • program:着色器程序对象。
    • name:属性变量名称
  2. gl.vertexAttrib[1234]f(index, v1,v2,v3,v4)

    定义:用于为通用顶点属性指定常量值。1234对应后面参数个数,如 gl.vertexAttrib3f(a_Position, 0.5, 0.5, 0.0) 就是 属性变量后再 3个参数。

    参数

    • index:属性变量对象。
    • [v1,v2,v3,v4]:对应个数的变量值,浮点数类型。
  3. gl.createBuffer()

    定义:创建并初始化一个用于储存顶点数据或着色数据的缓冲对象。

    参数:无。

  4. gl.bindBuffer(target, buffer);

    定义:将缓冲对象绑定。

    参数

    • target:绑定的点,可以是:
      • gl.ARRAY_BUFFER: 包含顶点属性的 Buffer,如顶点坐标,纹理坐标数据或顶点颜色数据。
      • gl.ELEMENT_ARRAY_BUFFER: 用于元素索引的 Buffer。
    • buffer:缓冲对象
  5. gl.bufferData有多个重载方法,[参考](WebGLRenderingContext.bufferData() - Web API | MDN (opens new window))

    这里用的是gl.bufferData(target, srcData, usage)

    定义:创建并初始化了 Buffer 对象的数据存储区。

    参数

    • target和gl.bindBuffer的一样;

    • srcData:二进制数据

    • usage指定数据存储区的使用方法,

      • gl.STATIC_DRAW: 缓冲区的内容可能经常使用,而不会经常更改。内容被写入缓冲区,但不被读取。
  6. gl.vertexAttribPointer(index, size, type, normalized, stride, offset)

    定义:告诉GPU从当前绑定的缓冲区(bindBuffer() 指定的缓冲区)中读取顶点数据。

    参数

    • index:指定要修改的顶点属性
    • size:指定每个顶点属性的组成数量,必须是 1,2,3 或 4。
    • type:指定数组中每个元素的数据类型,可以是:
      • gl.BYTE:有符号的 8 位字节
      • gl.FLOAT:32 位 IEEE 标准的浮点数
    • normalized:当转换为浮点数时是否应该将整数数值归一化到特定的范围。 对于类型gl.BYTEgl.SHORT,如果是 true 则将值归一化为 [-1, 1]。 对于类型gl.FLOATgl.HALF_FLOAT,此参数无效。
    • stride:以字节为单位指定连续顶点属性开始之间的偏移量 (即数组中一行长度)。不能大于 255。如果 stride 为 0,则假定该属性是紧密打包的,即不交错属性,每个属性在一个单独的块中,下一个顶点的属性紧跟当前顶点之后。
    • offset:指定顶点属性数组中第一部分的字节偏移量。必须是类型的字节长度的倍数。
  7. gl.enableVertexAttribArray(index)

    定义:打开属性数组列表中指定索引处的通用顶点属性数组。不被激活的属性是不会被使用的。

    参数

    • index:指定要激活的顶点属性
  8. 绘制线的类型:gl.LINE_STRIPgl.LINE_LOOPgl.LINES

Last Updated: 7/23/2026, 1:26:50 PM