WebGL技巧:鼠标控制物体旋转

鼠标控制物体旋转

该技巧很好实现。
在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平面上的矩形,使用纹理填充,设计了视图矩阵和投影矩阵,通过鼠标操作模型矩阵对图形进行旋转变换。下面说明整个示例的流程:

  1. 着色器设计:
    1. 顶点着色器需要定义顶点坐标a_Position和模型视图投影矩阵变量u_MvpMatrix对顶点坐标进行操作,定义a_TexCoord和v_TexCoord分别接收并传递顶点对应的纹理坐标。
    2. 片元着色器需要定义纹理对象u_Sampler接收图像,定义v_TexCoord接收内插后的纹理坐标。
  2. 着色器初始化:包括创建着色器、着色器存入信息、着色器编译、创建program对象,program连接着色器、program内部连接等,还包括错误信息的打印。上述过程封装在initShader()函数中,创建的program对象挂载在gl绘图上下文下。
  3. 配置顶点信息:将顶点坐标、对应纹理坐标保存在缓冲区中分配给对应变量,将坐标索引存到缓冲区中,该缓冲区绑定到gl.ELEMENT_ARRAY_BUFFER。上述过程封装在initVertexBuffers()函数中。
  4. 配置纹理信息:包括创建纹理对象、创建image对象并加载图像,图像加载完成后(onload),处理图像-开启纹理单元-绑定纹理对象配置纹理参数-配置纹理图像-最后将纹理单元传递给着色器。上述过程封装在initTextures()和loadTexture()中。
  5. 鼠标事件响应:按照最开始的介绍,绑定onmousedown/onmouseup/onmousemove三个事件的响应函数,从callback函数的固定参数ev中获取鼠标位置,借以和上一状态的鼠标位置进行比较,计算获得需要旋转的角度,结果体现在currentAngle数组中。
  6. 绘制过程:采用requestAnimationFrame方法,只要标签激活,就会不停地绘制。每次绘制根据currentAngle进行旋转变换。
  7. 一些细节,如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)
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在WebGL中使用鼠标控制物体旋转,你需要完成以下步骤: 1. 获取canvas元素和WebGL上下文。 2. 在canvas元素上添加鼠标事件监听器,以便在用户拖动鼠标时捕获鼠标移动的位置。 3. 计算鼠标移动的距离和方向,并将其转换为物体旋转的角度。 4. 在WebGL中设置物体旋转矩阵,以便将其旋转到新的角度。 以下是一个简单的示例代码: ```javascript // 获取canvas元素和WebGL上下文 const canvas = document.getElementById("canvas"); const gl = canvas.getContext("webgl"); // 定义物体的初始旋转角度 let angleX = 0; let angleY = 0; // 添加鼠标事件监听器 let dragging = false; let lastX, lastY; canvas.addEventListener("mousedown", function(event) { dragging = true; lastX = event.clientX; lastY = event.clientY; }); canvas.addEventListener("mousemove", function(event) { if (dragging) { let deltaX = event.clientX - lastX; let deltaY = event.clientY - lastY; angleY += deltaX / 100; angleX += deltaY / 100; lastX = event.clientX; lastY = event.clientY; } }); canvas.addEventListener("mouseup", function(event) { dragging = false; }); // 设置物体旋转矩阵 function setMatrixUniforms() { let perspectiveMatrix = makePerspective(45, 1.0, 0.1, 100.0); let modelViewMatrix = makeTranslation(0.0, 0.0, -6.0); modelViewMatrix = multiply(modelViewMatrix, makeXRotation(angleX)); modelViewMatrix = multiply(modelViewMatrix, makeYRotation(angleY)); let matrix = multiply(perspectiveMatrix, modelViewMatrix); gl.uniformMatrix4fv(shaderProgram.pMatrixUniform, false, new Float32Array(matrix)); } ``` 在这个例子中,我们假设已经定义了一些用于设置和传递矩阵的函数。你需要根据自己的代码和需求来调整这些函数。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值