2 min read
TechnologieGLTF Model Optimization für Web
3D Modelle für Web optimieren. GLTF/GLB Format, Draco Compression, Texture Optimization und LOD Strategien.
GLTFGLBDraco3D OptimizationWeb 3DTexture Compression

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
# 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 1024Node.js Optimization Script
// 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
// 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
// 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
// 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
// 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:
- Draco: 90%+ Geometry Reduction
- WebP Textures: 50-70% Texture Savings
- LOD: Adaptive Quality nach Distanz
- gltf-transform: Powerful CLI & API
Essentiell für performante 3D Web-Experiences.
Bildprompts
- "3D model optimization pipeline diagram, before and after comparison"
- "Texture compression comparison, WebP vs PNG quality"
- "LOD levels visualization, low to high poly transition"