🎨 3D Mudelid ja Animatsioon
GLTF/GLB laadimine, Blenderi põhitööd, skelett-animatsioon, morph targets — too oma 3D maailm ellu!
📦 Samm 1 — GLTF/GLB Mudelite Laadimine
GLTF on "JPEG 3D maailma jaoks" — avatud standard, kompaktne, toetab texture'e, animatsioone ja skelette. GLB on selle binaarne versioon (üks fail).
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
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)
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
- Modelleeri Blenderis (kuubik → sculpt → retopology)
- UV unwrap → mapi tekstuurid
- Armature (luustik) → weight paint
- Animeeri (keyframe'id) → NLA editor
- Ekspordi GLTF/GLB formaadis
# 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.