鼠标控制物体旋转
该技巧很好实现。
在WebGL中,物体旋转是通过模型矩阵来控制的,要实现鼠标控制物体旋转,只需要根据鼠标的移动实时修改模型矩阵,将矩阵传输到缓冲区并实时绘制即可。
示例程序RotateObject.js展示了这一过程,值得注意的一些细节如下:
- 示例程序设计的旋转包括绕x轴旋转和绕y轴旋转,保存在数组currentAngle中,在主函数中调用initEventHandlers()函数,挂载鼠标响应事件。
// 注册事件响应函数
let currentAngle = [0.0, 0.0] // 绕x轴旋转角度,绕y轴旋转角度
initEventHandlers(canvas, currentAngle);
- initEventHandlers()函数通过html标签的onmousedown/onmouseup/onmousemove方法绑定响应事件,响应事件对旋转角度进行修改
// 鼠标响应事件
function initEventHandlers(canvas, currentAngle) {
let dragging = false // 是否拖动
let lastX = -1, lastY = -1// 鼠标的最后位置
canvas.onmousedown = function (ev) {
let x = ev.clientX, y = ev.clientY
// 如果鼠标在<canvas>内就开始拖动
let rect = ev.target.getBoundingClientRect()
if (rect.left <= x && x < rect.right && rect.top <= y && y <= rect.bottom) {
lastX = x, lastY = y
dragging = true
}
}
// 松开鼠标
canvas.onmouseup = function (ev) { dragging = false }
// 移动鼠标
canvas.onmousemove = function (ev) {
let x = ev.clientX, y = ev.clientY
if (dragging) {
let factor = 100 / canvas.clientHeight
let dx = factor * (x - lastX)
let dy = factor * (y - lastY)
// 将沿Y轴旋转的角度控制在-90到90度之间
currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0)
currentAngle[1] += dx
}
lastX = x, lastY = y
}
}
整个示例绘制了一个z=0平面上的矩形,使用纹理填充,设计了视图矩阵和投影矩阵,通过鼠标操作模型矩阵对图形进行旋转变换。下面说明整个示例的流程:
- 着色器设计:
- 顶点着色器需要定义顶点坐标a_Position和模型视图投影矩阵变量u_MvpMatrix对顶点坐标进行操作,定义a_TexCoord和v_TexCoord分别接收并传递顶点对应的纹理坐标。
- 片元着色器需要定义纹理对象u_Sampler接收图像,定义v_TexCoord接收内插后的纹理坐标。
- 着色器初始化:包括创建着色器、着色器存入信息、着色器编译、创建program对象,program连接着色器、program内部连接等,还包括错误信息的打印。上述过程封装在initShader()函数中,创建的program对象挂载在gl绘图上下文下。
- 配置顶点信息:将顶点坐标、对应纹理坐标保存在缓冲区中分配给对应变量,将坐标索引存到缓冲区中,该缓冲区绑定到gl.ELEMENT_ARRAY_BUFFER。上述过程封装在initVertexBuffers()函数中。
- 配置纹理信息:包括创建纹理对象、创建image对象并加载图像,图像加载完成后(onload),处理图像-开启纹理单元-绑定纹理对象配置纹理参数-配置纹理图像-最后将纹理单元传递给着色器。上述过程封装在initTextures()和loadTexture()中。
- 鼠标事件响应:按照最开始的介绍,绑定onmousedown/onmouseup/onmousemove三个事件的响应函数,从callback函数的固定参数ev中获取鼠标位置,借以和上一状态的鼠标位置进行比较,计算获得需要旋转的角度,结果体现在currentAngle数组中。
- 绘制过程:采用requestAnimationFrame方法,只要标签激活,就会不停地绘制。每次绘制根据currentAngle进行旋转变换。
- 一些细节,如canvas元素的获取、绘图上下文的获取、开启深度检测(不开启也可以)、背景色的设置等没有说明。
下面展示全部的代码。代码较长,保留了纹理图像非2的n次幂图像的处理方式。
// RotateObject.js
// 顶点着色器
var VSHADER_SOURCE =
'attribute vec4 a_Position;\n' +
'uniform mat4 u_MvpMatrix;\n' +
'attribute vec2 a_TexCoord;\n' +
'varying vec2 v_TexCoord;\n' +
'void main(){\n' +
' gl_Position = u_MvpMatrix * a_Position;\n' +
' v_TexCoord = a_TexCoord;\n' +
'}\n'
// 片元着色器
var FSHADER_SOURCE =
'precision mediump float;\n' +
'uniform sampler2D u_Sampler;\n' +
'varying vec2 v_TexCoord;\n' +
'void main(){\n' +
' gl_FragColor = texture2D(u_Sampler, v_TexCoord);\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
}
// 配置纹理信息
if (!initTextures(gl, n)) {
console.log('Failed to configure the texture')
return
}
// 注册事件响应函数
let currentAngle = [0.0, 0.0] // 绕x轴旋转角度,绕y轴旋转角度
initEventHandlers(canvas, currentAngle);
// 矩阵准备
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
}
let tick = function () { // 开始绘制
draw(gl, n, viewProjMatrix, u_MvpMatrix, currentAngle)
requestAnimationFrame(tick)
}
tick()
}
// 顶点坐标
function initVertexBuffers(gl) {
// 数据准备
let vertices = new Float32Array([
// 顶点坐标,纹理坐标
-2, 2, -2, -2, 2, 2, 2, -2
])
let texCoords = new Float32Array([
0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0
])
let indices = new Uint8Array([
0, 1, 2, 1, 3, 2
])
let n = 4
// 顶点坐标
if (!initArrayBuffer(gl, vertices, 2, gl.FLOAT, 'a_Position')) {
return -1
}
// 纹理坐标
if (!initArrayBuffer(gl, texCoords, 2, gl.FLOAT, 'a_TexCoord')) {
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 initTextures(gl, n) {
// 创建纹理对象
let texture = gl.createTexture()
if (!texture) {
console.log('Failed to create texture')
return
}
// 获取u_Sampler(纹理图像)的存储位置
let u_Sampler = gl.getUniformLocation(gl.program, 'u_Sampler')
if (!u_Sampler) {
console.log('Failed to get the storage loaction of u_Sampler')
return
}
// 创建一个image对象
let image = new Image()
// 注册图像加载事件的响应函数
image.onload = function () {
loadTexture(gl, n, texture, u_Sampler, image)
}
// 浏览器加载图像
image.src = '../image/sky.jpg'
// image.src = '../image/image_Hakurei Reimu.jpg'
return true
}
function loadTexture(gl, n, texture, u_Sampler, image) {
// 对纹理图像进行Y轴反转
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 1)
// 开启0号纹理单元
gl.activeTexture(gl.TEXTURE0)
// 向target绑定纹理对象
gl.bindTexture(gl.TEXTURE_2D, texture)
// 手动修改纹理图像
// if (!isPowerofTwo(image.width) || !isPowerofTwo(image.height)) {
// // Scale up the texture to the next highest power of two dimensions.
// let canvas = document.createElement('canvas')
// canvas.width = nextHighestPowerOfTwo(image.width)
// canvas.height = nextHighestPowerOfTwo(image.height)
// let ctx = canvas.getContext('2d')
// ctx.drawImage(image, 0, 0, image.width, image.height)
// image = canvas
// }
// 配置纹理参数
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)
// 配置纹理图像
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image)
// 将0号纹理传递给着色器
gl.uniform1i(u_Sampler, 0)
}
// x是否为2的n次幂,位运算符
function isPowerofTwo(x) {
return (x & (x - 1)) == 0
}
// x最接近的大一点的2的n次幂
function nextHighestPowerOfTwo(x) {
--x
for (let i = 1; i < 32; i <<= 1) {
x = x | (x >> i)
}
return x + 1
}
// 鼠标响应事件
function initEventHandlers(canvas, currentAngle) {
let dragging = false // 是否拖动
let lastX = -1, lastY = -1// 鼠标的最后位置
canvas.onmousedown = function (ev) {
let x = ev.clientX, y = ev.clientY
// 如果鼠标在<canvas>内就开始拖动
let rect = ev.target.getBoundingClientRect()
if (rect.left <= x && x < rect.right && rect.top <= y && y <= rect.bottom) {
lastX = x, lastY = y
dragging = true
}
}
// 松开鼠标
canvas.onmouseup = function (ev) { dragging = false }
// 移动鼠标
canvas.onmousemove = function (ev) {
let x = ev.clientX, y = ev.clientY
if (dragging) {
let factor = 100 / canvas.clientHeight
let dx = factor * (x - lastX)
let dy = factor * (y - lastY)
// 将沿Y轴旋转的角度控制在-90到90度之间
currentAngle[0] = Math.max(Math.min(currentAngle[0] + dy, 90.0), -90.0)
currentAngle[1] += dx
}
lastX = x, lastY = y
}
}
var g_MvpMatrix = new Matrix4() // 模型视图投影矩阵
// 绘制函数
function draw(gl, n, viewProjMatrix, u_MvpMatrix, currentAngle) {
// 计算模型视图投影矩阵
g_MvpMatrix.set(viewProjMatrix)
g_MvpMatrix.rotate(currentAngle[0], 1.0, 0.0, 0.0)
g_MvpMatrix.rotate(currentAngle[0], 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)
}