UNPKG

cannon-es-control

Version:

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

255 lines (209 loc) 8.7 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>cannon.js + three.js - cloth simulation</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' import { OrbitControls } from 'https://unpkg.com/three@0.122.0/examples/jsm/controls/OrbitControls.js' /** * Example on how to build a simple physics cloth with * just structurals constraints. * More info here: * https://viscomp.alexandra.dk/?p=147 */ // Specify the simulation constants const timeStep = 1 / 60 let lastCallTime const clothMass = 1 // 1 kg in total const clothSize = 1 // 1 meter const Nx = 12 // number of horizontal particles in the cloth const Ny = 12 // number of vertical particles in the cloth const mass = (clothMass / Nx) * Ny const restDistance = clothSize / Nx const sphereSize = 0.1 const movementRadius = 0.2 // Parametric function // https://threejs.org/docs/index.html#api/en/geometries/ParametricGeometry function clothFunction(u, v, target) { const x = (u - 0.5) * restDistance * Nx const y = (v + 0.5) * restDistance * Ny const z = 0 target.set(x, y, z) return target } // three.js variables let camera, scene, renderer, stats let controls let clothGeometry let sphereMesh // cannon.js variables let world let sphereBody const particles = [] initCannon() initThree() animate() function initCannon() { world = new CANNON.World() world.gravity.set(0, -9.81, 0) // Max solver iterations: Use more for better force propagation, but keep in mind that it's not very computationally cheap! world.solver.iterations = 20 // Materials const clothMaterial = new CANNON.Material('cloth') const sphereMaterial = new CANNON.Material('sphere') const cloth_sphere = new CANNON.ContactMaterial(clothMaterial, sphereMaterial, { friction: 0, restitution: 0, }) // Adjust constraint equation parameters // Contact stiffness - use to make softer/harder contacts cloth_sphere.contactEquationStiffness = 1e9 // Stabilization time in number of timesteps cloth_sphere.contactEquationRelaxation = 3 // Add contact material to the world world.addContactMaterial(cloth_sphere) // Create sphere // Make it a little bigger than the three.js sphere // so the cloth doesn't clip thruogh const sphereShape = new CANNON.Sphere(sphereSize * 1.3) sphereBody = new CANNON.Body({ type: CANNON.Body.KINEMATIC, }) sphereBody.addShape(sphereShape) world.addBody(sphereBody) // Create cannon particles for (let i = 0; i < Nx + 1; i++) { particles.push([]) for (let j = 0; j < Ny + 1; j++) { const index = j * (Nx + 1) + i const point = clothFunction(i / (Nx + 1), j / (Ny + 1), new THREE.Vector3()) const particle = new CANNON.Body({ // Fix in place the first row mass: j === Ny ? 0 : mass, }) particle.addShape(new CANNON.Particle()) particle.linearDamping = 0.5 particle.position.set(point.x, point.y - Ny * 0.9 * restDistance, point.z) particle.velocity.set(0, 0, -0.1 * (Ny - j)) particles[i].push(particle) world.addBody(particle) } } // Connect the particles with distance constraints function connect(i1, j1, i2, j2) { world.addConstraint(new CANNON.DistanceConstraint(particles[i1][j1], particles[i2][j2], restDistance)) } for (let i = 0; i < Nx + 1; i++) { for (let j = 0; j < Ny + 1; j++) { if (i < Nx) connect(i, j, i + 1, j) if (j < Ny) connect(i, j, i, j + 1) } } } function initThree() { // Camera camera = new THREE.PerspectiveCamera(30, window.innerWidth / window.innerHeight, 0.5, 10000) camera.position.set(Math.cos(Math.PI / 4) * 3, 0, Math.sin(Math.PI / 4) * 3) // Scene scene = new THREE.Scene() scene.fog = new THREE.Fog(0x000000, 500, 10000) // Renderer renderer = new THREE.WebGLRenderer({ antialias: true }) renderer.setSize(window.innerWidth, window.innerHeight) renderer.setClearColor(scene.fog.color) renderer.outputEncoding = THREE.sRGBEncoding document.body.appendChild(renderer.domElement) // Stats.js stats = new Stats() document.body.appendChild(stats.dom) // Orbit controls controls = new OrbitControls(camera, renderer.domElement) controls.enableDamping = true controls.enablePan = false controls.dampingFactor = 0.3 controls.minDistance = 1 controls.maxDistance = 5 // Lights const ambientLight = new THREE.AmbientLight(0xffffff, 0.4) scene.add(ambientLight) const directionalLight = new THREE.DirectionalLight(0xffffff, 1.75) directionalLight.position.set(5, 5, 5) scene.add(directionalLight) // Cloth material const clothTexture = new THREE.TextureLoader().load('images/sunflower.jpg') clothTexture.wrapS = THREE.RepeatWrapping clothTexture.wrapT = THREE.RepeatWrapping clothTexture.anisotropy = 16 clothTexture.encoding = THREE.sRGBEncoding const clothMaterial = new THREE.MeshPhongMaterial({ map: clothTexture, side: THREE.DoubleSide, }) // Cloth geometry clothGeometry = new THREE.ParametricGeometry(clothFunction, Nx, Ny) // Cloth mesh const clothMesh = new THREE.Mesh(clothGeometry, clothMaterial) scene.add(clothMesh) // Sphere const sphereGeometry = new THREE.SphereGeometry(sphereSize, 20, 20) const sphereMaterial = new THREE.MeshPhongMaterial({ color: 0x888888 }) sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial) scene.add(sphereMesh) window.addEventListener('resize', onWindowResize) } function onWindowResize() { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) } function animate() { requestAnimationFrame(animate) controls.update() updatePhysics() render() stats.update() } // Step the physics world function updatePhysics() { const time = performance.now() / 1000 if (!lastCallTime) { world.step(timeStep) } else { const dt = time - lastCallTime world.step(timeStep, dt) } lastCallTime = time } function render() { // Make the three.js cloth follow the cannon.js particles for (let i = 0; i < Nx + 1; i++) { for (let j = 0; j < Ny + 1; j++) { const index = j * (Nx + 1) + i clothGeometry.vertices[index].copy(particles[i][j].position) } } clothGeometry.normalsNeedUpdate = true clothGeometry.verticesNeedUpdate = true // Move the ball in a circular motion const { time } = world sphereBody.position.set(movementRadius * Math.sin(time), 0, movementRadius * Math.cos(time)) // Make the three.js ball follow the cannon.js one // Copying quaternion is not needed since it's a sphere sphereMesh.position.copy(sphereBody.position) renderer.render(scene, camera) } </script> </body> </html>