jakke-graphics-ts
Version:
My common graphics utils for building my aec apps.
403 lines • 13.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BVHBoundingBox = exports.BVHTriangle = exports.BVHTree = void 0;
/**
* BVHTree for fast traverse of triangles.
*/
class BVHTree {
constructor() {
/**
* Triangles included.
*/
this.triangles = [];
/**
* Triangle hashes included in BVHTree.
*/
this.triangleHashes = new Set();
this._boundingBox = BVHBoundingBox.create();
}
/**
* Get bounding box of this BVHTree.
*/
get boundingBox() { return this._boundingBox; }
/**
* Add triangle at this three.
* @param triangle
* @returns Result of adding triangle action.
*/
addTriangle(triangle) {
const hash = triangle.getHash();
if (this.triangleHashes.has(hash)) {
return { result: false, message: "Given triangle is already included." };
}
this.triangles.push(triangle);
this.triangleHashes.add(hash);
this._boundingBox.addVertex(triangle.v1);
this._boundingBox.addVertex(triangle.v2);
this._boundingBox.addVertex(triangle.v3);
return { result: true };
}
/**
* Build BVHTree's structure.
* @returns
*/
calculateTree() {
const sortedTriangles = this.getSortedTriangles();
return this.buildBVHTree(sortedTriangles);
}
/**
* Internal method for calculating the tree structure.
* @param triangles
* @returns
*/
buildBVHTree(triangles) {
if (triangles.length <= 1) {
const leaf = new BVHTree();
triangles.forEach(t => leaf.addTriangle(t));
return leaf;
}
const node = new BVHTree();
triangles.forEach(t => node.addTriangle(t));
const diagonal = node.boundingBox.getDiagonal();
if (!diagonal) {
// fallback: 균등하게 분할
const mid = Math.floor(triangles.length / 2);
const leftTriangles = triangles.slice(0, mid);
const rightTriangles = triangles.slice(mid);
node.leftChild = this.buildBVHTree(leftTriangles);
node.leftChild.parent = node;
node.rightChild = this.buildBVHTree(rightTriangles);
node.rightChild.parent = node;
return node;
}
const axis = diagonal.x >= diagonal.y && diagonal.x >= diagonal.z
? BVHSplitAxis.X
: diagonal.y >= diagonal.z
? BVHSplitAxis.Y
: BVHSplitAxis.Z;
const enumerator = new BVHTriangleEnumerator(triangles);
const [leftTriangles, rightTriangles] = enumerator.split(axis);
node.leftChild = this.buildBVHTree(leftTriangles);
node.leftChild.parent = node;
node.rightChild = this.buildBVHTree(rightTriangles);
node.rightChild.parent = node;
return node;
}
/**
* Internal method for sorting triangles.
* @returns
*/
getSortedTriangles() {
var _a;
const diagonal = (_a = this._boundingBox) === null || _a === void 0 ? void 0 : _a.getDiagonal();
if (!diagonal) {
throw new Error("Bounding box diagonal is not defined. Make sure to call calculateBoundingBox() before sorting.");
}
const isXMajor = diagonal.x > diagonal.y;
return this.triangles
.map(t => t.clone())
.sort((a, b) => {
const ca = a.getCentroid();
const cb = b.getCentroid();
return isXMajor
? ca.x - cb.x || ca.y - cb.y
: ca.y - cb.y || ca.x - cb.x;
});
}
/**
* Get all nodes included in this tree.
* @returns
*/
getAllNodes() {
const nodes = [];
this.traverseTree(nodes);
return nodes;
}
/**
* Get all node's bounding box.
* @returns
*/
getAllBoundingBoxes() {
return this.getAllNodes().map(n => n._boundingBox);
}
/**
* Internal method for traverse tree for collecting all nodes.
* @param nodes
*/
traverseTree(nodes) {
var _a, _b;
nodes.push(this);
(_a = this.leftChild) === null || _a === void 0 ? void 0 : _a.traverseTree(nodes);
(_b = this.rightChild) === null || _b === void 0 ? void 0 : _b.traverseTree(nodes);
}
/**
* Get root node of this tree.
* @returns
*/
getRoot() {
let current = this;
while (current.parent) {
current = current.parent;
}
return current;
}
/**
* Get the intersection point between the given line and tree.
* This uses Möller Trumbore's soluion for determine the collision.
* Currently, it returns only the first collision point.
* @param p1 The start point of line.
* @param p2 The end point of line.
* @param onlyOnLine Parameter for testing the getting collision point.
* When this set true, it returns the collision point including outbound of the line.
* @returns Intersection point when only the intersection exists.
*/
getRayCollision(p1, p2, onlyOnLine) {
var _a, _b;
if (!this.isRayCollideAABB(p1, p2))
return;
if (!this.leftChild && !this.rightChild) {
for (const triangle of this.triangles) {
const ptTest = triangle.getPointOnTrianglePlane(p1, p2);
if (!ptTest)
continue;
const onLine = ptTest.subtract(new BVHVertex(p1))
.dot(ptTest.subtract(new BVHVertex(p2))) <= 0;
if (!onlyOnLine || onLine)
return ptTest;
}
}
return ((_a = this.leftChild) === null || _a === void 0 ? void 0 : _a.getRayCollision(p1, p2, onlyOnLine))
|| ((_b = this.rightChild) === null || _b === void 0 ? void 0 : _b.getRayCollision(p1, p2, onlyOnLine));
}
/**
* Internal method for determine the collision between line and bounding box.
* @param p1 The start point of line.
* @param p2 The end point of line.
* @returns
*/
isRayCollideAABB(p1, p2) {
const ptMin = this._boundingBox.min;
const ptMax = this._boundingBox.max;
const intersections = [
this.raycastOnPlane(p1, p2, "z", p2.z - p1.z > 0 ? ptMin.z : ptMax.z, ptMin, ptMax),
this.raycastOnPlane(p1, p2, "x", p2.x - p1.x > 0 ? ptMin.x : ptMax.x, ptMin, ptMax),
this.raycastOnPlane(p1, p2, "y", p2.y - p1.y > 0 ? ptMin.y : ptMax.y, ptMin, ptMax)
];
return intersections.filter(pt => pt !== undefined).length === 1;
}
/**
* Internal method of testing the collision between line and AABB.
* @param p1 The start point of line.
* @param p2 The end point of line.
* @param axis x, y, z can be set.
* @param value
* @param ptMin The minimum point of bounding box.
* @param ptMax The maximum point of bounding box.
* @returns
*/
raycastOnPlane(p1, p2, axis, value, ptMin, ptMax) {
const delta = p2[axis] - p1[axis];
if (delta === 0)
return undefined;
const t = (value - p1[axis]) / delta;
const pt = {
x: p1.x + t * (p2.x - p1.x),
y: p1.y + t * (p2.y - p1.y),
z: p1.z + t * (p2.z - p1.z)
};
return this.isPointInside(pt, ptMin, ptMax) ? pt : undefined;
}
/**
*
* @param pt
* @param min
* @param max
* @returns
*/
isPointInside(pt, min, max) {
return min.x <= pt.x && pt.x <= max.x
&& min.y <= pt.y && pt.y <= max.y
&& min.z <= pt.z && pt.z <= max.z;
}
}
exports.BVHTree = BVHTree;
class BVHTriangle {
constructor(v1, v2, v3) {
const bvhV1 = new BVHVertex(v1);
const bvhV2 = new BVHVertex(v2);
const bvhV3 = new BVHVertex(v3);
const dupV1V2 = bvhV1.getHash() === bvhV2.getHash();
const dupV2V3 = bvhV2.getHash() === bvhV3.getHash();
const dupV3V1 = bvhV3.getHash() === bvhV1.getHash();
const isDuplicated = dupV1V2 && dupV2V3 && dupV3V1;
if (isDuplicated) {
throw new Error("Cannot create BVHTriangle with all identical vertices");
}
this.v1 = bvhV1;
this.v2 = bvhV2;
this.v3 = bvhV3;
}
getCentroid() {
return {
x: (this.v1.x + this.v2.x + this.v3.x) / 3,
y: (this.v1.y + this.v2.y + this.v3.y) / 3,
z: (this.v1.z + this.v2.z + this.v3.z) / 3
};
}
getHash() {
return `${this.v1.getHash()}-${this.v2.getHash()}-${this.v3.getHash()}`;
}
clone() {
return new BVHTriangle(this.v1.toObject(), this.v2.toObject(), this.v3.toObject());
}
isDetZero(pt1, pt2) {
const V1V2 = this.v2.subtract(this.v1);
const V1V3 = this.v3.subtract(this.v1);
const d = new BVHVertex(pt2).subtract(new BVHVertex(pt1)).normalized();
const det = V1V2.dot(d.cross(V1V3));
return det === 0;
}
getPointOnTrianglePlane(pt1, pt2) {
if (this.isDetZero(pt1, pt2))
return;
const V1 = this.v1;
const V2 = this.v2;
const V3 = this.v3;
const P1 = new BVHVertex(pt1);
const P2 = new BVHVertex(pt2);
const V1V2 = V2.subtract(V1);
const V1V3 = V3.subtract(V1);
const V1P1 = P1.subtract(V1);
const d = P2.subtract(P1).normalized();
const det = V1V2.dot(d.cross(V1V3));
const v = d.dot(V1V3.cross(V1P1)) / det;
const w = d.dot(V1P1.cross(V1V2)) / det;
const u = 1 - (v + w);
const result = V1.multiply(u).add(V2.multiply(v)).add(V3.multiply(w));
const validParams = [u, v, w].every(val => val >= 0 && val <= 1);
return validParams ? result : undefined;
}
}
exports.BVHTriangle = BVHTriangle;
class BVHBoundingBox {
constructor() {
this.vertexHashes = new Set();
this._initialized = false;
this._min = new BVHVertex({ x: Infinity, y: Infinity, z: Infinity });
this._max = new BVHVertex({ x: -Infinity, y: -Infinity, z: -Infinity });
}
static create() {
return new BVHBoundingBox();
}
get min() { return this._min; }
get max() { return this._max; }
get isEmpty() { return !this._initialized; }
addVertex(v) {
const hash = v.getHash();
if (this.vertexHashes.has(hash)) {
return { result: false, message: "Given point is already included." };
}
this.vertexHashes.add(hash);
this._min = new BVHVertex({
x: Math.min(this._min.x, v.x),
y: Math.min(this._min.y, v.y),
z: Math.min(this._min.z, v.z)
});
this._max = new BVHVertex({
x: Math.max(this._max.x, v.x),
y: Math.max(this._max.y, v.y),
z: Math.max(this._max.z, v.z)
});
this._initialized = true;
return { result: true };
}
getCentroid() {
if (!this._initialized)
return;
return new BVHVertex({
x: (this._min.x + this._max.x) * 0.5,
y: (this._min.y + this._max.y) * 0.5,
z: (this._min.z + this._max.z) * 0.5
});
}
getDiagonal() {
if (!this._initialized)
return;
return new BVHVertex({
x: this._max.x - this._min.x,
y: this._max.y - this._min.y,
z: this._max.z - this._min.z
});
}
}
exports.BVHBoundingBox = BVHBoundingBox;
class BVHVertex {
constructor(v) {
this.x = v.x;
this.y = v.y;
this.z = v.z;
}
getHash() {
const x = Math.round(this.x * BVHVertex.PRECISION);
const y = Math.round(this.y * BVHVertex.PRECISION);
const z = Math.round(this.z * BVHVertex.PRECISION);
return `${x},${y},${z}`;
}
add(v) {
return new BVHVertex({ x: this.x + v.x, y: this.y + v.y, z: this.z + v.z });
}
subtract(v) {
return new BVHVertex({ x: this.x - v.x, y: this.y - v.y, z: this.z - v.z });
}
dot(v) {
return this.x * v.x + this.y * v.y + this.z * v.z;
}
cross(v) {
return new BVHVertex({
x: this.y * v.z - this.z * v.y,
y: this.z * v.x - this.x * v.z,
z: this.x * v.y - this.y * v.x
});
}
normalized() {
const length = this.getLength();
return new BVHVertex({ x: this.x / length, y: this.y / length, z: this.z / length });
}
getLength() {
return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2) + Math.pow(this.z, 2));
}
multiply(t) {
return new BVHVertex({ x: this.x * t, y: this.y * t, z: this.z * t });
}
toObject() {
return { x: this.x, y: this.y, z: this.z };
}
equals(v) {
return this.getHash() === v.getHash();
}
clone() {
return new BVHVertex({ x: this.x, y: this.y, z: this.z });
}
}
BVHVertex.PRECISION = 1e6;
class BVHTriangleEnumerator {
constructor(triangles) {
this.triangles = triangles;
}
split(axis) {
const sorted = [...this.triangles].sort((a, b) => {
const ca = a.getCentroid();
const cb = b.getCentroid();
return ca[axis] - cb[axis];
});
const mid = Math.floor(sorted.length / 2);
return [sorted.slice(0, mid), sorted.slice(mid)];
}
}
var BVHSplitAxis;
(function (BVHSplitAxis) {
BVHSplitAxis["X"] = "x";
BVHSplitAxis["Y"] = "y";
BVHSplitAxis["Z"] = "z";
})(BVHSplitAxis || (BVHSplitAxis = {}));
//# sourceMappingURL=bvhTree.js.map