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
setIntervalkullanı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 (detaylarTHREE.Clockbaş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.renderfonksiyonu 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).