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,539 @@
|
||||
# GLTF Model Optimization für Web
|
||||
|
||||
**Meta-Description:** 3D Modelle für Web optimieren. GLTF/GLB Format, Draco Compression, Texture Optimization und LOD Strategien.
|
||||
|
||||
**Keywords:** GLTF, GLB, Draco, 3D Optimization, Web 3D, Texture Compression, LOD, Three.js
|
||||
|
||||
---
|
||||
|
||||
## Einführung
|
||||
|
||||
**GLTF** (GL Transmission Format) ist das "JPEG der 3D-Welt". Mit **Draco Compression** können Modelle um bis zu 95% verkleinert werden. Optimierte 3D-Assets sind essentiell für schnelle Ladezeiten und gute User Experience.
|
||||
|
||||
---
|
||||
|
||||
## GLTF Format Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ GLTF/GLB FORMAT STRUCTURE │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ GLTF (Text + Binary): │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ model.gltf (JSON - Scene Description) │ │
|
||||
│ │ model.bin (Binary - Geometry Data) │ │
|
||||
│ │ textures/ │ │
|
||||
│ │ ├── diffuse.png (Texture Files) │ │
|
||||
│ │ ├── normal.png │ │
|
||||
│ │ └── metallic.png │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ GLB (Single Binary): │
|
||||
│ ┌─────────────────────────────────────────────────────┐ │
|
||||
│ │ model.glb │ │
|
||||
│ │ ┌─────────────────────────────────────────────┐ │ │
|
||||
│ │ │ Header (12 bytes) │ │ │
|
||||
│ │ │ JSON Chunk (Scene + Materials) │ │ │
|
||||
│ │ │ Binary Chunk (Geometry + Textures) │ │ │
|
||||
│ │ └─────────────────────────────────────────────┘ │ │
|
||||
│ └─────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ File Size Breakdown (typical): │
|
||||
│ ├── Textures: 80% │
|
||||
│ ├── Geometry: 15% │
|
||||
│ └── Metadata: 5% │
|
||||
│ │
|
||||
│ Supported Extensions: │
|
||||
│ ├── KHR_draco_mesh_compression (Geometry) │
|
||||
│ ├── KHR_texture_basisu (Textures) │
|
||||
│ ├── KHR_mesh_quantization (Vertices) │
|
||||
│ └── EXT_meshopt_compression (Alternative) │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## gltf-transform CLI
|
||||
|
||||
```bash
|
||||
# Installation
|
||||
npm install -g @gltf-transform/cli
|
||||
|
||||
# Basic Optimization
|
||||
gltf-transform optimize input.glb output.glb
|
||||
|
||||
# Mit Draco Compression
|
||||
gltf-transform draco input.glb output.glb
|
||||
|
||||
# Texture Optimization (WebP)
|
||||
gltf-transform webp input.glb output.glb --quality 80
|
||||
|
||||
# Full Optimization Pipeline
|
||||
gltf-transform optimize input.glb output.glb \
|
||||
--compress draco \
|
||||
--texture-compress webp \
|
||||
--texture-size 1024
|
||||
|
||||
# Modell analysieren
|
||||
gltf-transform inspect input.glb
|
||||
|
||||
# Mesh vereinfachen
|
||||
gltf-transform simplify input.glb output.glb --ratio 0.5
|
||||
|
||||
# Texturen resizen
|
||||
gltf-transform resize input.glb output.glb --width 1024 --height 1024
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Node.js Optimization Script
|
||||
|
||||
```typescript
|
||||
// scripts/optimize-model.ts
|
||||
import { Document, NodeIO, Transform } from '@gltf-transform/core';
|
||||
import { dedup, draco, textureCompress, weld, simplify } from '@gltf-transform/functions';
|
||||
import { KHRDracoMeshCompression, KHRTextureBasisu } from '@gltf-transform/extensions';
|
||||
import draco3d from 'draco3dgltf';
|
||||
import sharp from 'sharp';
|
||||
|
||||
interface OptimizationOptions {
|
||||
dracoCompression: boolean;
|
||||
textureFormat: 'webp' | 'jpeg' | 'png';
|
||||
textureQuality: number;
|
||||
maxTextureSize: number;
|
||||
simplifyRatio: number;
|
||||
weldVertices: boolean;
|
||||
}
|
||||
|
||||
async function optimizeModel(
|
||||
inputPath: string,
|
||||
outputPath: string,
|
||||
options: OptimizationOptions
|
||||
): Promise<{ originalSize: number; optimizedSize: number; reduction: number }> {
|
||||
const io = new NodeIO()
|
||||
.registerExtensions([KHRDracoMeshCompression, KHRTextureBasisu])
|
||||
.registerDependencies({
|
||||
'draco3d.decoder': await draco3d.createDecoderModule(),
|
||||
'draco3d.encoder': await draco3d.createEncoderModule()
|
||||
});
|
||||
|
||||
// Modell laden
|
||||
const document = await io.read(inputPath);
|
||||
const originalSize = (await io.writeBinary(document)).byteLength;
|
||||
|
||||
// Transformationen Pipeline
|
||||
const transforms: Transform[] = [];
|
||||
|
||||
// 1. Duplikate entfernen
|
||||
transforms.push(dedup());
|
||||
|
||||
// 2. Vertices zusammenführen
|
||||
if (options.weldVertices) {
|
||||
transforms.push(weld({ tolerance: 0.0001 }));
|
||||
}
|
||||
|
||||
// 3. Mesh vereinfachen
|
||||
if (options.simplifyRatio < 1) {
|
||||
transforms.push(simplify({
|
||||
ratio: options.simplifyRatio,
|
||||
error: 0.001
|
||||
}));
|
||||
}
|
||||
|
||||
// 4. Texturen optimieren
|
||||
transforms.push(textureCompress({
|
||||
encoder: sharp,
|
||||
targetFormat: options.textureFormat,
|
||||
resize: [options.maxTextureSize, options.maxTextureSize],
|
||||
quality: options.textureQuality
|
||||
}));
|
||||
|
||||
// 5. Draco Compression
|
||||
if (options.dracoCompression) {
|
||||
transforms.push(draco({
|
||||
quantizePosition: 14,
|
||||
quantizeNormal: 10,
|
||||
quantizeTexcoord: 12,
|
||||
quantizeColor: 8
|
||||
}));
|
||||
}
|
||||
|
||||
// Transformationen anwenden
|
||||
await document.transform(...transforms);
|
||||
|
||||
// Speichern
|
||||
await io.write(outputPath, document);
|
||||
|
||||
const optimizedSize = (await io.readBinary(outputPath)).byteLength;
|
||||
const reduction = ((originalSize - optimizedSize) / originalSize) * 100;
|
||||
|
||||
return {
|
||||
originalSize,
|
||||
optimizedSize,
|
||||
reduction
|
||||
};
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
const result = await optimizeModel('./models/robot.glb', './models/robot-optimized.glb', {
|
||||
dracoCompression: true,
|
||||
textureFormat: 'webp',
|
||||
textureQuality: 80,
|
||||
maxTextureSize: 1024,
|
||||
simplifyRatio: 0.75,
|
||||
weldVertices: true
|
||||
});
|
||||
|
||||
console.log(`Original: ${(result.originalSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`Optimized: ${(result.optimizedSize / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`Reduction: ${result.reduction.toFixed(1)}%`);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Batch Optimization
|
||||
|
||||
```typescript
|
||||
// scripts/batch-optimize.ts
|
||||
import { glob } from 'glob';
|
||||
import path from 'path';
|
||||
|
||||
async function batchOptimize(
|
||||
inputDir: string,
|
||||
outputDir: string,
|
||||
options: OptimizationOptions
|
||||
) {
|
||||
const files = await glob(`${inputDir}/**/*.{glb,gltf}`);
|
||||
|
||||
console.log(`Found ${files.length} models to optimize`);
|
||||
|
||||
const results: Array<{
|
||||
file: string;
|
||||
originalSize: number;
|
||||
optimizedSize: number;
|
||||
reduction: number;
|
||||
}> = [];
|
||||
|
||||
for (const file of files) {
|
||||
const relativePath = path.relative(inputDir, file);
|
||||
const outputPath = path.join(outputDir, relativePath.replace(/\.gltf$/, '.glb'));
|
||||
|
||||
// Output Verzeichnis erstellen
|
||||
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
||||
|
||||
console.log(`Optimizing: ${relativePath}`);
|
||||
|
||||
try {
|
||||
const result = await optimizeModel(file, outputPath, options);
|
||||
results.push({ file: relativePath, ...result });
|
||||
} catch (error) {
|
||||
console.error(`Failed: ${relativePath}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// Report
|
||||
const totalOriginal = results.reduce((sum, r) => sum + r.originalSize, 0);
|
||||
const totalOptimized = results.reduce((sum, r) => sum + r.optimizedSize, 0);
|
||||
|
||||
console.log('\n=== Optimization Report ===');
|
||||
console.log(`Total Original: ${(totalOriginal / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`Total Optimized: ${(totalOptimized / 1024 / 1024).toFixed(2)} MB`);
|
||||
console.log(`Total Reduction: ${((1 - totalOptimized / totalOriginal) * 100).toFixed(1)}%`);
|
||||
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## LOD Generation
|
||||
|
||||
```typescript
|
||||
// scripts/generate-lod.ts
|
||||
import { Document, NodeIO } from '@gltf-transform/core';
|
||||
import { simplify, dedup, weld } from '@gltf-transform/functions';
|
||||
|
||||
interface LODConfig {
|
||||
levels: Array<{
|
||||
suffix: string;
|
||||
ratio: number;
|
||||
distance: number; // Für Three.js LOD
|
||||
}>;
|
||||
}
|
||||
|
||||
const defaultLODConfig: LODConfig = {
|
||||
levels: [
|
||||
{ suffix: '_lod0', ratio: 1.0, distance: 0 }, // Original
|
||||
{ suffix: '_lod1', ratio: 0.5, distance: 10 }, // 50%
|
||||
{ suffix: '_lod2', ratio: 0.25, distance: 25 }, // 25%
|
||||
{ suffix: '_lod3', ratio: 0.1, distance: 50 } // 10%
|
||||
]
|
||||
};
|
||||
|
||||
async function generateLODs(
|
||||
inputPath: string,
|
||||
outputDir: string,
|
||||
config: LODConfig = defaultLODConfig
|
||||
) {
|
||||
const io = new NodeIO();
|
||||
const originalDocument = await io.read(inputPath);
|
||||
|
||||
const baseName = path.basename(inputPath, path.extname(inputPath));
|
||||
const results: string[] = [];
|
||||
|
||||
for (const level of config.levels) {
|
||||
const document = originalDocument.clone();
|
||||
|
||||
if (level.ratio < 1) {
|
||||
await document.transform(
|
||||
dedup(),
|
||||
weld(),
|
||||
simplify({ ratio: level.ratio, error: 0.001 })
|
||||
);
|
||||
}
|
||||
|
||||
const outputPath = path.join(outputDir, `${baseName}${level.suffix}.glb`);
|
||||
await io.write(outputPath, document);
|
||||
|
||||
results.push(outputPath);
|
||||
console.log(`Generated ${level.suffix}: ${level.ratio * 100}%`);
|
||||
}
|
||||
|
||||
// LOD Manifest
|
||||
const manifest = {
|
||||
baseName,
|
||||
levels: config.levels.map((level, i) => ({
|
||||
file: `${baseName}${level.suffix}.glb`,
|
||||
ratio: level.ratio,
|
||||
distance: level.distance
|
||||
}))
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(outputDir, `${baseName}_lod_manifest.json`),
|
||||
JSON.stringify(manifest, null, 2)
|
||||
);
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// Three.js LOD Loader
|
||||
import * as THREE from 'three';
|
||||
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
||||
|
||||
async function loadLODModel(manifestUrl: string): Promise<THREE.LOD> {
|
||||
const manifest = await fetch(manifestUrl).then(r => r.json());
|
||||
const loader = new GLTFLoader();
|
||||
|
||||
const lod = new THREE.LOD();
|
||||
|
||||
for (const level of manifest.levels) {
|
||||
const gltf = await loader.loadAsync(level.file);
|
||||
lod.addLevel(gltf.scene, level.distance);
|
||||
}
|
||||
|
||||
return lod;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Texture Optimization
|
||||
|
||||
```typescript
|
||||
// scripts/optimize-textures.ts
|
||||
import sharp from 'sharp';
|
||||
import { Document, NodeIO, Texture } from '@gltf-transform/core';
|
||||
|
||||
interface TextureOptimizationConfig {
|
||||
maxSize: number;
|
||||
format: 'webp' | 'jpeg' | 'png' | 'basis';
|
||||
quality: number;
|
||||
normalMapQuality: number;
|
||||
generateMipmaps: boolean;
|
||||
}
|
||||
|
||||
async function optimizeTextures(
|
||||
document: Document,
|
||||
config: TextureOptimizationConfig
|
||||
): Promise<void> {
|
||||
const textures = document.getRoot().listTextures();
|
||||
|
||||
for (const texture of textures) {
|
||||
const image = texture.getImage();
|
||||
if (!image) continue;
|
||||
|
||||
const mimeType = texture.getMimeType();
|
||||
const name = texture.getName() || 'unnamed';
|
||||
|
||||
// Textur-Typ erkennen
|
||||
const isNormalMap = name.toLowerCase().includes('normal');
|
||||
const quality = isNormalMap ? config.normalMapQuality : config.quality;
|
||||
|
||||
// Mit Sharp verarbeiten
|
||||
let pipeline = sharp(image);
|
||||
|
||||
// Metadaten für Größenberechnung
|
||||
const metadata = await pipeline.metadata();
|
||||
const originalWidth = metadata.width || 0;
|
||||
const originalHeight = metadata.height || 0;
|
||||
|
||||
// Resize wenn nötig
|
||||
if (originalWidth > config.maxSize || originalHeight > config.maxSize) {
|
||||
const scale = config.maxSize / Math.max(originalWidth, originalHeight);
|
||||
pipeline = pipeline.resize(
|
||||
Math.round(originalWidth * scale),
|
||||
Math.round(originalHeight * scale),
|
||||
{ fit: 'inside' }
|
||||
);
|
||||
}
|
||||
|
||||
// Format konvertieren
|
||||
let outputBuffer: Buffer;
|
||||
|
||||
switch (config.format) {
|
||||
case 'webp':
|
||||
outputBuffer = await pipeline
|
||||
.webp({ quality, lossless: isNormalMap })
|
||||
.toBuffer();
|
||||
texture.setMimeType('image/webp');
|
||||
break;
|
||||
|
||||
case 'jpeg':
|
||||
outputBuffer = await pipeline
|
||||
.jpeg({ quality, mozjpeg: true })
|
||||
.toBuffer();
|
||||
texture.setMimeType('image/jpeg');
|
||||
break;
|
||||
|
||||
case 'png':
|
||||
outputBuffer = await pipeline
|
||||
.png({ compressionLevel: 9 })
|
||||
.toBuffer();
|
||||
texture.setMimeType('image/png');
|
||||
break;
|
||||
}
|
||||
|
||||
texture.setImage(new Uint8Array(outputBuffer));
|
||||
|
||||
console.log(`Optimized texture: ${name} (${originalWidth}x${originalHeight} → compressed)`);
|
||||
}
|
||||
}
|
||||
|
||||
// Power-of-Two Konvertierung (für ältere GPUs)
|
||||
function toPowerOfTwo(value: number): number {
|
||||
let result = 1;
|
||||
while (result < value) {
|
||||
result *= 2;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async function ensurePowerOfTwo(document: Document): Promise<void> {
|
||||
const textures = document.getRoot().listTextures();
|
||||
|
||||
for (const texture of textures) {
|
||||
const image = texture.getImage();
|
||||
if (!image) continue;
|
||||
|
||||
const metadata = await sharp(image).metadata();
|
||||
const width = metadata.width || 0;
|
||||
const height = metadata.height || 0;
|
||||
|
||||
const pot2Width = toPowerOfTwo(width);
|
||||
const pot2Height = toPowerOfTwo(height);
|
||||
|
||||
if (width !== pot2Width || height !== pot2Height) {
|
||||
const resized = await sharp(image)
|
||||
.resize(pot2Width, pot2Height, { fit: 'fill' })
|
||||
.toBuffer();
|
||||
|
||||
texture.setImage(new Uint8Array(resized));
|
||||
console.log(`Resized to Power-of-2: ${width}x${height} → ${pot2Width}x${pot2Height}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Compression Comparison
|
||||
|
||||
| Method | Geometry Reduction | Texture Reduction | Decompression |
|
||||
|--------|-------------------|-------------------|---------------|
|
||||
| **Draco** | 90-95% | - | WASM (~300KB) |
|
||||
| **Meshopt** | 70-80% | - | Fast, <50KB |
|
||||
| **Quantization** | 30-50% | - | None needed |
|
||||
| **WebP** | - | 50-70% | Native browser |
|
||||
| **Basis/KTX2** | - | 70-85% | GPU direct |
|
||||
|
||||
---
|
||||
|
||||
## React Three Fiber Integration
|
||||
|
||||
```tsx
|
||||
// components/OptimizedModel.tsx
|
||||
'use client';
|
||||
|
||||
import { useGLTF, useProgress, Html } from '@react-three/drei';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
// Mit Draco Loader
|
||||
export function OptimizedModel({ url }: { url: string }) {
|
||||
const { scene } = useGLTF(url, true); // true = Draco Loader
|
||||
return <primitive object={scene} />;
|
||||
}
|
||||
|
||||
// Preload für bessere UX
|
||||
useGLTF.preload('/models/robot-optimized.glb', true);
|
||||
|
||||
// Loading Progress
|
||||
function LoadingProgress() {
|
||||
const { progress } = useProgress();
|
||||
|
||||
return (
|
||||
<Html center>
|
||||
<div className="loading">
|
||||
Loading... {progress.toFixed(0)}%
|
||||
</div>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
||||
// Lazy Loading mit LOD
|
||||
export function LODModel({ manifest }: { manifest: string }) {
|
||||
// Implement LOD loading...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fazit
|
||||
|
||||
GLTF Optimization bietet:
|
||||
|
||||
1. **Draco**: 90%+ Geometry Reduction
|
||||
2. **WebP Textures**: 50-70% Texture Savings
|
||||
3. **LOD**: Adaptive Quality nach Distanz
|
||||
4. **gltf-transform**: Powerful CLI & API
|
||||
|
||||
Essentiell für performante 3D Web-Experiences.
|
||||
|
||||
---
|
||||
|
||||
## Bildprompts
|
||||
|
||||
1. "3D model optimization pipeline diagram, before and after comparison"
|
||||
2. "Texture compression comparison, WebP vs PNG quality"
|
||||
3. "LOD levels visualization, low to high poly transition"
|
||||
|
||||
---
|
||||
|
||||
## Quellen
|
||||
|
||||
- [glTF Transform Documentation](https://gltf-transform.dev/)
|
||||
- [Draco 3D Compression](https://google.github.io/draco/)
|
||||
- [GLTF Specification](https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html)
|
||||
- [OptimizeGLB](https://optimizeglb.com/)
|
||||
Reference in New Issue
Block a user