Three.js中實現對InstanceMesh的碰撞檢測

當時明月在曾照彩雲歸發表於2023-09-20

1. 概述

之前的文章提到,在Three.js中使用InstanceMesh來實現效能最佳化,可以實現單個Mesh的拾取功能

那,能不能實現碰撞檢測呢?肯定是可以的,不過Three.js中並沒有直接的API可以實現對InstanceMesh的碰撞檢測,需要手動實現

回顧本文的描述的Three.js的場景前提:

  • 使用InstanceMesh來構建數量眾多的橋柱,這些柱子都是圓柱且材質相同
  • 使用一個初始圓柱和一系列的變化矩陣,構建了這個場景
  • 有的橋柱是直立的,有的橋柱是傾斜的

本文所採用的方法如下:

  1. 場景初始載入時,透過初始圓柱和變換矩陣,計算每個橋柱的三維包圍盒從而計算二維包圍盒(XZ平面上)
  2. 每一幀分為兩輪檢測,第一次為粗檢測,第二次為細檢測
  3. 每一幀計算待碰撞物體(假設為船)的三維包圍盒從而計算二維包圍盒(XZ平面上)
  4. 粗檢測階段,判斷橋柱的二維包圍盒和船的二維包圍盒是否相交,相交則進入細檢測階段,否則判定不相交
  5. 細檢測階段,將船的包圍盒(假設為長方體)的頂點進行逆變換,變換矩陣為待檢測的這個橋柱的變換矩陣,求出逆變換後的長方體的六個頂點在XZ平面上的最大多邊形,判斷這個多邊形是否於初始柱子的二維包圍盒是否相交

詳細內容如下

2. 初始場景載入

在場景載入時,透過初始圓柱和變換矩陣,計算每個橋柱的三維包圍盒從而計算XZ平面上的二維包圍盒

for (let index = 0; index < matrixList.length; index++) {
    const matrix = matrixList[index];

    const positionAttribute = geo.getAttribute("position") as THREE.BufferAttribute;
    const vertices = positionAttribute.array;

    const box = new THREE.Box3().setFromBufferAttribute(positionAttribute);

    const points = new Array<THREE.Vector3>();
    for (let i = 0; i < vertices.length; i += 3) {
        const vertex = new THREE.Vector3(vertices[i], vertices[i + 1], vertices[i + 2]);
        vertex.applyMatrix4(matrix);
        points.push(vertex);
    }
    box.setFromPoints(points);

    box2dList.push(new THREE.Box2(new THREE.Vector2(box.min.x, box.min.z), new THREE.Vector2(box.max.x, box.max.z)));
}

3. 粗檢測

粗檢測函式較為簡單,判斷橋柱的二維包圍盒和船的二維包圍盒是否相交

function roughDetectionCollided(shipBox2d: THREE.Box2, pillarBox2d: THREE.Box2) {
  return shipBox2d.intersectsBox(pillarBox2d);
}

注意,此處使用的是包圍盒(矩形),而橋柱在XZ平面上應是圓形,在精度要求較高時應使用圓形判斷而不是矩形

function testBoxBox(pillarBox: THREE.Box2, shipBox: THREE.Box2) {
  const pillarBoxCenter = pillarBox.getCenter(new THREE.Vector2());
  const pillarBoxSize = pillarBox.getSize(new THREE.Vector2());
  const circle = new SAT.Circle(new SAT.Vector(pillarBoxCenter.x, pillarBoxCenter.y), pillarBoxSize.x / 2);
  const box = new SAT.Polygon(new SAT.Vector(), [
    new SAT.Vector(shipBox.min.x, shipBox.min.y),
    new SAT.Vector(shipBox.max.x, shipBox.min.y),
    new SAT.Vector(shipBox.max.x, shipBox.max.y),
    new SAT.Vector(shipBox.min.x, shipBox.max.y)
  ]);
  return SAT.testPolygonCircle(box, circle);
}

4. 細檢測

細檢測函式較為複雜,將船的包圍盒(假設為長方體)的頂點進行逆變換,變換矩陣為待檢測的這個橋柱的變換矩陣,求出逆變換後的長方體的六個頂點在XZ平面上的最大多邊形,判斷這個多邊形是否於初始柱子的二維包圍盒是否相交

function fineDetectionCollided(shipPosList: Array<THREE.Vector3>, pillarBox: THREE.Box2, matrix: THREE.Matrix4) {
  const shipPosMatrixedList = new Array<THREE.Vector3>();
  const shipPosListScalared = shipPosList.map(vector3 => vector3.clone().multiplyScalar(1000));
  for (let i = 0; i < shipPosListScalared.length; i++) {
    const transformedVector = new THREE.Vector3().copy(shipPosListScalared[i]).applyMatrix4(matrix.clone().invert());
    shipPosMatrixedList.push(transformedVector);
  }

  const points = shipPosMatrixedList.map((pos) => new Point(pos.x, pos.z));
  const selectedPoints: Point[] = [];
  const maxArea: number[] = [0];
  const result: Point[] = [];

  findLargestPolygon(points, selectedPoints, maxArea, result);
  const sortedPoints = sortPointsInCounterClockwiseOrder(result);

  const satShipPolygon = new SAT.Polygon(new SAT.Vector(), [
    new SAT.Vector(sortedPoints[0].x, sortedPoints[0].y),
    new SAT.Vector(sortedPoints[1].x, sortedPoints[1].y),
    new SAT.Vector(sortedPoints[2].x, sortedPoints[2].y),
    new SAT.Vector(sortedPoints[3].x, sortedPoints[3].y),
    new SAT.Vector(sortedPoints[4].x, sortedPoints[4].y),
    new SAT.Vector(sortedPoints[5].x, sortedPoints[5].y),
  ]);

  const pillarBoxCenter = pillarBox.getCenter(new THREE.Vector2());
  const pillarBoxSize = pillarBox.getSize(new THREE.Vector2());
  const circle = new SAT.Circle(new SAT.Vector(pillarBoxCenter.x, pillarBoxCenter.y), pillarBoxSize.x / 2);

  return SAT.testPolygonCircle(satShipPolygon, circle);
}

5. 碰撞檢測

最後,在場景每一幀更新時,呼叫碰撞檢測函式,碰撞檢測函式則是先呼叫粗檢測函式,粗檢測相交後再呼叫細檢測函式

function detectionCollided() {
  const ship = scene.getObjectByName(ModelName.Ship);
  const collidedIndex = new Array<number>();
  if (!ship) return collidedIndex;
  const shipBox3d = new THREE.Box3().setFromObject(ship);

  const shipBox2d = new THREE.Box2().setFromPoints([new THREE.Vector2(shipBox3d.min.x, shipBox3d.min.z), new THREE.Vector2(shipBox3d.max.x, shipBox3d.max.z)]);
  box2dList.forEach((pillarBox2d, i) => {
    if (roughDetectionCollided(shipBox2d, pillarBox2d)) {
      const matrix = matrixList[i]
      const positionAttribute = geo.getAttribute("position") as THREE.BufferAttribute;
      const points = new Array<THREE.Vector3>();
      const vertices = positionAttribute.array

      for (let j = 0; j < vertices.length; j += 3) {
        const vertex = new THREE.Vector3(vertices[j], vertices[j + 1], vertices[j + 2]);
        points.push(vertex);
      }
      const box3d = new THREE.Box3().setFromPoints(points);

      const box2d = new THREE.Box2().setFromPoints([new THREE.Vector2(box3d.min.x, box3d.min.z), new THREE.Vector2(box3d.max.x, box3d.max.z)])

      const minPoint = shipBox3d.min;
      const maxPoint = shipBox3d.max;
      const shipPos = [
        new THREE.Vector3(minPoint.x, minPoint.y, minPoint.z),
        new THREE.Vector3(minPoint.x, minPoint.y, maxPoint.z),
        new THREE.Vector3(minPoint.x, maxPoint.y, minPoint.z),
        new THREE.Vector3(minPoint.x, maxPoint.y, maxPoint.z),
        new THREE.Vector3(maxPoint.x, minPoint.y, minPoint.z),
        new THREE.Vector3(maxPoint.x, minPoint.y, maxPoint.z),
        new THREE.Vector3(maxPoint.x, maxPoint.y, minPoint.z),
        new THREE.Vector3(maxPoint.x, maxPoint.y, maxPoint.z)
      ];

      if (Math.abs(pillarBox2d.max.x - pillarBox2d.min.x - pillarBox2d.max.y + pillarBox2d.min.y) < 1e-10) {
        if (testBoxBox(pillarBox2d, shipBox2d)) collidedIndex.push(i);
      } else if (fineDetectionCollided(shipPos, box2d, matrix)) {
        collidedIndex.push(i);
      }
    }
  });
  return collidedIndex;
}

6. 進一步最佳化

在實測中,上述這種方式執行起來還算流暢,主要是因為細檢測雖然消耗效能但是隻執行少數幾次,粗檢測則幾乎只是比大小,參考下面的Three.js中Box2.js的原始碼:

intersectsBox( box ) {
    // using 4 splitting planes to rule out intersections
    return box.max.x < this.min.x || box.min.x > this.max.x ||
        box.max.y < this.min.y || box.min.y > this.max.y ? false : true;
}

這裡提出三個最佳化方向:

  • 使用Web Worker來開啟新執行緒進行計算,將計算過程抽離主執行緒,保證繪製、互動的流暢
  • 使用空間劃分,如BVH,將包圍盒進行劃分,能有效減少碰撞檢測時的檢測次數,而不必每個包圍盒都檢測一次
  • 使用OBB進行簡化程式碼,Three.js中支援OBB,和上述程式碼中採用的AABB式包圍盒相比,OBB式包圍盒更好地支援矩陣變換

7. 參考資料

[1] SAT.js (jriecken.github.io)

[2] InstancedMesh – three.js docs (threejs.org)

[3] Three.js使用InstancedMesh實現效能最佳化 - 當時明月在曾照彩雲歸 - 部落格園 (cnblogs.com)

相關文章