Moderne Webseiten sind schon lange nicht mehr nur eine Plattform zur Darstellung von Text und Bildern, sie bieten neben Interaktionen auch dynamische Inhalte an. Zu Zeiten des Flash-Players konnte man Besucher nicht nur mit 2D-Animationen erfreuen, sondern ihnen durchaus auch komplexere Visualisierung und Spiele in 3D bieten. Die Bedeutung von Flash ist jedoch im Webbereich immer weiter zurückgegangen, da die Browser mittlerweile die meisten Funktionalitäten nativ unterstützen. Seit über zehn Jahren gibt es den WebGL-Standard, der beschreibt, wie man eine 3D-Welt in den Browser bringen kann.
Im letzten Jahrzehnt sind einige Frameworks und Bibliotheken entstanden, welche Entwickler unterstützen, die Komplexität von WebGL zu bändigen. Im Bereich der 3D-Grafik ist wohl eine der bekanntesten Frameworks Three.js, welches einige Abstraktionen mit sich bringt:
Ein einfaches Beispiel benötigt einige Komponenten:
Folgender Code-Schnipsel rendert einen Würfel:
1import * as THREE from "three";
2
3// Erstellt eine Szene
4const scene = new THREE.Scene();
5
6// Erstellt eine perspektivische Kamera
7const camera = new THREE.PerspectiveCamera(
8 70,
9 window.innerWidth / window.innerHeight,
10 0.01,
11 10
12);
13
14// Bewegt die Kamera nach oben und hinten
15camera.position.set(0, 5, 5);
16
17// Rotiert die Kamera, so dass sie etwas nach unten zeigt,
18// damit wir mehrere Flächen vom Würfel sehen können
19camera.rotation.x = -45 * THREE.MathUtils.DEG2RAD;
20
21// Erstellt die Würfelgeometrie
22const geometry = new THREE.BoxGeometry(1, 1, 1);
23
24// Erstellt ein einfaches Material für den Würfel
25const material = new THREE.MeshPhongMaterial({ color: "rgb(10,180,180)" });
26
27// Erstellt das eigentliche Mesh-Objekt, mit der Geometrie und dem Material
28const mesh = new THREE.Mesh(geometry, material);
29
30// Rotiert den Würfel, so dass eine Ecke in die Kamera schaut
31mesh.rotation.y = 45 * THREE.MathUtils.DEG2RAD;
32
33// Fügt den Würfel in die Szene ein
34scene.add(mesh);
35
36// Erstellt ein weißes Punktlicht
37const light = new THREE.PointLight("white", 1, 100);
38
39// Verschiebt das Licht, damit die Flächen des Würfels unterschiedlich
40// beleuchtet sind
41light.position.set(5, 10, 10);
42
43// Fügt das Licht in die Szene ein
44scene.add(light);
45
46// Erstellt einen WebGL-Renderer
47const renderer = new THREE.WebGLRenderer();
48
49// Setzt die Größe auf die des Fensters
50renderer.setSize(window.innerWidth, window.innerHeight);
51
52// Fügt das canvas-Element in das Dokument ein
53document.body.appendChild(renderer.domElement);
54
55// Rendert die Scene mit der Kamera ein einziges Mal
56renderer.render(scene, camera);
Das Ergebnis sieht folgendermaßen aus:
Wenn man allerdings Three.js in eine bestehende React-Anwendung einbinden will, gestaltet sich das als etwas aufwändiger, da alle Objekte in Three.js einen eigenen Lebenszyklus haben, der unabhängig von React ist. Eine Abhilfe dafür schafft aber react-three-fiber, ein Three.js Renderer für React. react-three-fiber bindet Three.js nur in React ein, der Rest wird nach wie vor von Three.js gestemmt. Die Bibliothek erlaubt dem Entwickler, die Szene mit den in React üblichen Komponenten zu beschreiben und diese wiederzuverwenden. Ein Mesh-Objekt, das aus einer Geometrie und Material besteht, kann durch die JSX-Syntax folgendermaßen ausgedrückt werden.
1<mesh rotation={[45, 0, 0]}>
2 <boxGeometry args={[1, 1, 1]} />
3 <meshPhongMaterial color={'rgb(10,180,180)'} />
4</mesh>
Der Code erstellt genau wie das vorherige Würfelbeispiel einen Würfel mit einem Material, allerdings in einer wesentlich kompakteren und übersichtlicheren Form.
Folgendes Beispiel beschreibt eine etwas komplexere Szene. Die Szene zeigt ein sich drehendes byteleaf-Logo, das mit einer kurzen Animation erscheint:
1import React, { Suspense } from "react";
2
3import { Canvas } from "@react-three/fiber";
4import { Logo } from "./Logo";
5import { AppearingRotatingGroup } from "./AppearingRotatingGroup";
6
7export default () => (
8 // Im canvas-Element wird die Three.js-Szene gerendert
9 <Canvas>
10 {/*
11 Fügt ein Umgebungslicht in die Szene, das alle Objekte von allen
12 */}
13 <ambientLight intensity={0.25} />
14 {/*
15 Fügt ein Punktlicht hinzu
16 */}
17 <pointLight position={[10, 10, 10]} intensity={0.5} />
18 {/*
19 Für das dynamische Laden muss React's Suspense-Mechanismus verwendet werden
20 */}
21 <Suspense fallback={null}>
22 {/*
23 Eigene Komponente, die die Kinder-Element zu begin
24 drehend hochskaliert, für eine schöne Erscheinungsanimation
25 */}
26 <AppearingRotatingGroup>
27 {/*
28 Unser Logo
29 */}
30 <Logo />
31 </AppearingRotatingGroup>
32 </Suspense>
33 </Canvas>
34);
Die AppearingRotationGroup kapselt die Kinder-Elemente in eine Gruppe und animiert sie. Das ermöglicht ein hohes Maß an Wiederverwendbarkeit. Der Code sieht folgendermaßen aus:
1import React, { useRef } from "react";
2import { Group, MathUtils } from "three";
3import { useFrame } from "@react-three/fiber";
4
5export const AppearingRotatingGroup: React.FC = ({ children }) => {
6 // Eine Referenz für die Gruppe, damit diese animiert werden kann
7 // null! ist ein kleiner TypeScript-Hack,
8 // damit wir nicht immer auf null überprüfen müssen, da es garantiert ist,
9 // dass die Referenz in useFrame nie null ist
10 const group = useRef<Group>(null!);
11
12 // Die Rotationsgeschwindigkeit, die in jedem Frame verringert wird
13 const rotationSpeed = useRef(5);
14
15 // Jeden Frame wird die Gruppe animiert
16 useFrame((state, delta) => {
17
18 // Dreht die Gruppe um die vertikale Achse
19 group.current.rotation.y -= delta * rotationSpeed.current;
20
21 // Die Skalierung fängt an bei 0 und wird auf 1 hoch animiert
22 const scale = MathUtils.lerp(
23 group.current.scale.x,
24 1,
25 delta * rotationSpeed.current
26 );
27
28 group.current.scale.setScalar(scale);
29
30 // verringert die Rotationsgeschwindigkeit gegen 0 damit diese langsamer wird
31 // und irgendwann gänzlich aufhört
32 rotationSpeed.current = MathUtils.lerp(rotationSpeed.current, 0, delta);
33});
34
35 return (
36 <group ref={group} scale={0}>
37 {children}
38 </group>
39 );
40};
Und schließlich unser Logo als Komponente. Das Logo ist eine GLTF-Datei, die wir laden. Anschließend versehen wir das Logo noch mit einer kleinen Drehanimation.
1import React, { useRef } from "react";
2import { Mesh } from "three";
3import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader";
4
5import { useFrame, useLoader } from "@react-three/fiber";
6
7export const Logo: React.VFC = () => {
8
9 // Eine Referenz zum Logo, damit die Drehung animiert werden kann
10 const primitiveRef = useRef<Mesh>(null!);
11
12 // Das Logo-Objekt wird jeden Frame etwas gedreht
13 useFrame((state, delta) => {
14 primitiveRef.current.rotation.y -= delta;
15 });
16
17 // Ein React Hook, der das Logo aus einer Datei lädt
18 const logo = useLoader(GLTFLoader, "./logo3d.glb");
19
20 // Das eigentlich Objekt
21 return <primitive ref={primitiveRef} object={logo.scene} />;
22};
Das Ergebnis sieht folgendermaßen aus:
Wie man an den beiden Beispielen sieht, bietet react-three-fiber nicht nur eine einfache Integration von Three.js in eine React-Anwendung, sondern erhöht die Wiederverwendbarkeit und verkürzt damit erheblich die Code-Menge. Auch können die 3D-Komponenten, wie es in einer React-Anwendung üblich ist, getestet werden.
Aber: Man muss bei Animationen immer auf die Performance achten, damit diese flüssig sind und nicht ruckeln. Für eine Animation mit 60 FPS darf also jeder Frame nur knapp 16 Millisekunden brauchen. Wenn man nun also anfängt mit dieser Rate alle Komponenten durch den React-Renderer zu jagen, könnte das zu Performance-Engpässen führen. Deswegen rät die Dokumentation von react-three-fiber davon ab, Animationen mit Hilfe der React-üblichen Konstrukte (z. B. useState) umzusetzen. Stattdessen wird die Verwendung vom useFrame-Hook oder von Bibliotheken wie z. B. react-spring empfohlen (siehe den Performance Stolperfallen Link in der offiziellen Dokumentation).
Neben der Einarbeitung in Three.js muss man sich auch in react-three-fiber einarbeiten, das neben den schon genannten Hooks auch die JSX-Syntax beinhaltet.
Alles in allem bietet react-three-fiber jedoch eine gute Möglichkeit Three.js in eine React-Anwendung einzubetten.