holodepth

WebGL · Framebuffer · Offscreen rendering

WebGL: Framebuffer ve offscreen rendering

WebGL’de her çizim bir hedefe ihtiyaç duyar. Varsayılan hedef tarayıcıdaki <canvas> (default framebuffer) olsa da, modern grafik akışında görüntüyü ekrana basmadan önce “ekran dışı” bellek alanlarında üretmek gerekir. Bu mekanizmanın adı Framebuffer Object (FBO)’dur.

Bu sayfa, Three.js tarafındaki “render target kullanım senaryoları”na girmeden, WebGL seviyesinde render edilen veri nereye yazılıyor? sorusunu cevaplar: default framebuffer mı, custom framebuffer mı; hangi attachment’lar var; GPU seviyesinde bind → draw → unbind akışı nasıl çalışıyor?

Framebuffer nedir? (GPU bellek hedefi)

Framebuffer, bir görüntünün oluşması için gereken bellek bileşenlerini (renk, derinlik, stencil) bir arada tutan bir “hedef” nesnesidir. En önemli fark şudur: default framebuffer ekrana gider; custom framebuffer ise ekranda görünmeyen bir hedefe çizim yapar.

Buradaki “hedef” kelimesini kelimesi kelimesine alın: pipeline’ın ürettiği fragment sonuçları bir yerlere yazılmak zorundadır. Framebuffer, “bu sonuçlar nereye yazılacak?” sorusunun cevabıdır. Default framebuffer ekrana giden yolu temsil eder; FBO ise çıktıyı bir texture veya renderbuffer içine yönlendirerek sahnenin ara sonuçlarını bellekte tutar.

  • Default framebuffer: Tarayıcı/sistem tarafından yönetilir ve canvas’a sunulur. Renk çıktısı doğrudan ekrana gidecek buffer’a yazılır; derinlik/stencil gibi yardımcı buffer’lar da tarayıcının sağladığı varsayılan yapı içinde tutulur.
  • Custom framebuffer (FBO): Geliştiricinin oluşturduğu, texture veya renderbuffer gibi attachment’lara yazan offscreen hedeftir. Burada siz hedefin boyutunu, formatını ve hangi attachment’ların bağlı olduğunu kontrol edersiniz; yani “çizim tahtası” artık sizin kurduğunuz bellek düzenidir.

Sezgi: canvas “son perde”dir; FBO ise perde arkasındaki üretim masasıdır. Aynı pipeline çalışır, sadece çıktı adresi değişir.

Attachments: veri nereye yazılıyor?

Framebuffer tek başına boş bir kabuktur. Bir şey “tutabilmesi” için içine attachment takmanız gerekir. Attachment’lar, pipeline’ın ürettiği çıktının nereye yazılacağını belirler.

Attachment’ları bir “slot” gibi düşünün: pipeline’ın farklı türde çıktıları vardır (renk, derinlik, maske). Framebuffer ise bu çıktıları hangi bellek parçasına yazacağını bilmek ister. Bu bellek parçası çoğu zaman bir texture (sonradan shader’da okunacaksa) veya bir renderbuffer (sadece test/karşılaştırma için tutulacaksa) olabilir.

  • Color buffer: RGBA renk çıktısının yazıldığı hedef. Çoğu senaryoda texture tercih edilir; çünkü offscreen sonucu bir sonraki adımda tekrar örneklemek (sample etmek) istersiniz.
  • Depth buffer: Hangi fragment’in önde/arkada kaldığını belirleyen derinlik verisi.
  • Stencil buffer: Maskeleme ve seçici çizim için kullanılan katman.

Küçük ama kritik sözleşme: attachment’ların boyutları (genişlik/yükseklik) ve desteklenen formatları uyumlu değilse, framebuffer “tam” sayılmaz ve çizim hedefi olarak kullanılamaz. Pratikte bu, “neden siyah ekran görüyorum?” türü hataların sık kök nedenlerinden biridir.

Özellik tablosu: default vs offscreen

Özellik Default framebuffer (canvas) Custom framebuffer (FBO)
Görünürlük Doğrudan ekrana sunulur Ekranda görünmez, bellekte tutulur
Yönetim Tarayıcı / sistem Geliştirici (attachment, boyut, format)
Çıktı hedefi Renk + (çoğu zaman) otomatik depth/stencil Renk texture’ları + opsiyonel depth/stencil
Kullanım niyeti Son görüntüyü sunmak Offscreen üretim: ara sonuçları saklamak

Render akışı: bind → draw → unbind

GPU seviyesinde framebuffer kullanımı disiplinli bir sırayı izler. Buradaki amaç, “şu an nereye yazıyoruz?” hedefini netleştirmektir.

En pratik zihinsel model: bind bir “yönlendirme”dir. Pipeline çalışırken üretilen renk/derinlik/stencil çıktıları, o an bağlı olan framebuffer’ın attachment’larına gider. Bu yüzden bind/unbind, “hangi tahtaya yazıyoruz?” anahtarını çevirir.

  • Bind: GPU’ya “artık ekrana değil, şu hedefe çiz” dersiniz (bindFramebuffer). Bu noktada hedefin boyutu değiştiyse, viewport’u da o hedefe göre ayarlamak gerekir; aksi halde görüntü kırpılabilir veya ölçek hissi bozulabilir.
  • Draw: Normal çizim hattı çalışır; fragment çıktıları attachment’lara yazılır. Bu aşamada depth test / blending gibi state’ler “hedefe yazımı” doğrudan etkiler. Ayrıca çoğu akışta, önce hedef buffer’ı temizlemek (clear) önemlidir; çünkü offscreen hedefler de bir önceki kareden kalan değerleri taşıyabilir.
  • Unbind: GPU’yu tekrar default framebuffer’a döndürürsünüz (bindFramebuffer(null)).

Bu akışın güzelliği şurada: pipeline aynı pipeline’dır; değişen şey yalnızca çıktı hedefidir.

Kısa sonuç: Offscreen hedefe çizmek, “ekrana çizmekten farklı bir render pipeline” değil; aynı pipeline’ın çıktısını farklı bir bellek adresine yazmaktır. Bu ayrımı oturttuğunuzda, birçok “neden siyah çıktı aldım?” sorusu hedef/attachment/state üçlüsünde daha hızlı çözülür.

bind(FBO1) clear() draw scene unbind() fullscreen pass present()

Attachments (live thumbnails)

COLOR (FBO1)
NORMAL (FBO1)
DEPTH (FBO1)
FINAL (SCREEN)
GPU Memory Stack
FRAMEBUFFER 0 SCREEN
  • Color: backbuffer
  • Depth: default
  • Presented: yes
FRAMEBUFFER 1 OFFSCREEN
  • Color: RGBA8 texture
  • Normal: RGB8 texture
  • Depth: 16-bit buffer
Active Target
Framebuffer Routing Lab: Aynı sahne çizimi, hangi framebuffer bağlıysa o hedefin attachment’larına yazılır. Bu demo “efekt” değil; çıktı yönlendirme sezgisini (bind → draw → unbind → present) canlı kurar.

WebGL2 ve multiple render targets (MRT)

WebGL2 ile birlikte, tek bir çizim geçişinde birden fazla renk hedefini aynı anda yazabilmek mümkün olur. Bu, “tek çizim → çok çıktı” sözleşmesidir.

MRT’yi doğru çerçevelemek önemli: burada çoğunlukla birden fazla color attachment kastedilir. Yani fragment shader, aynı anda birden fazla “renk çıktısı” üretebilir ve GPU bu çıktıları farklı texture’lara yazar. Hedeflerin hangileri aktif, WebGL2’de drawBuffers benzeri bir seçimle belirlenir.

  • Tek geçiş, çok çıktı: Aynı sahne çiziminde; bir texture’a renk, başka bir texture’a normal, bir diğerine derinlik benzeri veriler yazılabilir.
  • Parça başına tek hesap, çok kayıt: Aynı fragment için “ışık hesabını” bir kez yapıp, farklı amaçlara farklı çıktılar yazabilirsiniz (ör. renk + yardımcı bilgiler). Bu, akışı daha deterministik kılar: “aynı yüzey için aynı anda üretilmiş” veriler elde edersiniz.
  • Sınırlar ve sözleşmeler: Kaç hedefe yazabileceğiniz donanım/driver limitleriyle sınırlıdır; ayrıca attachment’ların format ve boyut uyumu korunmalıdır. Aksi halde hedef “tam” sayılmaz ve çizim beklediğiniz gibi çalışmayabilir.
  • Neden önemli? Bu kabiliyet, deferred rendering gibi ileri aydınlatma akışlarının temel yapı taşlarından biridir.

Burada amaç deferred rendering’ı anlatmak değil; MRT’nin neyi mümkün kıldığını görmektir: pipeline tek geçişte, tek sahne çiziminden birden fazla çıktı yüzeyi üretebilir.

Three.js bağlantısı (köprü cümlesi)

Bu teknik detayları bilmek, üst katmandaki araçları daha bilinçli kullanmanızı sağlar. Köprü cümlesi şu:

Altın bağlantı

Three.js’te kullandığın WebGLRenderTarget, aslında bir framebuffer wrapper’ıdır. Bir render target oluşturduğunuzda, Three.js arka planda bir FBO kurar; attachment texture’larını bağlar ve render sırasında bind/unbind işlemlerini sizin yerinize yönetir.

Burada amaç “post-processing nasıl yapılır?”ı tekrar etmek değil; üstteki kavramların altta hangi mekanizmaya oturduğunu görmektir. Siyah ekran, bozuk çıktı veya çözünürlük taşması gibi sorunlar, çoğu zaman efekt zincirinden önce attachment veya hedef boyutu sözleşmesinde yakalanır.