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;
}
2
3
4
5
# 2.片段着色器
片段着色器不进行更改:
precision mediump float;
void main(){
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
}
2
3
4
5
# 3.创建WebGL对象
首先还是建立canvas标签
<canvas id='webgl' width='500' height='500'></canvas>
然后在js中通过 document.getElementById 获取到canvas对象;再通过canvas来获取WebGL对象。
let canvas = document.getElementById('webgl');
let gl = canvas.getContext('webgl');
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);
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);
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);
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);
});
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>
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);
}
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();
});
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);
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);
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);
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.BYTE和gl.SHORT,如果是 true 则将值归一化为 [-1, 1]。 对于类型gl.FLOAT和gl.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);
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>
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
]);
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>
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
左下角为第一个点,也就是 (0, 0) 点,然后右下角是第二个点 (0.5, 0) 点,右上角是第三个点 (0.5, 0.5) ,左上角是第四个点 (0, 0.5)。
# 2. gl.LINE_LOOP
这就是闭合的线,最后一个和第一个点进行连线。
gl.drawArrays(gl.LINE_LOOP, 0, 4);
# 3. gl.LINES
绘制一系列单独线段。每两个点作为端点,线段之间不连接。
gl.drawArrays(gl.LINES, 0, 4);
可以看见,第一个点连接第二个点后,第二个没有去连接第三个点,以此类推。
# 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);
}
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
gl.getAttribLocation(program, name);定义:改方法用于获取着色器程序对象中指定属性变量的属性对象。
参数:
- program:着色器程序对象。
- name:属性变量名称
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]:对应个数的变量值,浮点数类型。
gl.createBuffer()定义:创建并初始化一个用于储存顶点数据或着色数据的缓冲对象。
参数:无。
gl.bindBuffer(target, buffer);定义:将缓冲对象绑定。
参数:
- target:绑定的点,可以是:
gl.ARRAY_BUFFER: 包含顶点属性的 Buffer,如顶点坐标,纹理坐标数据或顶点颜色数据。gl.ELEMENT_ARRAY_BUFFER: 用于元素索引的 Buffer。
- buffer:缓冲对象
- target:绑定的点,可以是:
gl.bufferData有多个重载方法,[参考](WebGLRenderingContext.bufferData() - Web API | MDN (opens new window))这里用的是
gl.bufferData(target, srcData, usage)定义:创建并初始化了 Buffer 对象的数据存储区。
参数:
target和
gl.bindBuffer的一样;srcData:二进制数据usage指定数据存储区的使用方法,gl.STATIC_DRAW: 缓冲区的内容可能经常使用,而不会经常更改。内容被写入缓冲区,但不被读取。
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.BYTE和gl.SHORT,如果是 true 则将值归一化为 [-1, 1]。 对于类型gl.FLOAT和gl.HALF_FLOAT,此参数无效。 - stride:以字节为单位指定连续顶点属性开始之间的偏移量 (即数组中一行长度)。不能大于 255。如果 stride 为 0,则假定该属性是紧密打包的,即不交错属性,每个属性在一个单独的块中,下一个顶点的属性紧跟当前顶点之后。
- offset:指定顶点属性数组中第一部分的字节偏移量。必须是类型的字节长度的倍数。
gl.enableVertexAttribArray(index)定义:打开属性数组列表中指定索引处的通用顶点属性数组。不被激活的属性是不会被使用的。
参数:
- index:指定要激活的顶点属性
绘制线的类型:
gl.LINE_STRIP、gl.LINE_LOOP、gl.LINES