Three.js Animation — Creative Web

Three.js Animation Animasi membuat scene 3D menjadi hidup. Di Three.js, kita bisa menganimasikan posisi, rotasi, skala, warna, dan hampir semua properti…

Animasi di three.js artinya mengubah properti benda (rotasi, posisi, skala) sedikit tiap frame, lalu render lagi. Bayangin meja putar di studio foto produk Raka: mejanya berputar dengan kecepatan tetap, misalnya satu putaran tiap 8 detik, gak peduli kameranya memotret 60 atau 120 foto per detik. Kecepatan itu milik waktu, bukan milik jumlah foto. Animasi yang baik juga begitu: dihitung dari waktu.

Di lesson ini Mug Sablon Kopi dari lesson sebelumnya kita taruh di meja putar: berputar sendiri, berhenti dengan halus di sisi sablon saat diminta, dan melompat dengan klip keyframe.

🧪 Tebakan cepat: mug berputar 0,8 radian per detik. Raka pindah ke tab lain selama 10 detik, lalu kembali. Loop-nya memakai timer.getDelta() tanpa timer.connect(document). Apa yang terjadi di frame pertama setelah tab kembali? Tebak dulu, jawabannya ada di bagian "Tab tersembunyi dan gerakan dikurangi".

Loop: requestAnimationFrame

function animate() {
  requestAnimationFrame(animate); // minta dipanggil lagi sebelum repaint berikutnya
  cube.rotation.x += 0.01;
  renderer.render(scene, camera);
}
animate();

Waktu, bukan frame: THREE.Timer

rotation.y += 0.01 menambah per frame: 0,6 radian per detik di layar 60 Hz, 1,2 di 120 Hz. Supaya sama di mana pun, kalikan kecepatan per detik dengan delta, lama frame barusan dalam detik.

const timer = new THREE.Timer();
timer.connect(document);  // pakai Page Visibility API, dibahas di bawah
const KECEPATAN = 0.8;    // radian per detik: 1 putaran = 2π / 0,8 = 7,85 detik

renderer.setAnimationLoop((time) => {
  timer.update(time);              // sekali per frame, sebelum dibaca
  const delta = timer.getDelta();  // detik sejak frame sebelumnya
  meja.rotation.y += KECEPATAN * delta;
  renderer.render(scene, camera);
});
Layar delta per frame Tambah per frame Per detik
60 Hz 0,0167 s 0,0133 rad 0,8 rad
120 Hz 0,0083 s 0,0067 rad 0,8 rad

Easing: berhenti di sisi sablon

Tombol "Lihat sablon" memutar meja ke kelipatan 2π terdekat (sablon menghadap kamera, lesson Geometri & Material) dalam 0,8 detik:

const easeInOutCubic = (p) => (p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2);
function lihatSablon() {
  berputar = false;
  const ke = Math.round(meja.rotation.y / (2 * Math.PI)) * 2 * Math.PI; // paling jauh setengah putaran
  putaran = { dari: meja.rotation.y, ke, t: 0 };
}
// di dalam loop:
if (putaran) {
  putaran.t += delta;
  const p = Math.min(putaran.t / 0.8, 1);   // 0 sampai 1
  meja.rotation.y = putaran.dari + (putaran.ke - putaran.dari) * easeInOutCubic(p);
  if (p === 1) putaran = null;
}

Easing adalah kurva yang mengubah kemajuan waktu p jadi kemajuan gerak: di p 0,25 gerakannya baru 0,0625, di 0,5 tepat 0,5, di 0,75 sudah 0,9375. Pelan, cepat, pelan, seperti orang yang memutar meja dengan tangan. Tanpa easing (p apa adanya), mug mulai dan berhenti mendadak.

Klip keyframe: AnimationMixer

Keyframe adalah nilai di detik tertentu; three.js mengisi nilai di antaranya. Bawaannya lurus (linear): di tengah-tengah waktu antara dua keyframe, nilainya juga di tengah-tengah, dan sudut hasil slerp pun berubah rata. Klip "lompat" (1,2 detik) dibuat tangan, persis bentuk klip yang datang dari Blender di gltf.animations:

const SUMBU_Y = new THREE.Vector3(0, 1, 0);
const q = new THREE.Quaternion();
const lompat = new THREE.AnimationClip('lompat', 1.2, [
  new THREE.NumberKeyframeTrack('.position[y]', [0, 0.6, 1.2], [0, 0.4, 0]),
  new THREE.QuaternionKeyframeTrack('.quaternion', [0, 0.4, 0.8, 1.2],
    [0, 1, 2, 3].flatMap((i) => q.setFromAxisAngle(SUMBU_Y, (i * 2 * Math.PI) / 3).toArray())),
]);
const mixer = new THREE.AnimationMixer(mug);
const aksi = mixer.clipAction(lompat).setLoop(THREE.LoopOnce);
tombolLompat.addEventListener('click', () => aksi.reset().play());
// di dalam loop, sesudah menghitung delta:
mixer.update(delta);

Tab tersembunyi dan gerakan dikurangi

Selama tab tersembunyi, requestAnimationFrame berhenti. Saat tab kembali, frame pertamanya datang 10 detik setelah frame terakhir, jadi tanpa connect delta-nya 10 detik dan meja melompat 0,8 × 10 = 8 radian (1,27 putaran) dalam satu frame. Itu jawaban tebakannya. Ada dua obat:

Untuk pengunjung yang sistemnya meminta gerakan dikurangi (vertigo, mabuk gerak), jangan berputar sendiri:

const gerakDikurangi = matchMedia('(prefers-reduced-motion: reduce)');
let berputar = !gerakDikurangi.matches;
gerakDikurangi.addEventListener('change', () => { berputar = !gerakDikurangi.matches; });

"Lihat sablon" lalu pindah langsung tanpa animasi, dan klip lompat dilewati.

Jangan bikin objek baru di dalam loop

Tiap new THREE.Vector3() atau .clone() di loop jadi sampah yang nanti dibersihkan garbage collector (GC), dan selama itu JavaScript berhenti sebentar. Diukur di Chrome (laptop, 120 fps, 8 detik, 1.000 partikel uap): memakai ulang satu vektor 1 kali GC; new THREE.Vector3() per partikel 7 kali; posisi.clone() per partikel 34 kali, total 6,6 ms. Di laptop tiap jeda cuma sekitar 0,3 ms; di HP yang CPU-nya 4 kali lebih lambat, perkiraannya sekitar 1,3 ms, padahal satu frame 120 Hz cuma 8,3 ms. Buat objek sementara sekali di luar loop:

const sementara = new THREE.Vector3();               // sekali saja
for (const p of partikel) p.position.add(sementara.set(0, 0.3 * delta, 0));

⚠️ Jebakan yang sering ketemu

TL;DR: Animasi = ubah properti tiap frame lalu render. requestAnimationFrame (atau renderer.setAnimationLoop) jalan sekali per refresh layar dan berhenti di tab tersembunyi; callback-nya menerima time, bukan delta. Hitung gerak dari waktu: THREE.Timer (update(time), getDelta(), getElapsed()), bukan THREE.Clock yang deprecated sejak r183; += KECEPATAN * delta, Math.sin(t) * 3 untuk bolak-balik -3 sampai 3. Easing (easeInOutCubic) membuat mulai dan berhenti halus. Klip keyframe: AnimationClip + NumberKeyframeTrack / QuaternionKeyframeTrack, AnimationMixer, clipAction(...).play(), mixer.update(delta). timer.connect(document) mencegah lompatan setelah tab tersembunyi, prefers-reduced-motion mematikan putaran otomatis, dan objek sementara dibuat sekali di luar loop.

Yang akan kamu pelajari