UNPKG

cannon-es-control

Version:

A lightweight 3D physics engine written in JavaScript with control system tools

324 lines (257 loc) 10.8 kB
<!DOCTYPE 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>