Menu
Back to Blog
2 min read
Technologie

GLTF 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

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 1024

Node.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

MethodGeometry ReductionTexture ReductionDecompression
**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:

  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