Initial commit: Portfolio Website
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>
This commit is contained in:
@@ -0,0 +1,612 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user