Çoklu Işık Kaynakları

Önceki bölümlerde OpenGL’de aydınlatma hakkında birçok şey öğrendik. Phong shader, materyaller, aydınlatma haritaları ve farklı ışık türleri gibi konulara değindik. Bu bölümde, önceki bilgileri birleştirerek 6 aktif ışık kaynağına sahip tam aydınlatılmış bir sahne oluşturacağız. Güneş benzeri bir ışık kaynağını yönlendirilmiş ışık (directional light) olarak simüle edeceğiz, sahne boyunca 4 noktasal ışık (point light) yerleştireceğiz ve ayrıca bir el feneri (flashlight) ekleyeceğiz.

Bir sahnede birden fazla ışık kaynağı kullanırken, aydınlatma hesaplamalarını GLSL fonksiyonlarına kapsüllemek (encapsulate) istiyoruz. Bunun sebebi, birden fazla ışık türüyle çalışırken her biri farklı hesaplamalar gerektirdiğinden kodun hızla karmaşık hale gelmesidir. Eğer tüm bu hesaplamaları yalnızca ana fonksiyon (main function) içinde yapmaya çalışırsak, kod hızla anlaşılması zor bir hale gelir.

GLSL’de fonksiyonlar, C fonksiyonları ile benzer şekilde çalışır. Bir fonksiyon adı, bir dönüş tipi (return type) ve eğer fonksiyon ana fonksiyondan önce tanımlanmamışsa kod dosyasının üst kısmında bir prototip tanımlaması yapmamız gerekir. Yönlendirilmiş ışık (directional light), noktasal ışık (point light) ve spot ışık (spotlight) olmak üzere her bir ışık türü için ayrı fonksiyonlar oluşturacağız.

Bir sahnede birden fazla ışık kaynağı kullanırken genellikle şu yaklaşım izlenir: Her shader’ın çıktı rengini temsil eden tek bir renk vektörümüz olur. Sahnedeki her ışık kaynağı, shader’ın çıktısına olan katkısını hesaplayarak bu renk vektörüne ekler. Yani her ışık, sahnede kendi bireysel etkisini hesaplar ve nihai çıktı rengine katkıda bulunur. Genel yapı aşağıdaki gibi olacaktır:

out vec4 FragColor;

void main()
{
  // çıktı renk değerini tanımla
  vec3 output = vec3(0.0);
  // yönlendirilmiş ışığın katkısını çıktıya ekle
  output += someFunctionToCalculateDirectionalLight();
  // tüm noktasal ışıklar için aynı işlemi yap
  for(int i = 0; i < nr_of_point_lights; i++)
  	output += someFunctionToCalculatePointLight();
  // ve diğer ışıkları da ekle (örneğin spot ışıkları)
  output += someFunctionToCalculateSpotLight();
  
  FragColor = vec4(output, 1.0);
}

Gerçek kod uygulamaya göre farklılık gösterebilir, ancak genel yapı aynı kalır. Her bir ışık kaynağının etkisini hesaplayan fonksiyonlar tanımlarız ve bu fonksiyonların ürettiği renk değerlerini bir çıktı renk vektörüne ekleriz. Örneğin, iki ışık kaynağı bir fragmente yakınsa, bu ışıkların birleşik katkısı, tek bir ışık kaynağı tarafından aydınlatılan bir fragmente kıyasla daha parlak bir görüntü oluşturacaktır.

Yönlendirilmiş Işık (Directional Light)

Fragment shader içinde, bir yönlendirilmiş ışığın ilgili fragment'e olan katkısını hesaplayan bir fonksiyon tanımlamak istiyoruz. Bu fonksiyon, belirli parametreleri alarak hesaplanan yönlendirilmiş ışık rengini döndürecektir.

Öncelikle, bir yönlendirilmiş ışık kaynağı için minimum düzeyde gerekli değişkenleri belirlememiz gerekiyor. Bu değişkenleri DirLight adlı bir struct içinde saklayabilir ve bir uniform olarak tanımlayabiliriz. Struct içindeki değişkenler, önceki bölümden hatırlayacağınız kavramlardır:

struct DirLight {
    vec3 direction;
  
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  
uniform DirLight dirLight;

Daha sonra, dirLight uniform değişkenini aşağıdaki prototipe sahip bir fonksiyona aktarabiliriz:

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir);  

Gördüğünüz gibi, bu fonksiyonun hesaplamalarını gerçekleştirebilmesi için bir DirLight struct ve iki vektör gereklidir. Önceki bölümü başarıyla tamamladıysanız, bu fonksiyonun içeriği sizin için sürpriz olmayacaktır.

vec3 CalcDirLight(DirLight light, vec3 normal, vec3 viewDir)
{
    vec3 lightDir = normalize(-light.direction);
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    // specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // sonuçları birleştir
    vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    return (ambient + diffuse + specular);
}

Temelde, önceki bölümdeki kodu kopyaladık ve yönlendirilmiş ışığın katkı vektörünü hesaplamak için fonksiyon argümanları olarak verilen vektörleri kullandık. Ortaya çıkan ambient, diffuse ve specular katkıları, tek bir renk vektörü olarak döndürülür.

Noktasal Işık (Point Light)

Yönlendirilmiş ışıklara benzer şekilde, bir noktasal ışığın belirli bir shader çıktısına (fragment) olan katkısını hesaplayan bir fonksiyon tanımlamak istiyoruz. Bu hesaplamaya ışığın zayıflaması (attenuation) da dahil edilmelidir.

Yönlendirilmiş ışıklarda olduğu gibi, bir noktasal ışık için gerekli tüm değişkenleri içeren bir struct tanımlamak istiyoruz:

struct PointLight {    
    vec3 position;
    
    float constant;
    float linear;
    float quadratic;  

    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};  
#define NR_POINT_LIGHTS 4  
uniform PointLight pointLights[NR_POINT_LIGHTS];

Gördüğünüz gibi, sahnemizde kaç tane noktasal ışık (point light) olacağını belirlemek için GLSL’de bir ön işlemci direktifi (pre-processor directive) kullandık. Daha sonra bu NR_POINT_LIGHTS sabitini kullanarak PointLight struct’larından oluşan bir dizi (array) oluşturduk.

GLSL’de diziler, C dizileriyle (C arrays) aynıdır ve köşeli parantezler ([ ]) kullanılarak tanımlanır. Şu anda 4 adet PointLight struct oluşturduk ve bunları veriyle doldurmamız gerekiyor.

Noktasal ışık fonksiyonunun prototipi ise şu şekildedir:

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir);  

Fonksiyon, gerekli tüm verileri argüman olarak alır ve bu belirli noktasal ışığın (point light) shader çıktısına (fragment) yaptığı renk katkısını temsil eden bir vec3 değeri döndürür.

Yine, akıllı bir kopyala-yapıştır işlemiyle aşağıdaki fonksiyon elde edilir:

vec3 CalcPointLight(PointLight light, vec3 normal, vec3 fragPos, vec3 viewDir)
{
    vec3 lightDir = normalize(light.position - fragPos);
    // diffuse shading
    float diff = max(dot(normal, lightDir), 0.0);
    // specular shading
    vec3 reflectDir = reflect(-lightDir, normal);
    float spec = pow(max(dot(viewDir, reflectDir), 0.0), material.shininess);
    // attenuation
    float distance    = length(light.position - fragPos);
    float attenuation = 1.0 / (light.constant + light.linear * distance + 
  			     light.quadratic * (distance * distance));    
    // sonuçları birleştir
    vec3 ambient  = light.ambient  * vec3(texture(material.diffuse, TexCoords));
    vec3 diffuse  = light.diffuse  * diff * vec3(texture(material.diffuse, TexCoords));
    vec3 specular = light.specular * spec * vec3(texture(material.specular, TexCoords));
    ambient  *= attenuation;
    diffuse  *= attenuation;
    specular *= attenuation;
    return (ambient + diffuse + specular);
} 

Bu işlevselliği böyle bir fonksiyon içine almak, birden fazla noktasal ışık için aydınlatmayı hesaplamayı kolaylaştırır ve kod tekrarını önler. Ana fonksiyon (main function) içinde, noktasal ışık dizisi (point light array) üzerinde dönen bir döngü oluşturarak her noktasal ışık için CalcPointLight fonksiyonunu çağırabiliriz.

Hepsini Bir Araya Getirelim

Artık yönlendirilmiş ışık (directional light) ve noktasal ışık (point light) için fonksiyonları tanımladığımıza göre, bunları ana fonksiyon (main function) içinde birleştirebiliriz.

void main()
{
    // özellikler
    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);

    // faz 1: Directional lighting
    vec3 result = CalcDirLight(dirLight, norm, viewDir);
    // faz 2: Point lights
    for(int i = 0; i < NR_POINT_LIGHTS; i++)
        result += CalcPointLight(pointLights[i], norm, FragPos, viewDir);    
    // faz 3: Spot light
    //result += CalcSpotLight(spotLight, norm, FragPos, viewDir);    
    
    FragColor = vec4(result, 1.0);
}

Her ışık türü, çıktı rengine kendi katkısını ekler ve tüm ışık kaynakları işlenene kadar bu süreç devam eder. Sonuç olarak elde edilen renk, sahnedeki tüm ışık kaynaklarının birleşik etkisini içerir.

CalcSpotLight fonksiyonunu oluşturmayı ise okuyucuya bir alıştırma olarak bırakıyoruz.

Yönlendirilmiş ışık (directional light) struct'ı için uniform değerlerini ayarlamak oldukça tanıdık gelmeli, ancak noktasal ışıkların (point lights) uniform değerlerini nasıl ayarlayacağınızı merak ediyor olabilirsiniz. Çünkü noktasal ışık uniform değişkeni, aslında bir PointLight struct dizisidir. Bu konuya daha önce değinmemiştik.

Neyse ki, bu işlem oldukça basittir. Bir struct’ın uniform değerlerini ayarlamak ile struct dizisinin uniform değerlerini ayarlamak temelde aynı mantıkla çalışır. Ancak bu sefer, uniform’un konumunu sorgularken uygun indis değerini de belirtmemiz gerekir:

lightingShader.setFloat("pointLights[0].constant", 1.0f);

Burada, pointLights dizisindeki ilk PointLight struct'ını indeksleyerek, içsel olarak constant değişkeninin konumunu alıyoruz ve bu değeri 1.0 olarak ayarlıyoruz.

Ayrıca, 4 noktasal ışık (point light) için birer konum vektörü tanımlamamız gerektiğini de unutmayalım. Bu ışıkları sahneye daha iyi yaymak için farklı konumlara yerleştireceğiz.

Bunun için, noktasal ışıkların (point lights) konumlarını içeren başka bir glm::vec3 dizisi tanımlayacağız:

glm::vec3 pointLightPositions[] = {
	glm::vec3( 0.7f,  0.2f,  2.0f),
	glm::vec3( 2.3f, -3.3f, -4.0f),
	glm::vec3(-4.0f,  2.0f, -12.0f),
	glm::vec3( 0.0f,  0.0f, -3.0f)
};

Daha sonra, pointLights dizisinden ilgili PointLight struct’ını indeksleyerek, position özelliğini az önce tanımladığımız konumlardan biriyle ayarlıyoruz. Ayrıca, artık sadece 1 değil, 4 ışık küpü (light cube) çizdiğinizden emin olun. Bunun için, her ışık objesi için ayrı bir model matrisi oluşturmanız gerekir, tıpkı önceden konteynerler için yaptığımız gibi.

Eğer sahneye bir el feneri (flashlight) de eklerseniz, tüm ışık kaynaklarının birleşik etkisi aşağıdaki gibi görünecektir:

Gördüğünüz gibi, gökyüzünde güneş benzeri bir global ışık kaynağı bulunuyor, sahneye dağılmış 4 noktasal ışık (point light) yerleştirilmiş ve oyuncunun bakış açısından görülebilen bir el feneri (flashlight) mevcut. Oldukça etkileyici görünüyor, değil mi?

Son uygulamanın tüm kaynak koduna buradan ulaşabilirsiniz.

Görselde, önceki bölümlerde kullandığımız varsayılan ışık özellikleriyle ayarlanmış tüm ışık kaynakları gösteriliyor. Ancak, bu değerlerle oynayarak oldukça ilginç sonuçlar elde edebilirsiniz.

Sanatçılar ve seviye tasarımcıları (level designers), genellikle büyük bir editör içinde tüm bu ışık değişkenlerini ayarlayarak ışıklandırmanın çevreyle uyumlu olmasını sağlarlar. Basit ortamımızı kullanarak bile, yalnızca ışıkların özelliklerini değiştirerek oldukça ilginç görseller oluşturabilirsiniz:

Ayrıca, aydınlatmayı daha iyi yansıtmak için arka plan temizleme rengini (clear color) değiştirdik. Sadece birkaç ışıklandırma parametresini ayarlayarak tamamen farklı atmosferler oluşturabileceğinizi görebilirsiniz.

Bu noktaya kadar, OpenGL'de aydınlatma konusunda oldukça iyi bir anlayışa sahip olmuş olmalısınız. Şimdiye kadar öğrendiğiniz bilgilerle, ilgi çekici ve görsel olarak zengin ortamlar ve atmosferler oluşturabilirsiniz.

Tüm farklı ışık değerleriyle oynamayı deneyin ve kendi benzersiz atmosferlerinizi oluşturun!

Alıştırmalar

Son görseldeki farklı atmosferleri, ışığın öznitelik değerlerini değiştirerek yeniden yaratabilir misiniz? çözüm.

Orijinal Kaynak: Multiple lights

Çeviri: Nezihe Sözen

Last updated

Was this helpful?