cannon-es-control
Version:
A lightweight 3D physics engine written in JavaScript with control system tools
324 lines (257 loc) • 10.8 kB
HTML
<html lang="en">
<head>
<meta charset="utf-8" />
<title>cannon.js + three.js - mouse pick</title>
<link rel="stylesheet" href="css/style.css" type="text/css" />
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0" />
<script type="module">
import { addTitle, addSourceButton } from './js/dom-utils.js'
addTitle()
addSourceButton()
</script>
</head>
<body>
<script type="module">
import * as CANNON from '../dist/cannon-es-control.js'
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'
/**
* Example of a pickable object.
*/
// three.js variables
let camera, scene, renderer, stats
let movementPlane
let clickMarker
let raycaster
let cubeMesh
// cannon.js variables
let world
const timeStep = 1 / 60
let lastCallTime
let jointBody
let jointConstraint
let cubeBody
let isDragging = false
// To be synced
const meshes = []
const bodies = []
initThree()
initCannon()
animate()
function initThree() {
// Camera
camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.5, 1000)
camera.position.set(0, 2, 10)
// Scene
scene = new THREE.Scene()
scene.fog = new THREE.Fog(0x000000, 500, 1000)
// Renderer
renderer = new THREE.WebGLRenderer({ antialias: true })
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.setClearColor(scene.fog.color)
renderer.outputEncoding = THREE.sRGBEncoding
renderer.shadowMap.enabled = true
renderer.shadowMap.type = THREE.PCFSoftShadowMap
document.body.appendChild(renderer.domElement)
// Stats.js
stats = new Stats()
document.body.appendChild(stats.dom)
// Lights
const ambientLight = new THREE.AmbientLight(0x666666)
scene.add(ambientLight)
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2)
const distance = 20
directionalLight.position.set(-distance, distance, distance)
directionalLight.castShadow = true
directionalLight.shadow.mapSize.width = 2048
directionalLight.shadow.mapSize.height = 2048
directionalLight.shadow.camera.left = -distance
directionalLight.shadow.camera.right = distance
directionalLight.shadow.camera.top = distance
directionalLight.shadow.camera.bottom = -distance
directionalLight.shadow.camera.far = 3 * distance
directionalLight.shadow.camera.near = distance
scene.add(directionalLight)
// Raycaster for mouse interaction
raycaster = new THREE.Raycaster()
// Floor
const floorGeometry = new THREE.PlaneBufferGeometry(100, 100, 1, 1)
floorGeometry.rotateX(-Math.PI / 2)
const floorMaterial = new THREE.MeshLambertMaterial({ color: 0x777777 })
const floor = new THREE.Mesh(floorGeometry, floorMaterial)
floor.receiveShadow = true
scene.add(floor)
// Click marker to be shown on interaction
const markerGeometry = new THREE.SphereBufferGeometry(0.2, 8, 8)
const markerMaterial = new THREE.MeshLambertMaterial({ color: 0xff0000 })
clickMarker = new THREE.Mesh(markerGeometry, markerMaterial)
clickMarker.visible = false // Hide it..
scene.add(clickMarker)
// Cube
const cubeGeometry = new THREE.BoxBufferGeometry(1, 1, 1, 10, 10)
const cubeMaterial = new THREE.MeshPhongMaterial({ color: 0x999999 })
cubeMesh = new THREE.Mesh(cubeGeometry, cubeMaterial)
cubeMesh.castShadow = true
meshes.push(cubeMesh)
scene.add(cubeMesh)
// Movement plane when dragging
const planeGeometry = new THREE.PlaneBufferGeometry(100, 100)
movementPlane = new THREE.Mesh(planeGeometry, floorMaterial)
movementPlane.visible = false // Hide it..
scene.add(movementPlane)
window.addEventListener('resize', onWindowResize)
}
function onWindowResize() {
camera.aspect = window.innerWidth / window.innerHeight
camera.updateProjectionMatrix()
renderer.setSize(window.innerWidth, window.innerHeight)
}
function initCannon() {
// Setup world
world = new CANNON.World()
world.gravity.set(0, -10, 0)
// Floor
const floorShape = new CANNON.Plane()
const floorBody = new CANNON.Body({ mass: 0 })
floorBody.addShape(floorShape)
floorBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0)
world.addBody(floorBody)
// Cube body
const cubeShape = new CANNON.Box(new CANNON.Vec3(0.5, 0.5, 0.5))
cubeBody = new CANNON.Body({ mass: 5 })
cubeBody.addShape(cubeShape)
cubeBody.position.set(0, 5, 0)
bodies.push(cubeBody)
world.addBody(cubeBody)
// Joint body, to later constraint the cube
const jointShape = new CANNON.Sphere(0.1)
jointBody = new CANNON.Body({ mass: 0 })
jointBody.addShape(jointShape)
jointBody.collisionFilterGroup = 0
jointBody.collisionFilterMask = 0
world.addBody(jointBody)
}
window.addEventListener('pointerdown', (event) => {
// Cast a ray from where the mouse is pointing and
// see if we hit something
const hitPoint = getHitPoint(event.clientX, event.clientY, cubeMesh, camera)
// Return if the cube wasn't hit
if (!hitPoint) {
return
}
// Move marker mesh on contact point
showClickMarker()
moveClickMarker(hitPoint)
// Move the movement plane on the z-plane of the hit
moveMovementPlane(hitPoint, camera)
// Create the constraint between the cube body and the joint body
addJointConstraint(hitPoint, cubeBody)
// Set the flag to trigger pointermove on next frame so the
// movementPlane has had time to move
requestAnimationFrame(() => {
isDragging = true
})
})
window.addEventListener('pointermove', (event) => {
if (!isDragging) {
return
}
// Project the mouse onto the movement plane
const hitPoint = getHitPoint(event.clientX, event.clientY, movementPlane, camera)
if (hitPoint) {
// Move marker mesh on the contact point
moveClickMarker(hitPoint)
// Move the cannon constraint on the contact point
moveJoint(hitPoint)
}
})
window.addEventListener('pointerup', () => {
isDragging = false
// Hide the marker mesh
hideClickMarker()
// Remove the mouse constraint from the world
removeJointConstraint()
})
function showClickMarker() {
clickMarker.visible = true
}
function moveClickMarker(position) {
clickMarker.position.copy(position)
}
function hideClickMarker() {
clickMarker.visible = false
}
// This function moves the virtual movement plane for the mouseJoint to move in
function moveMovementPlane(point, camera) {
// Center at mouse position
movementPlane.position.copy(point)
// Make it face toward the camera
movementPlane.quaternion.copy(camera.quaternion)
}
// Returns an hit point if there's a hit with the mesh,
// otherwise returns undefined
function getHitPoint(clientX, clientY, mesh, camera) {
// Get 3D point form the client x y
const mouse = new THREE.Vector2()
mouse.x = (clientX / window.innerWidth) * 2 - 1
mouse.y = -((clientY / window.innerHeight) * 2 - 1)
// Get the picking ray from the point
raycaster.setFromCamera(mouse, camera)
// Find out if there's a hit
const hits = raycaster.intersectObject(mesh)
// Return the closest hit or undefined
return hits.length > 0 ? hits[0].point : undefined
}
// Add a constraint between the cube and the jointBody
// in the initeraction position
function addJointConstraint(position, constrainedBody) {
// Vector that goes from the body to the clicked point
const vector = new CANNON.Vec3().copy(position).vsub(constrainedBody.position)
// Apply anti-quaternion to vector to tranform it into the local body coordinate system
const antiRotation = constrainedBody.quaternion.inverse()
const pivot = antiRotation.vmult(vector) // pivot is not in local body coordinates
// Move the cannon click marker body to the click position
jointBody.position.copy(position)
// Create a new constraint
// The pivot for the jointBody is zero
jointConstraint = new CANNON.PointToPointConstraint(constrainedBody, pivot, jointBody, new CANNON.Vec3(0, 0, 0))
// Add the constraint to world
world.addConstraint(jointConstraint)
}
// This functions moves the joint body to a new postion in space
// and updates the constraint
function moveJoint(position) {
jointBody.position.copy(position)
jointConstraint.update()
}
// Remove constraint from world
function removeJointConstraint() {
world.removeConstraint(jointConstraint)
jointConstraint = undefined
}
function animate() {
requestAnimationFrame(animate)
// Step the physics world
updatePhysics()
// Sync the three.js meshes with the bodies
for (let i = 0; i !== meshes.length; i++) {
meshes[i].position.copy(bodies[i].position)
meshes[i].quaternion.copy(bodies[i].quaternion)
}
renderer.render(scene, camera)
stats.update()
}
function updatePhysics() {
const time = performance.now() / 1000
if (!lastCallTime) {
world.step(timeStep)
} else {
const dt = time - lastCallTime
world.step(timeStep, dt)
}
lastCallTime = time
}
</script>
</body>
</html>