Cubemaps (Küp Haritaları)

Bir süredir 2B dokuları kullanıyoruz; ancak henüz keşfetmediğimiz daha fazla doku türü var. Bu bölümde birden fazla dokunun tek bir dokuya eşlendiği bir doku türünü ele alacağız: cube map (küp haritası).

Cubemap, bir küpün her yüzünü oluşturan 6 ayrı 2B dokuyu içeren bir dokudur; başka bir deyişle, dokulanmış bir küptür. Böyle bir küpün ne işe yaradığını merak edebilirsiniz. Neden 6 ayrı dokuyu tek bir yapıda birleştirme zahmetine girelim? Cubemap'lerin çok kullanışlı bir özelliği var: bir yön vektörü kullanılarak indekslenip/örneklenebilirler. Merkezi orijinde bulunan 1x1x1 boyutlarında bir küpü ve bu merkezde başlayan bir yön vektörünü hayal edin. Turuncu bir yön vektörüyle cubemap'ten doku değeri örneklemek şuna benzer:

Cubemap örnekleme diyagramı
circle-info

Yön vektörünün büyüklüğü önemli değildir. Bir yön sağlandığı sürece OpenGL, vektörün ulaştığı teksel'leri alır ve düzgün örneklenmiş doku değerini döndürür.

Böyle bir cubemap'i bağladığımız bir küp şekli hayal edersek, bu yön vektörü küpün (interpolated) yerel vertex konumuna benzer olacaktır. Bu sayede, küp orijinde ortalı olduğu sürece küpün gerçek konum vektörlerini doku koordinatları olarak kullanarak cubemap'i örnekleyebiliriz. Tüm vertex konumlarının cubemap örneklenirken doku koordinatı görevi gördüğü düşünülür. Sonuç, cubemap'in ilgili yüzünün dokusuna erişen bir doku koordinatıdır.


Cubemap Oluşturma

Cubemap, başka herhangi bir doku gibi bir dokudur; bu nedenle oluşturmak için bir doku üretir ve herhangi bir doku işlemine geçmeden önce onu uygun doku target'ına bağlarız. Bu sefer GL_TEXTURE_CUBE_MAP'e bağlıyoruz:

Cubemap 6 doku içerdiğinden, her yüz için parametreleri önceki bölümlere benzer şekilde ayarlayarak glTexImage2D'yi 6 kez çağırmamız gerekir. Bu sefer doku target parametresini cubemap'in belirli bir yüzüyle eşleşecek biçimde ayarlamamız gerekir. Bu, her cubemap yüzü için glTexImage2D'yi bir kez çağırmamız anlamına gelir.

OpenGL, 6 yüz için 6 özel doku target sunar:

Doku Target
Yön

GL_TEXTURE_CUBE_MAP_POSITIVE_X

Sağ

GL_TEXTURE_CUBE_MAP_NEGATIVE_X

Sol

GL_TEXTURE_CUBE_MAP_POSITIVE_Y

Üst

GL_TEXTURE_CUBE_MAP_NEGATIVE_Y

Alt

GL_TEXTURE_CUBE_MAP_POSITIVE_Z

Arka

GL_TEXTURE_CUBE_MAP_NEGATIVE_Z

Ön

OpenGL'in enum'larının çoğu gibi, bunların arka plandaki int değerleri de doğrusal olarak artmaktadır. Dolayısıyla bir doku konumları dizimiz varsa GL_TEXTURE_CUBE_MAP_POSITIVE_X'ten başlayarak her yinelemede enum'u 1 artırarak tüm doku target'ları üzerinde döngü kurabiliriz:

Burada textures_faces vektörü, tablodaki sırayla cubemap için gereken tüm doku konumlarını içerir. Bu işlem, bağlı cubemap'in her yüzü için bir doku üretir.

Cubemap de her doku gibi wrapping ve filtering metodları gerektirir:

GL_TEXTURE_WRAP_R'dan korkmayın; bu yalnızca dokunun R koordinatı için wrapping metodunu ayarlar ve konum için z'ye karşılık gelen 3. boyutu temsil eder. Wrapping metodunu GL_CLAMP_TO_EDGE olarak ayarladık; çünkü iki yüz arasında tam olarak konumlanan doku koordinatları, bazı donanım sınırlamaları nedeniyle bir yüze tam isabet etmeyebilir. GL_CLAMP_TO_EDGE kullanıldığında ise yüzler arasında örnekleme yapıldığında her zaman kenar değerleri döndürülür.

Cubemap kullanan nesneleri çizmeden önce ilgili doku birimini etkinleştirip cubemap'i bağlarız; normal 2B dokulardan büyük bir farkı yoktur.

Fragment shader'da da farklı bir sampler türü kullanmamız gerekir: samplerCube. texture fonksiyonuyla örneklenir; ancak bu sefer vec2 yerine bir vec3 yön vektörü kullanılır. Cubemap kullanan bir fragment shader örneği:

Bunlar güzel, ama neden zahmet edelim? Cubemap ile çok daha kolay uygulanan birkaç ilginç teknik var. Bu tekniklerden biri skybox oluşturmaktır.


Skybox

Skybox, tüm sahneyi kapsayan büyük bir küptür ve oyuncuya içinde bulunduğu ortamın aslında çok daha büyük olduğu yanılsamasını veren 6 çevre görüntüsü içerir. Oyunlarda kullanılan skybox örnekleri arasında dağların, bulutların veya yıldızlı gece göğünün görüntüleri sayılabilir. Yıldızlı gece göğü görüntüleri kullanan bir skybox örneğini aşağıdaki ekran görüntüsünde görebilirsiniz:

Morrowind skybox örneği

Tahmin etmiş olabileceğiniz gibi, skybox'lar cubemap'lere mükemmel uymaktadır: her yüzü dokulanması gereken 6 yüzlü bir küp. Önceki görüntüde gece gökyüzüne ait birkaç görüntü kullanılarak oyuncunun aslında küçük bir kutunun içindeyken gerçek bir evrende olduğu yanılsaması yaratılmıştır.

Internette bu tür skybox'lar bulmak oldukça kolaydır. Skybox görüntüleri genellikle şu düzende olur:

Skybox düzeni

Bu 6 yüzü bir küpe katlarsanız büyük bir manzarayı simüle eden, tamamen dokulu bir küp elde edersiniz. Bazı kaynaklar skybox'ı bu düzende sunar; bu durumda 6 yüz görüntüsünü elle çıkarmanız gerekir. Çoğunlukla ise 6 ayrı doku görüntüsü hâlinde sağlanırlar.

Bu bölümde kullanacağımız (yüksek kaliteli) skybox örneğini buradanarrow-up-right indirebilirsiniz.

Skybox Yüklemek

Skybox kendi başına bir cubemap olduğundan yüklemek, bu bölümün başında gördüklerimizden çok farklı değildir. Skybox'ı yüklemek için 6 doku konumundan oluşan bir vector kabul eden şu fonksiyonu kullanacağız:

Fonksiyon aslında önceki bölümde gördüğümüz tüm cubemap kodunu tek bir yönetilebilir fonksiyonda bir araya getirir.

Bu fonksiyonu çağırmadan önce cubemap enum'larının belirttiği sıraya göre doku yollarını bir vektöre yüklüyoruz:

Skybox'ı artık cubemapTexture id'siyle cubemap olarak yükledik ve bunu şimdiye kadar kullandığımız sıkıcı düz renkli arka plan yerine bir küpe bağlayabiliriz.

Skybox Göstermek

Skybox bir küp üzerine çizildiğinden, başka herhangi bir 3B nesne gibi yeni bir VAO, VBO ve vertex kümesi gereklidir. Vertex verisini buradanarrow-up-right alabilirsiniz.

3B küpü dokulamak için kullanılan cubemap, küpün yerel konumlarını doku koordinatları olarak kullanarak örneklenebilir. Küp orijinde (0,0,0) ortalandığında, her konum vektörü aynı zamanda orijinden bir yön vektörüdür. Bu yön vektörü, ilgili doku değerini almak için tam olarak ihtiyacımız olan şeydir. Bu nedenle yalnızca konum vektörleri sağlamamız yeterlidir; doku koordinatlarına gerek yoktur.

Skybox'ı render etmek için çok karmaşık olmayan yeni bir shader seti gereklidir. Yalnızca bir vertex attribute'umuz olduğundan vertex shader oldukça basittir:

Bu vertex shader'da ilginç olan nokta, gelen yerel konum vektörünü, fragment shader'da (interpolated olarak) kullanılmak üzere giden doku koordinatı olarak ayarlamamızdır. Fragment shader bu değerleri samplerCube'u örneklemek için kullanır:

Fragment shader nispeten basittir: vertex attribute'un interpolated konum vektörünü dokunun yön vektörü olarak alır ve cubemap'ten doku değerlerini örneklemek için kullanır.

Cubemap dokusunu bağladıktan ve skybox sampler'ını doldurduktan sonra skybox'ı render etmek kolaydır. Sahnenin arka planında kalması için depth yazımını devre dışı bırakarak sahnenin ilk nesnesi olarak çiziyoruz; bu sayede unit küp büyük ihtimalle diğer nesnelerden küçük olduğundan skybox her zaman arka planda kalır:

Bunu çalıştırırsanız bazı sorunlarla karşılaşabilirsiniz. Skybox'ın oyuncu etrafında ortalanmasını istiyoruz; oyuncu ne kadar hareket ederse etsin skybox yaklaşmamalı ve çevrenin gerçekten büyük olduğu yanılsaması korunmalıdır. Ancak mevcut view matrisi, skybox'ın tüm konumlarını döndürme, ölçekleme ve öteleme yoluyla dönüştürür; oyuncu hareket edince cubemap de hareket eder! View matrisinin öteleme kısmını kaldırmamız, yalnızca döndürmenin skybox'ın konum vektörlerini etkilemesi gerekir.

Temel Işıklandırmaarrow-up-right bölümünde dönüşüm matrislerinin öteleme bölümünü 4x4 matrisin sol üst 3x3 matrisini alarak kaldırabildiğimizden bahsetmiştik. View matrisini 3x3 matrise dönüştürüp (ötelemeyi kaldırarak) tekrar 4x4'e çevirerek bunu başarabiliriz:

Bu işlem tüm ötelemeyi kaldırır; ancak tüm döndürme dönüşümlerini korur; böylece kullanıcı sahneye bakmaya devam edebilir.

Sonuç, skybox sayesinde anında devasa görünen bir sahnedir. Temel konteyner etrafında uçarsanız ölçek hissini hemen fark edersiniz; bu da sahnenin gerçekçiliğini önemli ölçüde artırır. Sonuç şuna benzer:

Skybox sonucu

Farklı skybox'lar deneyerek sahnenizin görünümü üzerinde ne kadar büyük bir etki yarattıklarını görün.

Bir Optimizasyon

Şu anda skybox'ı sahnedeki diğer nesnelerin tamamından önce render ettik. Bu iyi çalışıyor; ancak çok verimli değil. Skybox'ı önce render edersek, sonunda skybox'ın yalnızca küçük bir kısmının görünür olmasına rağmen ekrandaki her piksel için fragment shader çalıştırırız; early depth testing kullanılarak kolayca atılabilecek fragment'ler bant genişliğini boşa harcamış olur.

Bu nedenle skybox'ı en son render ederek hafif bir performans artışı sağlayacağız. Bu sayede depth buffer, sahnenin tüm derinlik değerleriyle dolmuş olacak ve yalnızca early depth testinin geçtiği fragment'lar için skybox'ı render ederiz; bu da fragment shader çağrılarını büyük ölçüde azaltır. Sorun şu ki skybox yalnızca 1x1x1 boyutlarında bir küp olduğundan çoğu depth testini geçer ve diğer nesnelerin üstüne görünebilir. Depth testi olmadan render etmek de bir çözüm değildir; skybox en son render edildiğinden yine de diğer nesnelerin üzerine yazacaktır. Depth buffer'ını, skybox'ın maksimum derinlik değeri olan 1.0'a sahip olduğuna inandırmak gerekir; böylece önünde başka bir nesne olduğu her yerde depth testini geçemez.

Koordinat Sistemleriarrow-up-right bölümünde vertex shader çalıştıktan sonra perspektif bölümünün uygulandığını ve gl_Position'ın xyz koordinatlarını w bileşenine böldüğünü söylemiştik. Ayrıca Derinlik Testiarrow-up-right bölümünden, bu bölümün z bileşeninin vertex'in derinlik değerine eşit olduğunu biliyoruz. Bu bilgiyi kullanarak çıkış pozisyonunun z bileşenini w bileşenine eşitleyebiliriz; bu durumda perspektif bölümü uygulandığında z bileşeni w/w = 1.0 olur:

Ortaya çıkan normalize edilmiş cihaz koordinatları her zaman 1.0 — yani maksimum derinlik değeri — z değerine sahip olacaktır. Skybox yalnızca hiçbir nesnenin görünmediği yerlerde render edilecektir (yalnızca orada depth testini geçebilir; geri kalan her şey skybox'ın önündedir).

Depth fonksiyonunu varsayılan GL_LESS yerine GL_LEQUAL olarak ayarlamamız gerekiyor. Depth buffer, skybox için 1.0 değerleriyle dolu olacağından skybox'ın depth testini küçük veya eşit değerlerle geçmesi gerekir.

Optimize edilmiş kaynak kodu buradanarrow-up-right bulabilirsiniz.


Ortam Eşleme

Artık tüm çevre ortamı tek bir doku nesnesine eşlenmiş durumda; bu bilgiyi yalnızca skybox için değil daha fazlası için kullanabiliriz. Bir ortam cubemap'i kullanarak nesnelere yansıtıcı veya kırıcı özellikler verebiliriz. Ortam cubemap'ini bu şekilde kullanan tekniklere environment mapping (ortam eşleme) denir; en yaygın ikisi reflection (yansıma) ve refraction (kırılmadır).

Reflection (Yansıma)

Yansıma, bir nesnenin (ya da nesnenin bir kısmının) çevresini yansıtması; yani nesnenin renklerinin, izleyicinin açısına bağlı olarak az ya da çok çevresine eşit olmasıdır. Ayna, yansıtıcı bir nesne örneğidir: izleyicinin açısına göre çevresini yansıtır.

Yansımanın temelleri o kadar zor değildir. Aşağıdaki görüntü, bir yansıma vektörünün nasıl hesaplanacağını ve cubemap'i örneklemek için nasıl kullanılacağını göstermektedir:

Yansıma teorisi

Görüş yönü vektörü $\color{gray}{\bar{I}}$'ya ve nesnenin normal vektörü $\color{red}{\bar{N}}$'ye dayanarak yansıma vektörü $\color{green}{\bar{R}}$'yi hesaplarız. Bunu GLSL'in yerleşik reflect fonksiyonuyla hesaplayabiliriz. Elde edilen $\color{green}{\bar{R}}$ vektörü, cubemap'i indekslemek/örneklemek için yön vektörü olarak kullanılır; bu cubemap bir çevre renk değeri döndürür. Etkisi, nesnenin skybox'ı yansıtıyormuş gibi görünmesidir.

Sahnede zaten bir skybox kurulumu olduğundan yansımalar oluşturmak çok zor değildir. Konteynere yansıtıcı özellikler kazandırmak için kullanılan fragment shader'ı değiştiriyoruz:

Önce görüş/kamera yönü vektörü I'yi hesaplıyor, ardından skybox cubemap'ini örneklemek için kullandığımız yansıma vektörü R'yi elde ediyoruz. Fragment'ın interpolated Normal ve Position değişkenine ihtiyaç duyduğumuzdan vertex shader'ı da güncellememiz gerekiyor:

Normal vektörler kullandığımızdan bunları normal matrisiyle dönüştürmemiz gerekiyor. Position çıkış vektörü bir dünya uzayı konum vektörüdür; fragment shader'da görüş yönü vektörünü hesaplamak için kullanılır.

Normal'ler kullanıldığından vertex verisiniarrow-up-right ve attribute pointer'ları güncellemeyi unutmayın. Ayrıca cameraPos uniform'unu ayarlayın.

Konteyneri render etmeden önce cubemap dokusunu bağlamayı da unutmayalım:

Kodu derleyip çalıştırdığınızda mükemmel bir ayna gibi davranan bir konteyner elde edersiniz. Çevredeki skybox konteyner üzerinde mükemmel biçimde yansıtılır:

Yansıma sonucu

Tam kaynak kodunu buradanarrow-up-right bulabilirsiniz.

Yansıma tüm nesneye (konteyner gibi) uygulandığında nesne çelik veya krom gibi yüksek yansıtıcı bir materyale sahipmiş gibi görünür. Daha ilginç bir nesne (örneğin model yükleme bölümlerindeki sırt çantası modeli) yüklenirse nesnenin tamamen kromdan yapılmış gibi göründüğü efekti elde ederiz:

Sırt çantası yansıma efekti

Bu gerçekten harika görünüyor; ancak gerçekte modellerin tamamı tamamen yansıtıcı değildir. Reflection map (yansıma haritası) adı verilen teknikler, modellere ekstra detay katmanı ekler. Diffuse ve specular haritaları gibi, yansıma haritaları da bir fragment'ın yansıtıcılığını belirlemek için örnekleyebileceğimiz doku görüntüleridir. Bu haritalarla modelin hangi bölgelerinin yansıma göstereceğini ve ne yoğunlukta olacağını belirleyebiliriz.

Refraction (Kırılma)

Ortam eşlemenin bir diğer türü olan refraction, yansımaya benzer. Kırılma, ışığın içinden geçtiği materyalin değişmesine bağlı olarak yön değiştirmesidir. Su yüzeyi gibi ortamlarda yaygın olarak görülür; ışık düz geçmez, biraz bükülür. Kolunuzun suya yarı batmış hâldeyken görünümünü düşünebilirsiniz.

Kırılma Snell yasasıylaarrow-up-right açıklanır; ortam haritalarıyla birlikte şuna benzer:

Kırılma teorisi

Yine bir görüş vektörü $\color{gray}{\bar{I}}$, normal vektör $\color{red}{\bar{N}}$ ve bu sefer bir kırılma vektörü $\color{green}{\bar{R}}$ var. Görüş vektörünün yönünün hafifçe büküldüğünü görebilirsiniz. Elde edilen bu bükülmüş vektör, cubemap'i örneklemek için kullanılır.

Kırılma, GLSL'in yerleşik refract fonksiyonuyla kolayca uygulanabilir; bu fonksiyon bir normal vektör, bir görüş yönü ve her iki materyalin kırılma indisleri arasındaki oranı bekler.

Kırılma indisi, ışığın bir materyalde ne kadar bükülüp bozulacağını belirler; her materyalin kendine özgü bir kırılma indisi vardır. En yaygın kırılma indisleri şunlardır:

Materyal
Kırılma İndisi

Hava

1.00

Su

1.33

Buz

1.309

Cam

1.52

Elmas

2.42

Nesnenin camdan yapıldığını varsayarsak, ışık/görüş ışını havadan cama geçtiğinden oran $\frac{1.00}{1.52} = 0.658$ olur.

Cubemap bağlı, normal'lerle vertex verisi sağlanmış ve kamera konumu uniform olarak ayarlanmış durumdayken değiştirmemiz gereken tek şey fragment shader'dır:

Kırılma indislerini değiştirerek tamamen farklı görsel sonuçlar elde edebilirsiniz. Uygulamayı derleyip konteyner nesnesi üzerinde çalıştırmak çok ilginç değildir; şu anda yalnızca bir büyüteç gibi davranır. Yüklenen 3B model üzerinde aynı shader'ları kullanmak istediğimiz etkiyi gösterir: cam gibi görünen bir nesne.

Kırılma sonucu

Doğru ışıklandırma, yansıma, kırılma ve vertex hareketi kombinasyonuyla oldukça güzel su grafikleri oluşturabilirsiniz. Fiziksel olarak doğru sonuçlar için ışığın nesneden çıkarken tekrar kırılması gerekir; burada yalnızca tek taraflı kırılma kullandık; ancak bu çoğu amaç için yeterlidir.

Dinamik Ortam Haritaları

Şimdiye kadar skybox için statik bir görüntü kombinasyonu kullandık. Bu harika görünüyor; ancak hareketli nesneler içerebilecek gerçek 3B sahneyi kapsamıyor. Tek nesne kullandığımızdan bunu pek fark etmedik. Aynaya benzer nesnelerin çevresinde birden fazla nesne bulunsaydı, yalnızca skybox aynada görünürdü (sanki sahnedeki tek nesne oymuş gibi).

Framebuffer'lar kullanılarak nesnenin 6 farklı açısından sahnenin bir dokusu oluşturulabilir ve her frame bu doku bir cubemap'te depolanabilir. Bu dinamik olarak üretilen cubemap daha sonra gerçekçi yansıma ve kırılma yüzeyleri oluşturmak için kullanılabilir. Buna dynamic environment mapping (dinamik ortam eşleme) denir.

Harika görünse de büyük bir dezavantajı var: ortam haritası kullanan her nesne için sahneyi 6 kez render etmemiz gerekiyor, bu da uygulamanız üzerinde büyük bir performans yükü bindiriyor. Modern uygulamalar mümkün olduğunca skybox kullanmaya ve dinamik ortam haritaları oluşturmak için cubemap'leri önceden hesaplamaya çalışır. Dinamik ortam eşleme harika bir teknik olmakla birlikte, çok fazla performans kaybetmeden gerçek bir render uygulamasında çalıştırmak, içlerinden çıkması güç pek çok akıllıca numara gerektirir.

Last updated