cannon-es-control
Version:
A lightweight 3D physics engine written in JavaScript with control system tools
355 lines (288 loc) • 11.8 kB
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>