threefiz
Version:
Implementation of a physics engine based on the [Three.js](https://github.com/mrdoob/three.js) library.
469 lines (440 loc) • 14.1 kB
text/typescript
import {
ArrowHelper,
BoxGeometry,
Color,
Matrix4,
Mesh,
MeshBasicMaterial,
OctahedronGeometry,
Ray,
Scene,
Sphere,
Vector3,
} from "three";
import { OBB } from "three/addons/math/OBB.js";
import { randInt } from "three/src/math/MathUtils.js";
import RigidBody from "./RigidBody";
const v = new Vector3();
class OBBs extends OBB {
vertices: {
initialValues: Vector3[];
values: Vector3[];
};
axes: {
initialValues: Vector3[];
values: Vector3[];
};
edges: {
initialValues: ReturnType<OBBs["getEdges"]>;
values: ReturnType<OBBs["getEdges"]>;
};
parent: RigidBody;
debug:
| {
obb: Mesh<BoxGeometry>;
collision: {
point: Mesh;
normal: ArrowHelper;
depth: Mesh;
};
points: Mesh[];
arrows: ArrowHelper[];
}
| any;
collision: {
point: Vector3;
normal: Vector3;
depth: number;
};
constructor(parent: RigidBody, halfSize: Vector3) {
super();
this.parent = parent;
this.halfSize = halfSize;
const vertices = this.getVertices();
this.vertices = {
initialValues: vertices,
values: vertices.map((v) => v.clone()),
};
const axes = this.getAxes();
this.axes = {
initialValues: axes,
values: axes.map((a) => a.clone()),
};
const edges = this.getEdges();
this.edges = {
initialValues: edges,
values: edges.map((e) => ({ ...e, ray: e.ray.clone() })),
};
this.debug = {};
this.collision = {
point: new Vector3(),
normal: new Vector3(),
depth: 0,
};
this.rotation.identity();
this.applyMatrix4(this.parent.mesh.matrixWorld);
this.center.copy(this.parent.getPosition(v));
}
updateValues() {
this.updateVertices();
this.updateAxes();
}
getCollision(object: OBBs | Sphere) {
this.updateValues();
if (object instanceof OBBs) {
object.updateValues();
return this.getCollisionOBB(object);
} else if (object instanceof Sphere) {
return this.getCollisionSphere(object);
}
return this.collision;
}
getCollisionOBB(obb: OBBs) {
let point = obb.center.clone();
const { normal, depth } = this.getNormalAndDepth(obb);
this.collision.normal = normal;
this.collision.depth = depth;
if (depth > 1e-10) {
const collisionVertices1 = this.getIntersectedVertices(obb);
const collisionVertices2 = obb.getIntersectedVertices(this);
if (collisionVertices1.length + collisionVertices2.length > 0) {
point.copy(
this.collisionPoint_Vertices(
obb,
collisionVertices1,
collisionVertices2
)
);
} else {
point.copy(this.collisionPoint_Edges(obb));
}
this.collision.point = point;
}
if (!this.isNormalHasGoodDirection()) this.collision.normal.negate();
this.onCollision(this.collision);
return this.collision;
}
getCollisionSphere(sphere: Sphere) {
const closestPoint = new Vector3();
this.clampPoint(sphere.center, closestPoint);
this.collision.point = closestPoint;
const normal = closestPoint.clone().sub(sphere.center).normalize();
this.collision.normal = normal;
const depth = this.collisionDepth_projectionSphere(sphere, normal);
this.collision.depth = depth;
return this.collision;
}
getNormalAndDepth(obb: OBBs) {
const thisAxes = this.axes.values.map((a) => a.clone());
const obbAxes = obb.axes.values.map((a) => a.clone());
const normal = new Vector3();
let minDepth = Infinity;
const AllAxes = [...thisAxes, ...obbAxes];
for (let i = 0; i < thisAxes.length; i++) {
for (let j = 0; j < obbAxes.length; j++) {
const crossProduct = new Vector3().crossVectors(
thisAxes[i],
obbAxes[j]
);
if (crossProduct.lengthSq() > 1e-8)
AllAxes.push(crossProduct.normalize());
}
}
AllAxes.map((axis) => {
let depth = this.collisionDepth_projectionOBB(obb, axis.clone());
if (depth < minDepth) {
normal.copy(axis);
minDepth = depth;
}
});
return { normal, depth: minDepth };
}
isNormalHasGoodDirection() {
const collisionPoint = this.collision.point;
const directonToCenter = new Vector3()
.subVectors(this.center, collisionPoint)
.normalize();
return directonToCenter.dot(this.collision.normal) > 0;
}
collisionPoint_Vertices(
obb: OBBs,
vertices1: Vector3[],
vertices2: Vector3[]
) {
let point: Vector3;
if (vertices1.length) {
point = this.getCenterPoint(vertices1);
} else {
point = obb.getCenterPoint(vertices2);
}
return point;
}
onCollision(_collision: typeof this.collision) {}
collisionPoint_Edges(obb: OBBs) {
this.updateEdges();
obb.updateEdges();
const thisIntersectedEdges = this.getIntersectedEdges(
obb,
this.edges.values
);
const obbIntersectedEdges = obb.getIntersectedEdges(this, obb.edges.values);
const { edges: finalIntersectedEdges, points: collisionPoints } =
this.collisonEdges_pickBestIntersectionResult(
thisIntersectedEdges,
obbIntersectedEdges
);
const finalIntersectedEdges_reversed = finalIntersectedEdges.map((e) =>
this.getReversedRay(e)
);
let collisionPointsfromReverseEdges: Vector3[] = [];
if (finalIntersectedEdges === thisIntersectedEdges.edges)
collisionPointsfromReverseEdges = this.getIntersectedEdges(
obb,
finalIntersectedEdges_reversed
).points;
else
collisionPointsfromReverseEdges = obb.getIntersectedEdges(
this,
finalIntersectedEdges_reversed
).points;
const point = this.getCenterPoint([
...collisionPoints,
...collisionPointsfromReverseEdges,
]);
return point;
}
getReversedRay(
edge: ReturnType<OBBs["getEdges"]>[number]
): ReturnType<OBBs["getEdges"]>[number] {
const { ray, length } = edge;
const reversedRay = new Ray(
ray.origin.clone().addScaledVector(ray.direction, length),
ray.direction.clone().negate()
);
return { ray: reversedRay, length };
}
collisonEdges_pickBestIntersectionResult(
inter_edges1: ReturnType<OBBs["getIntersectedEdges"]>,
inter_edges2: ReturnType<OBBs["getIntersectedEdges"]>
) {
const e1_l = inter_edges1.edges.length;
const e2_l = inter_edges1.edges.length;
if (e1_l > 0 && e2_l > 0) {
if (e1_l > e2_l) return inter_edges2;
return inter_edges1;
}
if (e1_l > 0) {
return inter_edges1;
}
return inter_edges2;
}
getCenterPoint(vertices: Vector3[]) {
const centerPoint = new Vector3();
vertices.map((v) => centerPoint.add(v));
centerPoint.multiplyScalar(1 / vertices.length);
return centerPoint;
}
collisionDepth_projectionOBB(obb: OBBs, normal: Vector3) {
const projection1 = this.getOBBProjection(normal);
const projection2 = obb.getOBBProjection(normal);
const overlap =
Math.min(projection1.max, projection2.max) -
Math.max(projection1.min, projection2.min);
return overlap;
}
collisionDepth_projectionSphere(sphere: Sphere, normal: Vector3) {
const sphereCenter = sphere.center;
const sphereCenterProjection = normal.dot(sphereCenter);
const sphereProjection = {
min: sphereCenterProjection - sphere.radius,
max: sphereCenterProjection + sphere.radius,
};
const OBBProjection = this.getOBBProjection(normal);
const overlap =
Math.min(OBBProjection.max, sphereProjection.max) -
Math.max(OBBProjection.min, sphereProjection.min);
return overlap;
}
getOBBProjection(normal: Vector3) {
const vertices = this.vertices.values;
let min = Infinity;
let max = -Infinity;
vertices.forEach((vertex) => {
const projection = vertex.dot(normal);
min = Math.min(min, projection);
max = Math.max(max, projection);
});
return { min, max };
}
getVertices() {
const vertices = [
new Vector3(-this.halfSize.x, -this.halfSize.y, -this.halfSize.z),
new Vector3(-this.halfSize.x, this.halfSize.y, -this.halfSize.z),
new Vector3(this.halfSize.x, this.halfSize.y, -this.halfSize.z),
new Vector3(this.halfSize.x, -this.halfSize.y, -this.halfSize.z),
new Vector3(-this.halfSize.x, -this.halfSize.y, this.halfSize.z),
new Vector3(-this.halfSize.x, this.halfSize.y, this.halfSize.z),
new Vector3(this.halfSize.x, this.halfSize.y, this.halfSize.z),
new Vector3(this.halfSize.x, -this.halfSize.y, this.halfSize.z),
];
return vertices;
}
updateVertices() {
const initial = this.vertices.initialValues;
this.vertices.values.forEach((vertex, i) => {
vertex
.copy(initial[i].clone())
.applyMatrix3(this.rotation)
.add(this.center);
});
}
getIntersectedVertices(obb: OBBs) {
const list: Vector3[] = [];
this.vertices.values.map((vertex) => {
if (obb.containsPoint(vertex)) {
list.push(vertex);
}
});
return list;
}
getAxes() {
return [new Vector3(1, 0, 0), new Vector3(0, 1, 0), new Vector3(0, 0, 1)];
}
updateAxes() {
const initial = this.axes.initialValues;
this.axes.values.forEach((axis, i) => {
axis.copy(initial[i]);
axis.applyMatrix3(this.rotation);
});
}
getEdges(): { ray: Ray; length: number }[] {
const v = this.vertices.values;
// each group is set of source vertex and directions for rays to define edges
// groups are determined by the order of the vertices in this.vertices.values
const vGroups = [
{ src: v[1], dir: [v[0], v[2], v[5]] },
{ src: v[3], dir: [v[0], v[2], v[7]] },
{ src: v[4], dir: [v[0], v[5], v[7]] },
{ src: v[6], dir: [v[2], v[5], v[7]] },
];
const rays: { ray: Ray; length: number }[] = [];
vGroups.forEach((group) => {
group.dir.map((dir) => {
rays.push({
ray: new Ray(group.src, dir.clone().sub(group.src).normalize()),
length: group.src.distanceTo(dir),
});
});
});
return rays;
}
updateEdges() {
const initial = this.edges.initialValues;
this.edges.values.forEach((edge, i) => {
edge.ray.copy(initial[i].ray);
edge.ray.direction.applyMatrix3(this.rotation);
});
}
getIntersectedEdges(
obb: OBBs,
edges: ReturnType<OBBs["getEdges"]>
): { edges: ReturnType<OBBs["getEdges"]>; points: Vector3[] } {
const edgesArr: ReturnType<OBBs["getEdges"]> = [];
const points: Vector3[] = [];
edges.map((edge) => {
let point = new Vector3();
if (
obb.intersectRay(edge.ray, point) &&
edge.ray.origin.distanceTo(point) <= edge.length
) {
edgesArr.push(edge);
points.push(point);
}
});
return { edges: edgesArr, points };
}
getArrowHelper(
scene: Scene,
direction: Vector3,
origin: Vector3,
id: number,
color?: Color
) {
const c = color || new Color(`hsl(${randInt(0, 360)}, 100%, 70%)`);
const arrow = new ArrowHelper(direction, origin, 10, c);
if (!this.debug.arrows) this.debug.arrows = [] as Mesh[];
if (!this.debug.arrows[id]) {
scene.add(arrow);
this.debug.arrows[id] = arrow;
}
this.debug.arrows[id].position.copy(arrow.position);
this.debug.arrows[id].setDirection(direction);
return this.debug.arrows[id];
}
getPointHelper(scene: Scene, point: Vector3, id: number, color?: Color) {
const c = color || new Color(`hsl(${randInt(0, 360)}, 100%, 70%)`);
const mesh = new Mesh(
new OctahedronGeometry(0.5, 0),
new MeshBasicMaterial({ color: c, wireframe: true })
);
if (!this.debug.points) this.debug.points = [] as Mesh[];
if (!this.debug.points[id]) {
scene.add(mesh);
this.debug.points[id] = mesh;
}
this.debug.points[id].position.copy(point);
return this.debug.points[id];
}
showOBB(scene: Scene, color: Color = new Color("yellow")) {
const geo = new BoxGeometry(
this.halfSize.x * 2,
this.halfSize.y * 2,
this.halfSize.z * 2
);
const mat = new MeshBasicMaterial({ color: color, wireframe: true });
const box = new Mesh(geo, mat);
this.debug.obb = box;
scene.add(box);
}
showCollision(scene: Scene) {
if (!this.debug.collision) {
const { point, normal, depth } = this.collision;
this.debug.collision = {
point: this.getPointHelper(scene, point!, this.parent.mesh.id + 100),
normal: this.getArrowHelper(
scene,
normal!,
point!,
this.parent.mesh.id + 100
),
depth: this.getPointHelper(
scene,
point!.clone().addScaledVector(normal!, depth!),
this.parent.mesh.id + 101
),
};
} else {
const {
point,
normal,
depth,
}: { point: Mesh; normal: ArrowHelper; depth: Mesh } =
this.debug.collision;
point.position.copy(this.collision.point!);
normal.setDirection(this.collision.normal!);
normal.position.copy(this.collision.point!);
depth.position.copy(
this.collision
.point!.clone()
.addScaledVector(this.collision.normal!, this.collision.depth!)
);
}
}
debugUpdate() {
this.debug.obb.setRotationFromMatrix(
new Matrix4().setFromMatrix3(this.rotation)
);
this.debug.obb.position.copy(this.center);
}
}
export { OBBs };