MOODUL 11b · TASE 4 — EDASIJÕUDNUD+

🎨 3D Mudelid ja Animatsioon

GLTF/GLB laadimine, Blenderi põhitööd, skelett-animatsioon, morph targets — too oma 3D maailm ellu!

⏱️ ~6-8 tundi 📝 5 harjutust 🔴 Edasijõudnud+

📦 Samm 1 — GLTF/GLB Mudelite Laadimine

ℹ️ Miks GLTF?

GLTF on "JPEG 3D maailma jaoks" — avatud standard, kompaktne, toetab texture'e, animatsioone ja skelette. GLB on selle binaarne versioon (üks fail).

modelLoader.ts — GLTF laadimine
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
import * as THREE from 'three';

class ModelLoader {
  private gltfLoader: GLTFLoader;
  private cache: Map<string, THREE.Group> = new Map();

  constructor() {
    this.gltfLoader = new GLTFLoader();

    // Draco nihutusmoodul väiksemate failide jaoks
    const dracoLoader = new DRACOLoader();
    dracoLoader.setDecoderPath('/draco/');
    this.gltfLoader.setDRACOLoader(dracoLoader);
  }

  async load(url: string): Promise<THREE.Group> {
    // Cache kontroll
    if (this.cache.has(url)) {
      return this.cache.get(url)!.clone();
    }

    return new Promise((resolve, reject) => {
      this.gltfLoader.load(
        url,
        (gltf) => {
          // Varjud sisse
          gltf.scene.traverse((child) => {
            if ((child as THREE.Mesh).isMesh) {
              child.castShadow = true;
              child.receiveShadow = true;
            }
          });

          this.cache.set(url, gltf.scene);
          resolve(gltf.scene.clone());
        },
        (progress) => {
          const pct = (progress.loaded / progress.total * 100).toFixed(0);
          console.log(`Laen: ${url} — ${pct}%`);
        },
        (error) => reject(error)
      );
    });
  }

  // Laadi mitu mudelit paralleelselt
  async loadBatch(urls: string[]): Promise<Map<string, THREE.Group>> {
    const results = new Map<string, THREE.Group>();
    const promises = urls.map(async (url) => {
      const model = await this.load(url);
      results.set(url, model);
    });
    await Promise.all(promises);
    return results;
  }
}

// Kasutamine:
// const loader = new ModelLoader();
// const tree = await loader.load('/models/tree.glb');
// scene.add(tree);

🦴 Samm 2 — Skelett-animatsioon

characterAnimator.ts — AnimationMixer
class CharacterAnimator {
  private mixer: THREE.AnimationMixer;
  private actions: Map<string, THREE.AnimationAction> = new Map();
  private currentAction: THREE.AnimationAction | null = null;

  constructor(model: THREE.Group, animations: THREE.AnimationClip[]) {
    this.mixer = new THREE.AnimationMixer(model);

    // Registreeri kõik animatsioonid
    for (const clip of animations) {
      const action = this.mixer.clipAction(clip);
      this.actions.set(clip.name, action);
      console.log(`Animatsioon: "${clip.name}" (${clip.duration.toFixed(1)}s)`);
    }
  }

  play(name: string, fadeTime = 0.3): void {
    const newAction = this.actions.get(name);
    if (!newAction || newAction === this.currentAction) return;

    newAction.reset();
    newAction.fadeIn(fadeTime);
    newAction.play();

    // Fade out eelmine
    if (this.currentAction) {
      this.currentAction.fadeOut(fadeTime);
    }

    this.currentAction = newAction;
  }

  playOnce(name: string, onComplete?: () => void): void {
    const action = this.actions.get(name);
    if (!action) return;

    action.reset();
    action.clampWhenFinished = true;
    action.loop = THREE.LoopOnce;
    action.play();

    if (onComplete) {
      this.mixer.addEventListener('finished', (e) => {
        if (e.action === action) onComplete();
      });
    }
  }

  update(delta: number): void {
    this.mixer.update(delta);
  }
}

// GLTF-st laadimine:
// const gltf = await gltfLoader.loadAsync('/models/character.glb');
// const animator = new CharacterAnimator(gltf.scene, gltf.animations);
// animator.play('idle');
// Mängutsüklis: animator.update(delta);

🎭 Samm 3 — Morph Targets (Blend Shapes)

morphTargets.ts — Näoilmed
class FaceController {
  private mesh: THREE.Mesh;
  private morphDict: Record<string, number> = {};

  constructor(model: THREE.Group) {
    // Leia mesh, millel on morph targets
    model.traverse((child) => {
      const m = child as THREE.Mesh;
      if (m.isMesh && m.morphTargetDictionary) {
        this.mesh = m;
        this.morphDict = m.morphTargetDictionary;
      }
    });
  }

  setExpression(name: string, weight: number): void {
    const index = this.morphDict[name];
    if (index !== undefined && this.mesh.morphTargetInfluences) {
      this.mesh.morphTargetInfluences[index] = THREE.MathUtils.clamp(weight, 0, 1);
    }
  }

  // Sujuv üleminek
  lerpExpression(name: string, target: number, speed: number, delta: number): void {
    const index = this.morphDict[name];
    if (index !== undefined && this.mesh.morphTargetInfluences) {
      const current = this.mesh.morphTargetInfluences[index];
      this.mesh.morphTargetInfluences[index] = THREE.MathUtils.lerp(current, target, speed * delta);
    }
  }

  smile(): void {
    this.setExpression('mouthSmile', 0.8);
    this.setExpression('eyeSquint', 0.3);
  }

  blink(duration = 0.15): void {
    this.setExpression('eyeBlink', 1);
    setTimeout(() => this.setExpression('eyeBlink', 0), duration * 1000);
  }
}

🔧 Samm 4 — Blenderi Põhitöövoog

💡 Blenderi → Three.js töövoog:
  • Modelleeri Blenderis (kuubik → sculpt → retopology)
  • UV unwrap → mapi tekstuurid
  • Armature (luustik) → weight paint
  • Animeeri (keyframe'id) → NLA editor
  • Ekspordi GLTF/GLB formaadis
Blender ekspordisätted
# Blenderis: File → Export → glTF 2.0 (.glb)
#
# Sätted:
#   Format: glTF Binary (.glb) — üks fail
#   Include: Selected Objects (ainult valitud)
#   Transform: +Y Up (Three.js standard)
#   Mesh:
#     ✅ Apply Modifiers
#     ✅ UVs, Normals, Vertex Colors
#     Compression: Draco (väiksem fail)
#   Animation:
#     ✅ Animations
#     ✅ Shape Keys (morph targets)
#     ✅ Skinning (skelett)
#     Sampling Rate: 1 (iga kaader)
#
# Optimeeri:
#   - Tekstuurid max 1024x1024 (mängus)
#   - Polygon count: all < 50k (mobiil), < 200k (desktop)
#   - Eemalda peidetud verteksid

📝 Harjutused

  • GLTF ModelLoader

    Loo ModelLoader klass: load, loadBatch, cache. Laadi vähemalt 3 erinevat mudelit (nt puu, maja, tegelane). Lisa loading progress bar.

  • Skelett-animatsiooni kontroller

    Loo CharacterAnimator: idle, walk, run, jump. Sujuv fade üleminek (crossfade). Kiirus sõltub liikumisest. Animatsiooni sündmused (jalajäljed).

  • Blenderi mudel → Three.js

    Loo Blenderis lihtne tegelane: kuubik-inimene → armature → walk cycle. Ekspordi GLB. Laadi Three.js-i ja mängi animatsioon. Lisa varjud.

  • 3D inventari süsteem

    Loo 3D inventar: mudelid kuvatakse OrbitControls'iga. Kliki esemele → 3D preview vaates saab pöörata. Item nimed ja kirjeldused HTML overlay'na.

  • Animeeritud 3D keskkond

    Ehita 3D stseen: tuulega liikuvad puud (vertex shader), voolav vesi (animated UV), päev/öö tsükkel. Kasuta vähemalt 5 erinevat mudelit.

← Moodul 11 Moodul 12: AI Mängudes →