# 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 { 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 { 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 { 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 ; } // Preload für bessere UX useGLTF.preload('/models/robot-optimized.glb', true); // Loading Progress function LoadingProgress() { const { progress } = useProgress(); return (
Loading... {progress.toFixed(0)}%
); } // 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/)