Skip to content

Commit b60b86c

Browse files
authored
Merge pull request #40 from react-spring/performance
Fix performance issues and introduce Performance example
2 parents 9703433 + 22b4448 commit b60b86c

File tree

9 files changed

+239
-21
lines changed

9 files changed

+239
-21
lines changed

dist/cannon-es.cjs.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9127,7 +9127,7 @@ class Trimesh extends Shape {
91279127
const n = this.vertices.length / 3,
91289128
verts = this.vertices;
91299129
const minx,miny,minz,maxx,maxy,maxz;
9130-
const v = tempWorldVertex;
9130+
const v = tempWorldVertex;
91319131
for(let i=0; i<n; i++){
91329132
this.getVertex(i, v);
91339133
quat.vmult(v, v);
@@ -9137,12 +9137,12 @@ class Trimesh extends Shape {
91379137
} else if(v.x > maxx || maxx===undefined){
91389138
maxx = v.x;
91399139
}
9140-
if (v.y < miny || miny===undefined){
9140+
if (v.y < miny || miny===undefined){
91419141
miny = v.y;
91429142
} else if(v.y > maxy || maxy===undefined){
91439143
maxy = v.y;
91449144
}
9145-
if (v.z < minz || minz===undefined){
9145+
if (v.z < minz || minz===undefined){
91469146
minz = v.z;
91479147
} else if(v.z > maxz || maxz===undefined){
91489148
maxz = v.z;
@@ -11803,24 +11803,35 @@ class World extends EventTarget {
1180311803
*/
1180411804

1180511805

11806-
step(dt, timeSinceLastCalled = 0, maxSubSteps = 10) {
11807-
if (timeSinceLastCalled === 0) {
11806+
step(dt, timeSinceLastCalled, maxSubSteps = 10) {
11807+
if (timeSinceLastCalled === undefined) {
1180811808
// Fixed, simple stepping
1180911809
this.internalStep(dt); // Increment time
1181011810

1181111811
this.time += dt;
1181211812
} else {
1181311813
this.accumulator += timeSinceLastCalled;
11814+
const t0 = performance.now();
1181411815
let substeps = 0;
1181511816

1181611817
while (this.accumulator >= dt && substeps < maxSubSteps) {
1181711818
// Do fixed steps to catch up
1181811819
this.internalStep(dt);
1181911820
this.accumulator -= dt;
1182011821
substeps++;
11821-
}
1182211822

11823-
const t = this.accumulator % dt / dt;
11823+
if (performance.now() - t0 > dt * 2 * 1000) {
11824+
// The framerate is not interactive anymore.
11825+
// We are at half of the target framerate.
11826+
// Better bail out.
11827+
break;
11828+
}
11829+
} // Remove the excess accumulator, since we may not
11830+
// have had enough substeps available to catch up
11831+
11832+
11833+
this.accumulator = this.accumulator % dt;
11834+
const t = this.accumulator / dt;
1182411835

1182511836
for (let j = 0; j !== this.bodies.length; j++) {
1182611837
const b = this.bodies[j];

dist/cannon-es.js

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9123,7 +9123,7 @@ class Trimesh extends Shape {
91239123
const n = this.vertices.length / 3,
91249124
verts = this.vertices;
91259125
const minx,miny,minz,maxx,maxy,maxz;
9126-
const v = tempWorldVertex;
9126+
const v = tempWorldVertex;
91279127
for(let i=0; i<n; i++){
91289128
this.getVertex(i, v);
91299129
quat.vmult(v, v);
@@ -9133,12 +9133,12 @@ class Trimesh extends Shape {
91339133
} else if(v.x > maxx || maxx===undefined){
91349134
maxx = v.x;
91359135
}
9136-
if (v.y < miny || miny===undefined){
9136+
if (v.y < miny || miny===undefined){
91379137
miny = v.y;
91389138
} else if(v.y > maxy || maxy===undefined){
91399139
maxy = v.y;
91409140
}
9141-
if (v.z < minz || minz===undefined){
9141+
if (v.z < minz || minz===undefined){
91429142
minz = v.z;
91439143
} else if(v.z > maxz || maxz===undefined){
91449144
maxz = v.z;
@@ -11799,24 +11799,35 @@ class World extends EventTarget {
1179911799
*/
1180011800

1180111801

11802-
step(dt, timeSinceLastCalled = 0, maxSubSteps = 10) {
11803-
if (timeSinceLastCalled === 0) {
11802+
step(dt, timeSinceLastCalled, maxSubSteps = 10) {
11803+
if (timeSinceLastCalled === undefined) {
1180411804
// Fixed, simple stepping
1180511805
this.internalStep(dt); // Increment time
1180611806

1180711807
this.time += dt;
1180811808
} else {
1180911809
this.accumulator += timeSinceLastCalled;
11810+
const t0 = performance.now();
1181011811
let substeps = 0;
1181111812

1181211813
while (this.accumulator >= dt && substeps < maxSubSteps) {
1181311814
// Do fixed steps to catch up
1181411815
this.internalStep(dt);
1181511816
this.accumulator -= dt;
1181611817
substeps++;
11817-
}
1181811818

11819-
const t = this.accumulator % dt / dt;
11819+
if (performance.now() - t0 > dt * 2 * 1000) {
11820+
// The framerate is not interactive anymore.
11821+
// We are at half of the target framerate.
11822+
// Better bail out.
11823+
break;
11824+
}
11825+
} // Remove the excess accumulator, since we may not
11826+
// have had enough substeps available to catch up
11827+
11828+
11829+
this.accumulator = this.accumulator % dt;
11830+
const t = this.accumulator / dt;
1182011831

1182111832
for (let j = 0; j !== this.bodies.length; j++) {
1182211833
const b = this.bodies[j];

examples/css/style.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,8 @@ body {
66
overflow: hidden;
77
font-family: Monospace;
88
}
9+
10+
/* more space for the Stats.js button name */
11+
.dg .property-name {
12+
width: 80% !important;
13+
}

examples/js/Demo.js

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ var Demo = function (options) {
4949
maxSubSteps: 20,
5050
})
5151

52+
var dummy = new THREE.Object3D()
53+
5254
// Extend settings with options
5355
options = options || {}
5456
for (var key in options) {
@@ -274,9 +276,21 @@ var Demo = function (options) {
274276
bodyQuat = b.quaternion
275277
}
276278

277-
visual.position.copy(bodyPos)
278-
if (b.quaternion) {
279-
visual.quaternion.copy(bodyQuat)
279+
if (visual.isInstancedMesh) {
280+
dummy.position.copy(bodyPos)
281+
if (b.quaternion) {
282+
dummy.quaternion.copy(bodyQuat)
283+
}
284+
285+
dummy.updateMatrix()
286+
287+
visual.setMatrixAt(b.instanceIndex, dummy.matrix)
288+
visual.instanceMatrix.needsUpdate = true
289+
} else {
290+
visual.position.copy(bodyPos)
291+
if (b.quaternion) {
292+
visual.quaternion.copy(bodyQuat)
293+
}
280294
}
281295
}
282296

@@ -980,6 +994,32 @@ Demo.prototype.addVisual = function (body) {
980994
}
981995
}
982996

997+
Demo.prototype.addVisualInstanced = function (bodies) {
998+
if (!Array.isArray(bodies) || !bodies.every((body) => body instanceof CANNON.Body)) {
999+
throw new Error('The argument passed to addVisualInstanced() is not an array of bodies')
1000+
}
1001+
1002+
// What geometry should be used?
1003+
const mesh = this.shape2mesh(bodies[0]).children[0]
1004+
1005+
const instancedMesh = new THREE.InstancedMesh(mesh.geometry.clone(), mesh.material.clone(), bodies.length)
1006+
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) // will be updated every frame
1007+
// Add bodies
1008+
1009+
instancedMesh.receiveShadow = true
1010+
instancedMesh.castShadow = true
1011+
1012+
bodies.forEach((body, i) => {
1013+
this.bodies.push(body)
1014+
this.visuals.push(instancedMesh)
1015+
body.instanceIndex = i
1016+
body.visualref = instancedMesh
1017+
body.visualref.visualId = this.bodies.length - 1
1018+
})
1019+
1020+
this.scene.add(instancedMesh)
1021+
}
1022+
9831023
Demo.prototype.addVisuals = function (bodies) {
9841024
for (var i = 0; i < bodies.length; i++) {
9851025
this.addVisual(bodies[i])

examples/performance.html

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>cannon.js - performance tests</title>
5+
<meta charset="utf-8" />
6+
<link rel="stylesheet" href="css/style.css" type="text/css" />
7+
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
8+
</head>
9+
<body>
10+
<script type="module">
11+
import * as CANNON from '../dist/cannon-es.js'
12+
import { Demo } from './js/Demo.js'
13+
14+
/**
15+
* Test the performance limits with a lot of stuff happening all at the same time.
16+
* The simulation should not slow down but rather skip ahead when introducing jank.
17+
* When there are too many object to simulate, the simulation will slow down but still run at a stable framerate.
18+
*/
19+
const demo = new Demo()
20+
21+
let rafId
22+
23+
demo.addScene('200 boxes', () => {
24+
setupFallingBoxes({ N: 200 })
25+
})
26+
27+
demo.addScene('500 boxes', () => {
28+
setupFallingBoxes({ N: 500 })
29+
})
30+
31+
demo.addScene('100 boxes + 32ms jank', () => {
32+
setupFallingBoxes({ N: 100, JANK: 32 })
33+
})
34+
35+
demo.addScene('100 boxes + 48ms jank', () => {
36+
setupFallingBoxes({ N: 100, JANK: 48 })
37+
})
38+
39+
demo.addScene('100 boxes + 64ms jank', () => {
40+
setupFallingBoxes({ N: 100, JANK: 64 })
41+
})
42+
43+
demo.addScene('100 boxes + 128ms jank', () => {
44+
setupFallingBoxes({ N: 100, JANK: 128 })
45+
})
46+
47+
function setupFallingBoxes({ N, JANK }) {
48+
if (rafId) cancelAnimationFrame(rafId)
49+
50+
const world = setupWorld(demo)
51+
52+
const size = 0.25
53+
const mass = 1
54+
55+
const boxShape = new CANNON.Box(new CANNON.Vec3(size, size, size))
56+
57+
const boxes = []
58+
for (let i = 0; i < N; i++) {
59+
// start with random positions
60+
const position = new CANNON.Vec3(
61+
(Math.random() * 2 - 1) * 2.5,
62+
Math.random() * 10,
63+
(Math.random() * 2 - 1) * 2.5
64+
)
65+
66+
const boxBody = new CANNON.Body({
67+
position,
68+
mass,
69+
})
70+
boxBody.addShape(boxShape)
71+
world.addBody(boxBody)
72+
// demo.addVisual(boxBody)
73+
boxes.push(boxBody)
74+
}
75+
76+
// Use instancing so three.js doesn't get in the way
77+
// of performance measuring
78+
demo.addVisualInstanced(boxes)
79+
80+
function animate(ms) {
81+
rafId = requestAnimationFrame(animate)
82+
83+
const time = ms / 1000
84+
85+
const index = Math.floor(Math.random() * N)
86+
87+
boxes[index].position.set(0, Math.random() * 10, 0)
88+
89+
if (JANK) {
90+
blockThread(JANK)
91+
}
92+
}
93+
rafId = requestAnimationFrame(animate)
94+
}
95+
96+
demo.start()
97+
98+
function setupWorld(demo) {
99+
const world = demo.getWorld()
100+
world.gravity.set(0, -50, 0)
101+
// world.broadphase = new CANNON.SAPBroadphase(world)
102+
103+
// world.solver.iterations = 20;
104+
// world.solver.tolerance = 0.001;
105+
// world.allowSleep = true;
106+
107+
// Static ground plane
108+
const groundShape = new CANNON.Plane()
109+
const groundBody = new CANNON.Body({ mass: 0 })
110+
groundBody.addShape(groundShape)
111+
groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
112+
world.addBody(groundBody)
113+
demo.addVisual(groundBody)
114+
115+
return world
116+
}
117+
118+
// Block the javascript thread for N milliseconds
119+
function blockThread(milliseconds = 0) {
120+
const start = performance.now()
121+
while (performance.now() < start + milliseconds) {}
122+
}
123+
</script>
124+
</body>
125+
</html>

images/performance.png

58.9 KB
Loading

index.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,19 @@ <h2>Examples</h2>
653653
/></a>
654654
</div>
655655
</div>
656+
657+
<div class="example">
658+
<a href="examples/performance.html" class="image"><img src="images/performance.png" alt="performance" /></a>
659+
<div class="caption">
660+
<a href="examples/performance.html" class="name">performance</a>
661+
<a
662+
href="https://github.com/react-spring/cannon-es/blob/master/examples/performance.html"
663+
target="_blank"
664+
class="github"
665+
><img src="images/github.svg" alt="Github"
666+
/></a>
667+
</div>
668+
</div>
656669
</div>
657670
</div>
658671
</body>

readme.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@ It's a type-safe flatbundle (esm and cjs) which allows for **tree shaking** and
66

77
These minor changes and improvements were also made:
88

9-
- These PRs from the original repo were merged: [schteppe/cannon.js#433](https://github.com/schteppe/cannon.js/pull/433), [schteppe/cannon.js#430](https://github.com/schteppe/cannon.js/pull/430), [schteppe/cannon.js#418](https://github.com/schteppe/cannon.js/pull/418), [schteppe/cannon.js#360](https://github.com/schteppe/cannon.js/pull/360), [schteppe/cannon.js#265](https://github.com/schteppe/cannon.js/pull/265)
9+
- These PRs from the original repo were merged: [schteppe/cannon.js#433](https://github.com/schteppe/cannon.js/pull/433), [schteppe/cannon.js#430](https://github.com/schteppe/cannon.js/pull/430), [schteppe/cannon.js#418](https://github.com/schteppe/cannon.js/pull/418), [schteppe/cannon.js#360](https://github.com/schteppe/cannon.js/pull/360), [schteppe/cannon.js#265](https://github.com/schteppe/cannon.js/pull/265), [schteppe/cannon.js#392](https://github.com/schteppe/cannon.js/pull/392)
1010
- The `ConvexPolyhedron` constructor now accepts an object instead of a list of arguments. [#6](https://github.com/react-spring/cannon-es/pull/6)
1111
- The `Cylinder` is now oriented on the Y axis. [#30](https://github.com/react-spring/cannon-es/pull/30)
12+
- `Body.applyImpulse()` and `Body.applyForce()` are now relative to the center of the body instead of the center of the world [86b0444](https://github.com/schteppe/cannon.js/commit/86b0444c93356aeaa25dd1af795fa162574c6f4b)
1213
- Added a property `World.hasActiveBodies: boolean` which will be false when all physics bodies are sleeping. This allows for invalidating frames when physics aren't active for increased performance.
1314
- Deprecated properties and methods have been removed.
1415
- The [original cannon.js debugger](https://github.com/schteppe/cannon.js/blob/master/tools/threejs/CannonDebugRenderer.js), which shows the wireframes of each body, has been moved to its own repo [cannon-es-debugger](https://github.com/react-spring/cannon-es-debugger).

0 commit comments

Comments
 (0)