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.
setIntervalsabit 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 oluncaclock.getDelta()ile “sıçramayı” yönetmek gerekir — ayrıntıTHREE.Clockve 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.renderaynı 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).