之前的文章提到,在Three.js中使用InstanceMesh来实现性能优化,可以实现单个Mesh的拾取功能
那,能不能实现碰撞检测呢?肯定是可以的,不过Three.js中并没有直接的API可以实现对InstanceMesh的碰撞检测,需要手动实现
回顾本文的描述的Three.js的场景前提:
本文所采用的方法如下:
详细内容如下
在场景加载时,通过初始圆柱和变换矩阵,计算每个桥柱的三维包围盒从而计算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)));
}
粗检测函数较为简单,判断桥柱的二维包围盒和船的二维包围盒是否相交
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);
}
细检测函数较为复杂,将船的包围盒(假设为长方体)的顶点进行逆变换,变换矩阵为待检测的这个桥柱的变换矩阵,求出逆变换后的长方体的六个顶点在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);
}
最后,在场景每一帧更新时,调用碰撞检测函数,碰撞检测函数则是先调用粗检测函数,粗检测相交后再调用细检测函数
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;
}
在实测中,上述这种方式运行起来还算流畅,主要是因为细检测虽然消耗性能但是只执行少数几次,粗检测则几乎只是比大小,参考下面的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;
}
这里提出三个优化方向:
[1] SAT.js (jriecken.github.io)
[2] InstancedMesh – three.js docs (threejs.org)
[3] Three.js使用InstancedMesh实现性能优化 - 当时明月在曾照彩云归 - 博客园 (cnblogs.com)