3-坐标系与鼠标交互

前言

这是webgl入门的第2节,内容包含weblg的坐标系和动态设置图形样式。

1. webgl坐标系统

由于webgl处理的是三维图形,所以它使用三维坐标系统。具有x轴、y轴、z轴。当你面对屏幕的时候,X轴是水平的(正方形向右),Y轴是垂直的(正方向向上(原文中有错)),而Z轴垂直于屏幕(正方向向外)
这套坐标系又叫右手坐标系。

webgl坐标系

如图所示,webgl的坐标系(原点在画布中间)跟canvas的坐标系(原点在画布左上角)是不同的。使用canvas显示webgl的时候,需要将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)

原书关于这里的描述是错误的,已订正

webgl坐标与canvas坐标

你可以点击这里通过修改gl_Position来查看效果。

2.绘制一个点(版本2)

这一节我们将讨论如何在Javascript和着色器之间传输数据。

在这一节中,webgl程序可以将顶点位置坐标从js传到着色器程序中我,然后在对应的位置将点绘制出来。

2.1 attribute变量

我们的目标是将位置信息从js传到着色器,有2种方式可以做到这点:attribute变量和uniform变量。使用哪个变量取决于要传递的数据本身。

  • attribute变量传输的是与顶点有关的数据
  • uniform变量传输的是那些对于所有顶点都相同的数据

attribute变量是GLSL ES变量,被用来从外部向顶点着色器内部传递数据,只有顶点着色器能使用它

使用attribute变量,程序应该包含如下步骤:

  1. 在顶点着色器中,声明attribute变量
  2. 将attribute变量赋值给gl_Position变量
  3. 向attribute变量传输数据

html代码(跟之前一样):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webgl学习</title>
</head>

<body>
<canvas id="canvas" width="400" height="400"></canvas>
<!-- 引入几个webgl帮助函数,这里的代码很简单,你可以直接点击查看 -->
<script src="https://mycode04-1252305175.cos.ap-guangzhou.myqcloud.com/webgl-lib/lib/webgl-utils.js"></script>
<script src="https://mycode04-1252305175.cos.ap-guangzhou.myqcloud.com/webgl-lib/lib/webgl-debug.js"></script>
<script src="https://mycode04-1252305175.cos.ap-guangzhou.myqcloud.com/webgl-lib/lib/cuon-utils.js"></script>
<script src="./index.js"></script>
</body>

</html>

javascript代码(顶点着色器不一样了):

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
//  顶点着色器 注意这里的\n不能省略。否则不符合语法
var VSHADER_SOURCE = `
attribute vec4 a_Position\n;
void main(){\n
gl_Position = a_Position;\n
gl_PointSize = 10.0;\n
}\n
`
// 片段(片元)着色器
var FSHADER_SOURCE = `
void main(){\n
gl_FragColor = vec4(1.0,0.0,0.0,1.0);\n
}\n
`

function main () {
// 获取canvas
var canvas = document.getElementById('canvas')
// 获取webgl绘图上下文
var gl = canvas.getContext('webgl')

if (!gl) {
console.log('Failed to get the rendering context')
retun
}

// 初始化着色器函数
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('设置着色器失败')
return
}
// 获取attribute的存储位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')

if (a_Position < 0) {
console.log('Failed to get the storage of a_Position')
return
}

// 将顶点位置传输给attribute变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)

// 指定清空canvas的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)

// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 画一个点
gl.drawArrays(gl.POINTS, 0, 1)
}

window.onload = function () {
main()
}

点击查看效果

3. 代码解析

3.1 声明attribute变量

我们在着色器中声明的attribute变量

attribute vec4 a_Position;

在这一行中,关键词attribute被称为存储限定符,表示接下来的变量是一个attribute变量。attribute变量必须声明为全局变量,数据将从着色器外部传递给该变量。

变量声明的格式如下:
<存储限定符><类型><变量名>

然后我们将attribute变量赋值给gl_Position

1
gl_Position = a_Position;

3.2 获取attribute变量的存储位置

每一个变量都有一个存储地址,我们通过存储地址向变量传输数据。为了向attribute变量传输数据,我们要先获取其存储地址。

1
2
// 获取attribute的存储位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')

方法的第一个参数是程序对象,它包含顶点着色器和片元着色器。gl.program是怎么来的,你可以查看html代码中引入的帮助函数cuon-utils.js

方法返回值是attribute变量的存储地址,这个地址被存储在js变量中。为了便于理解,本书存储变色器地址的js变量名跟着色器变量名一致(都是a_Position)。

gl.getAttribLocation(program,name)函数规范如下:

类型 参数 含义
参数 program 指定包含顶点着色器和片元着色器的程序对象
参数 name 指定想要获取存储地址的attribute变量名称
返回值 大于等于0 变量地址
返回值 -1 指定的attribute变量不存在,或其命名具有gl_或者webgl_前缀
错误 INVALID_OPERATION 程序对象未能成功连接
INVALID_VALUE name参数的长度大于attribute变量名的最大程度(256个字节)

3.3 向attribute变量赋值

1
2
// 将顶点位置传输给attribute变量
gl.vertexAttrib3f(a_Position, 0.0, 0.0, 0.0)

gl.vertexAttrib3f(location,v0,v1,v2)的用法

类型 参数 含义
参数 location 指定要修改的attribute变量的存储位置
v0 第一个分量的值(x的值)
v1 第二个分量的值(y的值)
v2 第三个分量的值(z的值)
返回值
错误 INVALID_OPERATION 没有当前program对象
INVALID_VALUE location大于等于attribute变量的最大数目默认为8.

gl.vertexAttrib3f() 同族函数

gl.vertexAttrib3f是一系列同族函数中的一个,该系列函数的任务就是象js顶点着色器中的attribute传值。

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

v0为x坐标,v1为y坐标,v2为z坐标,v3默认填1.0就可以。

3.4 webgl函数命名规范

<函数名称><参数个数><参数类型>

4 通过鼠标点击绘点

这一节我们将灵活的拓展js传输数据到顶点着色器的能力:在鼠标点击的位置上把点绘制出来。

html代码(跟之前相同)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>webgl学习</title>
</head>

<body>
<canvas id="canvas" width="400" height="400"></canvas>
<!-- 引入几个webgl帮助函数,这里的代码很简单,你可以直接点击查看 -->
<script src="https://mycode04-1252305175.cos.ap-guangzhou.myqcloud.com/webgl-lib/lib/webgl-utils.js"></script>
<script src="https://mycode04-1252305175.cos.ap-guangzhou.myqcloud.com/webgl-lib/lib/webgl-debug.js"></script>
<script src="https://mycode04-1252305175.cos.ap-guangzhou.myqcloud.com/webgl-lib/lib/cuon-utils.js"></script>
<script src="./index.js"></script>
</body>

</html>

js代码

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
//  顶点着色器 注意这里的\n不能省略。否则不符合语法
var VSHADER_SOURCE = `
attribute vec4 a_Position\n;
void main(){\n
gl_Position = a_Position;\n
gl_PointSize = 10.0;\n
}\n
`
// 片段(片元)着色器
var FSHADER_SOURCE = `
void main(){\n
gl_FragColor = vec4(1.0,0.0,0.0,1.0);\n
}\n
`

function main () {
// 获取canvas
var canvas = document.getElementById('canvas')
// 获取webgl绘图上下文
var gl = canvas.getContext('webgl')

if (!gl) {
console.log('Failed to get the rendering context')
retun
}

// 初始化着色器函数
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('设置着色器失败')
return
}
// 获取attribute的存储位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')

if (a_Position < 0) {
console.log('Failed to get the storage of a_Position')
return
}
// 注册鼠标点击事件响应函数
canvas.onmousedown = function (ev) {
click(ev, gl, canvas, a_Position)
}

// 指定清空canvas的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)

// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 鼠标的点击位置数组
var g_pointArr = []

// 鼠标点击处理函数
function click (ev, gl, canvas, a_Position) {
var x = ev.clientX
var y = ev.clientY
var rect = ev.target.getBoundingClientRect()
// 计算点击位置在webgl系统中的x坐标(原书中这里有错误)
var x = (x - rect.left - canvas.width / 2) / (canvas.width / 2)
// 计算点击位置在webgl系统中的y坐标(原书中有错误)
var y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)
// 将点x和y的坐标存入数组
g_pointArr.push(x)
g_pointArr.push(y)

// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)

var len = g_pointArr.length
for (var i = 0; i < len; i += 2) {
gl.vertexAttrib3f(a_Position, g_pointArr[i], g_pointArr[i + 1], 0.0)
gl.drawArrays(gl.POINTS, 0, 1)
}
}
}

window.onload = function () {
main()
}

点击查看效果

5 函数解析

5.1 注册鼠标响应函数

这跟我们普通的注册dom事件是一样的。

1
2
3
canvas.onmousedown = function (ev) {
click(ev, gl, canvas, a_Position)
}

5.2 计算鼠标点击时的坐标

这一段程序中比较难理解的可能就是这点了。其实很简单。

1
2
3
4
5
6
7
var x = ev.clientX
var y = ev.clientY
var rect = ev.target.getBoundingClientRect()
// 计算点击位置在webgl系统中的x坐标(原书中这里有错误)
var x = (x - rect.left - canvas.width / 2) / (canvas.width / 2)
// 计算点击位置在webgl系统中的y坐标(原书中有错误)
var y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)

以x坐标为例,我们简单的说明下。

ev.clientX为鼠标点击时鼠标距离浏览器左边的距离。
rect.left为canvas元素左边距离浏览器屏幕左边的距离。

那么x-rect.left就是鼠标距离浏览器左边的距离减去canvas元素距离浏览器左边的距离,得到的就是鼠标距离canvas左边的距离。

x - rect.left - canvas.width / 2 这个式子计算的结果的绝对值就是鼠标距离canvas中心点(webgl原点)的距离。

(x - rect.left - canvas.width / 2) / (canvas.width / 2)
就是鼠标点击时鼠标在wegl系统中的坐标值。

y坐标同理,因为我们就得到了鼠标点击时的x、y坐标。

6. 改变点的颜色

要改变点的颜色,就要使用片元着色器。可以使用uniform变量,将颜色值传给着色器。代码如下:

html代码同上,我这里就不写了。

js代码:

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
//  顶点着色器 注意这里的\n不能省略。否则不符合语法
var VSHADER_SOURCE = `
attribute vec4 a_Position\n;
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
void main(){\n
gl_FragColor = u_FragColor;\n
}\n
`

function main () {
// 获取canvas
var canvas = document.getElementById('canvas')
// 获取webgl绘图上下文
var gl = canvas.getContext('webgl')

if (!gl) {
console.log('Failed to get the rendering context')
retun
}

// 初始化着色器函数
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('设置着色器失败')
return
}
// 获取attribute的存储位置
var a_Position = gl.getAttribLocation(gl.program, 'a_Position')

if (a_Position < 0) {
console.log('Failed to get the storage of a_Position')
return
}
// 获取u_FragColor变量的存储位置
var u_FragColor = gl.getUniformLocation(gl.program, 'u_FragColor')

// 注册鼠标点击事件响应函数
canvas.onmousedown = function (ev) {
click(ev, gl, canvas, a_Position)
}

// 指定清空canvas的颜色
gl.clearColor(0.0, 0.0, 0.0, 1.0)

// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)
// 鼠标的点击位置数组
var g_pointArr = []
// 存储点颜色数组
var g_colorArr = []

// 鼠标点击处理函数
function click (ev, gl, canvas, a_Position) {
var x = ev.clientX
var y = ev.clientY
var rect = ev.target.getBoundingClientRect()
// 计算点击位置在webgl系统中的x坐标(原书中这里有错误)
var x = (x - rect.left - canvas.width / 2) / (canvas.width / 2)
// 计算点击位置在webgl系统中的y坐标(原书中有错误)
var y = (canvas.height / 2 - (y - rect.top)) / (canvas.height / 2)
// 将点x和y的坐标存入数组
g_pointArr.push([x, y])
// 将点的颜色存储到g_colorArr中

if (x >= 0.0 && y >= 0.0) {
// 第一象限 红色
g_colorArr.push([1.0, 0.0, 0.0, 1.0])
} else if (x < 0.0 && y < 0.0) {
// 第三象限 绿色
g_colorArr.push([0.0, 1.0, 0.0, 1.0])
} else {
// 其他象限 白色
g_colorArr.push([1.0, 1.0, 1.0, 1.0])
}

// 清空canvas
gl.clear(gl.COLOR_BUFFER_BIT)

var len = g_pointArr.length
for (var i = 0; i < len; i++) {
gl.vertexAttrib3f(a_Position, g_pointArr[i][0], g_pointArr[i][1], 0.0)
console.log('u_FragColor:', u_FragColor)
// 将点的颜色传给u_FragColor变量中
gl.uniform4f(
u_FragColor,
g_colorArr[i][0],
g_colorArr[i][1],
g_colorArr[i][2],
g_colorArr[i][3]
)
gl.drawArrays(gl.POINTS, 0, 1)
}
}
}

window.onload = function () {
main()
}

点击这里查看效果

7. 程序解析

7.1 声明uniform变量

使用片元着色器就必须使用uniform变量,当然你也可以使用varying变量,不过varying变量要复杂的多,我们暂时先不讲它。

使用uniform变量之前,首先需要按照与声明attribute变量相同的格式<存储限定符><类型><变量名>来声明uniform变量。

1
uniform vec4 u_FragColor

在声明uniform变量之前,我们还使用精度限定词指定了变量的范围和精度,本例中为中等精度。关于精度的详细讨论我们在第5章再讲。

1
precision mediump float

7.2 获取uniform变量的存储地址

gl.getUniformLocation(program,name)

类型 参数 含义
参数 program 指定包含顶点着色器和片元着色器的程序对象
name 指定想要获取存储地址的uniform变量名称
返回值 non-null 指定的uniform变量位置
null 指定的uniform变量不存在,或者其命名具有gl_或者webgl_前缀
错误 INVALID_OPPERATION 程序对象未能成功连接
INVALID_VALUE name参数长度大于uniform变量名的最大长度(默认256字节)

7.3 向uniform变量赋值

gl.uniform4f(location,v0,v1,v2,v3)

类型 参数 含义
参数 location 将要修改的uniform变量的存储位置
v0,v1,v2,v3 指定填充uniform变量的第1-4分量
返回值
错误 INVALID_OPERTAION 没有当前program对象,或者location是非法的变量存储位置

7.4 gl.uniform4f()同族函数

gl.uniform4f也有一系列的同族函数,gl.uniform1f()函数传递1个值,gl.uniform2f()函数传递2个值,gl.uniform3f()函数传递3个值,

总结

在这一章中我们学习了,一些webgl的核心函数,并学习了如何使用它们,我们重点学习了着色器的相关知识,它是webgl的基石。