在WebGL中使用射线选择模型

Published at 2018-08-21

DEMO

思路

首先要用到摄像机的位置、方向和鼠标在屏幕上的位置来得到射线的起点和方向,然后用得到的射线和模型的每个三角形测试来判断模型是否被选中。

构建射线

先实现简单的射线类:

function Ray(position, direction) {
  this.position = position
  this.direction = direction
}

通过camera.positioncamera.front可以直接得到一个以摄像机为原地,朝向摄像机正前方的射线。 然后,根据摄像机的fov和鼠标的位置坐标可以计算出鼠标所指方向偏离中心的x轴和y轴角度。

argl.canvas.addEventListener('mousemove', e => {
  let angleY = -((e.offsetY * camera.zoom / argl.options.height) - (camera.zoom / 2))
  let zoomX = (camera.zoom / argl.options.height) * argl.options.width
  let angleX = (e.offsetX * zoomX / argl.options.width) - (zoomX / 2)
}

接下来,再将朝向摄像机正前方的向量在camera.frontcamera.right两个向量所在的平面中旋转angleX度,再在旋转后的向量和camera.up两个向量所在的平面中旋转angleY度,就得到了鼠标所指向的方向。

let normalFR = vec3.cross([], camera.front, camera.right)
vec3.normalize(normalFR, normalFR)

let direction = rotateVec(camera.front, normalFR, glMatrix.toRadian(angleX))

let normalFU = vec3.cross([], direction, camera.up)
vec3.normalize(normalFU, normalFU)

direction = rotateVec(direction, normalFU, glMatrix.toRadian(angleY))

//平面内向量的旋转,normal为平面的法线,angle为旋转角度
function rotateVec(vec, normal, angle) {
  vec3.normalize(normal, normal)
  let t1 = vec3.scale([], vec, Math.cos(angle))
  let t2 = vec3.cross([], normal, vec)
  vec3.scale(t2, t2, Math.sin(angle))
  return vec3.add([], t1, t2)
}

然后就可以构建所需射线了:

let ray = new Ray(camera.position, direction)

射线三角形相交检测算法

使用的算法为Möller–Trumbore intersection algorithm

js实现如下, 我将其作为Ray类的一个方法:

Ray.prototype.intersectsTriangle = function (triangle) {
  const EPSILON = 0.0000001
  let a, f, u, v, t
  let edge1 = vec3.sub([], triangle[1], triangle[0])
  let edge2 = vec3.sub([], triangle[2], triangle[0])
  let h = vec3.cross([], this.direction, edge2)
  a = vec3.dot(edge1, h)
  if (a > -EPSILON && a < EPSILON)
    return false
  f = 1 / a
  let s = vec3.sub([], this.position, triangle[0])
  u = f * vec3.dot(s, h)
  if (u < 0.0 || u > 1.0)
    return false
  let q = vec3.cross([], s, edge1)
  v = f * vec3.dot(this.direction, q)
  if (v < 0.0 || u + v > 1.0)
    return false
  // At this stage we can compute t to find out where the intersection point is on the line.
  t = f * vec3.dot(edge2, q)
  if (t > EPSILON) // ray intersection
  {
    let temp = vec3.scale([], this.direction, t)
    let IntersectionPoint = vec3.add([], this.position, temp)
    // intersection point to use
    return true
  }
  else // This means that there is a line intersection but not a ray intersection.
    return false
}

检测射线与模型是否相交

通过循环,使用mesh.indicesmesh.vertices中的数据来构建每一个三角形,再将其坐标变换到世界空间,然后检测他们是否与射线相交

let flag = false
let len = suzanneMesh.indices.length

for (let i = 0; i < len; i += 3) {
  let index = [suzanneMesh.indices[i], suzanneMesh.indices[i + 1], suzanneMesh.indices[i + 2]]
  let triangle = new Array(3)
  triangle[0] = vec3.transformMat4([], [suzanneMesh.vertices[index[0] * 3], suzanneMesh.vertices[index[0] * 3 + 1], suzanneMesh.vertices[index[0] * 3 + 2]], model)
  triangle[1] = vec3.transformMat4([], [suzanneMesh.vertices[index[1] * 3], suzanneMesh.vertices[index[1] * 3 + 1], suzanneMesh.vertices[index[1] * 3 + 2]], model)
  triangle[2] = vec3.transformMat4([], [suzanneMesh.vertices[index[2] * 3], suzanneMesh.vertices[index[2] * 3 + 1], suzanneMesh.vertices[index[2] * 3 + 2]], model)

  if (ray.intersectsTriangle(triangle)) {
    flag = true
  }
}

然后即可利用布尔值flag来添加是否选中模型的效果了,demo中选中时模型变成红色。

完整代码

Deadalusmask/ArGL-demos/src/pick_test

DEMO