更新于 

鼠标交互

鼠标控制物体旋转

简单实现思路

一句话:根据鼠标移动情况更新作用于物体顶点旋转矩阵。

这是当鼠标左键按下时会发生的一些操作:

  1. 记录鼠标左键 初始坐标
  2. 鼠标移动时获取 当前坐标
  3. 当前坐标减去初始坐标获得 鼠标位移
  4. 根据位移计算 旋转矩阵
  5. 根据旋转矩阵 更新顶点位置
RotateObject Code
RotateObject运行结果
RotateObject运行结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec2 a_TexCoord;
uniform mat4 u_MvpMatrix;
varying vec2 v_TexCoord;
void main(){
gl_Position = u_MvpMatrix * a_Position;
v_TexCoord = a_TexCoord;
}
`;

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
uniform sampler2D u_Sampler;
varying vec2 v_TexCoord;
void main(){
gl_FragColor = texture2D(u_Sampler, v_TexCoord);
}
`;
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
function main() {
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to init shaders');
return;
}
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);

let n = initVertexBuffers(gl);

let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let vpMatrix = new Matrix4();
vpMatrix.setPerspective(
30, 1, 1, 100
).lookAt(
3, 7, 7, 0, 0, 0, 0, 1, 0
);
let currentAngle = [0, 0];
if (!initEventHandlers(canvas, currentAngle)) {
console.log('Failed to init evnet handlers');
return;
}
if (!initTextures(gl)) {
console.log('failed to init texture');
return;
}
let tick = function (ev) {
draw(gl, n, vpMatrix, u_MvpMatrix, currentAngle);
requestAnimationFrame(tick);
};
tick();
}
1
2
3
4
5
6
7
8
9
10
11
12
let g_MvpMatrix = new Matrix4();
function draw(gl, n, vpMatrix, u_MvpMatrix, currentAngle) {
g_MvpMatrix.set(vpMatrix);
g_MvpMatrix.rotate(
currentAngle[0], 1, 0, 0
).rotate(
currentAngle[1], 0, 1, 0
);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_MvpMatrix.elements);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, n, 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
37
38
39
40
41
42
43
44
45
function initVertexBuffers(gl) {
var vertices = new Float32Array([ // Vertex coordinates
1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, 1.0, // v0-v1-v2-v3 front
1.0, 1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0, -1.0, 1.0, 1.0, -1.0, // v0-v3-v4-v5 right
1.0, 1.0, 1.0, 1.0, 1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 up
-1.0, 1.0, 1.0, -1.0, 1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, // v1-v6-v7-v2 left
-1.0, -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, -1.0, -1.0, 1.0, // v7-v4-v3-v2 down
1.0, -1.0, -1.0, -1.0, -1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0 // v4-v7-v6-v5 back
]);

var texCoords = new Float32Array([ // Texture coordinates
1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v0-v1-v2-v3 front
0.0, 1.0, 0.0, 0.0, 1.0, 0.0, 1.0, 1.0, // v0-v3-v4-v5 right
1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.0, 0.0, // v0-v5-v6-v1 up
1.0, 1.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.0, // v1-v6-v7-v2 left
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0, // v7-v4-v3-v2 down
0.0, 0.0, 1.0, 0.0, 1.0, 1.0, 0.0, 1.0 // v4-v7-v6-v5 back
]);

// Indices of the vertices
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9, 10, 8, 10, 11, // up
12, 13, 14, 12, 14, 15, // left
16, 17, 18, 16, 18, 19, // down
20, 21, 22, 20, 22, 23 // back
]);

initArrayBuffer(gl, 'a_Position', vertices, 3, gl.FLOAT);
initArrayBuffer(gl, 'a_TexCoord', texCoords, 2, gl.FLOAT);
let indexBuffer = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);
return indices.length;
}

function initArrayBuffer(gl, attribute, data, num, type) {
let buffer = gl.createBuffer();
let a_Attribute = gl.getAttribLocation(gl.program, attribute);
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
gl.vertexAttribPointer(a_Attribute, num, type, false, 0, 0);
gl.enableVertexAttribArray(a_Attribute);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function initTextures(gl) {
let texture = gl.createTexture();
let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler');
let image = new Image();
image.onload = function () {
loadTexture(gl, texture, u_Sampler, image);
}
image.src = '../img/blueflower.jpg';
return true;
}

function loadTexture(gl, texture, u_Sampler, image) {
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1);
gl.activeTexture(gl.TEXTURE0);
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image);
gl.uniform1i(u_Sampler, 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
37
function initEventHandlers(canvas,currentAngle) {
let dragging = false;
let lastX = -1, lastY = -1;
canvas.onmousedown = function (ev) {
let x = ev.x, y = ev.y;
let rect = ev.target.getBoundingClientRect();
if (
(rect.left <= x && x < rect.right)
&&
(rect.top <= y && y < rect.bottom)
) {
dragging = true;
lastX = x;
lastY = y;
}
}

canvas.onmouseup = function (ev) {
dragging = false;
}
canvas.onmousemove = function (ev) {
let x = ev.x, y = ev.y;
if (dragging) {
let factor = 100 / canvas.height;
let dx = factor * (x - lastX);
let dy = factor * (y - lastY);
// currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 360), -360);
// currentAngle[1] = Math.max(Math.min(currentAngle[1] + dx, 360), -360);
currentAngle[0] = currentAngle[0] + dy;
currentAngle[1] = currentAngle[1] + dx;
}
lastX = x;
lastY = y;
}
return true;
}

选中物体

思考一下:如何使用数学过程来计算鼠标是否悬浮在某个图形上?

简单实现思路

一句话:魔术把戏,利用超出人眼反应速度的刷新帧数来对物体进行切换。

  1. 鼠标左键按下时:将立方体重绘为 单色 (假设红色)
  2. 读取鼠标点击处的 像素颜色
  3. 如果读取的颜色是 红色 ,就判断物体被选中
  4. 将纯色立方体 复原重绘 成原来的纹理
PickObject Code
PickObject运行结果
PickObject运行结果
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
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
uniform mat4 u_MvpMatrix;
uniform bool u_Clicked;
varying vec4 v_Color;
void main(){
gl_Position = u_MvpMatrix * a_Position;
if (u_Clicked) {
v_Color = vec4(1.0, 0.0, 0.0, 1.0);
} else {
v_Color = a_Color;
}
}
`;

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
`;
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
function main() {
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to init shaders');
return;
}
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);

let n = initVertexBuffers(gl);
let u_Clicked = gl.getUniformLocation(gl.program, 'u_Clicked');
let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let vpMatrix = new Matrix4();
vpMatrix.setPerspective(
30, 1, 1, 100
).lookAt(
0, 0, 7, 0, 0, 0, 0, 1, 0
);

// 初始化u_Clicked
gl.uniform1i(u_Clicked, 0);
let currentAngle = 0.0;

canvas.onmousedown = function (ev) {
let x = ev.x, y = ev.y;
let rect = ev.target.getBoundingClientRect();
if ((rect.left <= x && x < rect.right) && (rect.top <= y && y < rect.bottom)) {
// pressed inside <canvas>
// window.position → canvas.position
let x_in_canvas = x - rect.left;
let y_in_canvas = rect.bottom - y;
let picked = check(gl, n, x_in_canvas, y_in_canvas, currentAngle, u_Clicked, vpMatrix, u_MvpMatrix);
if (picked) {
alert('The cube was selected!');
}
}
}

// 旋转动画
let tick = function(){
currentAngle = animate(currentAngle);
draw(gl, n, currentAngle, vpMatrix, u_MvpMatrix);
requestAnimationFrame(tick);
}
tick();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function check(gl, n, x, y, currentAngle, u_Clicked, vpMatrix, u_MvpMatrix) {
let picked = false;
gl.uniform1i(u_Clicked, 1); // Pass true to u_Clicked
draw(gl, n, currentAngle, vpMatrix, u_MvpMatrix); // Draw Cube with red
let pixels = new Uint8Array(4); //Array for storing the pixel value
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
if (pixels[0] === 255) { //Selected
picked = true;
}
// rewrite the cube
gl.uniform1i(u_Clicked, 0);
draw(gl, n, currentAngle, vpMatrix, u_MvpMatrix);
return picked;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let g_MvpMatrix = new Matrix4();
function draw(gl, n, currentAngle, vpMatrix, u_MvpMatrix) {
g_MvpMatrix.set(vpMatrix);
g_MvpMatrix.rotate(
currentAngle, 1, 1, 1
);
gl.uniformMatrix4fv(u_MvpMatrix, false, g_MvpMatrix.elements);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.drawElements(gl.TRIANGLES, n, gl.UNSIGNED_BYTE, 0);
}

let ANGLE_STEP = 20.0;
let g_last = Date.now();
function animate(currentAngle) {
let now = Date.now();
let elapse = now - g_last;
g_last = now;
return (currentAngle + elapse * ANGLE_STEP / 1000) % 360;
}

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
function initVertexBuffers(gl) {
// Create a cube
// v6----- v5
// /| /|
// v1------v0|
// | | | |
// | |v7---|-|v4
// |/ |/
// v2------v3
var vertices = new Float32Array([ // Vertex coordinates
1.0, 1.0, 1.0, -1.0, 1.0, 1.0, -1.0,-1.0, 1.0, 1.0,-1.0, 1.0, // v0-v1-v2-v3 front
1.0, 1.0, 1.0, 1.0,-1.0, 1.0, 1.0,-1.0,-1.0, 1.0, 1.0,-1.0, // v0-v3-v4-v5 right
1.0, 1.0, 1.0, 1.0, 1.0,-1.0, -1.0, 1.0,-1.0, -1.0, 1.0, 1.0, // v0-v5-v6-v1 up
-1.0, 1.0, 1.0, -1.0, 1.0,-1.0, -1.0,-1.0,-1.0, -1.0,-1.0, 1.0, // v1-v6-v7-v2 left
-1.0,-1.0,-1.0, 1.0,-1.0,-1.0, 1.0,-1.0, 1.0, -1.0,-1.0, 1.0, // v7-v4-v3-v2 down
1.0,-1.0,-1.0, -1.0,-1.0,-1.0, -1.0, 1.0,-1.0, 1.0, 1.0,-1.0 // v4-v7-v6-v5 back
]);

var colors = new Float32Array([ // Colors
0.2, 0.58, 0.82, 0.2, 0.58, 0.82, 0.2, 0.58, 0.82, 0.2, 0.58, 0.82, // v0-v1-v2-v3 front
0.5, 0.41, 0.69, 0.5, 0.41, 0.69, 0.5, 0.41, 0.69, 0.5, 0.41, 0.69, // v0-v3-v4-v5 right
0.0, 0.32, 0.61, 0.0, 0.32, 0.61, 0.0, 0.32, 0.61, 0.0, 0.32, 0.61, // v0-v5-v6-v1 up
0.78, 0.69, 0.84, 0.78, 0.69, 0.84, 0.78, 0.69, 0.84, 0.78, 0.69, 0.84, // v1-v6-v7-v2 left
0.32, 0.18, 0.56, 0.32, 0.18, 0.56, 0.32, 0.18, 0.56, 0.32, 0.18, 0.56, // v7-v4-v3-v2 down
0.73, 0.82, 0.93, 0.73, 0.82, 0.93, 0.73, 0.82, 0.93, 0.73, 0.82, 0.93, // v4-v7-v6-v5 back
]);

// Indices of the vertices
var indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // front
4, 5, 6, 4, 6, 7, // right
8, 9,10, 8,10,11, // up
12,13,14, 12,14,15, // left
16,17,18, 16,18,19, // down
20,21,22, 20,22,23 // back
]);

// Write vertex information to buffer object
if (!initArrayBuffer(gl, vertices, gl.FLOAT, 3, 'a_Position')) return -1; // Coordinate Information
if (!initArrayBuffer(gl, colors, gl.FLOAT, 3, 'a_Color')) return -1; // Color Information

// Create a buffer object
var indexBuffer = gl.createBuffer();
if (!indexBuffer) {
return -1;
}
// Write the indices to the buffer object
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

return indices.length;
}
function initArrayBuffer (gl, data, type, num, attribute) {
// Create a buffer object
var buffer = gl.createBuffer();
if (!buffer) {
console.log('Failed to create the buffer object');
return false;
}
// Write date into the buffer object
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);
// Assign the buffer object to the attribute variable
var a_attribute = gl.getAttribLocation(gl.program, attribute);
if (a_attribute < 0) {
console.log('Failed to get the storage location of ' + attribute);
return false;
}
gl.vertexAttribPointer(a_attribute, num, type, false, 0, 0);
// Enable the assignment to a_attribute variable
gl.enableVertexAttribArray(a_attribute);

return true;
}
程序分析
readPixels读取点击处像素颜色
  • 读取来源是 颜色缓冲区
  • 读取位置由 x、y 确定
  • 读取大小由 width、height 确定
  • 读取结果存放在 pixels
  • pixels 必须是 Uint8Array 类型的数组
注意:

对于绑定在 gl.FRAMEBUFFER 上的 帧缓冲区对象
readPixels读取的是 帧缓冲区 ,而非颜色缓冲区。

通过阻塞代码运行观察重绘前的立方体
通过指定颜色判断是否选中物体方法的局限

如果场景中有很多的物体,可以 为每个物体分配一个唯一的颜色值
颜色缓冲区中:RGBA 每个分量都是8bit
那么RGBA一共就可以表示 32bit
也就是可以区分 2^32 种物体。

其他判断物体选中的方法
  • 使用简化的模型
  • 缩小绘图区域
  • 使用帧缓冲区对象

选中一个表面

实际上使用的是和PickObject同样的方法

与PickObject不同的地方在

用户点击鼠标重绘立方体时,
每个像素属于哪个面 的信息写入到 颜色缓冲区α分量 中。
即鼠标点击事件被触发之后,不是将物体颜色进行重写,
而是使用指定值对颜色透明度通道进行重写,
当然也可以对其它通道进行与立方体面序号绑定的重写。

PickFace Code
PickFace
PickFace
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
let VSHADER_SOURCE = `
attribute vec4 a_Position;
attribute vec4 a_Color;
attribute float a_Face; // Surface number
uniform mat4 u_MvpMatrix;
uniform int u_PickedFace; // selected surface number
varying vec4 v_Color;
void main(){
gl_Position = u_MvpMatrix * a_Position;
int face = int(a_Face); // Convert to int
vec3 color = (face == u_PickedFace) ? vec3(1.0) : a_Color.rgb;
if(u_PickedFace == 0){
// In case of 0, insert the face number into alpha
v_Color = vec4(color, a_Face/255.0);
}else{
v_Color = vec4(color, a_Color.a);
}
}
`;

let FSHADER_SOURCE = `
#ifdef GL_ES
precision mediump float;
#endif
varying vec4 v_Color;
void main(){
gl_FragColor = v_Color;
}
`;
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
function main() {
let canvas = document.getElementById('webgl');
let gl = getWebGLContext(canvas);
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('failed to init shaders');
return;
}
gl.clearColor(0, 0, 0, 1);
gl.enable(gl.DEPTH_TEST);
let n = initVertexBuffers(gl);
let currentAngle = 0;
let vpMatrix = new Matrix4().setPerspective(
30, 1, 1, 100
).lookAt(
7, 0, 0, 0, 0, 0, 0, 1, 0
);
let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix');
let u_PickedFace = gl.getUniformLocation(gl.program, 'u_PickedFace');
gl.uniform1i(u_PickedFace, -1);
canvas.onmousedown = function (ev) {
let x = ev.x, y = ev.y;
let rect = ev.target.getBoundingClientRect();
if ((rect.left <= x && x < rect.right) && (rect.top <= y && y < rect.bottom)) {
let x_in_canvas = x - rect.left;
let y_in_canvas = rect.bottom - y;
let face = checkFace(gl, n, vpMatrix, currentAngle, u_MvpMatrix, u_PickedFace, x_in_canvas, y_in_canvas);
gl.uniform1i(u_PickedFace, face);
draw(gl, n, vpMatrix, currentAngle, u_MvpMatrix);
}
}

let tick = function () {
currentAngle = animate(currentAngle);
draw(gl, n, vpMatrix, currentAngle, u_MvpMatrix);
requestAnimationFrame(tick);
}
tick();
}

function checkFace(gl,n,vpMatrix,currentAngle,u_MvpMatrix,u_PickedFace,x,y) {
let pixels = new Uint8Array(4);
gl.uniform1i(u_PickedFace, 0);
draw(gl, n, vpMatrix, currentAngle, u_MvpMatrix);
gl.readPixels(x, y, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
return pixels[3];
}

为什么在着色器中需要对a_Face进行强制转换?
1
2
3
4
5
6
attribute float a_Face;
void main(){
...
int face = int(a_Face);
...
}

答: 因为 attribute 类型的变量不支持 int 类型,并且需要与 int类型u_PickedFace 进行比较。

1
2
3
4
5
6
uniform int u_PickedFace;
void main(){
...
vec3 color = (face == u_PickedFace) ? vec3(1.0) : a_Color.rgb;
...
}

注意:在之后计算 α通道数值 的时候使用的是 未经转换的a_Face

1
v_Color = vec4( color,  a_Face/255.0);