Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
613 lines
17 KiB
Markdown
613 lines
17 KiB
Markdown
# Three.js & React Three Fiber für 3D Web
|
||
|
||
**Meta-Description:** 3D Webentwicklung mit Three.js und React Three Fiber. WebGL/WebGPU, interaktive Szenen und Performance-Optimierung.
|
||
|
||
**Keywords:** Three.js, React Three Fiber, 3D Web, WebGL, WebGPU, R3F, Drei, Interactive 3D
|
||
|
||
---
|
||
|
||
## Einführung
|
||
|
||
**Three.js** dominiert die 3D-Webentwicklung mit 270x mehr Downloads als Alternativen. **React Three Fiber** (R3F) bringt die deklarative React-Syntax in die 3D-Welt – perfekt für moderne Web-Projekte mit interaktiven Visualisierungen.
|
||
|
||
---
|
||
|
||
## Architecture Overview
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────┐
|
||
│ THREE.JS / R3F ARCHITECTURE │
|
||
├─────────────────────────────────────────────────────────────┤
|
||
│ │
|
||
│ Application Layer: │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ React Components / Hooks / State │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ R3F Layer: ▼ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ React Three Fiber (React Renderer for Three.js) │ │
|
||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||
│ │ │ Canvas │ │ Hooks │ │ Events │ │ │
|
||
│ │ │ (Root) │ │ (Frame) │ │(Pointer)│ │ │
|
||
│ │ └─────────┘ └─────────┘ └─────────┘ │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ Drei (Helpers): │ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ OrbitControls | Environment | Text3D | useGLTF │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ Three.js Core: ▼ │
|
||
│ ┌─────────────────────────────────────────────────────┐ │
|
||
│ │ Scene | Camera | Renderer | Lights | Geometries │ │
|
||
│ └─────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ Graphics API: ▼ │
|
||
│ ┌───────────────────┬────────────────────┐ │
|
||
│ │ WebGL 2 │ WebGPU │ │
|
||
│ │ (Supported) │ (2026 Ready) │ │
|
||
│ └───────────────────┴────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
---
|
||
|
||
## Setup & Installation
|
||
|
||
```bash
|
||
# React Three Fiber + Drei (Helpers)
|
||
npm install three @react-three/fiber @react-three/drei
|
||
|
||
# TypeScript Types
|
||
npm install -D @types/three
|
||
|
||
# Optional: Post-Processing, Physics
|
||
npm install @react-three/postprocessing
|
||
npm install @react-three/cannon
|
||
```
|
||
|
||
```typescript
|
||
// next.config.js (für Next.js)
|
||
const nextConfig = {
|
||
transpilePackages: ['three'],
|
||
webpack: (config) => {
|
||
config.externals.push({
|
||
'sharp': 'commonjs sharp',
|
||
'canvas': 'commonjs canvas',
|
||
});
|
||
return config;
|
||
}
|
||
};
|
||
```
|
||
|
||
---
|
||
|
||
## Basic Scene mit R3F
|
||
|
||
```tsx
|
||
// components/Scene.tsx
|
||
'use client';
|
||
|
||
import { Canvas } from '@react-three/fiber';
|
||
import { OrbitControls, Environment, PerspectiveCamera } from '@react-three/drei';
|
||
import { Suspense } from 'react';
|
||
|
||
export function Scene() {
|
||
return (
|
||
<Canvas>
|
||
<PerspectiveCamera makeDefault position={[0, 2, 5]} />
|
||
<OrbitControls enableDamping dampingFactor={0.05} />
|
||
|
||
<Suspense fallback={<LoadingBox />}>
|
||
<Environment preset="sunset" />
|
||
<ambientLight intensity={0.5} />
|
||
<directionalLight position={[10, 10, 5]} intensity={1} castShadow />
|
||
|
||
<RotatingBox position={[-1.5, 0, 0]} />
|
||
<FloatingSphere position={[1.5, 0, 0]} />
|
||
<Floor />
|
||
</Suspense>
|
||
</Canvas>
|
||
);
|
||
}
|
||
|
||
function LoadingBox() {
|
||
return (
|
||
<mesh>
|
||
<boxGeometry args={[1, 1, 1]} />
|
||
<meshBasicMaterial color="gray" wireframe />
|
||
</mesh>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Interaktive Objekte
|
||
|
||
```tsx
|
||
// components/InteractiveObjects.tsx
|
||
'use client';
|
||
|
||
import { useRef, useState } from 'react';
|
||
import { useFrame } from '@react-three/fiber';
|
||
import { MeshWobbleMaterial, RoundedBox, Text } from '@react-three/drei';
|
||
import * as THREE from 'three';
|
||
|
||
interface BoxProps {
|
||
position: [number, number, number];
|
||
}
|
||
|
||
export function RotatingBox({ position }: BoxProps) {
|
||
const meshRef = useRef<THREE.Mesh>(null);
|
||
const [hovered, setHovered] = useState(false);
|
||
const [clicked, setClicked] = useState(false);
|
||
|
||
useFrame((state, delta) => {
|
||
if (meshRef.current) {
|
||
meshRef.current.rotation.x += delta * 0.5;
|
||
meshRef.current.rotation.y += delta * 0.3;
|
||
|
||
// Hover Animation
|
||
const scale = hovered ? 1.2 : 1;
|
||
meshRef.current.scale.lerp(new THREE.Vector3(scale, scale, scale), 0.1);
|
||
}
|
||
});
|
||
|
||
return (
|
||
<mesh
|
||
ref={meshRef}
|
||
position={position}
|
||
onClick={() => setClicked(!clicked)}
|
||
onPointerOver={() => setHovered(true)}
|
||
onPointerOut={() => setHovered(false)}
|
||
>
|
||
<boxGeometry args={[1, 1, 1]} />
|
||
<meshStandardMaterial
|
||
color={clicked ? '#ff6b6b' : hovered ? '#4ecdc4' : '#45b7d1'}
|
||
metalness={0.5}
|
||
roughness={0.2}
|
||
/>
|
||
</mesh>
|
||
);
|
||
}
|
||
|
||
export function FloatingSphere({ position }: BoxProps) {
|
||
const meshRef = useRef<THREE.Mesh>(null);
|
||
|
||
useFrame((state) => {
|
||
if (meshRef.current) {
|
||
// Floating Animation
|
||
meshRef.current.position.y = position[1] + Math.sin(state.clock.elapsedTime) * 0.3;
|
||
}
|
||
});
|
||
|
||
return (
|
||
<mesh ref={meshRef} position={position}>
|
||
<sphereGeometry args={[0.7, 32, 32]} />
|
||
<MeshWobbleMaterial
|
||
color="#9b59b6"
|
||
metalness={0.8}
|
||
roughness={0.1}
|
||
factor={0.5} // Wobble Intensity
|
||
speed={2} // Wobble Speed
|
||
/>
|
||
</mesh>
|
||
);
|
||
}
|
||
|
||
export function Floor() {
|
||
return (
|
||
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, -1, 0]} receiveShadow>
|
||
<planeGeometry args={[20, 20]} />
|
||
<meshStandardMaterial color="#1a1a2e" metalness={0.2} roughness={0.8} />
|
||
</mesh>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 3D Model Loading (GLTF)
|
||
|
||
```tsx
|
||
// components/Model.tsx
|
||
'use client';
|
||
|
||
import { useRef, useEffect } from 'react';
|
||
import { useGLTF, useAnimations, Clone } from '@react-three/drei';
|
||
import { useFrame } from '@react-three/fiber';
|
||
import * as THREE from 'three';
|
||
|
||
interface ModelProps {
|
||
url: string;
|
||
position?: [number, number, number];
|
||
scale?: number;
|
||
animate?: boolean;
|
||
}
|
||
|
||
export function Model({ url, position = [0, 0, 0], scale = 1, animate = true }: ModelProps) {
|
||
const group = useRef<THREE.Group>(null);
|
||
const { scene, animations } = useGLTF(url);
|
||
const { actions, names } = useAnimations(animations, group);
|
||
|
||
useEffect(() => {
|
||
// Erste Animation abspielen
|
||
if (animate && names.length > 0) {
|
||
actions[names[0]]?.play();
|
||
}
|
||
}, [actions, names, animate]);
|
||
|
||
// Shadow Setup
|
||
useEffect(() => {
|
||
scene.traverse((child) => {
|
||
if (child instanceof THREE.Mesh) {
|
||
child.castShadow = true;
|
||
child.receiveShadow = true;
|
||
}
|
||
});
|
||
}, [scene]);
|
||
|
||
return (
|
||
<group ref={group} position={position} scale={scale}>
|
||
<Clone object={scene} />
|
||
</group>
|
||
);
|
||
}
|
||
|
||
// Preload für bessere Performance
|
||
useGLTF.preload('/models/robot.glb');
|
||
|
||
// Mit Draco Compression
|
||
export function CompressedModel({ url }: { url: string }) {
|
||
const { scene } = useGLTF(url, true); // true = Draco Loader
|
||
return <primitive object={scene} />;
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Custom Shaders
|
||
|
||
```tsx
|
||
// components/CustomShader.tsx
|
||
'use client';
|
||
|
||
import { useRef, useMemo } from 'react';
|
||
import { useFrame, extend } from '@react-three/fiber';
|
||
import { shaderMaterial } from '@react-three/drei';
|
||
import * as THREE from 'three';
|
||
|
||
// Custom Shader Material
|
||
const GradientMaterial = shaderMaterial(
|
||
// Uniforms
|
||
{
|
||
uTime: 0,
|
||
uColor1: new THREE.Color('#ff0000'),
|
||
uColor2: new THREE.Color('#0000ff'),
|
||
uIntensity: 1.0
|
||
},
|
||
// Vertex Shader
|
||
`
|
||
varying vec2 vUv;
|
||
varying vec3 vPosition;
|
||
|
||
void main() {
|
||
vUv = uv;
|
||
vPosition = position;
|
||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||
}
|
||
`,
|
||
// Fragment Shader
|
||
`
|
||
uniform float uTime;
|
||
uniform vec3 uColor1;
|
||
uniform vec3 uColor2;
|
||
uniform float uIntensity;
|
||
varying vec2 vUv;
|
||
varying vec3 vPosition;
|
||
|
||
void main() {
|
||
// Animated Gradient
|
||
float mixValue = sin(vUv.y * 3.14159 + uTime) * 0.5 + 0.5;
|
||
vec3 color = mix(uColor1, uColor2, mixValue);
|
||
|
||
// Wave Effect
|
||
float wave = sin(vPosition.x * 5.0 + uTime * 2.0) * 0.1;
|
||
color += wave * uIntensity;
|
||
|
||
gl_FragColor = vec4(color, 1.0);
|
||
}
|
||
`
|
||
);
|
||
|
||
// Extend für JSX Support
|
||
extend({ GradientMaterial });
|
||
|
||
// TypeScript Declaration
|
||
declare global {
|
||
namespace JSX {
|
||
interface IntrinsicElements {
|
||
gradientMaterial: any;
|
||
}
|
||
}
|
||
}
|
||
|
||
export function ShaderSphere() {
|
||
const materialRef = useRef<any>(null);
|
||
|
||
useFrame((state) => {
|
||
if (materialRef.current) {
|
||
materialRef.current.uTime = state.clock.elapsedTime;
|
||
}
|
||
});
|
||
|
||
return (
|
||
<mesh>
|
||
<sphereGeometry args={[1, 64, 64]} />
|
||
<gradientMaterial
|
||
ref={materialRef}
|
||
uColor1="#ff6b6b"
|
||
uColor2="#4ecdc4"
|
||
uIntensity={0.5}
|
||
/>
|
||
</mesh>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Post-Processing Effects
|
||
|
||
```tsx
|
||
// components/PostProcessing.tsx
|
||
'use client';
|
||
|
||
import { EffectComposer, Bloom, ChromaticAberration, Vignette } from '@react-three/postprocessing';
|
||
import { BlendFunction } from 'postprocessing';
|
||
|
||
export function Effects() {
|
||
return (
|
||
<EffectComposer>
|
||
{/* Bloom für leuchtende Effekte */}
|
||
<Bloom
|
||
intensity={1.5}
|
||
luminanceThreshold={0.9}
|
||
luminanceSmoothing={0.025}
|
||
mipmapBlur
|
||
/>
|
||
|
||
{/* Chromatische Aberration */}
|
||
<ChromaticAberration
|
||
blendFunction={BlendFunction.NORMAL}
|
||
offset={[0.002, 0.002]}
|
||
/>
|
||
|
||
{/* Vignette */}
|
||
<Vignette
|
||
offset={0.3}
|
||
darkness={0.5}
|
||
blendFunction={BlendFunction.NORMAL}
|
||
/>
|
||
</EffectComposer>
|
||
);
|
||
}
|
||
|
||
// Verwendung
|
||
function SceneWithEffects() {
|
||
return (
|
||
<Canvas>
|
||
<Scene />
|
||
<Effects />
|
||
</Canvas>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Physics mit Cannon
|
||
|
||
```tsx
|
||
// components/PhysicsScene.tsx
|
||
'use client';
|
||
|
||
import { Physics, usePlane, useBox, useSphere } from '@react-three/cannon';
|
||
import { useRef } from 'react';
|
||
import * as THREE from 'three';
|
||
|
||
export function PhysicsScene() {
|
||
return (
|
||
<Physics gravity={[0, -9.81, 0]}>
|
||
<PhysicsGround />
|
||
<PhysicsBox position={[0, 5, 0]} />
|
||
<PhysicsBox position={[0.5, 7, 0]} />
|
||
<PhysicsBall position={[-0.5, 10, 0]} />
|
||
</Physics>
|
||
);
|
||
}
|
||
|
||
function PhysicsGround() {
|
||
const [ref] = usePlane<THREE.Mesh>(() => ({
|
||
rotation: [-Math.PI / 2, 0, 0],
|
||
position: [0, 0, 0]
|
||
}));
|
||
|
||
return (
|
||
<mesh ref={ref} receiveShadow>
|
||
<planeGeometry args={[20, 20]} />
|
||
<meshStandardMaterial color="#333" />
|
||
</mesh>
|
||
);
|
||
}
|
||
|
||
function PhysicsBox({ position }: { position: [number, number, number] }) {
|
||
const [ref] = useBox<THREE.Mesh>(() => ({
|
||
mass: 1,
|
||
position,
|
||
args: [1, 1, 1]
|
||
}));
|
||
|
||
return (
|
||
<mesh ref={ref} castShadow>
|
||
<boxGeometry args={[1, 1, 1]} />
|
||
<meshStandardMaterial color="#ff6b6b" />
|
||
</mesh>
|
||
);
|
||
}
|
||
|
||
function PhysicsBall({ position }: { position: [number, number, number] }) {
|
||
const [ref] = useSphere<THREE.Mesh>(() => ({
|
||
mass: 2,
|
||
position,
|
||
args: [0.5]
|
||
}));
|
||
|
||
return (
|
||
<mesh ref={ref} castShadow>
|
||
<sphereGeometry args={[0.5, 32, 32]} />
|
||
<meshStandardMaterial color="#4ecdc4" />
|
||
</mesh>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Performance Optimierung
|
||
|
||
```tsx
|
||
// components/OptimizedScene.tsx
|
||
'use client';
|
||
|
||
import { useRef, useMemo } from 'react';
|
||
import { useFrame } from '@react-three/fiber';
|
||
import { Instances, Instance, Detailed, useGLTF } from '@react-three/drei';
|
||
import * as THREE from 'three';
|
||
|
||
// Instancing für viele gleiche Objekte
|
||
export function InstancedCubes({ count = 1000 }) {
|
||
const positions = useMemo(() => {
|
||
return Array.from({ length: count }, () => ({
|
||
position: [
|
||
(Math.random() - 0.5) * 20,
|
||
(Math.random() - 0.5) * 20,
|
||
(Math.random() - 0.5) * 20
|
||
] as [number, number, number],
|
||
rotation: [
|
||
Math.random() * Math.PI,
|
||
Math.random() * Math.PI,
|
||
Math.random() * Math.PI
|
||
] as [number, number, number],
|
||
scale: 0.5 + Math.random() * 0.5
|
||
}));
|
||
}, [count]);
|
||
|
||
return (
|
||
<Instances limit={count} castShadow receiveShadow>
|
||
<boxGeometry args={[1, 1, 1]} />
|
||
<meshStandardMaterial color="#45b7d1" />
|
||
{positions.map((props, i) => (
|
||
<Instance
|
||
key={i}
|
||
position={props.position}
|
||
rotation={props.rotation}
|
||
scale={props.scale}
|
||
/>
|
||
))}
|
||
</Instances>
|
||
);
|
||
}
|
||
|
||
// Level of Detail (LOD)
|
||
export function LODModel() {
|
||
return (
|
||
<Detailed distances={[0, 10, 25, 50]}>
|
||
{/* Höchste Qualität (nah) */}
|
||
<mesh>
|
||
<sphereGeometry args={[1, 64, 64]} />
|
||
<meshStandardMaterial color="red" />
|
||
</mesh>
|
||
|
||
{/* Mittlere Qualität */}
|
||
<mesh>
|
||
<sphereGeometry args={[1, 32, 32]} />
|
||
<meshStandardMaterial color="red" />
|
||
</mesh>
|
||
|
||
{/* Niedrige Qualität */}
|
||
<mesh>
|
||
<sphereGeometry args={[1, 16, 16]} />
|
||
<meshStandardMaterial color="red" />
|
||
</mesh>
|
||
|
||
{/* Sehr niedrige Qualität (weit) */}
|
||
<mesh>
|
||
<sphereGeometry args={[1, 8, 8]} />
|
||
<meshStandardMaterial color="red" />
|
||
</mesh>
|
||
</Detailed>
|
||
);
|
||
}
|
||
|
||
// Lazy Loading
|
||
import { lazy, Suspense } from 'react';
|
||
|
||
const HeavyModel = lazy(() => import('./HeavyModel'));
|
||
|
||
export function LazyScene() {
|
||
return (
|
||
<Suspense fallback={<LoadingIndicator />}>
|
||
<HeavyModel />
|
||
</Suspense>
|
||
);
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Best Practices
|
||
|
||
| Aspect | Recommendation |
|
||
|--------|----------------|
|
||
| **Geometry** | Instancing für viele gleiche Objekte |
|
||
| **Textures** | Compressed (WebP), Power of 2 |
|
||
| **Models** | Draco compression, LOD |
|
||
| **Shadows** | Nur wo nötig, Shadow Map Size |
|
||
| **Animations** | useFrame statt setInterval |
|
||
| **State** | Zustand außerhalb des Render Loops |
|
||
| **Cleanup** | dispose() für Geometries/Materials |
|
||
|
||
---
|
||
|
||
## Fazit
|
||
|
||
Three.js + React Three Fiber bietet:
|
||
|
||
1. **Deklarative Syntax**: React-Komponenten für 3D
|
||
2. **Ecosystem**: Drei, Cannon, Postprocessing
|
||
3. **WebGPU Ready**: Zukunftssicher mit WebGPU Support
|
||
4. **Performance**: Instancing, LOD, Lazy Loading
|
||
|
||
Die beste Kombination für 3D im Web.
|
||
|
||
---
|
||
|
||
## Bildprompts
|
||
|
||
1. "Interactive 3D scene with rotating objects, React Three Fiber demo"
|
||
2. "WebGL shader effect with gradient colors, custom material"
|
||
3. "Physics simulation with falling objects, cannon.js integration"
|
||
|
||
---
|
||
|
||
## Quellen
|
||
|
||
- [React Three Fiber Documentation](https://docs.pmnd.rs/react-three-fiber)
|
||
- [Three.js Journey](https://threejs-journey.com/)
|
||
- [Drei Helpers](https://github.com/pmndrs/drei)
|
||
- [What's New in Three.js 2026](https://www.utsubo.com/blog/threejs-2026-what-changed)
|