UNPKG

cannon-es-control

Version:

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

355 lines (288 loc) 11.8 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>cannon.js + three.js - physics shooter</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" /> <style> #instructions { position: fixed; left: 0; top: 0; width: 100%; height: 100%; display: flex; flex-direction: column; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.5); color: #ffffff; text-align: center; cursor: pointer; } #instructions span { font-size: 40px; } </style> <script type="module"> import { addTitle, addSourceButton } from './js/dom-utils.js' addTitle() addSourceButton() </script> </head> <body> <div id="instructions"> <span>Click to play</span> <br /> (W,A,S,D = Move, SPACE = Jump, MOUSE = Look, CLICK = Shoot) </div> <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' import { PointerLockControlsCannon } from './js/PointerLockControlsCannon.js' /** * Example of a really barebones version of a fps game. */ // three.js variables let camera, scene, renderer, stats let material // cannon.js variables let world let controls const timeStep = 1 / 60 let lastCallTime = performance.now() let sphereShape let sphereBody let physicsMaterial const balls = [] const ballMeshes = [] const boxes = [] const boxMeshes = [] const instructions = document.getElementById('instructions') initThree() initCannon() initPointerLock() animate() function initThree() { // Camera camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000) // Scene scene = new THREE.Scene() scene.fog = new THREE.Fog(0x000000, 0, 500) // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setSize(window.innerWidth, window.innerHeight) renderer.setClearColor(scene.fog.color) 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(0xffffff, 0.1) scene.add(ambientLight) const spotlight = new THREE.SpotLight(0xffffff, 0.9, 0, Math.PI / 4, 1) spotlight.position.set(10, 30, 20) spotlight.target.position.set(0, 0, 0) spotlight.castShadow = true spotlight.shadow.camera.near = 10 spotlight.shadow.camera.far = 100 spotlight.shadow.camera.fov = 30 // spotlight.shadow.bias = -0.0001 spotlight.shadow.mapSize.width = 2048 spotlight.shadow.mapSize.height = 2048 scene.add(spotlight) // Generic material material = new THREE.MeshLambertMaterial({ color: 0xdddddd }) // Floor const floorGeometry = new THREE.PlaneBufferGeometry(300, 300, 100, 100) floorGeometry.rotateX(-Math.PI / 2) const floor = new THREE.Mesh(floorGeometry, material) floor.receiveShadow = true scene.add(floor) window.addEventListener('resize', onWindowResize) } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) } function initCannon() { world = new CANNON.World() // Tweak contact properties. // Contact stiffness - use to make softer/harder contacts world.defaultContactMaterial.contactEquationStiffness = 1e9 // Stabilization time in number of timesteps world.defaultContactMaterial.contactEquationRelaxation = 4 const solver = new CANNON.GSSolver() solver.iterations = 7 solver.tolerance = 0.1 world.solver = new CANNON.SplitSolver(solver) // use this to test non-split solver // world.solver = solver world.gravity.set(0, -20, 0) // Create a slippery material (friction coefficient = 0.0) physicsMaterial = new CANNON.Material('physics') const physics_physics = new CANNON.ContactMaterial(physicsMaterial, physicsMaterial, { friction: 0.0, restitution: 0.3, }) // We must add the contact materials to the world world.addContactMaterial(physics_physics) // Create the user collision sphere const radius = 1.3 sphereShape = new CANNON.Sphere(radius) sphereBody = new CANNON.Body({ mass: 5, material: physicsMaterial }) sphereBody.addShape(sphereShape) sphereBody.position.set(0, 5, 0) sphereBody.linearDamping = 0.9 world.addBody(sphereBody) // Create the ground plane const groundShape = new CANNON.Plane() const groundBody = new CANNON.Body({ mass: 0, material: physicsMaterial }) groundBody.addShape(groundShape) groundBody.quaternion.setFromEuler(-Math.PI / 2, 0, 0) world.addBody(groundBody) // Add boxes both in cannon.js and three.js const halfExtents = new CANNON.Vec3(1, 1, 1) const boxShape = new CANNON.Box(halfExtents) const boxGeometry = new THREE.BoxBufferGeometry(halfExtents.x * 2, halfExtents.y * 2, halfExtents.z * 2) for (let i = 0; i < 7; i++) { const boxBody = new CANNON.Body({ mass: 5 }) boxBody.addShape(boxShape) const boxMesh = new THREE.Mesh(boxGeometry, material) const x = (Math.random() - 0.5) * 20 const y = (Math.random() - 0.5) * 1 + 1 const z = (Math.random() - 0.5) * 20 boxBody.position.set(x, y, z) boxMesh.position.copy(boxBody.position) boxMesh.castShadow = true boxMesh.receiveShadow = true world.addBody(boxBody) scene.add(boxMesh) boxes.push(boxBody) boxMeshes.push(boxMesh) } // Add linked boxes const size = 0.5 const mass = 0.3 const space = 0.1 * size const N = 5 const halfExtents2 = new CANNON.Vec3(size, size, size * 0.1) const boxShape2 = new CANNON.Box(halfExtents2) const boxGeometry2 = new THREE.BoxBufferGeometry(halfExtents2.x * 2, halfExtents2.y * 2, halfExtents2.z * 2) let last for (let i = 0; i < N; i++) { // Make the fist one static to support the others const boxBody = new CANNON.Body({ mass: i === 0 ? 0 : mass }) boxBody.addShape(boxShape2) const boxMesh = new THREE.Mesh(boxGeometry2, material) boxBody.position.set(5, (N - i) * (size * 2 + 2 * space) + size * 2 + space, 0) boxBody.linearDamping = 0.01 boxBody.angularDamping = 0.01 boxMesh.castShadow = true boxMesh.receiveShadow = true world.addBody(boxBody) scene.add(boxMesh) boxes.push(boxBody) boxMeshes.push(boxMesh) if (i > 0) { // Connect the body to the last one const constraint1 = new CANNON.PointToPointConstraint( boxBody, new CANNON.Vec3(-size, size + space, 0), last, new CANNON.Vec3(-size, -size - space, 0) ) const constranit2 = new CANNON.PointToPointConstraint( boxBody, new CANNON.Vec3(size, size + space, 0), last, new CANNON.Vec3(size, -size - space, 0) ) world.addConstraint(constraint1) world.addConstraint(constranit2) } last = boxBody } // The shooting balls const shootVelocity = 15 const ballShape = new CANNON.Sphere(0.2) const ballGeometry = new THREE.SphereBufferGeometry(ballShape.radius, 32, 32) // Returns a vector pointing the the diretion the camera is at function getShootDirection() { const vector = new THREE.Vector3(0, 0, 1) vector.unproject(camera) const ray = new THREE.Ray(sphereBody.position, vector.sub(sphereBody.position).normalize()) return ray.direction } window.addEventListener('click', (event) => { if (!controls.enabled) { return } const ballBody = new CANNON.Body({ mass: 1 }) ballBody.addShape(ballShape) const ballMesh = new THREE.Mesh(ballGeometry, material) ballMesh.castShadow = true ballMesh.receiveShadow = true world.addBody(ballBody) scene.add(ballMesh) balls.push(ballBody) ballMeshes.push(ballMesh) const shootDirection = getShootDirection() ballBody.velocity.set( shootDirection.x * shootVelocity, shootDirection.y * shootVelocity, shootDirection.z * shootVelocity ) // Move the ball outside the player sphere const x = sphereBody.position.x + shootDirection.x * (sphereShape.radius * 1.02 + ballShape.radius) const y = sphereBody.position.y + shootDirection.y * (sphereShape.radius * 1.02 + ballShape.radius) const z = sphereBody.position.z + shootDirection.z * (sphereShape.radius * 1.02 + ballShape.radius) ballBody.position.set(x, y, z) ballMesh.position.copy(ballBody.position) }) } function initPointerLock() { controls = new PointerLockControlsCannon(camera, sphereBody) scene.add(controls.getObject()) instructions.addEventListener('click', () => { controls.lock() }) controls.addEventListener('lock', () => { controls.enabled = true instructions.style.display = 'none' }) controls.addEventListener('unlock', () => { controls.enabled = false instructions.style.display = null }) } function animate() { requestAnimationFrame(animate) const time = performance.now() / 1000 const dt = time - lastCallTime lastCallTime = time if (controls.enabled) { world.step(timeStep, dt) // Update ball positions for (let i = 0; i < balls.length; i++) { ballMeshes[i].position.copy(balls[i].position) ballMeshes[i].quaternion.copy(balls[i].quaternion) } // Update box positions for (let i = 0; i < boxes.length; i++) { boxMeshes[i].position.copy(boxes[i].position) boxMeshes[i].quaternion.copy(boxes[i].quaternion) } } controls.update(dt) renderer.render(scene, camera) stats.update() } </script> </body> </html>