@perplexdotgg/bounce 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +30 -0
- package/package.json +32 -0
- package/src/builders/ConvexHullBuilder.ts +437 -0
- package/src/builders/ConvexHullBuilder2d.ts +344 -0
- package/src/builders/ConvexHullBuilder3d.ts +1689 -0
- package/src/builders/HeightMapBuilder.ts +414 -0
- package/src/builders/TriangleMeshBuilder.ts +92 -0
- package/src/collision/CastShapesModule.ts +184 -0
- package/src/collision/CollideShapesModule.ts +152 -0
- package/src/collision/HeightMapCaster.ts +38 -0
- package/src/collision/HeightMapCollider.ts +33 -0
- package/src/collision/TriangleCaster.ts +249 -0
- package/src/collision/TriangleCollider.ts +308 -0
- package/src/collision/TriangleCollider2.ts +379 -0
- package/src/collision/activeEdge.ts +146 -0
- package/src/collision/cast/cast.ts +139 -0
- package/src/collision/cast/castCompoundVsCompound.ts +59 -0
- package/src/collision/cast/castCompoundVsConvex.ts +116 -0
- package/src/collision/cast/castConvexVsCompound.ts +123 -0
- package/src/collision/cast/castConvexVsConvex.ts +213 -0
- package/src/collision/cast/castConvexVsHeightMap.ts +73 -0
- package/src/collision/cast/castConvexVsTriangleMesh.ts +56 -0
- package/src/collision/cast/castRayVsCompound.ts +44 -0
- package/src/collision/cast/castRayVsConvex.ts +45 -0
- package/src/collision/cast/castRayVsHeightMap.ts +58 -0
- package/src/collision/cast/castRayVsTriangleMesh.ts +58 -0
- package/src/collision/closestPoints/closestPoints.ts +23 -0
- package/src/collision/closestPoints/computeBarycentricCoordinates2d.ts +32 -0
- package/src/collision/closestPoints/computeBarycentricCoordinates3d.ts +81 -0
- package/src/collision/closestPoints/computeClosestPointOnLine.ts +30 -0
- package/src/collision/closestPoints/computeClosestPointOnTetrahedron.ts +96 -0
- package/src/collision/closestPoints/computeClosestPointOnTriangle.ts +195 -0
- package/src/collision/closestPoints/isOriginOutsideOfPlane.ts +25 -0
- package/src/collision/closestPoints/isOriginOutsideOfTrianglePlanes.ts +72 -0
- package/src/collision/collide/collide.ts +146 -0
- package/src/collision/collide/collideCompoundVsCompound.ts +60 -0
- package/src/collision/collide/collideCompoundVsConvex.ts +59 -0
- package/src/collision/collide/collideCompoundVsHeightMap.ts +73 -0
- package/src/collision/collide/collideCompoundVsTriangleMesh.ts +56 -0
- package/src/collision/collide/collideConvexVsCompound.ts +57 -0
- package/src/collision/collide/collideConvexVsConvex.ts +225 -0
- package/src/collision/collide/collideConvexVsConvexImp.ts +236 -0
- package/src/collision/collide/collideConvexVsHeightMap.ts +53 -0
- package/src/collision/collide/collideConvexVsTriangleMesh.ts +58 -0
- package/src/collision/collide/collideHeightMapVsCompound.ts +69 -0
- package/src/collision/collide/collideHeightMapVsConvex.ts +53 -0
- package/src/collision/collide/collideSphereVsSphere.ts +81 -0
- package/src/collision/collide/collideTriangleMeshVsCompound.ts +58 -0
- package/src/collision/collide/collideTriangleMeshVsConvex.ts +58 -0
- package/src/collision/epa/EpaConvexHullBuilder.ts +397 -0
- package/src/collision/epa/StaticArray.ts +154 -0
- package/src/collision/epa/TriangleFactory.ts +32 -0
- package/src/collision/epa/arrays.ts +99 -0
- package/src/collision/epa/binaryHeap.ts +82 -0
- package/src/collision/epa/structs.ts +227 -0
- package/src/collision/gjk/GjkModule.ts +864 -0
- package/src/collision/gjk/PenetrationDepthModule.ts +493 -0
- package/src/collision/gjk/SupportPoints.ts +50 -0
- package/src/collision/imp/MinkowskiDifference.ts +36 -0
- package/src/collision/imp/computeExploredDistanceLowerUpperBound.ts +40 -0
- package/src/collision/imp/finalizeImpResult.ts +69 -0
- package/src/collision/imp/findContactImp.ts +196 -0
- package/src/collision/imp/imp.ts +28 -0
- package/src/collision/imp/incrementalMinimumDistanceExploreDirection.ts +207 -0
- package/src/collision/mpr/findPortal.ts +152 -0
- package/src/collision/mpr/mpr.ts +29 -0
- package/src/collision/mpr/updatePortal.ts +52 -0
- package/src/constraints/BaseConstraint.ts +50 -0
- package/src/constraints/ConstraintOptions.ts +22 -0
- package/src/constraints/ConstraintSolver.ts +119 -0
- package/src/constraints/DistanceConstraint.ts +229 -0
- package/src/constraints/FixedConstraint.ts +203 -0
- package/src/constraints/HingeConstraint.ts +460 -0
- package/src/constraints/PointConstraint.ts +108 -0
- package/src/constraints/components/AngleComponent.ts +226 -0
- package/src/constraints/components/AxisComponent.ts +263 -0
- package/src/constraints/components/HingeComponent.ts +215 -0
- package/src/constraints/components/Motor.ts +36 -0
- package/src/constraints/components/PointConstraintComponent.ts +179 -0
- package/src/constraints/components/RotationEulerComponent.ts +139 -0
- package/src/constraints/components/Spring.ts +30 -0
- package/src/constraints/components/SpringComponent.ts +71 -0
- package/src/constraints/types.ts +6 -0
- package/src/helpers.ts +147 -0
- package/src/index.ts +50 -0
- package/src/math/BasicTransform.ts +19 -0
- package/src/math/NumberValue.ts +13 -0
- package/src/math/isometry.ts +64 -0
- package/src/math/mat3.ts +529 -0
- package/src/math/mat4.ts +588 -0
- package/src/math/quat.ts +193 -0
- package/src/math/scalar.ts +81 -0
- package/src/math/tensor.ts +17 -0
- package/src/math/vec3.ts +589 -0
- package/src/math/vec4.ts +10 -0
- package/src/physics/Body.ts +581 -0
- package/src/physics/CollisionFilter.ts +52 -0
- package/src/physics/SleepModule.ts +163 -0
- package/src/physics/broadphase/BodyPairsModule.ts +363 -0
- package/src/physics/broadphase/BvhModule.ts +237 -0
- package/src/physics/broadphase/BvhTree.ts +803 -0
- package/src/physics/broadphase/ConstraintPairsModule.ts +385 -0
- package/src/physics/broadphase/TriangleMeshBvhTree.ts +379 -0
- package/src/physics/manifold/ContactManifold.ts +227 -0
- package/src/physics/manifold/ContactManifoldModule.ts +623 -0
- package/src/physics/manifold/Face.ts +119 -0
- package/src/physics/manifold/ManifoldCache.ts +116 -0
- package/src/physics/manifold/clipping/clipPolyVsEdge.ts +131 -0
- package/src/physics/manifold/clipping/clipPolyVsPlane.ts +73 -0
- package/src/physics/manifold/clipping/clipPolyVsPoly.ts +72 -0
- package/src/physics/narrowphase/CollideBodiesModule.ts +755 -0
- package/src/physics/solver/ContactConstraintModule.ts +659 -0
- package/src/physics/solver/ManifoldConstraint.ts +420 -0
- package/src/physics/solver/estimateCollisionResponse.ts +146 -0
- package/src/shape/Aabb.ts +400 -0
- package/src/shape/Box.ts +231 -0
- package/src/shape/Capsule.ts +332 -0
- package/src/shape/CompoundShape.ts +288 -0
- package/src/shape/Convex.ts +130 -0
- package/src/shape/ConvexHull.ts +423 -0
- package/src/shape/Cylinder.ts +313 -0
- package/src/shape/HeightMap.ts +511 -0
- package/src/shape/Line.ts +14 -0
- package/src/shape/Plane.ts +116 -0
- package/src/shape/Ray.ts +81 -0
- package/src/shape/Segment.ts +25 -0
- package/src/shape/Shape.ts +77 -0
- package/src/shape/Sphere.ts +181 -0
- package/src/shape/TransformedShape.ts +51 -0
- package/src/shape/Triangle.ts +122 -0
- package/src/shape/TriangleMesh.ts +186 -0
- package/src/types.ts +1 -0
- package/src/world.ts +1335 -0
- package/tests/BodyPairsModule.test.ts +71 -0
- package/tests/BvhTree.test.ts +406 -0
- package/tests/test.md +642 -0
- package/tests/vec3.test.ts +12 -0
- package/tsconfig.json +20 -0
- package/vite.config.js +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 9547-3732 Québec inc. / perplex.gg
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<div style="display: flex;">
|
|
2
|
+
<h1>Bounce - Premium 3D Physics for the Web</h1>
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
Bounce provides fast and determinstic 3D physics for Typescript and Javascript projects. Bounce is written in pure Typsescript (no WebAssembly). See the official website at [https://perplex.gg/bounce](https://perplex.gg/bounce) for demos, API and more info.
|
|
6
|
+
|
|
7
|
+
## Credits and Acknowledgments
|
|
8
|
+
|
|
9
|
+
Bounce was created by:
|
|
10
|
+
- Durai Ziyaee - 95% of the code, physics R&D
|
|
11
|
+
- Hamza Kubba - 5% of the code, funding, direction, project management, QA, website
|
|
12
|
+
|
|
13
|
+
Although Bounce is mostly original code, it is heavily inspired by and draws heavily from:
|
|
14
|
+
- Jolt
|
|
15
|
+
- Rapier
|
|
16
|
+
- Box2D
|
|
17
|
+
- Godot
|
|
18
|
+
- gl-matrix
|
|
19
|
+
|
|
20
|
+
## Roadmap
|
|
21
|
+
|
|
22
|
+
- Release example code (ETA Oct-Nov 2025)
|
|
23
|
+
- Release the official character controller implementation (ETA Oct-Nov 2025)
|
|
24
|
+
- Create a dedicated documentation site (ETA Nov 2025)
|
|
25
|
+
- Fixing bugs. Please file an issue if you run into problems!
|
|
26
|
+
- Want to see something here? Feel free to submit an issue!
|
|
27
|
+
|
|
28
|
+
## How to contribute
|
|
29
|
+
|
|
30
|
+
If you like this project and would like to support our work, please consider contributing code via pull requests, or donating via [open collective](https://opencollective.com/perplexgg). Contributions are greatly appreciated!
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@perplexdotgg/bounce",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"description": "Bounce",
|
|
6
|
+
"main": "./build/bounce.js",
|
|
7
|
+
"types": "./build/bounce.d.ts",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"dev": "vite",
|
|
10
|
+
"build": "tsc && vite build",
|
|
11
|
+
"watch": "tsc --watch",
|
|
12
|
+
"test": "vitest"
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"monomorph": "^1.1.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^24.9.0",
|
|
19
|
+
"ts-node": "^10.9.2",
|
|
20
|
+
"tslib": "^2.8.1",
|
|
21
|
+
"typescript": "^5.9.3",
|
|
22
|
+
"vite": "^7.1.11",
|
|
23
|
+
"vite-plugin-conditional-compiler": "^0.3.1",
|
|
24
|
+
"vite-plugin-dts": "^4.5.4",
|
|
25
|
+
"vitest": "^3.2.4"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://codeberg.org/perplexdotgg/bounce"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import { Mat3 } from "../math/mat3";
|
|
2
|
+
import { Vec3 } from "../math/vec3";
|
|
3
|
+
import { NumberValue } from "../math/NumberValue";
|
|
4
|
+
import { Plane } from "../shape/Plane";
|
|
5
|
+
import ConvexHull, {
|
|
6
|
+
ConvexHullFace,
|
|
7
|
+
ConvexHullNoConvex,
|
|
8
|
+
ConvexHullPoint,
|
|
9
|
+
ConvexHullWithConvex,
|
|
10
|
+
updateShape,
|
|
11
|
+
} from "../shape/ConvexHull";
|
|
12
|
+
import { ConvexHullBuilder3d, ConvexHullBuilderResult } from "./ConvexHullBuilder3d";
|
|
13
|
+
import { degreesToRadians } from "../math/scalar";
|
|
14
|
+
|
|
15
|
+
const covarianceCanonical = /*@__PURE__*/ Mat3.create();
|
|
16
|
+
covarianceCanonical.fillDiagonal(1 / 60);
|
|
17
|
+
covarianceCanonical.fillOffDiagonal(1 / 120);
|
|
18
|
+
const covarianceAccumulated = /*@__PURE__*/ Mat3.create();
|
|
19
|
+
const covarianceTransformed = /*@__PURE__*/ Mat3.create();
|
|
20
|
+
const transform = /*@__PURE__*/ Mat3.create();
|
|
21
|
+
const transposedTransform = /*@__PURE__*/ Mat3.create();
|
|
22
|
+
const v1 = /*@__PURE__*/ Vec3.create();
|
|
23
|
+
const v2 = /*@__PURE__*/ Vec3.create();
|
|
24
|
+
const v3 = /*@__PURE__*/ Vec3.create();
|
|
25
|
+
const zero = /*@__PURE__*/ Vec3.create({ x: 0, y: 0, z: 0 });
|
|
26
|
+
const p = /*@__PURE__*/ Vec3.create();
|
|
27
|
+
const faceCentroid = /*@__PURE__*/ Vec3.create();
|
|
28
|
+
const faceNormal = /*@__PURE__*/ Vec3.create();
|
|
29
|
+
const tempVector = /*@__PURE__*/ Vec3.create();
|
|
30
|
+
const normal1 = /*@__PURE__*/ Vec3.create();
|
|
31
|
+
const normal2 = /*@__PURE__*/ Vec3.create();
|
|
32
|
+
const crossNormal1Normal2 = /*@__PURE__*/ Vec3.create();
|
|
33
|
+
const normal3 = /*@__PURE__*/ Vec3.create();
|
|
34
|
+
const p1 = /*@__PURE__*/ Plane.create();
|
|
35
|
+
const p2 = /*@__PURE__*/ Plane.create();
|
|
36
|
+
let p3 = /*@__PURE__*/ Plane.create();
|
|
37
|
+
const offsetMask = /*@__PURE__*/ Vec3.create();
|
|
38
|
+
const crossN1N2 = /*@__PURE__*/ Vec3.create();
|
|
39
|
+
const n = /*@__PURE__*/ Mat3.create();
|
|
40
|
+
const adj_n = /*@__PURE__*/ Mat3.create();
|
|
41
|
+
const transformedOffset = /*@__PURE__*/ Vec3.create();
|
|
42
|
+
|
|
43
|
+
export class ConvexHullBuilder {
|
|
44
|
+
buildFromPoints(
|
|
45
|
+
vertices: Float32Array,
|
|
46
|
+
convexRadius: number = 0.02,
|
|
47
|
+
hullTolerance: number = 1e-3,
|
|
48
|
+
convexHullPool: typeof ConvexHull.Pool = new ConvexHull.Pool(),
|
|
49
|
+
facePool: typeof ConvexHullFace.Pool = new ConvexHullFace.Pool(),
|
|
50
|
+
numberValuePool: typeof NumberValue.Pool = new NumberValue.Pool(),
|
|
51
|
+
planePool: typeof Plane.Pool = new Plane.Pool(),
|
|
52
|
+
pointPool: typeof ConvexHullPoint.Pool = new ConvexHullPoint.Pool(),
|
|
53
|
+
shapeNoConvexPointsPool: typeof Vec3.Pool = new Vec3.Pool(),
|
|
54
|
+
): ConvexHull {
|
|
55
|
+
const maxPoints = 1000;
|
|
56
|
+
const maxVertexIndices = 1000;
|
|
57
|
+
|
|
58
|
+
// get counts from item buffers and use as max counts
|
|
59
|
+
const hull = ConvexHull.create({
|
|
60
|
+
convexRadius,
|
|
61
|
+
faces: { pool: facePool },
|
|
62
|
+
vertexIdx: { pool: numberValuePool },
|
|
63
|
+
planes: { pool: planePool },
|
|
64
|
+
points: { pool: pointPool },
|
|
65
|
+
shapeNoConvexPoints: { pool: shapeNoConvexPointsPool },
|
|
66
|
+
}, convexHullPool) as ConvexHull;
|
|
67
|
+
hull.shapeNoConvex = new ConvexHullNoConvex(hull, hull.shapeNoConvexPoints);
|
|
68
|
+
hull.shapeWithConvex = new ConvexHullWithConvex(hull);
|
|
69
|
+
|
|
70
|
+
// Check convex radius
|
|
71
|
+
if (hull.convexRadius < 0.0) {
|
|
72
|
+
throw new Error("Invalid convex radius");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// set vertex data on polyhedron 3d
|
|
76
|
+
const polyhedron3d = new Vec3.Pool();
|
|
77
|
+
const polyhedron2d = new Vec3.Pool();
|
|
78
|
+
for (let i = 0; i < vertices.length; i += 3) {
|
|
79
|
+
Vec3.create({ x: vertices[i + 0], y: vertices[i + 1], z: vertices[i + 2] }, polyhedron3d);
|
|
80
|
+
Vec3.create({ x: vertices[i + 0], y: vertices[i + 1], z: vertices[i + 2] }, polyhedron2d);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Build convex hull
|
|
84
|
+
const builder = new ConvexHullBuilder3d();
|
|
85
|
+
const result = builder.buildConvexHull(polyhedron3d, polyhedron2d, hullTolerance, maxPoints);
|
|
86
|
+
if (result !== ConvexHullBuilderResult.Success && result !== ConvexHullBuilderResult.MaxVerticesReached) {
|
|
87
|
+
throw new Error("Hull building failed");
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const builder_faces = builder.faces;
|
|
91
|
+
|
|
92
|
+
// // Check the consistency of the resulting hull if we fully built it
|
|
93
|
+
// // TODO: revisit this error check because it seems to be failing even when the hull looks good visually
|
|
94
|
+
// if (result === ConvexHullBuilderResult.Success) {
|
|
95
|
+
// const [max_error_face, max_error_distance, max_error_idx, coplaner_distance] = builder.determineMaxError();
|
|
96
|
+
// if (max_error_distance > 4 * Math.max(coplaner_distance, hullTolerance)) {
|
|
97
|
+
// // Coplanar distance could be bigger than the allowed tolerance if the points are far apart
|
|
98
|
+
// throw new Error(
|
|
99
|
+
// `Hull building failed, point ${max_error_idx} had an error of ${max_error_distance} (relative to tolerance: ${
|
|
100
|
+
// max_error_distance / hullTolerance
|
|
101
|
+
// })`
|
|
102
|
+
// );
|
|
103
|
+
// }
|
|
104
|
+
// }
|
|
105
|
+
|
|
106
|
+
// Calculate center of mass and volume
|
|
107
|
+
hull.computedVolume = builder.getCenterOfMassAndVolume(hull.computedCenterOfMass);
|
|
108
|
+
// make center of mass zero if it is close to zero
|
|
109
|
+
if (hull.computedCenterOfMass.isCloseToZero(1e-6)) {
|
|
110
|
+
hull.computedCenterOfMass.zero();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Calculate covariance matrix
|
|
114
|
+
// See:
|
|
115
|
+
// - Why the inertia tensor is the inertia tensor - Jonathan Blow (http://number-none.com/blow/inertia/deriving_i.html)
|
|
116
|
+
// - How to find the inertia tensor (or other mass properties) of a 3D solid body represented by a triangle mesh (Draft) - Jonathan Blow, Atman J Binstock (http://number-none.com/blow/inertia/bb_inertia.doc)
|
|
117
|
+
|
|
118
|
+
// compute canonical covariance matrix
|
|
119
|
+
// this is the covariance matrix for a unit tetrahedron
|
|
120
|
+
// with vertices (0, 0, 0), (1, 0, 0), (0, 1, 0) and (0, 0, 1)
|
|
121
|
+
// with density 1 kg/m^3
|
|
122
|
+
// because of symmetry, we only need to "calculate". diagonal = 1 / 60, off-diagonal = 1 / 120
|
|
123
|
+
|
|
124
|
+
// start covariance actual at zero as it will be used for accumulation
|
|
125
|
+
covarianceAccumulated.fill(0);
|
|
126
|
+
|
|
127
|
+
for (const face of builder_faces) {
|
|
128
|
+
// Fourth point of the tetrahedron is at the center of mass, we subtract it from the other points so we get a tetrahedron with one vertex at zero
|
|
129
|
+
// The first point on the face will be used to form a triangle fan
|
|
130
|
+
let edge = face.firstEdge;
|
|
131
|
+
v1.subtractVectors(polyhedron3d.array[edge!.startIndex], hull.computedCenterOfMass);
|
|
132
|
+
|
|
133
|
+
// Get the 2nd point
|
|
134
|
+
edge = edge!.nextEdge;
|
|
135
|
+
v2.subtractVectors(polyhedron3d.array[edge!.startIndex], hull.computedCenterOfMass);
|
|
136
|
+
|
|
137
|
+
// Loop over the triangle fan
|
|
138
|
+
for (edge = edge!.nextEdge; edge !== face.firstEdge; edge = edge!.nextEdge) {
|
|
139
|
+
v3.subtractVectors(polyhedron3d.array[edge!.startIndex], hull.computedCenterOfMass);
|
|
140
|
+
|
|
141
|
+
// Affine transform that transforms a unit tetrahedon (with vertices (0, 0, 0), (1, 0, 0), (0, 1, 0) and (0, 0, 1) to this tetrahedron
|
|
142
|
+
transform.setColumns(v1, v2, v3);
|
|
143
|
+
|
|
144
|
+
// Calculate covariance matrix for this tetrahedron
|
|
145
|
+
const det_a = transform.computeDeterminant();
|
|
146
|
+
|
|
147
|
+
// C_transformed = det_a * (A * C_canonical * A^T)
|
|
148
|
+
transposedTransform.transposeMatrix(transform);
|
|
149
|
+
covarianceTransformed.multiplyMat3s(transform, covarianceCanonical);
|
|
150
|
+
covarianceTransformed.multiplyMat3s(covarianceTransformed, transposedTransform);
|
|
151
|
+
covarianceTransformed.multiplyByScalar(det_a);
|
|
152
|
+
|
|
153
|
+
// Add it
|
|
154
|
+
covarianceAccumulated.addMatrices(covarianceAccumulated, covarianceTransformed);
|
|
155
|
+
|
|
156
|
+
// Prepare for next triangle
|
|
157
|
+
v2.copy(v3);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Calculate inertia matrix assuming density is 1
|
|
162
|
+
hull.inertia.identity();
|
|
163
|
+
hull.inertia.multiplyByScalar(covarianceAccumulated.e0 + covarianceAccumulated.e4 + covarianceAccumulated.e8);
|
|
164
|
+
hull.inertia.subtractMatrices(hull.inertia, covarianceAccumulated);
|
|
165
|
+
|
|
166
|
+
// Convert polygons from the builder to our internal representation
|
|
167
|
+
const vertexMap = new Map<number, number>();
|
|
168
|
+
|
|
169
|
+
for (const builder_face of builder_faces) {
|
|
170
|
+
// Determine where the vertices go
|
|
171
|
+
if (hull.vertexIdx.length > maxVertexIndices) {
|
|
172
|
+
throw new Error(`vertex index array too large: (${hull.vertexIdx.length}), max allowed ${maxVertexIndices}`);
|
|
173
|
+
}
|
|
174
|
+
let firstVertex = hull.vertexIdx.length;
|
|
175
|
+
let numVertices = 0;
|
|
176
|
+
|
|
177
|
+
// Loop over vertices in face
|
|
178
|
+
let edge = builder_face.firstEdge;
|
|
179
|
+
do {
|
|
180
|
+
// Remap to new index, not all points in the original input set are required to form the hull
|
|
181
|
+
// TODO: is it okay to set new_idx to 0 here?
|
|
182
|
+
let newIdx: number = 0;
|
|
183
|
+
const originalIdx = edge!.startIndex;
|
|
184
|
+
const m = vertexMap.get(originalIdx);
|
|
185
|
+
if (m !== undefined) {
|
|
186
|
+
// Found, reuse
|
|
187
|
+
newIdx = m;
|
|
188
|
+
} else {
|
|
189
|
+
// This is a new point
|
|
190
|
+
// Make relative to center of mass
|
|
191
|
+
|
|
192
|
+
p.subtractVectors(polyhedron3d.array[originalIdx], hull.computedCenterOfMass);
|
|
193
|
+
|
|
194
|
+
// TODO: Update local bounds
|
|
195
|
+
|
|
196
|
+
// Add to point list
|
|
197
|
+
if (hull.points.length <= maxPoints) {
|
|
198
|
+
newIdx = hull.points.length;
|
|
199
|
+
hull.points.create({ position: p, numFaces: 0, faces: zero });
|
|
200
|
+
vertexMap.set(originalIdx, newIdx);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Append to vertex list
|
|
205
|
+
if (hull.vertexIdx.length >= maxVertexIndices) {
|
|
206
|
+
throw new Error(`vertex index array too large: (${hull.vertexIdx.length}), max allowed ${maxVertexIndices}`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// TODO: should newIdx be initialized to some value?
|
|
210
|
+
hull.vertexIdx.create({ value: newIdx });
|
|
211
|
+
numVertices++;
|
|
212
|
+
|
|
213
|
+
edge = edge!.nextEdge;
|
|
214
|
+
} while (edge !== builder_face.firstEdge);
|
|
215
|
+
|
|
216
|
+
// Add face
|
|
217
|
+
hull.faces.create({ firstVertex: firstVertex, numVertices: numVertices });
|
|
218
|
+
|
|
219
|
+
// Add plane
|
|
220
|
+
faceCentroid.fromArray(builder_face.centroid);
|
|
221
|
+
faceNormal.fromArray(builder_face.normal);
|
|
222
|
+
tempVector.subtractVectors(faceCentroid, hull.computedCenterOfMass);
|
|
223
|
+
faceNormal.normalize();
|
|
224
|
+
|
|
225
|
+
const plane = hull.planes.create(undefined);
|
|
226
|
+
plane.fromPointAndNormal(tempVector, faceNormal);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Test if GetSupportFunction can support this many points
|
|
230
|
+
if (hull.points.length > maxPoints) {
|
|
231
|
+
throw new Error(`Internal error: Too many points in hull (${hull.points.length}), max allowed ${maxPoints}`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
for (let p = 0; p < hull.points.length; ++p) {
|
|
235
|
+
// For each point, find faces that use the point
|
|
236
|
+
let faces: number[] = [];
|
|
237
|
+
|
|
238
|
+
for (let f = 0; f < hull.faces.length; ++f) {
|
|
239
|
+
const face = hull.faces.getAtIndex(f)!;
|
|
240
|
+
|
|
241
|
+
for (let v = 0; v < face.numVertices; ++v) {
|
|
242
|
+
const vertexIndex = hull.vertexIdx.getAtIndex(face.firstVertex + v)!;
|
|
243
|
+
if (vertexIndex.value === p) {
|
|
244
|
+
faces.push(f);
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (faces.length < 2) {
|
|
251
|
+
throw new Error("A point must be connected to 2 or more faces!");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Find the 3 normals that form the largest tetrahedron
|
|
255
|
+
// The largest tetrahedron we can get is ((1, 0, 0) x (0, 1, 0)) . (0, 0, 1) = 1, if the volume is only 5% of that,
|
|
256
|
+
// the three vectors are too coplanar and we fall back to using only 2 plane normals
|
|
257
|
+
let biggest_volume = 0.05;
|
|
258
|
+
const best3 = [-1, -1, -1];
|
|
259
|
+
|
|
260
|
+
// When using 2 normals, we get the two with the biggest angle between them with a minimal difference of 1 degree
|
|
261
|
+
// otherwise we fall back to just using 1 plane normal
|
|
262
|
+
let smallest_dot = Math.cos(degreesToRadians(1.0));
|
|
263
|
+
const best2 = [-1, -1];
|
|
264
|
+
|
|
265
|
+
for (let face1 = 0; face1 < faces.length; ++face1) {
|
|
266
|
+
const plane1 = hull.planes.getAtIndex(faces[face1])!;
|
|
267
|
+
normal1.copy(plane1.normal);
|
|
268
|
+
|
|
269
|
+
for (let face2 = face1 + 1; face2 < faces.length; ++face2) {
|
|
270
|
+
const plane2 = hull.planes.getAtIndex(faces[face2])!;
|
|
271
|
+
normal2.copy(plane2.normal);
|
|
272
|
+
|
|
273
|
+
crossNormal1Normal2.crossVectors(normal1, normal2);
|
|
274
|
+
|
|
275
|
+
// Determine the 2 face normals that are most apart
|
|
276
|
+
const dot = normal1.dot(normal2);
|
|
277
|
+
if (dot < smallest_dot) {
|
|
278
|
+
smallest_dot = dot;
|
|
279
|
+
best2[0] = faces[face1];
|
|
280
|
+
best2[1] = faces[face2];
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Determine the 3 face normals that form the largest tetrahedron
|
|
284
|
+
for (let face3 = face2 + 1; face3 < faces.length; ++face3) {
|
|
285
|
+
const plane3 = hull.planes.getAtIndex(faces[face3])!;
|
|
286
|
+
normal3.copy(plane3.normal);
|
|
287
|
+
|
|
288
|
+
const volume = Math.abs(crossNormal1Normal2.dot(normal3));
|
|
289
|
+
if (volume > biggest_volume) {
|
|
290
|
+
biggest_volume = volume;
|
|
291
|
+
best3[0] = faces[face1];
|
|
292
|
+
best3[1] = faces[face2];
|
|
293
|
+
best3[2] = faces[face3];
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// If we didn't find 3 planes, use 2, if we didn't find 2 use 1
|
|
300
|
+
let pointFaces: [number, number, number];
|
|
301
|
+
if (best3[0] !== -1) {
|
|
302
|
+
faces = [best3[0], best3[1], best3[2]];
|
|
303
|
+
pointFaces = [best3[0], best3[1], best3[2]];
|
|
304
|
+
} else if (best2[0] !== -1) {
|
|
305
|
+
faces = [best2[0], best2[1]];
|
|
306
|
+
pointFaces = [best2[0], best2[1], -1];
|
|
307
|
+
} else {
|
|
308
|
+
faces = [faces[0], -1, -1];
|
|
309
|
+
pointFaces = [faces[0], -1, -1];
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Copy the faces to the points buffer
|
|
313
|
+
const point = hull.points.getAtIndex(p)!;
|
|
314
|
+
point.numFaces = faces.length;
|
|
315
|
+
point.faces.fromArray(pointFaces);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If the convex radius is already zero, there's no point in further reducing it
|
|
319
|
+
if (hull.convexRadius > 0.0) {
|
|
320
|
+
// Find out how thin the hull is by walking over all planes and checking the thickness of the hull in that direction
|
|
321
|
+
let minSize = Number.MAX_VALUE;
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < hull.planes.length; i++) {
|
|
324
|
+
const plane = hull.planes.getAtIndex(i)!;
|
|
325
|
+
|
|
326
|
+
// Take the point that is furthest away from the plane as thickness of this hull
|
|
327
|
+
let maxDist = 0.0;
|
|
328
|
+
|
|
329
|
+
for (let j = 0; j < hull.points.length; j++) {
|
|
330
|
+
const point = hull.points.getAtIndex(j)!;
|
|
331
|
+
|
|
332
|
+
const dist = -plane.signedDistance(point.position); // Point is always behind plane, so we need to negate
|
|
333
|
+
if (dist > maxDist) {
|
|
334
|
+
maxDist = dist;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
minSize = Math.min(minSize, maxDist);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// We need to fit in 2x the convex radius in min_size, so reduce the convex radius if it's bigger than that
|
|
341
|
+
hull.convexRadius = Math.min(hull.convexRadius, 0.5 * minSize);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Now walk over all points and see if we have to further reduce the convex radius because of sharp edges
|
|
345
|
+
if (hull.convexRadius > 0.0) {
|
|
346
|
+
for (let i = 0; i < hull.points.length; i++) {
|
|
347
|
+
const point = hull.points.getAtIndex(i)!;
|
|
348
|
+
|
|
349
|
+
// If we have a single face, shifting back is easy and we don't need to reduce the convex radius
|
|
350
|
+
if (point.numFaces !== 1) {
|
|
351
|
+
// Get first two planes
|
|
352
|
+
p1.copy(hull.planes.getAtIndex(point.faces.x)!);
|
|
353
|
+
p2.copy(hull.planes.getAtIndex(point.faces.y)!);
|
|
354
|
+
|
|
355
|
+
if (point.numFaces === 3) {
|
|
356
|
+
// Get third plane
|
|
357
|
+
p3.copy(hull.planes.getAtIndex(point.faces.z)!);
|
|
358
|
+
|
|
359
|
+
// All 3 planes will be offset by the convex radius
|
|
360
|
+
offsetMask.replicate(1);
|
|
361
|
+
} else {
|
|
362
|
+
// Third plane has normal perpendicular to the other two planes and goes through the vertex position
|
|
363
|
+
if (point.numFaces !== 2) {
|
|
364
|
+
throw new Error("Invalid number of faces");
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
crossN1N2.crossVectors(p1.normal, p2.normal);
|
|
368
|
+
p3.fromPointAndNormal(point.position, crossN1N2);
|
|
369
|
+
|
|
370
|
+
// Only the first and 2nd plane will be offset, the 3rd plane is only there to guide the intersection point
|
|
371
|
+
offsetMask.set({ x: 1, y: 1, z: 0 });
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Plane equation: point . normal + constant = 0
|
|
375
|
+
// Offsetting the plane backwards with convex radius r: point . normal + constant + r = 0
|
|
376
|
+
// To find the intersection 'point' of 3 planes we solve:
|
|
377
|
+
// |n1x n1y n1z| |x| | r + c1 |
|
|
378
|
+
// |n2x n2y n2z| |y| = - | r + c2 | <=> n point = -r (1, 1, 1) - (c1, c2, c3)
|
|
379
|
+
// |n3x n3y n3z| |z| | r + c3 |
|
|
380
|
+
// Where point = (x, y, z), n1x is the x component of the first plane, c1 = plane constant of plane 1, etc.
|
|
381
|
+
// The relation between how much the intersection point shifts as a function of r is: -r * n^-1 (1, 1, 1) = r * offset
|
|
382
|
+
// Where offset = -n^-1 (1, 1, 1) or -n^-1 (1, 1, 0) in case only the first 2 planes are offset
|
|
383
|
+
// The error that is introduced by a convex radius r is: error = r * |offset| - r
|
|
384
|
+
// So the max convex radius given error is: r = error / (|offset| - 1)
|
|
385
|
+
|
|
386
|
+
// TODO: using a mat3 here instead of a mat4 because mat4.3x3 functions not implemented, as a result, not sure if this is correct
|
|
387
|
+
|
|
388
|
+
n.e0 = p1.normal.x;
|
|
389
|
+
n.e1 = p1.normal.y;
|
|
390
|
+
n.e2 = p1.normal.z;
|
|
391
|
+
n.e3 = p2.normal.x;
|
|
392
|
+
n.e4 = p2.normal.y;
|
|
393
|
+
n.e5 = p2.normal.z;
|
|
394
|
+
n.e6 = p3.normal.x;
|
|
395
|
+
n.e7 = p3.normal.y;
|
|
396
|
+
n.e8 = p3.normal.z;
|
|
397
|
+
n.transpose();
|
|
398
|
+
|
|
399
|
+
const det_n = n.computeDeterminant();
|
|
400
|
+
if (det_n === 0.0) {
|
|
401
|
+
// If the determinant is zero, the matrix is not invertible so no solution exists to move the point backwards and we have to choose a convex radius of zero
|
|
402
|
+
hull.convexRadius = 0.0;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
adj_n.adjointMatrix(n);
|
|
407
|
+
|
|
408
|
+
transformedOffset.transformVectorFromMat3(offsetMask, adj_n);
|
|
409
|
+
transformedOffset.scale(1 / det_n);
|
|
410
|
+
const offset = transformedOffset.length();
|
|
411
|
+
if (offset <= 1.0) {
|
|
412
|
+
throw new Error("Invalid offset");
|
|
413
|
+
}
|
|
414
|
+
const max_convex_radius = hullTolerance / (offset - 1.0);
|
|
415
|
+
hull.convexRadius = Math.min(hull.convexRadius, max_convex_radius);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Calculate the inner radius by getting the minimum distance from the origin to the planes of the hull
|
|
421
|
+
hull.innerRadius = Number.MAX_VALUE;
|
|
422
|
+
for (let i = 0; i < hull.planes.length; i++) {
|
|
423
|
+
const plane = hull.planes.getAtIndex(i)!;
|
|
424
|
+
hull.innerRadius = Math.min(hull.innerRadius, -plane.constant);
|
|
425
|
+
}
|
|
426
|
+
// Clamp against zero, this should do nothing as the shape is centered around the
|
|
427
|
+
// center of mass but for flat convex hulls there may be numerical round off issues
|
|
428
|
+
hull.innerRadius = Math.max(0.0, hull.innerRadius);
|
|
429
|
+
|
|
430
|
+
for (let i = 0; i < hull.points.length; i++) {
|
|
431
|
+
hull.shapeNoConvexPoints.create();
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
updateShape(hull);
|
|
435
|
+
return hull;
|
|
436
|
+
}
|
|
437
|
+
}
|