cannon-es-control
Version:
A lightweight 3D physics engine written in JavaScript with control system tools
1,041 lines (880 loc) • 32.9 kB
JavaScript
import * as CANNON from '../../dist/cannon-es-control'
import * as THREE from 'https://unpkg.com/three@0.122.0/build/three.module.js'
import Stats from 'https://unpkg.com/three@0.122.0/examples/jsm/libs/stats.module.js'
import * as dat from 'https://unpkg.com/dat.gui@0.7.7/build/dat.gui.module.js'
import { OrbitControls } from 'https://unpkg.com/three@0.122.0/examples/jsm/controls/OrbitControls.js'
import { SmoothieChart, TimeSeries } from './smoothie.js'
import { addTitle, addSourceButton } from './dom-utils.js'
import { bodyToMesh } from './three-conversion-utils.js'
/**
* Demo utility class. If you want to learn how to connect cannon.js with three.js, please look at the examples/threejs_* instead.
*/
class Demo extends CANNON.EventTarget {
sceneFolder
scenes = []
listeners = {}
// array used to keep in sync the visuals with the bodies
// they will have always the same length
bodies = []
visuals = []
gui
smoothie
smoothieCanvas
renderModes = ['solid', 'wireframe']
dummy = new THREE.Object3D()
constructor(options) {
super(options)
// Global settings
this.settings = {
stepFrequency: 60,
quatNormalizeSkip: 2,
quatNormalizeFast: true,
gx: 0,
gy: 0,
gz: 0,
iterations: 3,
tolerance: 0.0001,
k: 1e6,
d: 3,
scene: 0,
paused: false,
rendermode: 'solid',
constraints: false,
contacts: false, // Contact points
cm2contact: false, // center of mass to contact points
normals: false, // contact normals
axes: false, // "local" frame axes
shadows: false,
aabbs: false,
profiling: false,
maxSubSteps: 20,
...options,
}
if (this.settings.stepFrequency % 60 !== 0) {
throw new Error('stepFrequency must be a multiple of 60.')
}
// Init cannon.js
this.world = new CANNON.World()
// Init three.js
this.initThree()
// Init the geometry caches
this.initGeometryCaches()
// Init stats.js
this.initStats()
// Init smoothie.js
this.initSmoothie()
// Init dat.gui
this.initGui()
// Add title info
addTitle()
// Add view-source button
addSourceButton()
// Start the loop!
this.animate()
// Attach listeners
window.addEventListener('resize', this.resize)
document.addEventListener('keypress', this.onKeyPress)
}
initGui = () => {
// DAT GUI
this.gui = new dat.GUI()
this.gui.domElement.parentNode.style.zIndex = 3
// Render mode
const renderFolder = this.gui.addFolder('Rendering')
renderFolder.add(this.settings, 'rendermode', { Solid: 'solid', Wireframe: 'wireframe' }).onChange((mode) => {
this.setRenderMode(mode)
})
renderFolder.add(this.settings, 'contacts')
renderFolder.add(this.settings, 'cm2contact')
renderFolder.add(this.settings, 'normals')
renderFolder.add(this.settings, 'constraints')
renderFolder.add(this.settings, 'axes')
renderFolder
.add(this.settings, 'shadows')
.onChange((shadows) => {
if (shadows) {
this.spotLight.castShadow = true
this.renderer.shadowMap.autoUpdate = true
} else {
this.spotLight.castShadow = false
this.renderer.shadowMap.autoUpdate = false
}
})
.setValue(true)
renderFolder.add(this.settings, 'aabbs')
renderFolder.add(this.settings, 'profiling').onChange((profiling) => {
if (profiling) {
this.world.doProfiling = true
this.smoothie.start()
this.smoothieCanvas.style.display = 'block'
} else {
this.world.doProfiling = false
this.smoothie.stop()
this.smoothieCanvas.style.display = 'none'
}
})
// World folder
const worldFolder = this.gui.addFolder('World')
// Pause
worldFolder.add(this.settings, 'paused').onChange((paused) => {
if (paused) {
this.smoothie.stop()
} else {
this.smoothie.start()
}
this.resetCallTime = true
})
worldFolder.add(this.settings, 'stepFrequency', 10, 60 * 10, 10)
worldFolder.add(this.settings, 'maxSubSteps', 1, 50, 1)
const maxg = 100
worldFolder.add(this.settings, 'gx', -maxg, maxg).onChange((gx) => {
if (isNaN(gx)) {
return
}
this.world.gravity.set(gx, this.settings.gy, this.settings.gz)
})
worldFolder.add(this.settings, 'gy', -maxg, maxg).onChange((gy) => {
if (isNaN(gy)) {
return
}
this.world.gravity.set(this.settings.gx, gy, this.settings.gz)
})
worldFolder.add(this.settings, 'gz', -maxg, maxg).onChange((gz) => {
if (isNaN(gz)) {
return
}
this.world.gravity.set(this.settings.gx, this.settings.gy, gz)
})
worldFolder.add(this.settings, 'quatNormalizeSkip', 0, 50, 1).onChange((skip) => {
if (isNaN(skip)) {
return
}
this.world.quatNormalizeSkip = skip
})
worldFolder.add(this.settings, 'quatNormalizeFast').onChange((fast) => {
this.world.quatNormalizeFast = !!fast
})
// Solver folder
const solverFolder = this.gui.addFolder('Solver')
solverFolder
.add(this.settings, 'iterations', 1, 50, 1)
.onChange((it) => {
this.world.solver.iterations = it
})
solverFolder.add(this.settings, 'k', 10, 10000000).onChange((k) => {
this.setGlobalSpookParams(this.settings.k, this.settings.d, 1 / this.settings.stepFrequency)
})
solverFolder.add(this.settings, 'd', 0, 20, 0.1).onChange((d) => {
this.setGlobalSpookParams(this.settings.k, this.settings.d, 1 / this.settings.stepFrequency)
})
solverFolder
.add(this.settings, 'tolerance', 0.0, 10.0, 0.01)
.onChange((t) => {
this.world.solver.tolerance = t
})
// Scene picker folder
this.sceneFolder = this.gui.addFolder('Scenes')
this.sceneFolder.open()
}
updateGui = () => {
// First level
this.gui.__controllers.forEach((controller) => {
controller.updateDisplay()
})
// Second level
Object.values(this.gui.__folders).forEach((folder) => {
folder.__controllers.forEach((controller) => {
controller.updateDisplay()
})
})
}
setRenderMode = (mode) => {
if (!this.renderModes.includes(mode)) {
throw new Error(`Render mode ${mode} not found!`)
}
switch (mode) {
case 'solid':
this.currentMaterial = this.solidMaterial
this.spotLight.intensity = 1
this.ambientLight.color.setHex(0x222222)
break
case 'wireframe':
this.currentMaterial = this.wireframeMaterial
this.spotLight.intensity = 0
this.ambientLight.color.setHex(0xffffff)
break
}
// set the materials
this.visuals.forEach((visual) => {
if (visual.material) {
visual.material = this.currentMaterial
}
visual.traverse((child) => {
if (child.material) {
child.material = this.currentMaterial
}
})
})
this.settings.rendermode = mode
}
initStats = () => {
this.stats = new Stats()
document.body.appendChild(this.stats.domElement)
}
/**
* Add a scene to the demo app
* @method addScene
* @param {String} title Title of the scene
* @param {Function} initfunc A function this takes one argument, app, and initializes a physics scene. The function runs app.setWorld(body), app.addVisual(body), app.removeVisual(body) etc.
*/
addScene = (title, initfunc) => {
if (typeof title !== 'string') {
throw new Error('1st argument of Demo.addScene(title,initfunc) must be a string!')
}
if (typeof initfunc !== 'function') {
throw new Error('2nd argument of Demo.addScene(title,initfunc) must be a function!')
}
this.scenes.push(initfunc)
const index = this.scenes.length - 1
this.sceneFolder.add({ [title]: () => this.changeScene(index) }, title)
}
/**
* Restarts the current scene
* @method restartCurrentScene
*/
restartCurrentScene = () => {
this.bodies.forEach((body) => {
body.position.copy(body.initPosition)
body.velocity.copy(body.initVelocity)
if (body.initAngularVelocity) {
body.angularVelocity.copy(body.initAngularVelocity)
body.quaternion.copy(body.initQuaternion)
}
})
}
updateVisuals = () => {
// Copy position data into visuals
for (let i = 0; i < this.bodies.length; i++) {
const body = this.bodies[i]
const visual = this.visuals[i]
// Interpolated or not?
let position = body.interpolatedPosition
let quaternion = body.interpolatedQuaternion
if (this.settings.paused) {
position = body.position
quaternion = body.quaternion
}
if (visual.isInstancedMesh) {
this.dummy.position.copy(position)
this.dummy.quaternion.copy(quaternion)
this.dummy.updateMatrix()
visual.setMatrixAt(body.instanceIndex, this.dummy.matrix)
visual.instanceMatrix.needsUpdate = true
} else {
visual.position.copy(position)
visual.quaternion.copy(quaternion)
}
}
// Render contacts
this.contactMeshCache.restart()
if (this.settings.contacts) {
// if ci is even - use body i, else j
for (let i = 0; i < this.world.contacts.length; i++) {
const contact = this.world.contacts[i]
for (let ij = 0; ij < 2; ij++) {
const mesh = this.contactMeshCache.request()
const b = ij === 0 ? contact.bi : contact.bj
const r = ij === 0 ? contact.ri : contact.rj
mesh.position.set(b.position.x + r.x, b.position.y + r.y, b.position.z + r.z)
}
}
}
this.contactMeshCache.hideCached()
// Lines from center of mass to contact point
this.cm2contactMeshCache.restart()
if (this.settings.cm2contact) {
for (let i = 0; i < this.world.contacts.length; i++) {
const contact = this.world.contacts[i]
for (let ij = 0; ij < 2; ij++) {
const line = this.cm2contactMeshCache.request()
const b = ij === 0 ? contact.bi : contact.bj
const r = ij === 0 ? contact.ri : contact.rj
line.scale.set(r.x, r.y, r.z)
makeSureNotZero(line.scale)
line.position.copy(b.position)
}
}
}
this.cm2contactMeshCache.hideCached()
this.distanceConstraintMeshCache.restart()
this.p2pConstraintMeshCache.restart()
if (this.settings.constraints) {
this.world.constraints.forEach((constraint) => {
switch (true) {
// Lines for distance constraints
case constraint instanceof CANNON.DistanceConstraint: {
constraint.equations.forEach((equation) => {
const { bi, bj } = equation
const line = this.distanceConstraintMeshCache.request()
// Remember, bj is either a Vec3 or a Body.
const vector = bj.position || bj
line.scale.set(vector.x - bi.position.x, vector.y - bi.position.y, vector.z - bi.position.z)
makeSureNotZero(line.scale)
line.position.copy(bi.position)
})
break
}
// Lines for point to point constraints
case constraint instanceof CANNON.PointToPointConstraint: {
constraint.equations.forEach((equation) => {
const { bi, bj } = equation
const relLine1 = this.p2pConstraintMeshCache.request()
const relLine2 = this.p2pConstraintMeshCache.request()
const diffLine = this.p2pConstraintMeshCache.request()
if (equation.ri) {
relLine1.scale.set(equation.ri.x, equation.ri.y, equation.ri.z)
}
if (equation.rj) {
relLine2.scale.set(equation.rj.x, equation.rj.y, equation.rj.z)
}
// BUG this is not exposed anymore in the ContactEquation, this sections needs to be updated
if (equation.penetrationVec) {
diffLine.scale.set(-equation.penetrationVec.x, -equation.penetrationVec.y, -equation.penetrationVec.z)
}
makeSureNotZero(relLine1.scale)
makeSureNotZero(relLine2.scale)
makeSureNotZero(diffLine.scale)
relLine1.position.copy(bi.position)
relLine2.position.copy(bj.position)
if (equation.bj && equation.rj) {
equation.bj.position.vadd(equation.rj, diffLine.position)
}
})
break
}
}
})
}
this.p2pConstraintMeshCache.hideCached()
this.distanceConstraintMeshCache.hideCached()
// Normal lines
this.normalMeshCache.restart()
if (this.settings.normals) {
for (let i = 0; i < this.world.contacts.length; i++) {
const constraint = this.world.contacts[i]
const bi = constraint.bi
const bj = constraint.bj
const line = this.normalMeshCache.request()
const constraintNormal = constraint.ni
const body = bi
line.scale.set(constraintNormal.x, constraintNormal.y, constraintNormal.z)
makeSureNotZero(line.scale)
line.position.copy(body.position)
constraint.ri.vadd(line.position, line.position)
}
}
this.normalMeshCache.hideCached()
// Frame axes for each body
this.axesMeshCache.restart()
if (this.settings.axes) {
for (let i = 0; i < this.bodies.length; i++) {
const body = this.bodies[i]
const mesh = this.axesMeshCache.request()
mesh.position.copy(body.position)
mesh.quaternion.copy(body.quaternion)
}
}
this.axesMeshCache.hideCached()
// AABBs
this.bboxMeshCache.restart()
if (this.settings.aabbs) {
for (let i = 0; i < this.bodies.length; i++) {
const body = this.bodies[i]
if (body.computeAABB) {
if (body.aabbNeedsUpdate) {
body.computeAABB()
}
// Todo: cap the infinite AABB to scene AABB, for now just dont render
if (
isFinite(body.aabb.lowerBound.x) &&
isFinite(body.aabb.lowerBound.y) &&
isFinite(body.aabb.lowerBound.z) &&
isFinite(body.aabb.upperBound.x) &&
isFinite(body.aabb.upperBound.y) &&
isFinite(body.aabb.upperBound.z) &&
body.aabb.lowerBound.x - body.aabb.upperBound.x != 0 &&
body.aabb.lowerBound.y - body.aabb.upperBound.y != 0 &&
body.aabb.lowerBound.z - body.aabb.upperBound.z != 0
) {
const mesh = this.bboxMeshCache.request()
mesh.scale.set(
body.aabb.lowerBound.x - body.aabb.upperBound.x,
body.aabb.lowerBound.y - body.aabb.upperBound.y,
body.aabb.lowerBound.z - body.aabb.upperBound.z
)
mesh.position.set(
(body.aabb.lowerBound.x + body.aabb.upperBound.x) * 0.5,
(body.aabb.lowerBound.y + body.aabb.upperBound.y) * 0.5,
(body.aabb.lowerBound.z + body.aabb.upperBound.z) * 0.5
)
}
}
}
}
this.bboxMeshCache.hideCached()
}
changeScene = (n) => {
this.dispatchEvent({ type: 'destroy' })
// unbind all listeners
Object.keys(this.listeners).forEach((event) => {
this.listeners[event].forEach((callback) => {
this.removeEventListener(event, callback)
})
})
this.listeners = {}
// clear any previous logs
console.clear()
this.settings.paused = false
this.updateGui()
this.buildScene(n)
}
start = () => {
this.buildScene(0)
}
buildScene = (n) => {
// Remove current bodies
this.bodies.forEach((body) => this.world.removeBody(body))
// Remove all visuals
this.removeAllVisuals()
// Remove all constraints
while (this.world.constraints.length) {
this.world.removeConstraint(this.world.constraints[0])
}
// Run the user defined "build scene" function
this.scenes[n]()
// Read the newly set data to the gui
this.settings.iterations = this.world.solver.iterations
this.settings.gx = this.world.gravity.x + 0.0
this.settings.gy = this.world.gravity.y + 0.0
this.settings.gz = this.world.gravity.z + 0.0
this.settings.quatNormalizeSkip = this.world.quatNormalizeSkip
this.settings.quatNormalizeFast = this.world.quatNormalizeFast
this.updateGui()
this.restartGeometryCaches()
}
initGeometryCaches = () => {
// Material
this.materialColor = 0xdddddd
this.solidMaterial = new THREE.MeshLambertMaterial({ color: this.materialColor })
//THREE.ColorUtils.adjustHSV( solidMaterial.color, 0, 0, 0.9 );
this.wireframeMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, wireframe: true })
this.currentMaterial = this.solidMaterial
const contactDotMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff })
this.particleMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 })
this.triggerMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true })
const contactPointGeometry = new THREE.SphereGeometry(0.1, 6, 6)
this.contactMeshCache = new GeometryCache(this.scene, () => {
return new THREE.Mesh(contactPointGeometry, contactDotMaterial)
})
this.cm2contactMeshCache = new GeometryCache(this.scene, () => {
const geometry = new THREE.Geometry()
geometry.vertices.push(new THREE.Vector3(0, 0, 0))
geometry.vertices.push(new THREE.Vector3(1, 1, 1))
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xff0000 }))
})
const bboxGeometry = new THREE.BoxGeometry(1, 1, 1)
const bboxMaterial = new THREE.MeshBasicMaterial({
color: this.materialColor,
wireframe: true,
})
this.bboxMeshCache = new GeometryCache(this.scene, () => {
return new THREE.Mesh(bboxGeometry, bboxMaterial)
})
this.distanceConstraintMeshCache = new GeometryCache(this.scene, () => {
const geometry = new THREE.Geometry()
geometry.vertices.push(new THREE.Vector3(0, 0, 0))
geometry.vertices.push(new THREE.Vector3(1, 1, 1))
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xff0000 }))
})
this.p2pConstraintMeshCache = new GeometryCache(this.scene, () => {
const geometry = new THREE.Geometry()
geometry.vertices.push(new THREE.Vector3(0, 0, 0))
geometry.vertices.push(new THREE.Vector3(1, 1, 1))
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0xff0000 }))
})
this.normalMeshCache = new GeometryCache(this.scene, () => {
const geometry = new THREE.Geometry()
geometry.vertices.push(new THREE.Vector3(0, 0, 0))
geometry.vertices.push(new THREE.Vector3(1, 1, 1))
return new THREE.Line(geometry, new THREE.LineBasicMaterial({ color: 0x00ff00 }))
})
this.axesMeshCache = new GeometryCache(this.scene, () => {
const mesh = new THREE.Object3D()
const origin = new THREE.Vector3(0, 0, 0)
const gX = new THREE.Geometry()
const gY = new THREE.Geometry()
const gZ = new THREE.Geometry()
gX.vertices.push(origin)
gY.vertices.push(origin)
gZ.vertices.push(origin)
gX.vertices.push(new THREE.Vector3(1, 0, 0))
gY.vertices.push(new THREE.Vector3(0, 1, 0))
gZ.vertices.push(new THREE.Vector3(0, 0, 1))
const lineX = new THREE.Line(gX, new THREE.LineBasicMaterial({ color: 0xff0000 }))
const lineY = new THREE.Line(gY, new THREE.LineBasicMaterial({ color: 0x00ff00 }))
const lineZ = new THREE.Line(gZ, new THREE.LineBasicMaterial({ color: 0x0000ff }))
mesh.add(lineX)
mesh.add(lineY)
mesh.add(lineZ)
return mesh
})
}
restartGeometryCaches = () => {
this.contactMeshCache.restart()
this.contactMeshCache.hideCached()
this.cm2contactMeshCache.restart()
this.cm2contactMeshCache.hideCached()
this.distanceConstraintMeshCache.restart()
this.distanceConstraintMeshCache.hideCached()
this.normalMeshCache.restart()
this.normalMeshCache.hideCached()
}
animate = () => {
requestAnimationFrame(this.animate)
if (!this.settings.paused) {
this.updatePhysics()
this.updateVisuals()
}
this.controls.update()
this.renderer.render(this.scene, this.camera)
this.stats.update()
}
lastCallTime = 0
resetCallTime = false
updatePhysics = () => {
// Step world
const timeStep = 1 / this.settings.stepFrequency
const now = performance.now() / 1000
if (!this.lastCallTime) {
// last call time not saved, cant guess elapsed time. Take a simple step.
this.world.step(timeStep)
this.lastCallTime = now
return
}
let timeSinceLastCall = now - this.lastCallTime
if (this.resetCallTime) {
timeSinceLastCall = 0
this.resetCallTime = false
}
this.world.step(timeStep, timeSinceLastCall, this.settings.maxSubSteps)
this.lastCallTime = now
}
resize = () => {
this.camera.aspect = window.innerWidth / window.innerHeight
this.camera.updateProjectionMatrix()
this.renderer.setSize(window.innerWidth, window.innerHeight)
}
onKeyPress = (event) => {
switch (event.code) {
case 'Space': // Space - restart
this.restartCurrentScene()
break
case 'KeyH': // h - toggle widgets
if (this.stats.domElement.style.display == 'none') {
this.stats.domElement.style.display = 'block'
info.style.display = 'block'
} else {
this.stats.domElement.style.display = 'none'
info.style.display = 'none'
}
break
case 'KeyA': // a - AABBs
this.settings.aabbs = !this.settings.aabbs
this.updateGui()
break
case 'KeyC': // c - constraints
this.settings.constraints = !this.settings.constraints
this.updateGui()
break
case 'KeyP': // p - pause
this.settings.paused = !this.settings.paused
this.resetCallTime = true
this.updateGui()
break
case 'KeyS': // s - step
const timeStep = 1 / this.settings.stepFrequency
this.world.step(timeStep)
this.updateVisuals()
break
case 'KeyM': // m - toggle materials
const index = this.renderModes.indexOf(this.settings.rendermode)
index++
index = index % this.this.renderModes.length // begin at 0 if we exceeded number of modes
this.setRenderMode(this.renderModes[index])
this.updateGui()
break
case 'Digit1':
case 'Digit2':
case 'Digit3':
case 'Digit4':
case 'Digit5':
case 'Digit6':
case 'Digit7':
case 'Digit8':
case 'Digit9':
// Change scene
const digit = Number(event.code.slice(-1)) - 1
// Only if scene is present and if no input field is active
if (this.scenes.length > digit && !document.activeElement.localName.match(/input/)) {
this.changeScene(digit)
}
break
}
}
initThree = () => {
// Camera
this.camera = new THREE.PerspectiveCamera(24, window.innerWidth / window.innerHeight, 5, 2000)
this.camera.position.set(0, 20, 30)
this.camera.lookAt(0, 0, 0)
// Scene
this.scene = new THREE.Scene()
this.scene.fog = new THREE.Fog(0x222222, 1000, 2000)
// Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true })
this.renderer.setSize(window.innerWidth, window.innerHeight)
document.body.appendChild(this.renderer.domElement)
this.renderer.setClearColor(this.scene.fog.color, 1)
this.renderer.shadowMap.enabled = true
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap
// Lights
this.ambientLight = new THREE.AmbientLight(0xffffff, 0.1)
this.scene.add(this.ambientLight)
this.spotLight = new THREE.SpotLight(0xffffff, 0.9, 0, Math.PI / 8, 1)
this.spotLight.position.set(-30, 40, 30)
this.spotLight.target.position.set(0, 0, 0)
this.spotLight.castShadow = true
this.spotLight.shadow.camera.near = 10
this.spotLight.shadow.camera.far = 100
this.spotLight.shadow.camera.fov = 30
// spotLight.shadow.bias = -0.0001
this.spotLight.shadow.mapSize.width = 2048
this.spotLight.shadow.mapSize.height = 2048
this.scene.add(this.spotLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.15)
directionalLight.position.set(-30, 40, 30)
directionalLight.target.position.set(0, 0, 0)
this.scene.add(directionalLight)
// Orbit controls
this.controls = new OrbitControls(this.camera, this.renderer.domElement)
this.controls.rotateSpeed = 1.0
this.controls.zoomSpeed = 1.2
this.controls.enableDamping = true
this.controls.enablePan = false
this.controls.dampingFactor = 0.2
this.controls.minDistance = 10
this.controls.maxDistance = 500
}
initSmoothie = () => {
this.smoothieCanvas = document.createElement('canvas')
this.smoothieCanvas.width = window.innerWidth
this.smoothieCanvas.height = window.innerHeight
this.smoothieCanvas.style.opacity = 0.5
this.smoothieCanvas.style.position = 'absolute'
this.smoothieCanvas.style.top = '0px'
this.smoothieCanvas.style.zIndex = 1
document.body.appendChild(this.smoothieCanvas)
this.smoothie = new SmoothieChart({
labelOffsetY: 50,
maxDataSetLength: 100,
millisPerPixel: 2,
grid: {
strokeStyle: 'none',
fillStyle: 'none',
lineWidth: 1,
millisPerLine: 250,
verticalSections: 6,
},
labels: {
fillStyle: 'rgb(180, 180, 180)',
},
})
this.smoothie.streamTo(this.smoothieCanvas)
// Create time series for each profile label
const lines = {}
const colors = [
[255, 0, 0],
[0, 255, 0],
[0, 0, 255],
[255, 255, 0],
[255, 0, 255],
[0, 255, 255],
]
Object.keys(this.world.profile).forEach((label, i) => {
const color = colors[i % colors.length]
lines[label] = new TimeSeries({
label,
fillStyle: `rgb(${color[0]},${color[1]},${color[2]})`,
maxDataLength: 500,
})
})
// Add a random value to each line every second
this.world.addEventListener('postStep', () => {
Object.keys(this.world.profile).forEach((label) => {
lines[label].append(this.world.time * 1000, this.world.profile[label])
})
})
// Add to SmoothieChart
Object.keys(this.world.profile).forEach((label, i) => {
const color = colors[i % colors.length]
this.smoothie.addTimeSeries(lines[label], {
strokeStyle: `rgb(${color[0]},${color[1]},${color[2]})`,
lineWidth: 2,
})
})
// Pause it
this.world.doProfiling = false
this.smoothie.stop()
this.smoothieCanvas.style.display = 'none'
}
setGlobalSpookParams(k, d, h) {
// Set for all constraints
for (let i = 0; i < this.world.constraints.length; i++) {
const constraint = this.world.constraints[i]
for (let j = 0; j < constraint.equations.length; j++) {
const equation = constraint.equations[j]
equation.setSpookParams(k, d, h)
}
}
// Set for all contact materals
for (let i = 0; i < this.world.contactmaterials.length; i++) {
const contactMaterial = this.world.contactmaterials[i]
contactMaterial.contactEquationStiffness = k
contactMaterial.frictionEquationStiffness = k
contactMaterial.contactEquationRelaxation = d
contactMaterial.frictionEquationRelaxation = d
}
this.world.defaultContactMaterial.contactEquationStiffness = k
this.world.defaultContactMaterial.frictionEquationStiffness = k
this.world.defaultContactMaterial.contactEquationRelaxation = d
this.world.defaultContactMaterial.frictionEquationRelaxation = d
}
getWorld() {
return this.world
}
addVisual(body) {
if (!(body instanceof CANNON.Body)) {
throw new Error('The argument passed to addVisual() is not a body')
}
// if it's a particle paint it red, if it's a trigger paint it as green, otherwise just gray
const isParticle = body.shapes.every((s) => s instanceof CANNON.Particle)
const material = isParticle ? this.particleMaterial : body.isTrigger ? this.triggerMaterial : this.currentMaterial
// get the correspondant three.js mesh
const mesh = bodyToMesh(body, material)
// enable shadows on every object
mesh.traverse((child) => {
child.castShadow = true
child.receiveShadow = true
})
this.bodies.push(body)
this.visuals.push(mesh)
this.scene.add(mesh)
}
addVisuals(bodies) {
bodies.forEach((body) => {
this.addVisual(body)
})
}
addVisualsInstanced(bodies) {
if (
!Array.isArray(bodies) ||
!bodies.every((body) => body instanceof CANNON.Body && body.type === bodies[0].type)
) {
throw new Error('The argument passed to addVisualsInstanced() is not an array of bodies of the same type')
}
// all bodies are the same, so pick the first
const body = bodies[0]
// if it's a particle paint it red, otherwise just gray
const material = body.shapes.every((s) => s instanceof CANNON.Particle)
? this.particleMaterial
: this.currentMaterial
// get the three.js mesh correspondant of the first body since they're of the same type
const meshGroup = bodyToMesh(body, material)
// extract the mesh from the group
let mesh
meshGroup.traverse((child) => {
if (child.isMesh) mesh = child
})
// the clone is there because of this issue
// https://github.com/mrdoob/three.js/issues/17701
const instancedMesh = new THREE.InstancedMesh(mesh.geometry.clone(), mesh.material.clone(), bodies.length)
instancedMesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage) // will be updated every frame
instancedMesh.receiveShadow = true
instancedMesh.castShadow = true
// Add bodies
bodies.forEach((body, i) => {
this.bodies.push(body)
this.visuals.push(instancedMesh)
body.instanceIndex = i
})
this.scene.add(instancedMesh)
}
removeVisual(body) {
const index = this.bodies.findIndex((b) => b.id === body.id)
if (index === -1) {
return
}
const visual = this.visuals[index]
this.bodies.splice(index, 1)
this.visuals.splice(index, 1)
this.scene.remove(visual)
}
removeAllVisuals() {
while (this.bodies.length) {
this.removeVisual(this.bodies[0])
}
}
addEventListener(event, callback) {
if (this.listeners[event]) {
this.listeners[event].push(callback)
} else {
this.listeners[event] = [callback]
}
super.addEventListener(event, callback)
}
}
class GeometryCache {
geometries = []
gone = []
constructor(scene, createFunc) {
this.scene = scene
this.createFunc = createFunc
}
request = () => {
const geometry = this.geometries.length > 0 ? this.geometries.pop() : this.createFunc()
this.scene.add(geometry)
this.gone.push(geometry)
return geometry
}
restart = () => {
while (this.gone.length) {
this.geometries.push(this.gone.pop())
}
}
hideCached = () => {
this.geometries.forEach((geometry) => {
this.scene.remove(geometry)
})
}
}
function makeSureNotZero(vector) {
if (vector.x === 0) {
vector.x = 1e-6
}
if (vector.y === 0) {
vector.y = 1e-6
}
if (vector.z === 0) {
vector.z = 1e-6
}
}
export { Demo }