选中三维物体
此处采用了取巧的方法来判断选中的物体:将物体绘制成不同颜色,根据鼠标点击处颜色来判断选中的物体。
示例程序PickObject展示了选中三维物体的一种方式,重要的细节如下:
- 示例程序在点击的时候重绘立方体,通过向着色器传值并重绘将立方体绘制为红色,再提取点击处的颜色:如果点击处为红色,则代表选中,否则未选中。选中状态判断完毕后,再向向着色器传值、重绘,恢复本来颜色。
从颜色缓冲区读取点击处颜色的方法为:
gl.readPixels(x, y, width, height, format, type, pixels)
从颜色缓冲区中读取由x、y、width、height参数确定的矩形块中的所有像素值,并保存在pixels指定的数组中。
参数:
x、y: 指定颜色缓冲区中矩形块左上角的坐标,同时也是读取的第一个像素的坐标
width、height: 指定矩形块的宽度和高度,以像素为单位
format: 指定像素值的颜色格式,必须为gl.RGBA
type: 指定像素值的数据格式,必须为gl.UNSIGNED_BYTE
pixels: 指定用来接收像素值数据的Uint8Array类型化数组
返回值: 无
错误:
INVALID_VALUE: pixels为null,或者,width或height是负值
INVALID_OPERATION: pixels的长度不够存储所有像素值数据
INVALID_ENUM: format或type的值无效
相信有了这一方法,程序的思路就呼之欲出了。
实现选中判断的程序设计如下:
-
着色器设计:顶点着色器中加入bool变量u_Clicked,采用if结构,对v_Clolor进行区别处理。
-
鼠标点击事件:采用document中的方法获取点击处坐标,将坐标转换为WebGL坐标,获取颜色。
首先判断是否在窗口中,主要使用点击事件中ev对象的ev.target.getBoundingClientRect()方法和clientX、clientY属性.
canvas.onmousedown = function (ev) { let x = ev.clientX, y = ev.clientY 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, y_in_canvas = rect.bottom - y let picked = check(gl, n, x_in_canvas, y_in_canvas, currentAngle, u_Clicked, viewProjMatrix, u_MvpMatrix) if (picked) alert('The cube was selected!') } }
点击在窗口中时,直接将立方体重绘为红色,查看点击处的颜色判断是否点击到,之后再重绘为正常颜色。
function check(gl, n, x_in_canvas, y_in_canvas, currentAngle, u_Clicked, viewProjMatrix, u_MvpMatrix) { let picked = false gl.uniform1i(u_Clicked, 1) // 将立方体绘制为红色 draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix) // 读取点击位置的像素颜色值 let pixels = new Uint8Array(4) // 储存像素的数组 gl.readPixels(x_in_canvas, y_in_canvas, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels) if (pixels[0] == 255)// 如果pixels[0]是255则说明点击在物体上 picked = true // 重绘正常状态立方体 gl.uniform1i(u_Clicked, 0) draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix) return picked }
全部代码如下所示:
// PickObject.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'uniform bool u_Clicked;\n' +
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' if (u_Clicked){\n' +
' v_Color = vec4(1.0,0.0,0.0,1.0);\n' +
' } else{\n' +
' v_Color = a_Color;\n' +
' }\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'precision mediump float;\n' +
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_FragColor = v_Color;\n' +
'}\n'
// 主函数
function main() {
// 获取canvas元素
let canvas = document.getElementById('webgl')
// 获取webgl上下文
let gl = getWebGLContext(canvas)
if (!gl) {
console.log('Failed to get the rendering context for WebGL')
return
}
// 初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log('Failed to initialize shaders')
return
}
gl.clearColor(0.0, 0.0, 0.0, 1.0)
gl.enable(gl.DEPTH_TEST)
// 设置顶点坐标和颜色
let n = initVertexBuffers(gl)
if (n < 0) {
console.log('Failed to set the positions of the vertices')
return
}
// 矩阵准备
let viewProjMatrix = new Matrix4()
viewProjMatrix.setPerspective(30.0, canvas.width / canvas.clientHeight, 1.0, 10)
viewProjMatrix.lookAt(3.0, 3.0, 7.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0)
// 获取u_MvpMatrix地址
let u_MvpMatrix = gl.getUniformLocation(gl.program, 'u_MvpMatrix')
if (!u_MvpMatrix) {
console.log('Failed to get the storage location of u_MvpMatrix')
return -1
}
// 获取u_Clicked地址
let u_Clicked = gl.getUniformLocation(gl.program, 'u_Clicked')
if (!u_Clicked) {
console.log('Failed to get the storage location of u_Clicked')
return -1
}
gl.uniform1i(u_Clicked, 0) // 将false传给u_Clicked变量
let currentAngle = 0.0 // 当前旋转角度
// 注册事件响应函数
canvas.onmousedown = function (ev) {
let x = ev.clientX, y = ev.clientY
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, y_in_canvas = rect.bottom - y
let picked = check(gl, n, x_in_canvas, y_in_canvas, currentAngle, u_Clicked, viewProjMatrix, u_MvpMatrix)
if (picked) alert('The cube was selected!')
}
}
draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)
}
// 顶点坐标
function initVertexBuffers(gl) {
// 数据准备
// 顶点坐标
let vertices = new Float32Array([
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
])
// 顶点颜色
let colors = new Float32Array([
0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, // v0-v1-v2-v3 front(blue)
0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, // v0-v3-v4-v5 right(green)
1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, 1.0, 0.4, 0.4, // v0-v5-v6-v1 up(red)
1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, // 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
0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0, 0.4, 1.0, 1.0, // v4-v7-v6-v5 back
])
// 顶点索引
let indices = new Uint8Array([
0, 1, 2, 0, 2, 3, // 前
4, 5, 6, 4, 6, 7, // 右
8, 9, 10, 8, 10, 11, // 上
12, 13, 14, 12, 14, 15, // 左
16, 17, 18, 16, 18, 19, // 下
20, 21, 22, 20, 22, 23, // 后
])
// 将顶点坐标和颜色写入缓冲区
if (!initArrayBuffer(gl, vertices, 3, gl.FLOAT, 'a_Position')) {
return -1
}
if (!initArrayBuffer(gl, colors, 3, gl.FLOAT, 'a_Color')) {
return -1
}
// 索引
let indexBuffer = gl.createBuffer()
if (!indexBuffer) {
console.log('Failed to create indexBuffer')
return -1
}
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer)
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW)
//返回顶点数量
return indices.length
}
function initArrayBuffer(gl, data, num, type, attribute) {
let buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW)
let 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)
gl.enableVertexAttribArray(a_attribute)
return true
}
function check(gl, n, x_in_canvas, y_in_canvas, currentAngle, u_Clicked, viewProjMatrix, u_MvpMatrix) {
let picked = false
gl.uniform1i(u_Clicked, 1) // 将立方体绘制为红色
draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)
// 读取点击位置的像素颜色值
let pixels = new Uint8Array(4) // 储存像素的数组
gl.readPixels(x_in_canvas, y_in_canvas, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, pixels)
if (pixels[0] == 255)// 如果pixels[0]是255则说明点击在物体上
picked = true
// 重绘正常状态立方体
gl.uniform1i(u_Clicked, 0)
draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix)
return picked
}
var g_MvpMatrix = new Matrix4() // 模型视图投影矩阵
// 绘制函数
function draw(gl, n, currentAngle, viewProjMatrix, u_MvpMatrix) {
// 计算模型视图投影矩阵
g_MvpMatrix.set(viewProjMatrix)
g_MvpMatrix.rotate(currentAngle, 0.0, 1.0, 0.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)
}
选中表面
采用同样的方式,也可以对表面进行选择。此处采用颜色缓冲区中的α分量记录“每个像素属于哪个面”的信息。
rgba的每个分量都是8比特,所以仅使用一个分类最多可以区分255个物体
此处将顶点着色器展示如下:
// PickFace.js
// 顶点着色器程序
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'attribute vec4 a_Color;\n' +
'attribute float a_Face;\n' + // 表面编号(attribute只能使用float相关变量)
'uniform mat4 u_MvpMatrix;\n' +
'uniform int u_PickedFace;\n' + // 被选中表面的编号
'varying vec4 v_Color;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' int face = int(a_Face);\n' + // 转为int类型
' vec3 color = (face == u_PickedFace) ? vec3(1.0):a_Color.rgb;\n' +
' if (u_PickedFace == 0){\n' + // 将表面编号写入α分量
' v_Color = vec4(color,a_Face/255.0);\n' +
' } else{\n' +
' v_Color = vec4(color,a_Color.a);\n' +
' }\n' +
'}\n'
从着色器的设计基本可以知道程序的思路。