Cannon.js ist eine Physik-Engine mit der du realistische und komplexe Physik-Interaktionen in einer 3D-Szene simulieren kannst. Three.js ist eine JavaScript-Bibliothek mit der man diese 3D-Szene darstellen und im Webbrowser rendern kann. Die Kombination dieser beiden Tools bietet Entwicklern eine Plattform für interaktive 3D-Animationen und Grafiken.
Wenn du mehr zu den Basics von Three.js erfahren willst, wirst du in einem Gastbeitrag von mir fündig.
Eine der Stärken von Cannon.js ist, dass es eine Vielzahl von Kollisionsformen unterstützt. Mit dieser Funktion können Entwickler realistische Interaktionen zwischen Objekten erzeugen, die in einer 3D-Szene platziert sind. Zum Beispiel kann man ein einfaches Flipper-Spiel erstellen, bei dem der Ball von den Flippern abprallt und die Punkte gezählt werden.
Ein weiteres Beispiel ist die Erstellung von physikbasierten Animationen die auf realistischen Eigenschaften basieren. Zum Beispiel kann man ein Modell eines Autos erstellen und dann mit Cannon.js und Three.js eine Animation erstellen, bei der das Auto realistisch beschleunigt, bremst und in Kurven fährt.
Zusammenspiel von Three.js und Cannon.js
Warum benötigt es überhaupt zwei verschiedene Packages? Three.js ist eine Render Engine, d.h. es werden einfach nur 3D Objekte dargestellt. Dinge wie Schwerkraft und Kollisionen werden dabei nicht berücksichtigt.
Wenn man also einen Würfel und eine Fläche in die Three.js Welt setzt passiert erstmal gar nichts. Nichts fällt nach unten, denn es gibt überhaupt kein "unten".
Hier kommt Cannon.js ins Spiel. Diese Library berechnet die Position und Rotation aller Objekte in der Welt. Hierbei werden Kollision und Gravitation berücksichtigt.
Der Ablauf sieht also folgendermaßen aus:
- Die Welt wird initialisiert und von Three.js dargestellt.
- Cannon.js berechnet die Position und Rotation aller Objekte nach einem bestimmten Zeitschritt.
- Die neuen Positionen und Rotationen werden in der Three.js Welt geupdatet.
- Nächster Simulations-Schritt ab 2.
Tutorial: Springende Bälle
Als Einstieg in den Umgang mit Cannon.js möchte ich dir zeigen wie man einfache Objekte auf einer Fläche springen lassen kann.
Zur Installation der beiden Packages gibt es auf den jeweiligen Seiten (Three und Cannon) die Dateien zum Download.
Die Grundlage: Licht, Kamera, Action
Für jedes Three.js Projekt benötigt man drei Komponenten: Licht, Kamera und ein Renderer.
Das Licht bestimmt die Helligkeit der ganzen Szene. Die Kamera nimmt das Licht auf und der Renderer stellt diese Bild der Kamera dann im Browser dar.
//New scene
const scene = new THREE.Scene();
//Create the camera
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 16, 20);
camera.rotation.x = -1;
//Create two light sources
const light1 = new THREE.SpotLight();
light1.position.set(5, 10, 40);
light1.angle = Math.PI / 4;
light1.penumbra = 0.5;
light1.castShadow = true;
light1.shadow.camera.near = 0.5;
light1.shadow.camera.far = 200;
scene.add(light1);
const light2 = new THREE.SpotLight();
light2.position.set(-8, 10, 20);
light2.angle = Math.PI / 4;
light2.penumbra = 0.5;
light2.castShadow = true;
light2.shadow.camera.near = 0.5;
light2.shadow.camera.far = 200;
scene.add(light2);
// Create the renderer
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
Wir erstellen also eine Szene und eine Kamera, setzen die Position und fügen noch zwei Lichter hinzu.
Diese Lichter sind SpotLights, was man sich wie ein Theater-Scheinwerfer vorstellen kann. Mit 'position' und 'angle' platziert man die Lichter. Die Eigenschaft 'penumbra' bestimmt wie weich das Licht an den Rändern dargestellt wird. 0 bedeutet einen perfekt, scharf abgegrenzten Lichtkegel, was sehr unrealistisch ist. Daher wählen wir hier einen Wert von 0,5.
Die 'castShadow" Eigenschaft beschreibt ob das Objekt einen Schatten wirft und die beiden Werte 'shadow.camera.near' und 'shadow.camera.far' legen fest in welchen Abständen zur Kamera die Schatten dargestellt werden sollen.
Als letztes Element wird der Renderer erstellt und in das HTML Dokument eingefügt.
Die Cannon.js Welt
Wie oben schon beschrieben, bewegt sich in der Three.js Szene erstmal gar nichts (außer man erstellt Animationen). Man braucht eine Cannon.js Welt in der sich alle Objekte physik-basiert bewegen können.
// Create the physics world
let damping = 0.01;
const world = new CANNON.World();
world.gravity.set(0, -9.81, 0);
world.broadphase = new CANNON.NaiveBroadphase();
Hier definieren wir eine Dämpfung die später noch wichtig wird. Außerdem erstellen wir eine 'World' in der die Cannon.js Objekte leben.
Dieser Welt weisen wir eine Gravitation zu. Die Fallbeschleunigung auf der Erde beträgt im Durchschnitt 9,81 m/s2 (Der Wert ist tatsächlich nicht überall gleich - am Äquator würde die Waage etwas weniger anzeigen als an den Polen).
Die 'broadphase' wird zur Erkennung von Kollisionen benutzt. Nur über diese Collision Detection könnte man schon mehrere Blogbeiträge schreiben, daher die Kurzform: Im Hintergrund wird für jeden Simulations-Schritt berechnet ob sich die Objekte berühren. Das wird mit Bounding Boxes (sozusagen ein 3D-Rahmen um das Objekt) für alle Paare von Objekten durchgeführt.
Die Kugeln
Im aktuellen Stand würde man nach wie vor ein schwarzes Bild sehen, da es noch keine Objekte in der Szene gibt. Wir benutzen hier mehrere Kugeln mit verschiedenen Massen:
const normalMaterial = new THREE.MeshNormalMaterial();
const phongMaterial = new THREE.MeshPhongMaterial();
const sphereGeometry = new THREE.SphereGeometry(1, 32, 32);
let mat1 = new CANNON.Material();
let sphereShape = new CANNON.Sphere(1);
let sphereMeshes = [];
let sphereBodies = [];
for (let i = 0; i < 10; ++i) {
let sphereMesh = new THREE.Mesh(sphereGeometry, normalMaterial);
sphereMesh.position.x = -12 + i * 3;
sphereMesh.position.y = 3 + i * 0.5;
sphereMesh.castShadow = true;
scene.add(sphereMesh);
let sphereBody = new CANNON.Body({ mass: (i + 1) * 50, material: mat1 });
sphereBody.linearDamping = damping;
sphereBody.addShape(sphereShape);
sphereBody.position.x = sphereMesh.position.x;
sphereBody.position.y = sphereMesh.position.y;
sphereBody.position.z = sphereMesh.position.z;
world.addBody(sphereBody);
sphereMeshes.push(sphereMesh);
sphereBodies.push(sphereBody);
}
Es werden zwei verschiedenen Materialien benutzt: Das 'NormalMaterial' färbt die Oberfläche in verschiedenen Farben ein, je nach dem wo diese Oberfläche hin zeigt. Das 'PhongMaterial' wird für den Boden benutzt und kann Reflektionen und Schatten besonders gut darstellen.
Wichtig: Es reicht nicht aus, die Objekte nur in Three.js zu erstellen. Wir benötigen eine Kugel in der 3D Welt und eine Kugel in der Physik-Welt von Cannon.js
In der For-Loop werden insgesamt 10 Kugeln erstellt und positioniert. Die Y-Position (als die Höhe über dem Boden) ist ansteigend von Kugel zu Kugel. In Zeile 17 erstellen wir einen Cannon-Body, also einen Körper in der Cannon World. Auch hier wird die Masse größer für jedes Objekt.
Zeile 18 gibt dem Objekt eine gewissen Reibung die wir anfangs definiert haben. Danach in Zeile 19 geben wir dem Körper eine Form, nämlich die einer Kugel mit Radius 1.
Zuletzt werden in Zeile 20-22 die Positionen der Three.js Kugel auf die der Cannon.js Kugel überschrieben.
Der Boden
Würde man jetzt die Animation starten, würden die Kugeln einfach nur ins ewige Nichts fallen. Immerhin - die Bewegung ist schonmal physikalisch korrekt aber es fehlt noch ein Boden unter den Füßen Kugeln.
const planeGeometry = new THREE.PlaneGeometry(35, 35);
const planeMesh = new THREE.Mesh(planeGeometry, phongMaterial);
planeMesh.rotateX(-Math.PI / 2);
planeMesh.receiveShadow = true;
scene.add(planeMesh);
let groundMaterial = new CANNON.Material();
const planeShape = new CANNON.Plane();
const planeBody = new CANNON.Body({ mass: 0, material: groundMaterial });
planeBody.addShape(planeShape);
planeBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(planeBody);
let contactMat_ground = new CANNON.ContactMaterial(groundMaterial, mat1, {
friction: 0.0,
restitution: 0.8,
});
world.addContactMaterial(mat1_ground);
Auch hier erstellt man zuerst eine Three.js Ebene und dann eine Cannon.js Ebene und fügt beide in die jeweiligen Welten ein.
Die Cannon.js Ebene hat eine Masse von 0. Das bedeutet sie bleibt einfach an der exakt gleichen Position fixiert. Zeile 3 und 10 rotieren diese Ebene dann noch, damit sie nach "oben" (also in Y Richtung) zeigt.
Nun muss man noch definieren, dass es überhaupt zu einem Kontakt kommen kann. Dafür benutzt man ein 'ContactMaterial'. Die Variable 'mat1' haben wir oben schon benutzt für die Kugeln und das 'groundMaterial' für die Ebene.
Beim Erstellen von 'contactMat_ground' teilen wir Cannon.js mit, dass das Kugel Material und das Boden Material miteinander kollidieren können. Die Eigenschaften 'friction' und 'restitution' definieren die Reibung und Sprungstärke.
Letzter Schritt: Start der Simulation
Endlich sind wir soweit, die Zeit im 3D System kann gestartet werden. Normalerweise redet man bei Three.js eher von Frames anstatt von Zeit aber Cannon.js rechnet mit Sekunden.
function animate() {
requestAnimationFrame(animate);
// Step the physics simulation forward
world.step(1 / 60);
// Update the position of the box in the Three.js scene
for (let i = 0; i < sphereMeshes.length; ++i) {
sphereMeshes[i].position.copy(sphereBodies[i].position);
sphereMeshes[i].quaternion.copy(sphereBodies[i].quaternion);
}
renderer.render(scene, camera);
}
animate();
Mit jedem Schritt der Simulation bewegen wir die Welt ein Bruchteil einer Sekunden weiter (Zeile 5). Damit werden alle Positionen neu berechnet).
Doch das Berechnen von Cannon.js reicht nicht aus - wir müssen die neuen Positionen auch in die 3D Welt von Three.js übertragen. Daher updaten wir alle Positionen und Rotationen der Kugeln.
Das gesamte Projekt findest du hier.
Fazit: Cannon.js als Physik Engine
Mit Cannon.js kannst du eine "echte" Welt simulieren - also Kollisionen, Gravitation, Kräfte, etc.
Kombiniert man das mit einer Render Engine wie Three.js ergeben sich viele neue Möglichkeiten: Von einfachen 3D Animationen zu komplexen Spiel-Welten in der man sich interaktiv bewegen kann.
Als Web-Programmierer ergeben sich dadurch einige neue Möglichkeiten. Es können einzelne Komponenten auf Websites durch Animationen ersetzt werden oder ganze Apps in der 3D Welt programmiert werden.
Wenn du noch Inspiration brauchst, lohnt es sich immer auf Awwwards vorbei zu schauen. Oder du benutzt das Beispiel aus diesem Post als Grundlage und erstellst eigene 3D Welten.