holodepth

Three.js · Animasyon

Animasyon döngüsü ve zaman yönetimi

3B sahnede bir nesnenin hareket etmesi, aslında saniyede onlarca kez sahnenin yeniden çizilmesidir; film kareleri gibi: her karede konum biraz değişir, göz bunu süreklilik olarak okur. Kamera veya ışık sabit kalsa bile dönen bir küp, “canlı” görünüm için bu döngüye ihtiyaç duyar; tek seferlik render() yalnızca fotoğraf çeker.

Profesyonel projede hedef iki yönlüdür: akıcılık (tarayıcı yenilemesiyle uyum) ve zaman tutarlılığı (144 Hz monitörde de 60 Hz telefonda da aynı saniyede aynı mesafe). Bunun için requestAnimationFrame kareyi planlar, delta time hareketi FPS’ten ayırır, THREE.Clock süreyi ölçer. Güncelle → çiz sırası, render döngüsü ile aynı kalıptadır.

Bu sayfa döngü ve zamanın iskeletini kurar: rAF, tick, delta, getDelta() tuzakları, zaman API tablosu ve tam örnek döngü kodu. Nesneyi nasıl yumuşak taşıyacağınız (lerp, easing, mixer); Lerp & Easing konusundadır; önce saatin doğru işlemesi, sonra hareketin estetiği.

Döngü, zaman ve kare hızı

requestAnimationFrame: modern döngü

Eskiden animasyonlar için setInterval kullanılırdı; modern web standartlarında tek bir "kral" vardır: requestAnimationFrame (rAF).

rAF, setInterval’e göre üç pratik nedenden “varsayılan döngü”dür:

  • Akıllı senkronizasyon: Geri çağrı, tarayıcının yenileme ritmine (çoğu masaüstünde ~60 Hz, oyuncu monitörlerinde 120–144 Hz) hizalanır — ekran ile çizim aynı “nefes”e girer. setInterval sabit milisaniye sayar; yenileme fazıyla kaydıkça bazen iki kare atlar, bazen gereksiz ara kare üretir. rAF FPS’i sabit yapmaz; yalnızca planlamayı ekrana uyarlar — hareket hızını sabitlemek delta time işidir.
  • Performans dostu: Sekme arka plandayken veya pencere küçültülünce tarayıcı çoğu zaman rAF’i duraklatır veya seyrekleştirir; görünmeyen sekmede sürekli render() çalıştırmazsınız. Mobil ve dizüstünde pil ömrü için belirgindir. Döngü durunca bile mantıksal zamanı doğru sürdürmek istiyorsanız yeniden görünür olunca clock.getDelta() ile “sıçramayı” yönetmek gerekir — ayrıntı THREE.Clock ve alttaki callout’ta.
  • Daha düzgün çizim: Güncelleme kodu, bir sonraki boyama ( paint) öncesinde çalıştırılır; sahne hesapları ile renderer.render aynı rAF geri çağrısında bitirilirse kare bütünlüğü korunur (tick sırası). Ağır JS/GPU işi kare bütçesini aşarsa yine tekleme olur — rAF sihir değildir, yalnızca doğru zamanda çalışma fırsatı verir.

requestAnimationFrame ekranın yenileme hızına bağlı çalışır; bu yüzden farklı cihaz ve monitörlerde farklı FPS değerleri oluşabilir. Bu da delta time ile kare hızından bağımsız hareket gerektirmesinin nedenidir.

Tick: güncelle, sonra çiz

Oyun ve etkileşimli 3B geliştirmede her requestAnimationFrame geri çağrısındaki animate gövdesi genelde bir tick (kare mantığı) sayılır. Tek tick = tek “dünya anı”: önce sahne durumu güncellenir, sonra ekrana basılır. Bu sıra tersine çevrilmez; önce çizip sonra taşırsanız izleyici bir kare geriden görür.

Güncelleme aşaması bu tick’te toplanır: klavye / fare girdisi, orbit veya fizik adımı, AnimationMixer.update(delta), parçacık sistemleri, kamera takibi. Burada kullanılan süre çoğu zaman delta time ile ölçülür; amaç “bu karede ne kadar zaman geçti?” sorusuna göre konum ve hız hesaplamak. Mantıksal durum (matrisler, uniform’lar) bu aşamada oturur; henüz piksel üretilmemiştir.

Çizim aşaması tek (veya bilinçli olarak birkaç) renderer.render(scene, camera) çağrısıdır; güncellenmiş sahneyi GPU’ya gönderir. Bu döngü, Renderer · render döngüsü sayfasındaki kalıbı her kare tekrarlar; pipeline böylece sürekli yeniden işler: culling, malzeme, ışık, raster. Statik vitrinde bir kez render() yeterliyken, canlı sahnenin kalbi bu tick ritminde atar.

Tipik iskelet: requestAnimationFrame(animate)getDelta() → güncellemeler → render(). Tam örnek aşağıdaki kod kutusunda. Tick başına gereksiz çift render() veya güncellemeden önce çizim yaygın performans ve “titreme” hatalarıdır; rAF doğru zamanı verir, sırayı siz korursunuz.

Delta time: kare hızından bağımsızlık (FPS independence)

En sık yapılan hata, nesneleri her karede sabit bir adımla hareket ettirmektir; örneğin cube.rotation.y += 0.01. Bu ifade aslında “saniyede şu kadar radyan” demek değildir; kare başına 0,01 radyan demektir. Kare sayısı saniyede arttıkça dönüş hızı da artar; hareket, ekranın FPS’ine bağımlı kalır.

Sorun: 60 Hz’de bu kod “normal” hissedilirken 144 Hz monitörde yaklaşık 2,4 kat daha hızlı döner; düşük FPS’li telefonda ise ağır çalışır. Aynı projeyi farklı cihazlarda test eden ekip, “bazı kullanıcılarda çok hızlı” şikâyetini çoğu zaman bu kalıptan alır. Oyun ve simülasyonda hedef kare hızından bağımsızlık (frame-rate independence); yani hızı saniye cinsinden tanımlamaktır.

Çözüm (delta time): Bir önceki tick’ten bu yana geçen süreyi ölçün (çoğunlukla saniye cinsinden delta) ve her güncellemeyi bununla çarpın: position += hız * delta, rotation += açısalHız * delta. Donanım saniyede 60 veya 144 kare üretse de, saniye başına toplam yer değiştirme aynı kalır. Ölçümü elle yapmak yerine THREE.Clock ve getDelta() kullanılır; büyük delta sıçramaları (sekme değişimi, donma) ayrı ele alınır; alttaki Holodepth notu.

THREE.Clock ile tipik kullanım:

const delta = clock.getDelta();
mesh.position.x += 2 * delta; // saniyede 2 birim (birim/s × saniye)

2 * delta ifadesi, hızın birim/saniye cinsinden verildiği düşünüldüğünde her karede ne kadar yer değiştireceğini verir; FPS artsın azalsın, saniyede toplam mesafe aynı kalır.

THREE.Clock: zamanı ölçmek

Three.js, zamanı yönetmek için THREE.Clock sunar. İki temel okuma birbirinin yerine geçmez; kısaca ayrım şöyledir:

  • .getElapsedTime() sürekli artan toplam süreyi (saniye) döndürür; genelde matematiksel / periyodik animasyonlar için kullanılır (sinüs, dalga, dönen değerler).
  • .getDelta() yalnızca iki kare arasındaki süreyi verir; fiziksel hız, ivme ve sabit birim/saniye ile hareket gerektiren güncellemeler için tercih edilir.

Uygulamada aynı karede hem toplam süreye hem delta'ya ihtiyaç varsa: Three.js kaynaklarında getElapsedTime() içeride getDelta() çağırdığı için önce const delta = clock.getDelta() deyip, toplam süre için clock.elapsedTime özelliğini okumak güvenli bir kalıptır (aşağıdaki birleşik örnek).

Teknik tablo: zaman yönetimi yöntemleri

Yöntem Kullanım alanı Avantajı
getElapsedTime() Dalgalanma, yüzen nesneler, sürekli dönüşler. Sin / cos ile periyodik hareketlerde doğrudan kullanım.
getDelta() Karakter hareketi, mermi hızı, fiziksel düşüş. 60 FPS / 144 FPS farkında aynı gerçek zaman hızı.
Date.now() Düz JavaScript zaman damgası. Three.js'e bağlı değildir; rAF ile doğal senkron garantisi yoktur.

Uygulama: profesyonel render döngüsü

Aşağıdaki örnek, aynı animate içinde hem süre tabanlı dalga hareketini (elapsedTime ile sinüs) hem delta ile FPS bağımsız dönüşü birleştirir. mesh, scene, camera ve renderer önceden tanımlıdır.

const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);

  const delta = clock.getDelta();
  const elapsedTime = clock.elapsedTime;

  // Dalga hareketi (sürekli artan süre → sin/cos için uygun)
  mesh.position.y = Math.sin(elapsedTime) * 0.5;

  // FPS bağımsız dönüş (radyan/s × saniye)
  mesh.rotation.y += 2 * delta;

  renderer.render(scene, camera);
}

animate();

Holodepth notu: getDelta() tuzağı

getDelta() metodunu aynı döngü içinde birden fazla kez çağırmayın. getElapsedTime() da içeride getDelta() tetiklediği için peş peşe getElapsedTime(); getDelta(); yazmak ikinci bir delta ölçümü yaratır ve süreleri bozar. Hem delta hem toplam süre gerekiyorsa: önce const delta = clock.getDelta(), toplam süre için clock.elapsedTime (veya yalnızca biri yeterliyse tek bir API kullanın).