WebAssembly ile Fizik Motoru Simülasyonu Geliştirme

Fizik simülasyonları, web platformunda her zaman bir sıkıntı noktası olmuştur. JavaScript’in tek thread’li yapısı, garbage collection duraklamaları ve yorumlanan kodun yavaşlığı; rijit cisim dinamiği, parçacık sistemleri veya akışkan simülasyonları gibi hesaplama yoğun senaryolarda can sıkıcı sonuçlar doğuruyordu. Bunu bizzat yaşadım. Bir proje kapsamında tarayıcı tabanlı bir mühendislik simülasyon aracı geliştirirken, JavaScript ile yazılmış fizik motorumuzun yüz civarında rijit cisimle bile frame drop yaşattığını görünce alternatif arayışına girdim. WebAssembly bu noktada devreye girdi ve sonuçlar gerçekten şaşırtıcıydı.

WebAssembly’nin Fizik Simülasyonu İçin Neden Anlamlı Olduğu

WebAssembly, C, C++ veya Rust gibi dillerde yazılmış kodun tarayıcıda çalışabilmesi için derlenmiş bir binary format sunar. Ama bunu salt bir “hız artışı” olarak düşünmek eksik bir bakış açısı olur. Asıl mesele, deterministik çalışma zamanı davranışı ve bellek yönetimi üzerindeki kontrol.

Fizik motorları için bu iki özellik kritiktir. Bir rigid body simülasyonu yaparken frameler arası tutarlılık şarttır. JavaScript’in garbage collector’ı tam integration loop çalışırken devreye girerse, o frame’de hesaplanan kuvvetler ve pozisyonlar bir sonraki frame ile tutarsızlık yaratabilir. WASM modüllerinde bu sorun yoktur; belleği kendiniz yönetirsiniz.

Öte yandan WASM, SIMD (Single Instruction Multiple Data) talimatlarını destekler. Modern fizik motorları vektör hesaplamalarını toplu olarak yapabilmek için SIMD’den yoğun biçimde yararlanır. Bu, tek işlemde birden fazla float üzerinde aynı operasyonu çalıştırmak demektir.

Ortam Kurulumu: Emscripten ile Başlangıç

Fizik motorlarının büyük çoğunluğu C veya C++ ile yazılmıştır. Bullet Physics, Box2D, ODE bunların başında gelir. Bu motorları WASM’a derlemek için Emscripten araç zincirine ihtiyaç duyarsınız.

# Emscripten SDK kurulumu
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk

# En güncel sürümü indir ve aktifleştir
./emsdk install latest
./emsdk activate latest

# Ortam değişkenlerini ayarla (her oturum için gerekli)
source ./emsdk_env.sh

# Kurulumu doğrula
emcc --version

Emscripten kurulduktan sonra basit bir test yaparak C kodunun WASM’a derlendiğini doğrulayalım:

# Basit bir C dosyası oluştur
cat > test_wasm.c << 'EOF'
#include <stdio.h>
#include <emscripten/emscripten.h>

EMSCRIPTEN_KEEPALIVE
float hesapla_kuvvet(float kutle, float ivme) {
    return kutle * ivme;
}

int main() {
    printf("WASM modulu hazirn");
    return 0;
}
EOF

# WASM'a derle
emcc test_wasm.c -o test_wasm.js 
  -s WASM=1 
  -s EXPORTED_FUNCTIONS='["_hesapla_kuvvet", "_main"]' 
  -s EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'

# Çıktı dosyalarını kontrol et
ls -lh test_wasm.js test_wasm.wasm

Box2D ile 2D Fizik Simülasyonu

Box2D, 2D oyun ve simülasyon projelerinde altın standart sayılır. Orijinali C++ ile yazılmıştır ve Emscripten aracılığıyla WASM’a derlenmiş versiyonları mevcuttur. Ancak kendi derlemenizi yapmanız, optimizasyon parametreleri üzerinde tam kontrol sağlar.

# Box2D kaynak kodunu indir
git clone https://github.com/erincatto/box2d.git
cd box2d

# Build dizini oluştur ve CMake ile yapılandır
mkdir build_wasm && cd build_wasm

emcmake cmake .. 
  -DBOX2D_BUILD_UNIT_TESTS=OFF 
  -DBOX2D_BUILD_TESTBED=OFF 
  -DCMAKE_BUILD_TYPE=Release

# Derle
emmake make -j$(nproc)

Box2D’yi derledikten sonra, kendi wrapper kodunuzu yazmanız gerekir. Bu wrapper, JavaScript ile WASM arasında veri alışverişini yönetir:

// box2d_wrapper.cpp
#include <box2d/box2d.h>
#include <emscripten/emscripten.h>
#include <vector>
#include <cstring>

static b2World* dunya = nullptr;
static std::vector<b2Body*> cisimler;

extern "C" {

EMSCRIPTEN_KEEPALIVE
void simulasyon_baslat(float yercekim_x, float yercekim_y) {
    b2Vec2 yercekim(yercekim_x, yercekim_y);
    dunya = new b2World(yercekim);
}

EMSCRIPTEN_KEEPALIVE
int cisim_ekle(float x, float y, float genislik, float yukseklik, int dinamik_mi) {
    b2BodyDef bodyDef;
    bodyDef.position.Set(x, y);
    
    if (dinamik_mi) {
        bodyDef.type = b2_dynamicBody;
    }
    
    b2Body* cisim = dunya->CreateBody(&bodyDef);
    
    b2PolygonShape sekil;
    sekil.SetAsBox(genislik / 2.0f, yukseklik / 2.0f);
    
    if (dinamik_mi) {
        b2FixtureDef fixtureDef;
        fixtureDef.shape = &sekil;
        fixtureDef.density = 1.0f;
        fixtureDef.friction = 0.3f;
        fixtureDef.restitution = 0.5f;
        cisim->CreateFixture(&fixtureDef);
    } else {
        cisim->CreateFixture(&sekil, 0.0f);
    }
    
    cisimler.push_back(cisim);
    return cisimler.size() - 1;
}

EMSCRIPTEN_KEEPALIVE
void adim_at(float zaman_adimi, int hiz_iterasyon, int pozisyon_iterasyon) {
    dunya->Step(zaman_adimi, hiz_iterasyon, pozisyon_iterasyon);
}

EMSCRIPTEN_KEEPALIVE
void pozisyonlari_al(float* buffer, int cisim_sayisi) {
    for (int i = 0; i < cisim_sayisi && i < (int)cisimler.size(); i++) {
        b2Vec2 pos = cisimler[i]->GetPosition();
        float aci = cisimler[i]->GetAngle();
        buffer[i * 3] = pos.x;
        buffer[i * 3 + 1] = pos.y;
        buffer[i * 3 + 2] = aci;
    }
}

EMSCRIPTEN_KEEPALIVE
void simulasyonu_temizle() {
    cisimler.clear();
    delete dunya;
    dunya = nullptr;
}

} // extern "C"

Bu wrapper’ı WASM’a derleyelim:

emcc box2d_wrapper.cpp 
  -I../include 
  -L. -lbox2d 
  -o fizik_motoru.js 
  -s WASM=1 
  -s MODULARIZE=1 
  -s EXPORT_NAME="FizikMotoru" 
  -s EXPORTED_FUNCTIONS='["_simulasyon_baslat","_cisim_ekle","_adim_at","_pozisyonlari_al","_simulasyonu_temizle"]' 
  -s EXPORTED_RUNTIME_METHODS='["ccall","cwrap","HEAPF32"]' 
  -s INITIAL_MEMORY=67108864 
  -s ALLOW_MEMORY_GROWTH=1 
  -O3 
  -msimd128

-O3 ve -msimd128 flag’lerine dikkat edin. Bu iki parametre, vektörel hesaplamalar için ciddi performans artışı sağlar. Özellikle msimd128, destekleyen tarayıcılarda SIMD talimatlarını aktifleştirir.

JavaScript Tarafında Entegrasyon

WASM modülünü JavaScript’ten kullanmak için bellek yönetimine özen göstermeniz gerekiyor. Shared memory üzerinden veri transferi, her frame’de gereksiz kopyalama işleminden kaçınmanın temel yöntemidir:

// fizik_sahne.js
class FizikSahne {
  constructor() {
    this.modul = null;
    this.cismSayisi = 0;
    this.pozisyonBuffer = null;
    this.pozisyonPtr = null;
    this.animasyonId = null;
  }

  async baslat(canvas) {
    // WASM modülünü yükle
    this.modul = await FizikMotoru();
    
    // Simülasyonu başlat (yerçekimi: x=0, y=-10)
    this.modul._simulasyon_baslat(0, -10);
    
    // Canvas context al
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    
    // Zemin ekle (statik cisim)
    this.modul._cisim_ekle(0, -10, 50, 1, 0);
    this.cismSayisi++;
    
    // Dinamik cisimler ekle
    for (let i = 0; i < 50; i++) {
      const x = (Math.random() - 0.5) * 20;
      const y = Math.random() * 30 + 5;
      this.modul._cisim_ekle(x, y, 1, 1, 1);
      this.cismSayisi++;
    }
    
    // Pozisyon verisi için WASM heap'te bellek ayır
    // Her cisim için: x, y, aci = 3 float = 12 byte
    const bufferBoyutu = this.cismSayisi * 3 * 4;
    this.pozisyonPtr = this.modul._malloc(bufferBoyutu);
    this.pozisyonBuffer = new Float32Array(
      this.modul.HEAPF32.buffer,
      this.pozisyonPtr,
      this.cismSayisi * 3
    );
    
    this.cizimDongusuBaslat();
  }

  cizimDongusuBaslat() {
    const sabitZamanAdimi = 1 / 60;
    
    const guncelle = () => {
      // Fizik adımını hesapla
      this.modul._adim_at(sabitZamanAdimi, 8, 3);
      
      // Pozisyonları WASM heap'ten oku
      this.modul._pozisyonlari_al(this.pozisyonPtr, this.cismSayisi);
      
      // Canvas'ı temizle ve yeniden çiz
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
      this.cisimleriCiz();
      
      this.animasyonId = requestAnimationFrame(guncelle);
    };
    
    this.animasyonId = requestAnimationFrame(guncelle);
  }

  cisimleriCiz() {
    const olcek = 20; // pixel/metre
    const merkezX = this.canvas.width / 2;
    const merkezY = this.canvas.height / 2;
    
    this.ctx.fillStyle = '#3498db';
    
    for (let i = 0; i < this.cismSayisi; i++) {
      const x = this.pozisyonBuffer[i * 3] * olcek + merkezX;
      const y = -this.pozisyonBuffer[i * 3 + 1] * olcek + merkezY;
      const aci = this.pozisyonBuffer[i * 3 + 2];
      
      this.ctx.save();
      this.ctx.translate(x, y);
      this.ctx.rotate(-aci);
      this.ctx.fillRect(-10, -10, 20, 20);
      this.ctx.restore();
    }
  }

  durdur() {
    if (this.animasyonId) {
      cancelAnimationFrame(this.animasyonId);
    }
    // Belleği temizle
    if (this.pozisyonPtr) {
      this.modul._free(this.pozisyonPtr);
    }
    this.modul._simulasyonu_temizle();
  }
}

Web Worker ile Ana Thread’i Koruma

Yüzlerce ya da binlerce cisimle çalıştığınızda, fizik hesaplamalarını Web Worker’a taşımak şarttır. Aksi halde kullanıcı etkileşimi bloke olur:

// fizik_worker.js
let modul = null;
let cismSayisi = 0;
let pozisyonPtr = null;

importScripts('fizik_motoru.js');

FizikMotoru().then((m) => {
  modul = m;
  self.postMessage({ tip: 'hazir' });
});

self.onmessage = (e) => {
  const { tip, veri } = e.data;
  
  switch (tip) {
    case 'baslat':
      modul._simulasyon_baslat(0, -10);
      break;
      
    case 'cisim_ekle':
      modul._cisim_ekle(veri.x, veri.y, veri.g, veri.h, veri.dinamik ? 1 : 0);
      cismSayisi++;
      
      if (pozisyonPtr) modul._free(pozisyonPtr);
      pozisyonPtr = modul._malloc(cismSayisi * 3 * 4);
      break;
      
    case 'adim':
      modul._adim_at(1/60, 8, 3);
      modul._pozisyonlari_al(pozisyonPtr, cismSayisi);
      
      // Transferable object olarak gönder - kopyalama yok!
      const tampon = modul.HEAPF32.buffer.slice(
        pozisyonPtr,
        pozisyonPtr + cismSayisi * 3 * 4
      );
      
      self.postMessage(
        { tip: 'pozisyonlar', veri: tampon, cismSayisi },
        [tampon]
      );
      break;
  }
};

Ana thread tarafında worker ile iletişim:

// ana_sayfa.js
const worker = new Worker('fizik_worker.js');
const canvas = document.getElementById('simCanvas');
const ctx = canvas.getContext('2d');

worker.onmessage = (e) => {
  const { tip, veri, cismSayisi } = e.data;
  
  if (tip === 'hazir') {
    worker.postMessage({ tip: 'baslat' });
    
    // Cisimler ekle
    for (let i = 0; i < 200; i++) {
      worker.postMessage({
        tip: 'cisim_ekle',
        veri: {
          x: (Math.random() - 0.5) * 30,
          y: Math.random() * 40 + 10,
          g: 0.8 + Math.random() * 0.4,
          h: 0.8 + Math.random() * 0.4,
          dinamik: true
        }
      });
    }
    
    // Animasyon döngüsünü başlat
    const dongu = () => {
      worker.postMessage({ tip: 'adim' });
      requestAnimationFrame(dongu);
    };
    requestAnimationFrame(dongu);
    
  } else if (tip === 'pozisyonlar') {
    const pozisyonlar = new Float32Array(veri);
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    
    for (let i = 0; i < cismSayisi; i++) {
      const x = pozisyonlar[i * 3] * 15 + canvas.width / 2;
      const y = -pozisyonlar[i * 3 + 1] * 15 + canvas.height * 0.8;
      
      ctx.fillStyle = `hsl(${i * 7 % 360}, 70%, 60%)`;
      ctx.beginPath();
      ctx.arc(x, y, 6, 0, Math.PI * 2);
      ctx.fill();
    }
  }
};

Performans Profilleme ve Optimizasyon

Simülasyonu production’a almadan önce profilleme yapmak kritiktir. Chrome DevTools’un WASM profiler desteği oldukça işlevseldir. Ancak komut satırından da hızlı bir benchmark yapabilirsiniz:

# WASM modülü binary boyutunu kontrol et
wasm-opt -O3 fizik_motoru.wasm -o fizik_motoru_opt.wasm

# Boyut karşılaştırması
wc -c fizik_motoru.wasm fizik_motoru_opt.wasm

# wasm2wat ile assembly çıktısını incele (SIMD talimatlarını doğrula)
wasm2wat fizik_motoru_opt.wasm | grep -i "simd|v128|f32x4" | head -20

# Node.js ile basit benchmark
node --experimental-wasm-simd -e "
const fs = require('fs');
const wasmBuffer = fs.readFileSync('fizik_motoru.wasm');
WebAssembly.compile(wasmBuffer).then(mod => {
  console.log('Modul derlendi, export sayisi:', WebAssembly.Module.exports(mod).length);
});
"

Emscripten derleme parametrelerini production için ayarlamak da önemlidir:

# Production build - maksimum optimizasyon
emcc box2d_wrapper.cpp 
  -I../include -L. -lbox2d 
  -o fizik_motoru.js 
  -s WASM=1 
  -s MODULARIZE=1 
  -s EXPORT_NAME="FizikMotoru" 
  -s EXPORTED_FUNCTIONS='["_simulasyon_baslat","_cisim_ekle","_adim_at","_pozisyonlari_al","_simulasyonu_temizle","_malloc","_free"]' 
  -s EXPORTED_RUNTIME_METHODS='["HEAPF32"]' 
  -s INITIAL_MEMORY=134217728 
  -s MAXIMUM_MEMORY=536870912 
  -s ALLOW_MEMORY_GROWTH=1 
  -s NO_EXIT_RUNTIME=1 
  -s ENVIRONMENT='web,worker' 
  -O3 
  -msimd128 
  --closure 1 
  -flto

--closure 1 ve -flto (Link Time Optimization) eklenmesi, özellikle JavaScript glue kodunu minimize ederek yükleme süresini kısaltır. Gerçek projede bunu ihmal etmemek gerekiyor.

Gerçek Dünya Senaryosu: Mühendislik Simülasyon Aracı

Pratikte bu teknoloji stack’ini kullandığım projede, bir köprü yük simülatörü geliştirmiştik. 500’den fazla rijit cisimden oluşan bir kafes yapı modelini tarayıcıda simüle etmemiz gerekiyordu. Pure JavaScript ile başladığımızda, 60 FPS yerine 8-12 FPS alıyorduk. WASM’a geçtikten sonra bu değer tutarlı biçimde 58-60 FPS’e çıktı.

Önemli bir ders olarak şunu söyleyebilirim: WASM’ın kazancı sadece hesaplama hızından gelmiyor. Bellek düzeni (memory layout) optimizasyonu da en az o kadar kritik. Cisim verilerini struct-of-arrays (SoA) formatında tutmak, arrays-of-structs (AoS) formatına kıyasla cache hit oranını önemli ölçüde artırıyor. Özellikle integration loop’ta tüm pozisyonları, sonra tüm hızları okuyup yazıyorsanız, SoA formatı CPU cache’ini çok daha verimli kullanıyor.

Bir diğer kritik nokta, JavaScript ve WASM arasındaki sınır geçişlerini minimize etmek. Her frame’de onlarca cisim için ayrı ayrı WASM fonksiyonu çağırmak yerine, tüm simülasyon adımını WASM içinde tamamlayıp sadece sonuç verisini okumak; gereksiz overhead’i ortadan kaldırıyor.

Sonuç

WebAssembly ile fizik motoru entegrasyonu, web platformunu gerçek anlamda hesaplama ağır iş yükleri için uygun hale getiriyor. Burada anlattıklarım teorik değil; production ortamında çalışan sistemlerden çıkarılmış dersler.

Başlamak için en pratik yol, mevcut C++ fizik motorlarından birini (Box2D veya Bullet) Emscripten ile derlemek ve ince bir JavaScript wrapper katmanı oluşturmaktır. Sıfırdan WASM yazmaya gerek yok; mevcut, test edilmiş kodu alıp WASM’a taşımak çok daha verimli.

Web Worker entegrasyonu ihmal edilmemelidir. Fizik hesaplamalarını ana thread’de çalıştırmak, özellikle kullanıcı etkileşimi olan uygulamalarda kabul edilemez gecikmelere yol açar. Transferable objects kullanarak Worker ve ana thread arasında sıfır kopyalama (zero-copy) veri transferi sağlamak, performans bütçenizi korumanın en iyi yoludur.

SIMD desteği artık modern tarayıcıların tamamında mevcut. Emscripten’de -msimd128 ile derlemek bedava performans anlamına geliyor; bu flag’i açık unutmayın. Son olarak, wasm-opt aracını build pipeline’ınıza entegre etmeyi alışkanlık haline getirin; binary boyutunu küçültmesi ve yükleme sürelerini iyileştirmesi için ek bir çaba gerektirmiyor.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir