🎮

Three.jsで衝突計算とか色々

2023/12/22に公開

はじめに

ただの個人的なメモです。
最近、趣味でWebGLゲームエンジンを個人開発している途中で、Box,Sphere,Capsuleそれぞれの物理演算をする必要が出てきたため、ナレッジとしてここに垂れ流します。

あまり多くの検証はしていないので、”こうしたらいいんじゃない?”みたいなのがあれば指摘してほしいです。

レポジトリは以下です。
https://github.com/foasho/NinjaCore/tree/master/src/lib/utils/IntersectsDetector

BoxとBoxの衝突判定

/**
 * 参考:SAT法による衝突判定
 * https://discussions.unity.com/t/calculate-collision-between-2-rotated-boxes-without-using-boxcollider-math/246630/3
 */
import { Vector3, Matrix4, Box3, Mesh, BoxGeometry } from "three";

// キャッシュ用の変数
const tempVertices = [
  new Vector3(-0.5, -0.5, -0.5),
  new Vector3(0.5, -0.5, -0.5),
  new Vector3(-0.5, 0.5, -0.5),
  new Vector3(0.5, 0.5, -0.5),
  new Vector3(-0.5, -0.5, 0.5),
  new Vector3(0.5, -0.5, 0.5),
  new Vector3(-0.5, 0.5, 0.5),
  new Vector3(0.5, 0.5, 0.5),
];
const b1 = new Box3();
const b2 = new Box3();
const av11 = new Vector3(1, 0, 0);
const av12 = new Vector3(0, 1, 0);
const av13 = new Vector3(0, 0, 1);
const av21 = new Vector3(1, 0, 0);
const av22 = new Vector3(0, 1, 0);
const av23 = new Vector3(0, 0, 1);

const projectBoxOnAxis = (
  box: Mesh,
  axis: Vector3,
  matrix: Matrix4
): {
  min: number;
  max: number;
} => {
  const vertices = tempVertices.map((vertex) => vertex.clone());
  let min = Infinity;
  let max = -Infinity;

  vertices.forEach((vertex) => {
    vertex.applyMatrix4(matrix);
    const projection = vertex.dot(axis);
    min = Math.min(min, projection);
    max = Math.max(max, projection);
  });

  return { min, max };
};

const isSeparatedOnAxis = (
  axis: Vector3,
  box1: Mesh,
  box2: Mesh,
  matrix1: Matrix4,
  matrix2: Matrix4
) => {
  const worldAxis = axis.clone().normalize();

  const projection1 = projectBoxOnAxis(box1, worldAxis, matrix1);
  const projection2 = projectBoxOnAxis(box2, worldAxis, matrix2);

  return projection1.max < projection2.min || projection2.max < projection1.min;
};

export const detectBoxBoxCollision = (
  boxMesh1: Mesh,
  boxMesh2: Mesh
): boolean => {
  // 回転率が微小な場合は、AABBで判定
  if (
    Math.abs(boxMesh1.rotation.x) < 0.1 &&
    Math.abs(boxMesh1.rotation.y) < 0.1 &&
    Math.abs(boxMesh1.rotation.z) < 0.1 &&
    Math.abs(boxMesh2.rotation.x) < 0.1 &&
    Math.abs(boxMesh2.rotation.y) < 0.1 &&
    Math.abs(boxMesh2.rotation.z) < 0.1
  ) {
    const box1 = b1.setFromObject(boxMesh1);
    const box2 = b2.setFromObject(boxMesh2);
    if (box1.intersectsBox(box2)) {
      return true;
    }
  }
  boxMesh1.updateMatrixWorld();
  boxMesh2.updateMatrixWorld();
  const matrix1 = boxMesh1.matrixWorld;
  const matrix2 = boxMesh2.matrixWorld;

  const axes1 = [
    av11.clone().applyMatrix4(matrix1).normalize(),
    av12.clone().applyMatrix4(matrix1).normalize(),
    av13.clone().applyMatrix4(matrix1).normalize(),
  ];
  const axes2 = [
    av21.clone().applyMatrix4(matrix2).normalize(),
    av22.clone().applyMatrix4(matrix2).normalize(),
    av23.clone().applyMatrix4(matrix2).normalize(),
  ];

  // ボックス1とボックス2の各軸に対する分離軸チェック
  for (let i = 0; i < 3; i++) {
    if (isSeparatedOnAxis(axes1[i], boxMesh1, boxMesh2, matrix1, matrix2))
      return false;
    if (isSeparatedOnAxis(axes2[i], boxMesh1, boxMesh2, matrix1, matrix2))
      return false;
  }

  // クロスプロダクトによる分離軸のチェック
  for (let i = 0; i < 3; i++) {
    for (let j = 0; j < 3; j++) {
      const crossAxis = axes1[i].clone().cross(axes2[j]);
      if (
        crossAxis.lengthSq() > 1e-10 &&
        isSeparatedOnAxis(crossAxis, boxMesh1, boxMesh2, matrix1, matrix2)
      )
        return false;
    }
  }

  // すべての軸において分離が見つからなかった場合、衝突している
  return true;
};

BoxBoxでの衝突判定から衝突点を出す

/**
 * 参考:SAT法による衝突判定
 * https://discussions.unity.com/t/calculate-collision-between-2-rotated-boxes-without-using-boxcollider-math/246630/3
 */
import { Vector3, Matrix4, Box3, Mesh, Raycaster } from "three";

// キャッシュ用の変数
const tempVertices = [
  new Vector3(-0.5, -0.5, -0.5),
  new Vector3(0.5, -0.5, -0.5),
  new Vector3(-0.5, 0.5, -0.5),
  new Vector3(0.5, 0.5, -0.5),
  new Vector3(-0.5, -0.5, 0.5),
  new Vector3(0.5, -0.5, 0.5),
  new Vector3(-0.5, 0.5, 0.5),
  new Vector3(0.5, 0.5, 0.5),
];
const b1 = new Box3();
const b2 = new Box3();
const av11 = new Vector3(1, 0, 0);
const av12 = new Vector3(0, 1, 0);
const av13 = new Vector3(0, 0, 1);
const av21 = new Vector3(1, 0, 0);
const av22 = new Vector3(0, 1, 0);
const av23 = new Vector3(0, 0, 1);
const ray = new Raycaster();
ray.firstHitOnly = true;
let castDirection = new Vector3();

export type ResultCollisionProps = {
  intersect: boolean;
  distance: number;
  castDirection: Vector3;
  recieveDirection: Vector3;
  point: Vector3;
};

export const getInitCollision = (): ResultCollisionProps => {
  return {
    intersect: false,
    distance: 0,
    castDirection: new Vector3(),
    recieveDirection: new Vector3(),
    point: new Vector3(),
  };
};

const projectBoxOnAxis = (
  box: Mesh,
  axis: Vector3,
  matrix: Matrix4
): {
  min: number;
  max: number;
} => {
  const vertices = tempVertices.map((vertex) => vertex.clone());
  let min = Infinity;
  let max = -Infinity;

  vertices.forEach((vertex) => {
    vertex.applyMatrix4(matrix);
    const projection = vertex.dot(axis);
    min = Math.min(min, projection);
    max = Math.max(max, projection);
  });

  return { min, max };
};

const isSeparatedOnAxis = (
  axis: Vector3,
  box1: Mesh,
  box2: Mesh,
  matrix1: Matrix4,
  matrix2: Matrix4
) => {
  const worldAxis = axis.clone().normalize();

  const projection1 = projectBoxOnAxis(box1, worldAxis, matrix1);
  const projection2 = projectBoxOnAxis(box2, worldAxis, matrix2);

  return projection1.max < projection2.min || projection2.max < projection1.min;
};

export const detectBoxBoxCollision = (
  boxMesh1: Mesh,
  boxMesh2: Mesh
): ResultCollisionProps => {
  const res = getInitCollision();
  let intersect = false;
  // 回転率が微小な場合は、AABBで判定
  if (
    Math.abs(boxMesh1.rotation.x) < 0.1 &&
    Math.abs(boxMesh1.rotation.y) < 0.1 &&
    Math.abs(boxMesh1.rotation.z) < 0.1 &&
    Math.abs(boxMesh2.rotation.x) < 0.1 &&
    Math.abs(boxMesh2.rotation.y) < 0.1 &&
    Math.abs(boxMesh2.rotation.z) < 0.1
  ) {
    const box1 = b1.setFromObject(boxMesh1);
    const box2 = b2.setFromObject(boxMesh2);
    if (box1.intersectsBox(box2)) {
      intersect = true;
    }
  }
  if (!intersect) {
    let satIntersect = true;
    boxMesh1.updateMatrixWorld();
    boxMesh2.updateMatrixWorld();
    const matrix1 = boxMesh1.matrixWorld;
    const matrix2 = boxMesh2.matrixWorld;

    const axes1 = [
      av11.clone().applyMatrix4(matrix1).normalize(),
      av12.clone().applyMatrix4(matrix1).normalize(),
      av13.clone().applyMatrix4(matrix1).normalize(),
    ];
    const axes2 = [
      av21.clone().applyMatrix4(matrix2).normalize(),
      av22.clone().applyMatrix4(matrix2).normalize(),
      av23.clone().applyMatrix4(matrix2).normalize(),
    ];

    // ボックス1とボックス2の各軸に対する分離軸チェック
    for (let i = 0; i < 3; i++) {
      if (isSeparatedOnAxis(axes1[i], boxMesh1, boxMesh2, matrix1, matrix2)) {
        satIntersect = false;
        break;
      }
      if (isSeparatedOnAxis(axes2[i], boxMesh1, boxMesh2, matrix1, matrix2)) {
        satIntersect = false;
        break;
      }
    }
    if (satIntersect) {
      // クロスプロダクトによる分離軸のチェック
      for (let i = 0; i < 3; i++) {
        for (let j = 0; j < 3; j++) {
          const crossAxis = axes1[i].clone().cross(axes2[j]);
          if (
            crossAxis.lengthSq() > 1e-10 &&
            isSeparatedOnAxis(crossAxis, boxMesh1, boxMesh2, matrix1, matrix2)
          ) {
            satIntersect = false;
            break;
          }
        }
      }
    }
    intersect = satIntersect;
  }

  // 衝突点の計算
  if (intersect) {
    /**
     * 計算量を減らすために、単純化する
     * @衝突点は、box1の中心から、box2の中心へRaycastして、box1の面との衝突点を求める
     */
    castDirection = new Vector3()
      .subVectors(boxMesh1.position, boxMesh2.position)
      .normalize();
    ray.set(boxMesh2.position, castDirection);
    const intersects = ray.intersectObject(boxMesh1, true);
    if (intersects.length > 0) {
      const point = intersects[0].point;
      res.distance = intersects[0].distance;
      res.point.copy(point);
      res.castDirection.copy(castDirection);
      res.recieveDirection.copy(castDirection.clone().negate());
    }
  }
  res.intersect = intersect;

  // すべての軸において分離が見つからなかった場合、衝突している
  return res;
};

BoxBox Testing

Vitestを利用

import { detectBoxBoxCollision } from "../../../lib/utils/IntersectsDetector/BoxBoxDetector";
import { Mesh, BoxGeometry, Vector3, Euler } from "three";

/**
 * BoxBoxDetector Overlap
 * @description 同じ位置にあるBox同士は衝突しているか
 */
it("Intersect BoxBox Overlap", () => {
  const box1 = new Mesh(new BoxGeometry(1, 1, 1));
  box1.rotation.copy(new Euler(0, 0, 0));
  box1.position.copy(new Vector3(0, 0, 0));
  const box2 = new Mesh(new BoxGeometry(1, 1, 1));
  box2.rotation.copy(new Euler(0, 0, 0));
  box2.position.copy(new Vector3(0, 0, 0));
  expect(detectBoxBoxCollision(box1, box2).intersect).toBe(true);
});

/**
 * BoxBoxDetector Not Overlap
 * @description 重なっていないBox同士は衝突していないか
 */
it("Intersect BoxBox Not Overlap", () => {
  const box1 = new Mesh(new BoxGeometry(1, 1, 1));
  box1.rotation.copy(new Euler(0, 0, 0));
  box1.position.copy(new Vector3(0, 0, 0));
  const box2 = new Mesh(new BoxGeometry(1, 1, 1));
  box2.rotation.copy(new Euler(0, 0, 0));
  box2.position.copy(new Vector3(2, 0, 0));
  expect(detectBoxBoxCollision(box1, box2).intersect).toBe(false);
});

/**
 * BoxBoxDetector Parts Overlap With Rotation
 * @description 回転しているBox同士は一部重なっているか
 */
it("Intersect BoxBox Parts Overlap With Rotation", () => {
  const box1 = new Mesh(new BoxGeometry(1, 1, 1));
  box1.rotation.copy(new Euler(Math.PI/4, Math.PI/4, Math.PI/4));
  box1.position.copy(new Vector3(0, 0, 0));
  const box2 = new Mesh(new BoxGeometry(1, 1, 1));
  box2.rotation.copy(new Euler(Math.PI/4, 0, Math.PI/4));
  box2.position.copy(new Vector3(0, 1.25, 0));
  expect(detectBoxBoxCollision(box1, box2).intersect).toBe(true);
});

/**
 * BoxBoxDetector Not Overlap With Rotation
 * @description 回転しているBox同士は衝突していないか
 */
it("Intersect BoxBox Not Overlap With Rotation", () => {
  const box1 = new Mesh(new BoxGeometry(1, 1, 1));
  box1.rotation.copy(new Euler(Math.PI/4, Math.PI/4, Math.PI/4));
  box1.position.copy(new Vector3(0, 0, 0));
  const box2 = new Mesh(new BoxGeometry(1, 1, 1));
  box2.rotation.copy(new Euler(Math.PI/4, 0, Math.PI/4));
  box2.position.copy(new Vector3(1, 1.25, 0));
  expect(detectBoxBoxCollision(box1, box2).intersect).toBe(false);
});

/**
 * BoxBoxDetector Parts Overlap With Rotation & Scale
 * @description すべてのTransformが適用されたBox同士は一部重なっているか
 */
it("Intersect BoxBox Parts Overlap With Rotation & Scale", () => {
  const box1 = new Mesh(new BoxGeometry(1, 1, 1));
  box1.rotation.copy(new Euler(Math.PI/4, Math.PI/4, Math.PI/4));
  box1.position.copy(new Vector3(0, 0.5, 0));
  const box2 = new Mesh(new BoxGeometry(1, 1, 1));
  box2.rotation.copy(new Euler(Math.PI/4, 0, Math.PI/4));
  box2.position.copy(new Vector3(1, 1.75, 0));
  box2.scale.copy(new Vector3(2, 2, 1));
  expect(detectBoxBoxCollision(box1, box2).intersect).toBe(true);
});

SphereSphereの衝突判定

import { getInitCollision, ResultCollisionProps } from "./Common";
import { Mesh, SphereGeometry, Vector3 } from "three";

const c1 = new Vector3();
const c2 = new Vector3();
const n = new Vector3();
const p = new Vector3();

export const detectSphereSphereCollision = (
  sphereMesh1: Mesh,
  sphereMesh2: Mesh
): ResultCollisionProps => {
  const res = getInitCollision();

  const radius1 = (sphereMesh1.geometry as SphereGeometry).parameters.radius;
  const radius2 = (sphereMesh2.geometry as SphereGeometry).parameters.radius;

  const scaledRadius1 = radius1 * Math.max(...sphereMesh1.scale.toArray());
  const scaledRadius2 = radius2 * Math.max(...sphereMesh2.scale.toArray());

  const rad = scaledRadius1 + scaledRadius2;
  c1.copy(sphereMesh1.position);
  c2.copy(sphereMesh2.position);
  const normal = n.subVectors(c2, c1);
  const distance = normal.length();

  if (distance <= rad) {
    normal.normalize();
    p.copy(c1).add(normal.clone().multiplyScalar(scaledRadius1));
    res.intersect = true;
    res.distance = distance;
    res.point.copy(p);
    res.castDirection.copy(normal);
    res.recieveDirection.copy(normal.clone().negate());
  }

  return res;
};

SphereSphere Testing

// detectSphereSphereCollision
import { detectSphereSphereCollision } from "../../../lib/utils/IntersectsDetector";
import { Mesh, Vector3, Euler, SphereGeometry } from "three";

/**
 * SphereSphereDetector Overlap
 * @description 同じ位置にあるBox同士は衝突しているか
 */
it("Intersect SphereSphere Overlap", () => {
  const sphere1 = new Mesh(new SphereGeometry(1, 1, 1));
  sphere1.position.copy(new Vector3(0, 0, 0));
  const sphere2 = new Mesh(new SphereGeometry(1, 1, 1));
  sphere2.position.copy(new Vector3(0, 0, 0));
  expect(detectSphereSphereCollision(sphere1, sphere2).intersect).toBe(true);
});

/**
 * SphereSphereDetector Not Overlap
 * @description 重なっていないBox同士は衝突していないか
 */
it("Intersect SphereSphere Not Overlap", () => {
  const sphere1 = new Mesh(new SphereGeometry(1, 32, 32));
  sphere1.position.copy(new Vector3(0, 0, 0));
  const sphere2 = new Mesh(new SphereGeometry(1, 32, 32));
  sphere2.position.copy(new Vector3(2.1, 0, 0));
  expect(detectSphereSphereCollision(sphere1, sphere2).intersect).toBe(false);
});

/**
 * SphereSphereDetector Parts Overlap With Rotation
 * @description 回転しているBox同士は一部重なっているか
 */
it("Intersect SphereSphere Parts Overlap With Rotation", () => {
  const sphere1 = new Mesh(new SphereGeometry(1, 32, 32));
  sphere1.rotation.copy(new Euler(Math.PI/4, Math.PI/4, Math.PI/4));
  sphere1.position.copy(new Vector3(0, 0, 0));
  const sphere2 = new Mesh(new SphereGeometry(1, 32, 32));
  sphere2.rotation.copy(new Euler(Math.PI/4, 0, Math.PI/4));
  sphere2.position.copy(new Vector3(0, 1.25, 0));
  expect(detectSphereSphereCollision(sphere1, sphere2).intersect).toBe(true);
});

/**
 * SphereSphereDetector Parts Overlap With Rotation & Scale
 * @description すべてのTransformが適用されたBox同士は一部重なっているか
 */
it("Intersect SphereSphere Parts Overlap With Rotation & Scale", () => {
  const sphere1 = new Mesh(new SphereGeometry(1, 1, 1));
  sphere1.rotation.copy(new Euler(Math.PI/4, Math.PI/4, Math.PI/4));
  sphere1.position.copy(new Vector3(0, 0.5, 0));
  const sphere2 = new Mesh(new SphereGeometry(1, 1, 1));
  sphere2.rotation.copy(new Euler(Math.PI/4, 0, Math.PI/4));
  sphere2.position.copy(new Vector3(1, 1.75, 0));
  sphere2.scale.copy(new Vector3(2, 2, 1));
  expect(detectSphereSphereCollision(sphere1, sphere2).intersect).toBe(true);
});

CapsuleCapsuleの衝突判定

/**
 * 参考: http://marupeke296.com/COL_3D_No27_CapsuleCapsule.html
 */
import { CapsuleGeometry, Line3, Mesh, Raycaster, Vector3 } from "three";
import { getInitCollision, ResultCollisionProps } from "./Common";

// 再利用可能な変数
const ray = new Raycaster();
ray.firstHitOnly = true;

// カプセルの線分を定義
const getCapsuleSegment = (capsuleMesh: Mesh): Line3 => {
  const geometry = capsuleMesh.geometry as CapsuleGeometry;
  const length = geometry.parameters.length;
  const start = new Vector3(0, length / 2, 0);
  const end = new Vector3(0, -length / 2, 0);
  capsuleMesh.updateMatrixWorld();
  start.applyMatrix4(capsuleMesh.matrixWorld);
  end.applyMatrix4(capsuleMesh.matrixWorld);

  return new Line3(start, end);
}

/**
 * Capsule同士の衝突判定
 * @description 2つの線分間の最短距離が双方の半径の合計よりも短いか否かで衝突判定を行う
 *
 * @param capsuleMesh1
 * @param capsuleMesh2
 * @returns
 */
export const detectCapsuleCapsuleCollision = (
  capsuleMesh1: Mesh,
  capsuleMesh2: Mesh
): ResultCollisionProps => {
  const res = getInitCollision();
  const segment1 = getCapsuleSegment(capsuleMesh1);
  const segment2 = getCapsuleSegment(capsuleMesh2);

  // 線分間の最短距離を計算する
  const closestSegmentsDistance = getClosestPointsBetweenLines(segment1, segment2);
  const radius1 = (capsuleMesh1.geometry as CapsuleGeometry).parameters.radius;
  const radius2 = (capsuleMesh2.geometry as CapsuleGeometry).parameters.radius;
  if (closestSegmentsDistance.distance <= radius1 + radius2) {
    res.intersect = true;
    // castDirectionは、capsule1 -> capsule2の方向
    const castDirection = closestSegmentsDistance.direction;
    // capsule1 -> capsule2のraycast
    ray.set(capsuleMesh1.position, castDirection);
    const intersects = ray.intersectObject(capsuleMesh2, true);
    if (intersects.length > 0) {
      const point = intersects[0].point;
      res.distance = intersects[0].distance;
      res.point.copy(point);
      res.castDirection.copy(castDirection.normalize());
      res.recieveDirection.copy(castDirection.normalize().negate());
    }
  }

  return res;
};

/**
 * 線分間の最短距離を計算する
 * @param line1
 * @param line2
 * @returns number
 */
export const getClosestPointsBetweenLines = (
  line1: Line3,
  line2: Line3
): {
  distance: number;
  direction: Vector3;
} => {
  const p1 = line1.start;
  const p2 = line1.end;
  const p3 = line2.start;
  const p4 = line2.end;

  const d1 = p2.clone().sub(p1);
  const d2 = p4.clone().sub(p3);
  const r = p1.clone().sub(p3);

  const a = d1.dot(d1);
  const e = d2.dot(d2);
  const f = d2.dot(r);

  let s: number, t: number;
  if (a <= Number.EPSILON && e <= Number.EPSILON) {
    s = t = 0;
  } else if (a <= Number.EPSILON) {
    t = f / e;
    t = Math.max(0, Math.min(t, 1));
    s = 0;
  } else {
    const c = d1.dot(r);
    if (e <= Number.EPSILON) {
      s = 0;
      t = Math.max(0, Math.min(-c / a, 1));
    } else {
      const b = d1.dot(d2);
      const denom = a * e - b * b;
      if (denom !== 0) {
        s = Math.max(0, Math.min((b * f - c * e) / denom, 1));
      } else {
        s = 0;
      }
      t = (b * s + f) / e;
      if (t < 0) {
        t = 0;
        s = Math.max(0, Math.min(-c / a, 1));
      } else if (t > 1) {
        t = 1;
        s = Math.max(0, Math.min((b - c) / a, 1));
      }
    }
  }

  const closestPoint1 = p1.clone().add(d1.clone().multiplyScalar(s));
  const closestPoint2 = p3.clone().add(d2.clone().multiplyScalar(t));

  // direction: capsule1 -> capsule2
 return {
    distance: closestPoint1.distanceTo(closestPoint2),
    direction: closestPoint2.clone().sub(closestPoint1).normalize(),
  }
};

CapsuleCapsule Testing

/**
 * 参考: http://marupeke296.com/COL_3D_No27_CapsuleCapsule.html
 */
import { detectCapsuleCapsuleCollision } from "../../../lib/utils/IntersectsDetector";
import { Mesh, BoxGeometry, Vector3, Euler, CapsuleGeometry } from "three";

/**
 * CapsuleCapsuleDetector Overlap
 * @description 同じ位置にあるCapsule/Capsuleは衝突しているか
 */
it("Intersect CapsuleCapsule Overlap", () => {
  const capsule1 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule1.position.copy(new Vector3(0, 0, 0));
  const capsule2 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule2.position.copy(new Vector3(0, 0, 0));
  expect(detectCapsuleCapsuleCollision(capsule1, capsule2).intersect).toBe(
    true
  );
});

/**
 * CapsuleCapsuleDetector Not Overlap
 * @description 重なっていないCapsule/Capsuleは衝突していないか
 */
it("Intersect CapsuleCapsule Not Overlap", () => {
  const capsule1 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule1.rotation.copy(new Euler(0, 0, 0));
  capsule1.position.copy(new Vector3(0, 0, 0));
  const capsule2 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule2.rotation.copy(new Euler(0, 0, 0));
  capsule2.position.copy(new Vector3(2.1, 0, 0));
  expect(detectCapsuleCapsuleCollision(capsule1, capsule2).intersect).toBe(
    false
  );
});

/**
 * CapsuleCapsuleDetector Parts Overlap With Rotation
 * @description 回転しているCapsule同士は一部重なっているか
 */
it("Intersect CapsuleCapsule Parts Overlap With Rotation", () => {
  const capsule1 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule1.rotation.copy(new Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4));
  capsule1.position.copy(new Vector3(0, 0, 0));
  const capsule2 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule2.rotation.copy(new Euler(Math.PI / 4, 0, Math.PI / 4));
  capsule2.position.copy(new Vector3(0, 2, 0));
  expect(detectCapsuleCapsuleCollision(capsule1, capsule2).intersect).toBe(
    true
  );
});

/**
 * CapsuleCapsuleDetector Not Overlap With Rotation
 * @description 回転しているCapsule同士は一部重なっているか
 */
it("Intersect CapsuleCapsule Not Overlap With Rotation", () => {
  const capsule1 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule1.rotation.copy(new Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4));
  capsule1.position.copy(new Vector3(0, 0, 0));
  const capsule2 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule2.rotation.copy(new Euler(Math.PI / 4, 0, Math.PI / 4));
  capsule2.position.copy(new Vector3(0, 2.3, 0));
  expect(detectCapsuleCapsuleCollision(capsule1, capsule2).intersect).toBe(
    false
  );
});

/**
 * CapsuleCapsuleDetector Parts Overlap With Rotation & Scale
 * @description すべてのTransformが適用されたCapsule同士は一部重なっているか
 */
it("Intersect CapsuleCapsule Parts Overlap With Rotation & Scale", () => {
  const capsule1 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule1.rotation.copy(new Euler(Math.PI / 4, Math.PI / 4, Math.PI / 4));
  capsule1.position.copy(new Vector3(0, 0, 0));
  const capsule2 = new Mesh(new CapsuleGeometry(1, 1, 1, 6));
  capsule2.rotation.copy(new Euler(Math.PI / 4, 0, Math.PI / 4));
  capsule2.position.copy(new Vector3(0, 2.5, 0));
  capsule2.scale.copy(new Vector3(2, 2, 1));
  expect(detectCapsuleCapsuleCollision(capsule1, capsule2).intersect).toBe(
    false
  );
});

Discussion