holodepth

WebGL · Aydınlatma · Shader matematiği

WebGL: Aydınlatma modelleri ve hesaplama mantığı

WebGL dünyasında ışık, fiziksel bir “obje”den ziyade matematiksel bir veri setidir. Bir yüzeyin aydınlanması, GPU tarafında milyonlarca kez çözülen bir geometri problemidir: yüzey yönü (normal), ışık yönü ve göz yönü arasındaki ilişkiden “renk” doğar.

Bu sayfa efekt öğretmez; ışığı WebGL seviyesinde bir hesaplama sözleşmesi olarak kurar. Lambert/Phong gibi modeller, “ışık yüzeyde nasıl davranır?” sorusunun farklı matematiksel cevaplarıdır.

Işık hesaplama mantığı: açı problemi

Işıklandırmanın temelinde üç ana değişken yatar: yüzey normali (\(N\)), ışık yönü (\(L\)) ve bu ikisi arasındaki noktasal çarpım (\(N \cdot L\)). Temel prensip şudur: bir yüzeyin ne kadar aydınlanacağı, ışığın yüzeye hangi açıyla geldiğine bağlıdır.

Bu ifadeyi daha “mühendisçe” okuyabiliriz: \(N \cdot L\), iki vektör birimlenmişse \(\cos(\theta)\) değerini verir. Yani hesap, aslında “ışık yüzeye ne kadar dik geliyor?” sorusunun sayısal cevabıdır. Bu yüzden aydınlatma hesaplarının ilk disiplin kuralı şudur: aynı uzayda, normalize vektörlerle hesap yap.

  • Dot product sezgisi: Işık yüzeye dik geliyorsa (açı \(0^\circ\)), \(N \cdot L\) maksimuma yaklaşır; ışık yüzeye paralelleştikçe değer düşer.
  • Clamp kuralı: Pratikte negatif değerler “ışık arkadan geliyor” demektir ve çoğu modelde \( \max(N \cdot L, 0) \) ile sıfırlanır.

Burada “uzay” vurgusu kritiktir: normal’ı model uzayında bırakıp ışık yönünü dünya uzayında kullanırsanız, dot product doğru bir açı ölçmez. Sonuç “ışık yanlış yerden geliyor” gibi görünür. Bu yüzden shader’larda sık görülen kalıp şudur: ya her şeyi world space’e taşı, ya her şeyi view space’e taşı — ama karıştırma.

Altın cümle

Işık aslında bir renk değil, bir açı problemidir.

Lighting stack

32

Active fragment

Light Response Laboratory: Yüzeyin üzerine gel ve tek bir fragment’in \(N\), \(L\) ve \(N \cdot L\) ilişkisini canlı oku. Bu demo “güzel render” değil; ışığın neden açı hesabı olduğunu hissettirmek içindir.

Normal vector (yüzey yönü)

Normal vektörü, yüzeyin “dışarıya” hangi yönde baktığını gösteren birim vektördür. Işık yönüyle yapılan dot product, ancak normal doğruysa anlamlıdır. Bu yüzden aydınlatmanın ilk şartı, doğru normaldır.

Normal’ı iki açıdan düşünmek faydalı olur: (1) yön doğru mu (içe mi dışa mı bakıyor?), (2) ölçek doğru mu (birim vektör mü?). Dot product, yön ve ölçek birlikte doğruyken “açı ölçer” gibi davranır.

  • Kritik rol: Normal, “yüzey hangi açıyla ışık alıyor?” sorusunun ana girdisidir.
  • Hata teşhisi: Işık “patlıyorsa”, yüzeyler ters aydınlanıyorsa veya gölge hissi ters dönüyorsa, sorun çoğu zaman normal yönü/uzayı/normalize edilmesiyle ilgilidir.

WebGL’de normal konusu ayrıca “uzay” konusudur: normal’lar genelde model uzayında gelir; ama siz ışık yönünü world veya view uzayında kuruyorsanız, normal’ı da aynı uzaya taşımanız gerekir. Özellikle ölçek içeren dönüşümlerde, normal’ı pozisyon gibi aynı matrisle taşımak her zaman doğru sonuç vermez; bu yüzden “normal dönüşümü” ayrı bir sözleşme olarak ele alınır (detayı matrisler sayfasında açacağız).

Mini pratik: bir yüzeyin önü/arkası karışıyorsa (parlama ters tarafta çıkıyor gibi), önce normal yönünü ve yüzün winding/culling sözleşmesini kontrol edin; çoğu “ters ışık” hatası burada yakalanır.

Lambert (diffuse lighting — mat yüzey)

Lambert, en temel diffuse modeldir: ışığın yüzeye çarptıktan sonra her yöne eşit dağıldığını varsayar. Parlama (highlight) üretmez; mat ve “pürüzsüz” bir tonlama verir.

Lambert’in güzelliği, “tek sezgi” ile çalışmasıdır: yüzey ışığa ne kadar dönükse o kadar aydınlıktır. Bu yüzden model, geometriyi okunur kılar; gölge hissi ve form algısı çoğunlukla bu diffuse terimle ortaya çıkar.

  • Mantık: Temel terim \( \max(N \cdot L, 0) \) etrafında kurulur.
  • Enerji sezgisi: \(N \cdot L\) aslında “ışık katkısının katsayısı”dır. Bu katsayı, yüzey rengiyle çarpılarak diffuse renk katkısına dönüşür.
  • Görünüm: Kağıt, duvar, toprak gibi yüzeylerin temel hissi.

Lambert’in sınırlaması da buradan gelir: kamera açısı değişse bile diffuse katkı aynı mantıkla kalır; yani parlak yüzeylerde beklediğiniz “kameraya göre parlayan” highlight davranışını üretmez. Bu, bir sonraki bölümdeki specular modellerin devreye girdiği noktadır.

Phong (specular lighting — parlak yüzey)

Phong yaklaşımı, diffuse’a ek olarak specular (yansıma) bileşeniyle “parlak nokta” hissi üretir. Buradaki sezgi: ışığın yansıma yönü, kameraya yaklaştıkça parlama artar.

Diffuse, yüzeyi “okunur” kılar; specular ise yüzeye “malzeme hissi” verir. Aynı geometri, aynı diffuse ile mat görünebilir; ama specular terimi eklendiğinde cilalı/plastik/metal benzeri tepkiler ortaya çıkar. Çünkü burada devreye giren şey, yüzeyin ışığı “nasıl yansıttığı”dır.

  • Yansıma vektörü sezgisi: Işığın geliş yönüne göre bir yansıma yönü kurulur; kameraya hizalandığında highlight oluşur.
  • Göz yönü (view direction): Parlama, yalnızca ışığın gelişine değil, kameranın baktığı yöne de bağlıdır. Bu yüzden specular, kamera hareket ettikçe “kayan” bir highlight üretir.
  • Shininess / güç: Highlight’ın ne kadar keskin olacağı, genelde bir üs (power) terimiyle kontrol edilir. Üs büyüdükçe parlama küçülür ama daha keskinleşir; bu da “cilalı” hissi güçlendirir.
  • Görünüm: Plastik, cilalı ahşap, metalimsi yüzeylerde keskin parlama.

Not: Burada modelin tam formülünü yazmıyoruz; amaç, specular’ın hangi verilerle (normal, ışık yönü, göz yönü) “kameraya bağlı parlama” ürettiğini zihne yerleştirmek.

Ambient lighting (ortam ışığı)

Ambient terimi, ışığın olmadığı bölgelerin tamamen zifiri karanlık kalmasını önlemek için kullanılan “taban” katkıdır. Gerçek hayatta ışığın ortamda sekerek gelmesini kaba bir şekilde temsil eder.

Ambient’ı “her yere eşit yayılan sabit ışık” gibi düşünün: yönü yoktur, gölge üretmez ve \(N \cdot L\) gibi açı hesaplarına girmez. Bu yüzden ambient, fiziksel olarak doğru olmaktan çok görsel okunabilirlik için kullanılan bir taban katmandır.

Önemli not: ambient tek başına “gerçekçilik” getirmez; ama sahnenin okunabilirliğini artıran bir minimum aydınlık seviyesi sağlar.

Mini sezgi: ambient’ı fazla açarsanız sahne “düzleşir” — çünkü formu ortaya çıkaran asıl sinyal diffuse/specular terimleridir. Ambient doğru dozda olduğunda, karanlık bölgeler tamamen kaybolmaz ama form hâlâ ışık açılarıyla okunur.

Combine lighting: denklemi kurmak

Pratikte sahne görünümü, farklı terimlerin toplanmasıyla kurulur. Klasik “Phong tarzı” özet şu cümlede toplanır: Son renk = Ambient + Diffuse + Specular. Burada amaç fiziksel doğruluk kanıtlamak değil; GPU’da her fragment için üretilecek rengi, okunabilir bileşenlere ayırmaktır.

Bu üç terim farklı sorulara cevap verir ve birbirinin yerine geçmez: diffuse yüzeyin forma göre ışığı nasıl “yayıldığını” (açıya bağlı mat ton); specular yansımanın gözle hizalanmasıyla oluşan parlama noktasını; ambient ise yönsüz bir taban parlaklığı vererek karanlıkta kaybolan detayları kurtarır. Birlikte kullanıldığında form (diffuse), okunabilir taban (ambient) ve yüzey “cilası” (specular) aynı karede bir araya gelir.

Shader tarafında bu toplam, doğrudan “hangi veri nereden geliyor?” sorusuna bağlanır. Diffuse çoğu zaman normal ile ışık yönü üzerinden kurulur (\(N \cdot L\) ailesi). Specular ise aynı normal/ışık dünyasına ek olarak göz (view) yönü ve seçtiğiniz yansıma modeli (ör. yansıma vektörü veya yarıvektör) ile beslenir — yani “ışık var ama parlama yok” durumunda şüpheli olan terim genelde specular tarafıdır. Ambient bu akışa açıdan girmeyebilir; çoğu uygulamada sabit veya yumuşak bir renk/katsayı ile tabanı sabitler.

Uygulamada her terim ayrıca bir katsayı veya renk çarpanı ile ölçeklenir (ör. materyalin albedo’su diffuse’ı, speküler güç specular’ı güçlendirir). Bu sayede aynı matematik iskeleti, farklı yüzey tiplerine “ton” kazandırır. Önemli sezgi: terimleri bağımsız düşünün — ambient’ı artırmak diffuse’ın yerini doldurmaz; sadece gölge diplerini yumuşatır ve sahneyi bazen düzleştirir.

Mini teşhis: sahne “düz ve plastik” görünüyorsa çoğu zaman diffuse+ambient baskındır; “keskin nokta parlama” eksikse specular terimi zayıftır veya view/ışık uzayı uyumsuzdur; ters aydınlanma veya patlamalar ise önce normal ve uzay birliğini (aynı koordinat sisteminde mı?) sorgulatır — birleşik denklem doğru olsa bile yanlış girdi, yanlış görüntü üretir.

Vertex vs fragment lighting (kritik ayrım)

Işığın pipeline içinde nerede hesaplandığı, kalite ile maliyet arasındaki en net anahtarlardan biridir. Aynı matematiksel model (ör. diffuse + specular) bile olsa, sonucu vertex shader’da bitirip yüzeye “yayıtmak” ile her fragment’te yeniden kurmak tamamen farklı maliyet ve görüntü profilleri üretir. Buradaki kritik nokta şudur: rasterization, köşeler arası veriyi interpolasyonla taşır; bu taşıma bazen renk için iyidir, bazen geometrik büyüklükler için yanıltıcıdır.

  • Vertex lighting (Gouraud): Aydınlatmanın ağır kısmı verteks başına bir kez çalışır; elde edilen renk (veya aydınlanma katsayısı) varying ile fragment shader’a iner ve üçgen içinde yumuşatılır. Çalışma sayısı kabaca “verteks sayısı” ile ölçeklenir; bu yüzden düşük geometri yoğunluğunda caziptir. Dezavantaj: specular gibi yüksek frekanslı detaylar üçgen içinde “kaçabilir” — parlama köşelerde doğru hesaplanıp içeride yanlış ortalamalanabilir; yüzey parlaklığı bazen bloklu veya kontrastı düşük görünür.
  • Per-fragment lighting (genelde “Phong shading” diye anılır): Burada tipik desen şudur: vertex shader, fragment’e ham geometri sinyalini taşır (ör. dünya/view uzayında interpolasyonlu normal); asıl \(N \cdot L\) ve specular hesabı fragment shader’da, her örnek için yeniden yapılır. Görüntü genelde daha tutarlı ve pürüzsüzdür; çünkü açı problemi piksel ölçeğinde çözülür. Karşılığında maliyet, ekranda kapladığınız alanla birlikte büyür: aynı kural, binlerce veya milyonlarca fragment üzerinde tekrarlanır.

İsim kargaşasına küçük bir düzeltme: “Phong” bazen specular modeli için, bazen de “ışığı fragment’te hesaplama” gölgelendirme tarzı için kullanılır. Bu sayfada ikinci anlam öne çıkar: nerede hesaplıyorsun? Model aynı kalsa bile, hesap yeri görüntüyü değiştirir.

Bu ayrım, shader veri trafiğini de somutlaştırır. Vertex lighting’de sık görülen yol: attribute’lardan gelen normal/konum ile vertex’te bir “renk skaleri/vektörü” üret → varying ile taşı → fragment’te sadece tonemap veya doku ile çarp. Per-fragment lighting’de ise: vertex, normali güvenli biçimde taşımayı (ve gerekirse yeniden normalize etmeyi) üstlenir; fragment, uniform ışık ve kamera verisini fragment başına tüketerek nihai rengi kurar. Uniform’lar her iki durumda da çizim boyunca sabit kalan girdileri taşır; değişen şey, ağır matematiğin verteks mi yoksa fragment mi başına işlendiğidir.

Mini sezgi: “Işığı vertex’e mi indirgeyeyim?” sorusunun cevabı çoğu zaman geometri çözünürlüğüne bağlıdır — üçgenler büyükse Gouraud hataları fark edilir; üçgenler küçük ve yoğunsa vertex lighting bazen görsel olarak yaklaşır ama yine de specular tarafında sürprizler yaşanabilir.

Performans perspektifi

Aydınlatma hesapları özellikle fragment shader katmanında “pahalı” hissedilir; çünkü aynı matematiksel blok, üçgen başına değil, üçgenin ekranda kapladığı örnek alanı kadar tekrarlanır. Işık sayısı arttıkça, tipik olarak her fragment için yapılan iş de artar: çoğu klasik modelde her ışık için yön/normal/spot gibi ek iç çarpışlar ve specular gibi ek terimler devreye girer. Bu yüzden “hangi ışık modelini seçiyorum?” sorusu, saf görsel bir tercih değil; aynı zamanda kaç kez aynı maliyetli işi koşturduğunuz sorusudur.

Maliyeti tek boyuta indirgemeyin: çözünürlük ve overdraw (aynı piksel üzerinde üst üste binen fragment yükü) gerçek iş yükünü büyütür. Yüksek çözünürlük, ekrandaki fragment sayısını doğrudan artırır; şeffaf yüzeyler veya üst üste duran katmanlar ise aynı alanda birden fazla tam fragment maliyeti doğurabilir. Işık hesabı da doku örneklemeyle birleştiğinde (ör. roughness/mask haritaları), fragment tarafındaki “tek satır” gibi görünen ekleme, pratikte binlerce kez ödenecek ek işe dönüşür.

Vertex tarafındaki maliyet ise kabaca verteks sayısı ile ölçeklenir. Bu yüzden aynı görsel kalite hedefi için bazen “bir kısmı vertex’e indir, bir kısmı fragment’te tut” dengesi kurulur — bunun sınırlarını 7. bölüm zaten çizdi. Performans açısından önemli olan şudur: fragment’e taşıdığınız her ağır işlem, ekran büyüdükçe ve sahne üst üste bindikçe daha agresif büyür.

Bu sayfa bir optimizasyon reçetesi sunmaz; ama şu zihinsel haritayı sabitler: fragment shader’da eklenen her anlamlı iş, pratikte “ekranı boydan boya kaç kez yeniden hesaplıyorum?” sorusuna bağlanır. Modeli doğru yerde (vertex vs fragment) ve doğru karmaşıklıkta kurmak, WebGL’de akıcılık için sıklıkla belirleyicidir — çünkü kare bütçenizin çoğu, sessizce fragment tarafında eriyebilir.

Bağlantı ve Three.js ilişkisi

Bu sayfada çizdiğimiz şey, WebGL tarafında “ışığın” soyut bir nesne değil, shader içinde akan veri ve matematik olduğuydu: vertex aşamasında normal gibi girdiler doğru uzaya taşınır (gerekirse varying ile fragment aşamasına köprülenir); fragment aşamasında ise \(N \cdot L\), specular terimleri ve ambient/diffuse toplamları nihai rengi üretir. Yani HoloDepth’te gördüğünüz parlama veya mat yüzey farkı, üst seviyede bir isimden önce GPU’da hangi terimlerin koştuğuna bağlıdır.

Three.js bu iskeleti, çoğu projede yeniden yazmanızı gerektirmemek için hazır materyal + hazır shader programları olarak paketler. Örneğin MeshLambertMaterial veya MeshPhongMaterial, burada anlattığımız diffuse/specular ayrımının kütüphanenin seçtiği bir uygulamasını temsil eder: siz renk/parlaklık gibi parametreleri değiştirdiğinizde, motor çoğu zaman bunları uniform ve benzeri GPU girdilerine çevirir; sahne ışıkları ve kamera da aynı çizim için shader’ın ihtiyaç duyduğu “sabit panel” verisini besler. Detaylı API ve sahne kurulumu Three.js dokümantasyonunun işi; buradaki kazanım, ekranda gördüğünüz terimin altta hangi hesap katmanına indiğini bilmektir.

Daha modern materyaller (ör. fizik tabanlı yansıma modelleri) matematik olarak daha zengin olsa da “üst katman / alt katman” ayrımı değişmez: yine bir shader programı, buffer’dan gelen geometri ve uniform’lardan gelen sahne verisiyle konuşur. WebGL sayfalarında hedefimiz Three.js’i yeniden anlatmak değil; aynı görsel sorunu motor kapalıyken nerede çözdüğünüzü göstermektir — böylece özel shader veya performans ayarı gerektiğinde, tartışma “hangi materyal?”den önce “hangi terim, hangi aşama?”e inebilir.