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ı (callback) işlemi, tarayıcının doğal yenileme ritmine (standart monitörlerde ~60 Hz, oyuncu monitörlerinde 120-144 Hz) kusursuz bir şekilde hizalanır; böylece donanımın ekran tazelemesi ile render döngüsü eşzamanlı çalışır. Geleneksel setInterval kullanımı sabit milisaniyeler üzerinden ilerlediğinden, ekranın yenileme fazıyla uyuşmazlık yaşatır ve "yırtılma" ya da gereksiz ara kare üretimi gibi sorunlara yol açar. Unutulmamalıdır ki rAF, saniyedeki kare hızını (FPS) sabit kılmaz; yalnızca render planlamasını donanımla uyumlu hale getirir. Animasyon hızlarının kare hızından bağımsız çalışmasını sağlamak delta time konseptinin görevidir.
  • Performans dostu: Tarayıcı sekmesi arka plana atıldığında veya pencere simge durumuna küçültüldüğünde, tarayıcı motoru rAF çağrılarını otomatik olarak duraklatır veya frekansını ciddi oranda düşürür. Bu sayede, ekranda görünmeyen bir sahne için arka planda boş yere render() döngüsü çalıştırılmaz; bu da özellikle mobil ve dizüstü cihazlarda işlemci yükünü azaltarak pil ömrünü korur. Sekme tekrar aktif hale geldiğinde sahnede zaman bazlı ani sıçramalar yaşamamak (mantıksal zamanı tutarlı sürdürmek) adına, clock.getDelta() kullanılarak geçen bu ölü zamanın yönetilmesi gerekir (detaylar THREE.Clock başlığında ve aşağıdaki notta mevcuttur).
  • Daha düzgün çizim: Güncelleme blokları, tarayıcının bir sonraki boyama (paint) adımından hemen önce işletilir. Sahne hesaplamaları ve renderer.render fonksiyonu aynı rAF döngüsü içerisinde art arda tamamlandığında, görsel bütünlük garanti altına alınır (tick sırası prensibi). Ancak rAF bir performans sihri değildir; yalnızca kodunuza en uygun zamanda çalışma fırsatı sunar. Eğer ağır JavaScript hesaplamaları veya GPU yükü, hedeflenen kare bütçesini (örneğin 60 FPS için ~16.6ms) aşarsa, sahnede tekleme (stuttering) ve kare düşüşleri yaşanması kaçınılmazdır.

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, sahne içi zaman yönetimini optimize etmek için THREE.Clock sınıfını sunar. Bu sınıfa ait iki temel zaman okuma metodu birbirinin alternatifi değildir; görev tanımları ve yapısal ayrımları şu şekildedir:

  • .getElapsedTime(), saatin başlatılmasından itibaren geçen sürekli artan toplam süreyi saniye cinsinden döndürür. Çoğunlukla sinüs dalgaları, salınımlar veya kesintisiz dönüş döngüleri gibi matematiksel ve periyodik animasyonlar kurgulanırken tercih edilir.
  • .getDelta(), yalnızca oluşturulan iki kare arasında geçen süreyi hesaplar. Kare hızındaki (FPS) dalgalanmalardan bağımsız, tutarlı bir simülasyon sunmak adına; fiziksel hız, ivme hesaplamaları ve saniye başına sabit birim ilerlemesi gerektiren nesne güncellemelerinde kritik bir rol oynar.

Geliştirme aşamasında, aynı render karesi içerisinde hem toplam süreye hem de delta değerine ihtiyaç duyuyorsanız önemli bir mimari detaya dikkat etmelisiniz: Three.js kaynak kodunda getElapsedTime() çağrısı, arka planda dahili olarak getDelta()'yı tetikler. Çift çağrı yapıp zaman adımlarını bozmamak adına önce
const delta = clock.getDelta()
ile kare süresini hesaplamak, ardından toplam süre değişkenine erişmek için metot yerine doğrudan clock.elapsedTime özelliğini okumak en güvenilir geliştirme kalıbıdır (bu yapı aşağıdaki birleşik örnekte gösterilmiştir).

Teknik tablo: zaman yönetimi yöntemleri

Yöntem Kullanım Alanı Avantajı
getElapsedTime() Dalgalanma efektleri, havada süzülen nesneler ve kesintisiz rotasyon animasyonları. Sinüs ve kosinüs tabanlı periyodik hareket denklemlerinde doğrudan ve pürüzsüz kullanım imkanı sunar.
getDelta() Karakter hareket mekanikleri, mermi yörüngeleri ve ivmeli fiziksel düşüş simülasyonları. Dalgalanan kare hızlarında (60 FPS / 144 FPS) donanımdan bağımsız, tutarlı gerçek zamanlı hız garantisi sağlar.
Date.now() Standart yerleşik JavaScript zaman damgası okumaları. Three.js mimarisinden tamamen bağımsız çalışabilmesidir; ancak rAF döngüsü ile doğal senkronizasyon garantisi sunmaz.

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