Işık Kaynakları
Last updated
Last updated
Şu ana kadar kullandığımız tüm ışıklandırmalar uzaydaki tek bir noktadan, tek bir kaynaktan geliyordu. İyi sonuçlar verir, ancak gerçek dünyada her biri farklı davranan çeşitli ışık türleri vardır. Nesnelerin üzerine ışık saçan bir ışık kaynağına light caster denir. Bu bölümde birkaç farklı light caster türünü tartışacağız. Farklı ışık kaynaklarını simüle etmeyi öğrenmek, ortamlarınızı daha da zenginleştirmek için alet kutunuzda bulunan başka bir araçtır.
Önce yönlü ışıktan, sonra daha önce sahip olduklarımızın bir uzantısı olan nokta ışıktan ve son olarak da spot ışıklardan bahsedeceğiz. Bir sonraki bölümde bu farklı ışık türlerinden birkaçını tek bir sahnede birleştireceğiz.
Bir ışık kaynağı çok uzakta olduğunda, bu ışık kaynağından gelen ışık ışınları birbirine paralel hale gelir. Bu, tüm ışık ışınlarının aynı yönden geldiği izlenimini verir ve bu, nesneye ve/veya izleyiciye bakılmaksızın aynıdır. Işık kaynağının sonsuz uzaklıkta olduğu varsayıldığında, tüm ışık ışınlarının aynı yönde olması nedeniyle bu ışık türüne yönsel ışık (directional light) denir ve ışık kaynağının konumundan bağımsızdır.
Yönsel ışığın iyi bir örneği güneştir. Güneş aslında sonsuz uzaklıkta değildir, ancak o kadar uzaktadır ki ışık hesaplamalarında sonsuz uzaklıkta olduğunu kabul edebiliriz. Bu durumda güneşten gelen tüm ışık ışınları paralel olarak modellenebilir:
Tüm ışık ışınları paralel olduğu için, ışık kaynağının konumu her nesneye göre aynı kaldığından, sahnedeki her nesne için aydınlatma hesaplamaları aynı olur.
Böyle bir yönsel ışığı, bir ışık yön vektörü tanımlayarak modelleyebiliriz. Bu durumda, gölgelendirici hesaplamalar çoğunlukla aynı kalır; tek fark, bu sefer ışığın direction (yön)
vektörünü doğrudan kullanmamız ve position (konum)
vektörünü kullanarak lightDir
vektörünü hesaplamamız gerekmediğidir:
Burada light.direction
vektörünü ters çevirdiğimizi unutmayın. Daha önce kullandığımız aydınlatma hesaplamaları, ışık yönünü fragmandan ışık kaynağına doğru bir yön olarak varsayar. Ancak insanlar genellikle ışık kaynağından gelen bir yön vektörünü tercih eder. Bu yüzden ışığın global yön vektörünü tersine çevirmemiz gerekiyor; böylece bu, ışık kaynağına doğru bir yön vektörü olur. Ayrıca, girdinin bir birim vektör olduğunu varsaymanın mantıksız olmasından dolayı, vektörü normalize ettiğinizden emin olun.
Sonuçta elde edilen lightDir
vektörü, difüz ve speküler hesaplamalarda daha önce olduğu gibi kullanılır.
Yönsel bir ışığın birden fazla nesne üzerinde aynı etkiye sahip olduğunu açıkça göstermek için, Koordinat Sistemleri bölümünün sonundaki konteyner partisi sahnesine geri dönüyoruz. Kaçırdıysanız, 10 farklı konteyner pozisyonu tanımlamış ve her konteyner için uygun yerel-den-dünya dönüşümlerini içeren farklı bir model matrisi üretmiştik:
Ayrıca ışık kaynağının yönünü belirtmeyi unutmayın (yönü ışık kaynağından itibaren tanımlıyoruz; ışığın aşağıya doğru yöneldiğini hızlıca görebilirsiniz):
Işığın pozisyon ve yön vektörlerini bir süredir vec3 olarak geçiyoruz, ancak bazı kişiler tüm vektörlerin vec4 olarak tanımlanmasını tercih eder. Pozisyon vektörlerini vec4 olarak tanımlarken, w bileşenini 1.0 olarak ayarlamak önemlidir, böylece çeviri ve projeksiyonlar düzgün şekilde uygulanır. Bununla birlikte, bir yön vektörünü vec4 olarak tanımlarken, çevirinin etkisi olmasını istemeyiz (sadece bir yönü temsil ettikleri için, başka bir şey değil), bu nedenle w bileşenini 0.0 olarak tanımlarız.
Yön vektörleri şöyle temsil edilebilir: vec4(-0.2f, -1.0f, -0.3f, 0.0f). Bu, ışık türleri için kolay bir kontrol işlevi de görebilir: w bileşeninin 1.0 olup olmadığını kontrol edebilirsiniz, böylece artık ışığın bir pozisyon vektörüne sahip olduğunu ve w'nin 0.0 eşit olduğunu görebiliriz, yani bir yön vektörüne sahibiz; bu nedenle hesaplamaları buna göre ayarlayın:
Eğlenceli bir gerçek: Eski OpenGL (sabit işlevsellik) aslında bir ışık kaynağının yönsel bir ışık mı yoksa konumsal bir ışık mı olduğunu bu şekilde belirlerdi ve aydınlatmasını buna göre ayarlardı.
Uygulamayı derler ve sahnenin içinde uçarsanız, ışık kaynağının tüm nesnelerin üzerine güneş benzeri bir ışık yaydığını görebilirsiniz. Difüz ve speküler bileşenlerin, gökyüzünde bir yerde ışık kaynağı varmış gibi tepki verdiğini görebiliyor musunuz? Şöyle görünür:
Uygulamanın tam kaynak kodunu burada bulabilirsiniz.
Yönsel ışıklar, tüm sahneyi aydınlatan küresel ışıklar için harikadır, ancak sahneye dağılmış birkaç nokta ışığı da isteriz. Nokta ışığı, belirli bir konumda bulunan ve tüm yönlere ışık yayan bir ışık kaynağıdır. Bu ışık kaynağından yayılan ışınlar mesafe arttıkça zayıflar. Ampuller ve meşaleleri birer nokta ışığı olarak düşünebilirsiniz.
Önceki bölümlerde basit bir nokta ışıkla çalıştık. Belirli bir pozisyonda, verilen bir ışık kaynağından tüm yönlere ışık yayan bir ışık kaynağımız vardı. Ancak tanımladığımız ışık kaynağı, ışık ışınlarının hiç solmadığı ve dolayısıyla ışık kaynağının çok güçlü olduğu izlenimi yarattı. Çoğu 3D uygulamada, yalnızca ışık kaynağına yakın bir alanı aydınlatan ve tüm sahneyi aydınlatmayan bir ışık kaynağını simüle etmek isteriz.
Önceki bölümlerdeki aydınlatma sahnesine 10 konteyneri ekleyecek olursanız, sahnenin arka kısmındaki konteynerin, ışığın önündeki konteynerle aynı yoğunlukta aydınlatıldığını fark edersiniz; ışık kaynağından uzaklaştıkça zayıflayan bir mantık henüz yoktur. Arka plandaki konteynerin, ışık kaynağına yakın olanlara göre sadece biraz daha az aydınlatılmasını istiyoruz.
Azalma (Attenuation)
Işık ışınlarının yol aldığı mesafe boyunca ışık yoğunluğunu azaltmaya azalma (attenuation) denir. Işık yoğunluğunu mesafe boyunca azaltmanın bir yolu basit bir doğrusal denklem kullanmaktır. Böyle bir denklem, uzak mesafelerdeki nesnelerin daha az parlak olmasını sağlayarak ışık yoğunluğunu doğrusal olarak azaltır. Ancak doğrusal bir işlev, bu durumu biraz yapay gösterebilir. Gerçek dünyada ışıklar genellikle yakındayken oldukça parlaktır, ancak bir ışık kaynağının parlaklığı mesafe arttıkça hızla azalır; kalan ışık yoğunluğu daha sonra mesafe arttıkça yavaşça azalır. Bu nedenle ışığın yoğunluğunu azaltmak için farklı bir denkleme ihtiyacımız var.
Neyse ki bazı zeki insanlar bunu bizim için çoktan çözdüler. Aşağıdaki formül, ışığın yoğunluk vektörüyle daha sonra çarptığımız bir azalma değeri hesaplar:
Sabit terim genellikle 1.0 olarak tutulur; bu, paydanın asla 1'den küçük olmamasını sağlamak içindir, çünkü aksi takdirde bazı mesafelerde yoğunluğu arttırır, bu da istediğimiz etki değildir.
Doğrusal terim, ışık kaynağının yoğunluğunu doğrusal olarak azaltan mesafe değeriyle çarpılır.
Kuadratik terim, mesafenin karesi ile çarpılır ve ışık kaynağının yoğunluğunu kuadratik olarak azaltır. Kuadratik terim, mesafe küçükken doğrusal terime göre daha az önemli olacak, ancak mesafe arttıkça daha büyük hale gelecektir.
Kuadratik terim nedeniyle ışık, mesafe küçükken neredeyse doğrusal bir şekilde azalacak, ta ki kuadratik terim doğrusal terimi geçene kadar ve ardından ışık yoğunluğu çok daha hızlı bir şekilde azalmaya başlayacak. Sonuçta ışık, yakın mesafedeyken oldukça yoğun olacak, ancak mesafe arttıkça hızla parlaklığını kaybedecek ve sonunda daha yavaş bir hızla parlaklığını kaybedecek. Aşağıdaki grafik, bu tür bir azalmanın 100 mesafelik etkisini göstermektedir:
Işığın, mesafe küçükken en yüksek yoğunluğa sahip olduğunu görebilirsiniz, ancak mesafe arttıkça yoğunluğu önemli ölçüde azalır ve 100 mesafe civarında yavaşça 0 yoğunluğuna ulaşır. Tam olarak istediğimiz şey budur.
Peki bu 3 terim için hangi değerleri belirlemeliyiz? Doğru değerleri ayarlamak birçok faktöre bağlıdır: ortam, kaplamak istediğiniz mesafe, ışığın türü vb. Çoğu durumda bu, tecrübe ve ölçülü bir ayar yapma meselesidir. Aşağıdaki tablo, belirli bir yarıçapı (mesafe) kaplayan gerçekçi bir ışık kaynağı simüle etmek için bu terimlerin alabileceği bazı değerleri göstermektedir. İlk sütun, belirli terimlerle bir ışığın kaplayacağı mesafeyi belirtir. Bu değerler çoğu ışık için iyi başlangıç noktalarıdır ve Ogre3D'nin wikisinden alınmıştır:
7
1.0
0.7
1.8
13
1.0
0.35
0.44
20
1.0
0.22
0.20
32
1.0
0.14
0.07
50
1.0
0.09
0.032
65
1.0
0.07
0.017
100
1.0
0.045
0.0075
160
1.0
0.027
0.0028
200
1.0
0.022
0.0019
325
1.0
0.014
0.0007
600
1.0
0.007
0.0002
3250
1.0
0.0014
0.000007
Gördüğünüz gibi, sabit terim KcK_cKc tüm durumlarda 1.0 olarak tutulmuştur. Doğrusal terim KlK_lKl genellikle daha büyük mesafeleri kaplamak için oldukça küçüktür ve kuadratik terim KqK_qKq ise daha da küçüktür. Bu değerlerle biraz deneme yaparak, uygulamanızda etkilerini görmeyi deneyin. Bizim ortamımızda, 32 ila 100 mesafesi çoğu ışık için genellikle yeterlidir.
Azalmayı uygulamak için fragment shader'da 3 ekstra değere ihtiyacımız olacak: denklemdeki sabit, doğrusal ve kuadratik terimler. Bunlar, daha önce tanımladığımız Light yapısında saklanabilir. Işığın yönünü position kullanarak tekrar hesaplamamız gerektiğini unutmayın, çünkü bu bir nokta ışığıdır (önceki bölümde yaptığımız gibi) ve yönsel ışık değildir.
Ardından uygulamamızda terimleri ayarlıyoruz: Işığın 50 mesafelik bir alanı kaplamasını istiyoruz, bu nedenle tablodan uygun sabit, doğrusal ve kuadratik terimleri kullanacağız:
Fragment shader'da azalmayı uygulamak nispeten basittir: denklemden azaltma değerini hesaplar ve bunu ortam, difüz ve speküler bileşenlerle çarparız.
Denklemin çalışması için ışık kaynağına olan mesafeye ihtiyacımız var. Bir vektörün uzunluğunu nasıl hesaplayabileceğimizi hatırlıyor musunuz? Mesafe terimini, fragman ile ışık kaynağı arasındaki fark vektörünü hesaplayarak ve bu vektörün uzunluğunu alarak elde edebiliriz. Bunun için GLSL'in yerleşik length işlevini kullanabiliriz:
Daha sonra bu azalma değerini, ortam, difüz ve speküler renklerle çarparak aydınlatma hesaplamalarına dahil ediyoruz:
Ortam bileşenini yalnız bırakabiliriz, böylece ortam aydınlatması mesafe boyunca azalmaz, ancak birden fazla ışık kaynağı kullanırsak tüm ortam bileşenleri birikmeye başlayacaktır. Bu durumda ortam aydınlatmasını da azaltmak isteyebiliriz. Ortamınıza en uygun olanı bulmak için biraz deneme yapın.
Uygulamayı çalıştırırsanız şu şekilde bir sonuç elde edersiniz:
Gördüğünüz gibi şu anda yalnızca öndeki konteynerler aydınlatılıyor ve en yakındaki konteyner en parlak olanıdır. Arkadaki konteynerler ise ışık kaynağından çok uzak oldukları için hiç aydınlatılmamaktadır. Uygulamanın kaynak kodunu buradan bulabilirsiniz.
Bir nokta ışığı, bu nedenle konumlandırılabilir ve aydınlatma hesaplamalarına azalma uygulanan bir ışık kaynağıdır. Aydınlatma cephaneliğimiz için bir başka ışık türü daha!
Son olarak ele alacağımız ışık türü spot ışığıdır. Spot ışık, ışık ışınlarını her yöne göndermek yerine, sadece belirli bir yöne gönderen ve ortamda bir yerde bulunan bir ışık kaynağıdır. Sonuç olarak, sadece spot ışığın belirli bir yarıçapı içindeki nesneler aydınlatılır ve geri kalan her şey karanlık kalır. Spot ışığın iyi bir örneği sokak lambası veya el feneridir.
OpenGL'de bir spot ışık, dünya uzayında bir konum, bir yön ve spot ışığın kesilme açısını temsil eder. Her fragman için, fragmanın spot ışığın kesilme yönleri arasında olup olmadığını hesaplarız ve eğer öyleyse, fragmanı buna göre aydınlatırız. Aşağıdaki resim, bir spot ışığın nasıl çalıştığı hakkında bir fikir verir:
LightDir: Fragmandan ışık kaynağına doğru olan vektör.
SpotDir: Spot ışığın hedef aldığı yön.
Fi (φ): Spot ışığın yarıçapını belirleyen kesilme açısı. Bu açının dışındaki her şey spot ışık tarafından aydınlatılmaz.
Teta (θ): LightDir ve SpotDir vektörleri arasındaki açı. Spot ışığın içinde olmak için θ değeri φ'den küçük olmalıdır.
Temelde yapmamız gereken şey, LightDir ve SpotDir vektörleri arasında nokta çarpımını (iki birim vektör arasındaki açının kosinüsünü döndürür) hesaplamak ve bunu kesilme açısı φ ile karşılaştırmaktır. Artık spot ışığın ne olduğunu (bir şekilde) anladığınıza göre, bir el feneri şeklinde bir tane oluşturalım.
El feneri, izleyicinin konumunda yer alan ve genellikle oyuncunun bakış açısına doğru nişanlanan bir spot ışıktır. Bir el feneri aslında normal bir spot ışıktır, ancak konumu ve yönü oyuncunun pozisyonuna ve bakış açısına göre sürekli güncellenir.
Bu nedenle, fragment shader için ihtiyaç duyacağımız değerler, spot ışığın pozisyon vektörü (fragmandan ışığa doğru yön vektörünü hesaplamak için), spot ışığın yön vektörü ve kesilme açısıdır. Bu değerleri Light yapısında saklayabiliriz:
Sonra uygun değerleri shader'a geçiriyoruz:
Gördüğünüz gibi kesilme değeri için bir açı belirlemek yerine, bir açıya dayalı olarak kosinüs değerini hesaplayıp sonucu fragment shader'a geçiriyoruz. Bunun nedeni, fragment shader'da LightDir ve SpotDir vektörleri arasındaki nokta çarpımını hesaplıyor olmamız ve nokta çarpımının bir kosinüs değeri döndürmesi, bir açı değil. Bir açıyı doğrudan bir kosinüs değeri ile karşılaştıramayız; shader'da açıyı elde etmek için nokta çarpımının ters kosinüsünü hesaplamamız gerekir ki bu pahalı bir işlemdir. Bu yüzden bazı performans tasarrufları yapmak için belirli bir kesilme açısının kosinüs değerini önceden hesaplayıp sonucu fragment shader'a geçiriyoruz. Bu şekilde, her iki açı da kosinüs olarak temsil edildiğinden, doğrudan karşılaştırma yapabiliriz.
Şimdi yapmamız gereken şey, θ değerini hesaplamak ve φ değeri ile karşılaştırmak; bu, spot ışığın içinde mi yoksa dışında mı olduğumuzu belirler:
Öncelikle lightDir vektörü ile negatif direction vektörü arasındaki nokta çarpımını hesaplıyoruz (ters çevrilmiş, çünkü vektörlerin ışık kaynağına doğru yönelmesini istiyoruz, aksi yönde değil). Tüm ilgili vektörleri normalize ettiğinizden emin olun.
if koşulunda neden < işareti yerine > işareti olduğunu merak ediyor olabilirsiniz. Spot ışığın içinde olmak için θ değeri, ışığın kesilme değerinden daha küçük olmamalı mı? Bu doğru, ancak açı değerlerinin kosinüs değerleri olarak temsil edildiğini ve 0 derecelik bir açının kosinüs değerinin 1.0 olarak, 90 derecelik bir açının ise 0.0 olarak temsil edildiğini unutmayın. Aşağıda görebileceğiniz gibi:
Şimdi kosinüs değeri 1.0'a ne kadar yakınsa, açının o kadar küçük olduğunu görebilirsiniz. Şimdi θ değerinin kesilme değerinden büyük olması gerektiği daha mantıklı hale geliyor. Kesilme değeri şu anda 12.5 derecelik kosinüs değerine, yani 0.976'ya eşittir. Dolayısıyla, θ kosinüs değeri 0.976 ile 1.0 arasında ise, fragmanın spot ışık içinde aydınlatılması sonucunu verir.
Uygulamayı çalıştırmak, yalnızca spot ışığın konisinin içinde doğrudan yer alan fragmanları aydınlatan bir spot ışıkla sonuçlanır. Şöyle görünecektir:
Uygulamanın tam kaynak kodunu buradan bulabilirsiniz.
Spot ışık hala biraz yapay görünüyor, çünkü spot ışığın sert kenarları var. Spot ışığın konisinin kenarına bir fragman ulaştığında, ışık tamamen kapanıyor, yumuşak bir geçiş yerine. Gerçekçi bir spot ışık, kenarlarında ışığı kademeli olarak azaltır.
Pürüzsüz kenarlı bir spot ışık etkisi oluşturmak için iç ve dış konisi olan bir spot ışık simüle etmek istiyoruz. İç koniyi önceki bölümde tanımlanan koni olarak ayarlayabiliriz, ancak ayrıca dış bir koni de istiyoruz. Bu dış koni, içten dış koninin kenarlarına kadar ışığı kademeli olarak azaltır.
Dış bir koni oluşturmak için basitçe, spot ışığın yön vektörü ile dış koninin vektörü arasındaki açıyı (yarıçapına eşit) temsil eden başka bir kosinüs değeri tanımlarız. Ardından, bir fragman iç ve dış koni arasında yer alıyorsa, 0.0 ile 1.0 arasında bir yoğunluk değeri hesaplar. Fragman iç koninin içindeyse, yoğunluğu 1.0'a, dış koninin dışındaysa 0.0'a eşit olur.
Böyle bir değeri aşağıdaki denklemi kullanarak hesaplayabiliriz:
Burada ϵ (epsilon), iç koni (ϕ) ile dış koni (γ) arasındaki kosinüs farkını temsil eder (ϵ=ϕ−γ). Ortaya çıkan I değeri, mevcut fragmandaki spot ışığın yoğunluğudur.
Bu formülün nasıl çalıştığını görselleştirmek biraz zordur, bu yüzden birkaç örnek değerle deneyelim:
Gördüğünüz gibi, θ değerine dayalı olarak dış kosinüs ile iç kosinüs arasında temel olarak interpolasyon yapıyoruz. Eğer hala ne olduğunu tam olarak anlamıyorsanız endişelenmeyin, bu formülü şimdilik kabul edebilir ve ileride daha deneyimli olduğunuzda geri dönebilirsiniz.
Artık spot ışığın dışında olduğunda negatif, iç koni içinde olduğunda 1.0'dan büyük ve kenarlar arasında bir yerde olduğunda ise arada bir yoğunluk değeri elde ediyoruz. Değerleri uygun şekilde clamp (sınırlarsak) if-else kontrolüne gerek kalmaz ve ışık bileşenlerini hesaplanan yoğunluk değeriyle çarpabiliriz:
İlk argümanını 0.0 ile 1.0 değerleri arasında sınırlayan clamp işlevini kullandığımıza dikkat edin. Bu, yoğunluk değerlerinin [0, 1] aralığının dışına çıkmamasını sağlar.
outerCutOff değerini Light yapısına eklediğinizden ve uygulamada uniform değerini ayarladığınızdan emin olun. Aşağıdaki resim için 12.5 iç kesilme açısı ve 17.5 dış kesilme açısı kullanıldı:
Ahh, çok daha iyi oldu. İç ve dış kesilme açılarıyla oynayın ve ihtiyaçlarınıza daha uygun bir spot ışık oluşturmaya çalışın. Uygulamanın kaynak kodunu buradan bulabilirsiniz.
Böyle bir el feneri/spot ışık türündeki lamba, korku oyunları için mükemmeldir ve yönsel ve nokta ışıklarıyla birleştirildiğinde, çevreyi gerçekten aydınlatmaya başlar.
Tüm farklı ışık türleri ve onların fragment shader'larıyla denemeler yapın. Bazı vektörleri ters çevirmeyi ve/veya < işareti yerine > kullanmayı deneyin. Farklı görsel sonuçları açıklamaya çalışın.
Orijinal Kaynak: Light casters
Çeviri: Nezihe Sözen
Burada d, fragmandan ışık kaynağına olan mesafeyi temsil eder. Azalma değerini hesaplamak için üç (ayarlanabilir) terim tanımlarız: bir sabit terim , bir doğrusal terim ve bir kuadratik terim .