Merhaba Üçgen

OpenGL’de her şey 3B uzaydadır, ancak ekran ve pencere 2B piksel dizisidir. Bu nedenle OpenGL’in görevinin büyük bir kısmı tüm 3B koordinatları ekranınıza sığacak 2B piksellere dönüştürmekle ilgilidir. 3B koordinatları 2B piksellere dönüştürme işlemi, OpenGL’in grafik iş hattı tarafından yönetilir. Grafik iş hattı iki büyük bölüme ayrılabilir: Birinci bölümde 3B koordinatları 2B koordinatlara, ikinci kısımda ise 2B koordinatları gerçek renkli piksellere dönüştürür. Bu eğitselde, grafik iş hattını ve süsşü püslü pikseller oluşturmak için bunu avantajımıza nasıl kullanabileceğimizi kısaca tartışacağız.

Grafik iş hattı giriş olarak bir dizi 3B koordinat alır ve bunları ekranınızdaki renkli 2B piksellere dönüştürür. Grafik iş hattı, her bir basamağın bir önceki basamağın çıktısını almasını gerektiren birkaç adıma ayrılabilir. Bu adımların tümü oldukça özelleştirilmiştir (belirli bir işlevi vardır) ve kolayca paralel olarak çalıştırılabilirler. Paralel yapıları nedeniyle, günümüzün grafik kartlarında, iş hattının her adımı için GPU’da küçük programlar çalıştırarak verilerinizi grafik iş hattı içinde hızlı bir şekilde işleyebileceğiniz binlerce küçük işlem çekirdeği bulunur.Bu küçük programlar, gölgelendirici (ing. shader) olarak isimlendirilir.

Bu gölgelendiricilerin bazıları geliştirici tarafından yapılandırılabilir. Bu bize iş hattının belirli bölümleri üzerinde çok daha hassas bir kontrol sağlıyor ve GPU’da çalıştıkları için CPU zamanından da tasarruf edebiliyorlar. Gölgelendiriciler OpenGL Shading Language(GLSL) ile yazılmıştır ve bir sonraki derste bunun içine daha çok gireceğiz.

Aşağıda grafik iş hattının tüm aşamalarının soyut bir gösterimini bulacaksınız. Mavi bölümlerin kendi gölgelendiricilerimizi yazabileceğimiz bölümleri temsil ettiğini unutmayın.

Gördüğünüz gibi, grafik iş hattı her biri tepe noktası(ing. vertex) verilerinizi tamamen işlenmiş bir piksele çeviren çok sayıda bölüm içermektedir. İş hattının nasıl çalıştığı hakkında size genel bir bilgi sağlamak için, iş hattının her bir bölümünü kısaca açıklayacağız.

Grafik boru hattına girdi olarak, burada Vertex Data adlı bir dizide üçgen oluşturması gereken üç adet 3-boyutlu koordinat listesini veriyoruz. Bir tepe noktası, temel olarak 3-boyutlu koordinat başına düşen veri topluluğudur. Bu tepe noktası verisi, istediğimiz verileri içerebilen tepe özellikleri kullanılarak temsil edilir, ancak sadelik için her tepe noktasının sadece 3-boyutlu bir konumdan ve bir renk değerinden oluştuğunu varsayalım.

OpenGL’in koordinatlar ve renk değerleri içerisinden ne yapacağını bilmesi için, verilerle ne tür sahneleme oluşturmak istediğinizin ipuçlarını vermenizi gerektirir. Verilerin bir nokta topluluğu mu, üçgenler topluluğu mu yoksa sadece uzun bir çizgi olarak mı oluşturulmasını istiyoruz?Bu ipuçlarına primitif denir ve çizim komutlarından herhangi birini çağırırken OpenGL’e verilir. Bu ipuçlarından bazıları GL_POINTS, GL_TRIANGLES ve GL_LINE_STRIP’dir.

İş hattının ilk kısmı, giriş olarak tek bir tepe noktası alan tepe noktası gölgelendiricisidir (ing. vertex shader). Tepe noktası gölgelendiricisinin temel amacı, 3-boyutlu koordinatları farklı 3-boyutlu koordinatlara dönüştürmektir ve bu gölgelendirici, tepe noktası özellikleri üzerinde bazı temel işlemler yapmamıza izin verir.

Primitif birleştirme aşaması, tüm primitifleri (GL_POINTS seçilmişse tepe noktası) oluşturan tepe noktası gölgelendiriciden girdi olarak alır ve tüm noktaları verilen primitif biçimde birleştirir. Bu örneğimizde bir üçgen.

Primitif birleştirme aşamasının çıktısı, geometri gölgelendiricisine iletilir. Geometri gölgelendirici, primitif oluşturan bir tepe noktası topluluğunu alır ve yeni (veya diğer) primitifler oluşturmak için yeni tepe noktaları çıkararak başka şekiller üretme kabiliyetine sahiptir. Bu örnekte, verilen şeklin dışında ikinci bir üçgen oluşturur.

Geometri gölgelendiricisinin çıktısı daha sonra, primitifleri nihai ekranda karşılık gelen piksellerle eşleştirdiği ve parça gölgelendiricinin (ing. fragment shader) kullanması için parçalar ile sonuçlanan pikselleştirme(ing. rasterization) aşamasına geçirilir. Parça gölgelendiricileri çalıştırılmadan önce kırpma işlemi gerçekleştirilir. Kırpma, görüşünüz dışındaki tüm parçaları atarak performansı artırır.

OpenGL’deki bir parça (ing. fragment), OpenGL’in tek bir piksel oluşturması için gereken tüm verilerdir.

Parça gölgelendiricinin temel amacı, bir pikselin son rengini hesaplamaktır ve bu genellikle gelişmiş OpenGL efektlerinin gerçekleştiği aşamadır. Parça gölgelendirici genellikle, son piksel rengini (ışıklar, gölgeler, ışığın rengi vb.) hesaplamak için kullanabileceği 3-boyutlu sahne hakkındaki verileri içerir.

Karşılık gelen tüm renk değerleri belirlendikten sonra, nihai nesne, alfa testi ve harmanlama aşaması dediğimiz bir aşamadan daha geçecektir. Bu aşama, parçanın karşılık gelen derinlik (ve şablon) değerini kontrol eder ve elde edilen parçanın diğer nesnelerin önünde mi yoksa arkasında mı olduğunu kontrol etmek için bunları kullanır. Aşama ayrıca alfa değerlerini de kontrol eder (alfa değerleri bir nesnenin opaklığını tanımlar) ve nesneleri buna göre harmanlar (ing. blending). Dolayısıyla, bir piksel çıktı rengi parça gölgelendiricisinde hesaplansa bile, son piksel rengi birden çok üçgen oluştururken yine de tamamen farklı bir şey olabilir.

Gördüğünüz gibi, grafik iş hattı oldukça karmaşık bir bütündür ve değiştirilebilir birçok parça içermektedir. Bununla birlikte, neredeyse tüm durumlar için sadece tepe ve parça gölgelendiriciyle çalışmak zorundayız. Geometri gölgelendiricisi isteğe bağlıdır ve genellikle varsayılan gölgelendiricide bırakılır.

Modern OpenGL’de kendimize ait en az bir tepe noktası ve parça gölgelendirici tanımlamamız gerekmektedir (GPU’da varsayılan tepe noktası/ parça gölgelendirici yoktur). Bu nedenle, Modern OpenGL’i öğrenmeye başlamak çoğu zaman zordur, çünkü ilk üçgeni oluşturmadan önce çok fazla bilgi gerekmektedir. Bu bölümün sonunda üçgeni oluşturduğunuzda, grafik programlama hakkında daha fazla şey öğrenmiş olacaksınız.

Köşe Nokta Girdisi

Bir şeyler çizmeye başlamak için öncelikle OpenGL’e girdi olarak tepe noktası verisi vermeliyiz. OpenGL 3-boyutlu bir grafik kütüphanesidir, bu yüzden OpenGL’de belirlediğimiz tüm koordinatlar 3-boyutludur (x, y ve z koordinatı). OpenGL, tüm 3-boyutlu koordinatları ekranınızdaki 2-boyutlu piksellere dönüştürmez; OpenGL 3-boyutlu koordinatları yalnızca 3 eksenin (x, y ve z) -1 ile 1.0 arasında belirli bir aralıktayken işler. Bu normalize koordinatlar aralığındaki tüm koordinatlar ekranınızda görünecektir (ve bu bölgenin dışındaki hiç bir koordinat görünmeyecektir).

Tek bir üçgen oluşturmak istediğimiz için, her tepe noktasında 3-boyutlu konumu olan toplam üç tepe noktası belirlemek istiyoruz. Onları float dizisindeki normalize edilmiş koordinatlar (OpenGL’in görünür bölgesi) olarak tanımlarız:

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
}; 

OpenGL 3-boyutlu alanda çalıştığı için, her tepe noktasını 0.0 olan z-koordinatına sahip bir 2-boyutlu üçgen biçiminde oluştururuz. Böylece üçgenin derinliği aynı kalır ve 2-boyutlu gibi görünür.

Normalize Edilmiş Koordinatlar (Normalized Device Coordinates-NDC)

Tepe noktası koordinatlarınız, tepe noktası gölgelendiricisinde işlendikten sonra, x, y ve z değerlerinin -1,0 ile 1,0 arasında değiştiği küçük bir alan olan normalleştirilmiş cihaz koordinatlarında olmalıdır. Bu aralığın dışında kalan tüm koordinatlar atılacak/ kırpılacak ve ekranınızda görünmeyecektir. Aşağıda normalize edilmiş cihaz koordinatları içerisinde belirlediğimiz üçgeni görebilirsiniz (z-eksenini görmezden geliyoruz):

Alışılmış ekran koordinatlarının aksine, (0,0) koordinatı sol-üst köşe yerine grafiğin merkezindedir. Neticede, tüm (dönüştürülmüş) koordinatların bu koordinat alanına girmesini isteriz, aksi takdirde görünmezler.

NDC koordinatlarınız daha sonra glViewport ile sağladığınız verileri kullanarak, görüş alanı dönüşümüyle ekran-uzay koordinatlarına dönüştürülür. Elde edilen ekran-uzay koordinatları daha sonra parça gölgelendiriciye girdi olarak parçalara dönüştürülür.

Tanımlanan köşe verisi ile, grafik iş hattının ilk süreci olan köşe noktası gölgelendiriciye girdi olarak göndermek istiyoruz. Bu, köşe noktası verilerini depoladığımız GPU’da bellek oluşturarak, OpenGL’in belleği nasıl yorumlayacağını yapılandırarak ve verilerin grafik kartına nasıl gönderileceğini belirleyerek yapılır. Köşe noktası gölgelendirici daha sonra bellekten söylediğimiz miktarda köşe noktasını işler.

Bu belleği, GPU üzerinde çok sayıda köşe noktası barındırabilen köşe noktası arabellek nesnesi (ing. Vertex Buffer Objet, VBO) ile yönetiyoruz. Bu arabellek nesnelerini kullanmamızın avantajı, bir kerede tek veri göndermeye gerek kalmadan grafik kartına aynı anda büyük veri grupları gönderebilmemizdir. CPU’dan grafik kartına veri göndermek nispeten yavaştır, bu yüzden mümkün olduğunca, aynı anda fazla veri göndermeye çalışıyoruz. Veriler grafik kartının belleğine girdikten sonra, köşe noktası gölgelendirici köşe noktalarına neredeyse anında erişebilir ve bu da onu son derece hızlı yapar.

Bir köşe noktası ara bellek nesnesi, OpenGL eğitselinde tartıştığımız gibi ilk OpenGL nesnesidir. Tıpkı OpenGL’deki herhangi bir nesne gibi bu arabellek benzersiz bir kimliğe sahiptir. Bu nedenle glGenBuffers işlevini kullanarak arabellek kimliğine sahip bir tane ara bellek nesnesi oluşturabiliriz:

unsigned int VBO;
glGenBuffers(1, &VBO); 

OpenGL birçok arabellek nesnesi türüne sahiptir ve bir köşe noktası arabellek nesnesinin türü GL_ARRAY_BUFFER’dir. OpenGL, farklı bir arabellek türüne sahip oldukları sürece, birden fazla arabelleğe bağlanmamıza izin verir. Yeni oluşturulan arabelleği glBindBuffer işleviyle GL_ARRAY_BUFFER hedefine bağlayabiliriz:

glBindBuffer(GL_ARRAY_BUFFER, VBO);

Bu noktadan sonra yaptığımız herhangi bir arabellek çağrısı (GL_ARRAY_BUFFER ile), VBO’ya bağlı olan arabelleği yapılandırmak için kullanılacaktır. Ardından, önceden tanımlanmış köşe noktası verilerini arabelleğe kopyalayan glBufferData işlevine bir çağrı yapabiliriz:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

glBufferData, özellikle kullanıcı tanımlı verileri, mevcutta bağlı olan arabelleğe kopyalamayı hedefleyen bir işlevdir. İlk argümanı, verileri kopyalamak istediğimiz arabellek türüdür: GL_ARRAY_BUFFER hedefine bağlı olan mevcut köşe noktası arabellek nesnesi. İkinci argüman, arabelleğe aktarmak istediğimiz verilerin boyutunu (bayt cinsinden) belirtir; köşe noktası verilerinin sizeof() ile ölçümlenmiş bir boyutu yeterlidir. Üçüncü parametre göndermek istediğimiz gerçek verilerdir.

Dördüncü parametre, grafik kartının verileri nasıl yönetmesini istediğimizi belirtmektedir. Bu, 3 şekilde olabilir:

  • GL_STATIC_DRAW: veriler büyük olasılıkla hiç değişmeyecek ya da çok nadiren değişecektir.

  • GL_DYNAMIC_DRAW: verilerin çok sık değişmesi muhtemeldir.

  • GL_STREAM_DRAW: veriler her çizdirildiğinde değişmektedir.

Üçgenin konum verileri değişmez ve her sahneleme çağrısı için aynı kalır, bu nedenle en iyi kullanım türü GL_STATIC_DRAW olmaktadır. Eğer, sık sık değişmesi muhtemel verileri içeren bir arabellek varsa, GL_DYNAMIC_DRAW veya GL_STREAM_DRAW kullanım türü, grafik kartının verileri daha hızlı yazmaya izin verecek şekilde belleğe yerleştirmesini sağlar.

As of now we stored the vertex data within memory on the graphics card as managed by a vertex buffer object named VBO. Next we want to create a vertex and fragment shader that actually processes this data, so let’s start building those.

Şu ana kadar, köşe noktası verilerini VBO adlı bir köşe noktası arabellek nesnesi tarafından yönetildiği gibi grafik kartındaki bellekte de sakladık. Sonraki adım olarak, bu verileri gerçekten işleyen bir köşe ve parça gölgelendirici elde etmek istiyoruz, bu yüzden bunları oluşturmaya başlayalım.

Köşe Noktası Tonlandırıcı (ing. Vertex Shader)

Vertex Shader, bizim tarafımızdan programlanabilen tonlandırıcılardan biridir. Modern OpenGL, sahneleme yapmak için en azından bir vertex shader ve fragment shader kurmamızı gerektirmektedir. Bu aşamada, tonlandırıcıları kısaca tanıtacağız ve ilk üçgeni çizmek için iki çok basit tonlandırıcıyı yapılandıracağız. Bir sonraki eğitselde tonlandırıcıları daha ayrıntılı olarak tartışacağız.

Yapmamız gereken ilk şey, vertex shader'ı GLSL (OpenGL Shading Language) dilinde yazmak ve daha sonra bu tonlandırıcıyı derleyerek uygulamamızda kullanabilmektir. Aşağıda GLSL’ de çok temel bir vertex shader kaynak kodunu bulacaksınız:

#version 330 core
layout (location = 0) in vec3 aPos;

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

Gördüğünüz gibi, GLSL C diline benzemektedir. Her tonlandırıcı program, sürümünün bir bildirimi ile başlar. OpenGL 3.3 ve üstü sürümlerde GLSL’ in sürüm numaraları OpenGL sürümüyle eşleşir (örneğin; GLSL 420 sürümü OpenGL sürüm 4.2’ye karşılık gelir). Ayrıca çekirdek profil işlevselliğini kullandığımızı da açıkça belirtiyoruz. GLSL’ deki bir vektör maksimum 4 boyuta sahiptir ve değerlerinin her biri sırasıyla vec.x, vec.y, vec.z ve vec.w yoluyla alınabilir ki her biri uzayda bir koordinatı temsil etmektedir.

Sonra, vertex shader'da tüm vertex attribute girdilerini in anahtar sözcüğüyle bildiririz. Şu anda yalnızca konum verilerini önemsiyoruz, bu nedenle yalnızca tek bir vertex attribute değerine ihtiyacımız var. GLSL, son ek basamağını temel alarak 1 ile 4 kayar nokta içeren bir vektör veri tipine sahiptir. Her vertex'te 3B bir koordinat olduğundan, aPos adında vec3 girdi değişkeni yaratıyoruz. Ayrıca girdi değişkeninin konumunu (location = 0) ayarladık ve daha sonra neden bu konuma ihtiyaç duyacağımızı göreceksiniz.

Vektör

Grafik programlamada, bir vektörün matematiksel kavramını oldukça sık kullanıyoruz, çünkü vektörler herhangi bir alandaki konumları/ yönleri düzgün bir şekilde temsil eder ve yararlı matematiksel özelliklere sahiptir. Vec.w bileşeninin uzayda bir konum olarak kullanılmadığını (4B ile değil, 3B ile ilgileniyoruz) ancak perspektif bölünümü (ing. perspective division) için kullanıldığını unutmayın. Daha sonraki bir eğitselde vektörleri çok daha derinlemesine tartışacağız.

Vertex shader çıktısını ayarlamak için, konum verilerini sahne arkasında vec4 olarak önceden tanımlanmış gl_Position değişkenine atamamız gerekir. main işlevinin sonunda, gl_Position’ı ne seçersek, vertex shader çıktısı olarak o kullanılacaktır. Girdimiz 3 boyutlu bir vektör olduğundan, bunu 4 boyutlu bir vektöre atamalıyız. Bunu vec4 yapıcısının içine vec3 değerlerini ekleyerek ve w bileşenini 1.0f’ye ayarlayarak yapabiliriz (nedenini bir sonraki eğitselde açıklayacağız).

Mevcut vertex shader muhtemelen hayal edebileceğimiz en basit vertex shader'dır çünkü girdi verilerinde hiçbir işlem yapmadık ve yalnızca tonlandırıcının çıktısına ilettik. Gerçek uygulamalarda, girdi verileri genellikle normalize edilmiş cihaz koordinatları biçiminde değildir, bu yüzden önce girdi verilerini OpenGL’ in görünür bölgesinde bulunan koordinatlara dönüştürmeliyiz.

Bir tonlandırıcıyı derlemek

Vertex shader'ın (kod dosyasının üstündeki bir const C karakter katarında depolanan) kaynak kodunu yazdık, ancak OpenGL’in gölgelendiriciyi kullanabilmesi için çalışma zamanında kaynak kodundan dinamik olarak derlemesi gerekir.

Yapmamız gereken ilk şey, yine bir id ile referans gösterilen bir gölgelendirici nesnesi oluşturmaktır. Bu nedenle köşe noktası gölgelendiriciyi unsigned int olarak saklıyoruz ve gölgelendiriciyi glCreateShader ile oluşturuyoruz:

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

GlCreateShader’a argüman olarak oluşturmak istediğimiz gölgelendiricinin türünü sağlıyoruz. Bir köşe noktası gölgelendirici oluşturduğumuzdan GL_VERTEX_SHADER’a geçiyoruz.

Sonra gölgelendirici kaynak kodunu gölgelendirici nesnesine ekliyoruz ve gölgelendiriciyi derliyoruz:

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

GlShaderSource işlevi, ilk argüman olarak derlenecek gölgelendirici nesnesini alır. İkinci argüman, kaynak kod olarak kaç tane karakter katarını geçirdiğimizi belirtir; bu yalnızca bir tanesidir. Üçüncü parametre köşe noktası gölgelendiricinin gerçek kaynak kodudur ve dördüncü parametreyi NULL olarak bırakabiliriz.

Muhtemelen derlemenin glCompileShader çağrısından sonra başarılı olup olmadığını ve eğer değilse, bu hataları düzeltebilmeniz için hangi hataların bulunduğunu kontrol etmek istersiniz. Derleme zamanı hatalarının kontrolü aşağıdaki gibi yapılır:

int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);

İlk olarak başarıyı göstermek için bir tamsayı ve hata mesajları için (varsa) bir depolama konteyneri tanımlarız. Sonra derlemenin glGetShaderiv ile başarılı olup olmadığını kontrol ederiz. Derleme başarısız olursa, glGetShaderInfoLog ile hata mesajını almalı ve hata mesajını yazdırmalıyız.

if(!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

Parça Gölgelendirici (ing. Fragment Shader)

Parça gölgelendirici, üçgen sahnelemek için oluşturacağımız ikinci ve son gölgelendiricidir. Parça gölgelendirici, piksellerinizin renk çıktısını hesaplamakla ilgilidir. İşleri basit tutmak için, parça gölgelendirici her zaman sabit olan turuncu bir renk veriyoruz.

Bilgisayar grafiklerindeki renkler 4 değerlik bir dizi olarak temsil edilir: RGBA olarak kısaltılmış kırmızı, yeşil, mavi ve alfa (saydamlık) bileşenleri. OpenGL veya GLSL’de bir renk tanımlarken, her bileşenin gücünü 0.0 ile 1.0 arasında bir değere ayarlarız. Örneğin, kırmızıyı 1.0f’ye ve yeşili 1.0f’ye ayarlarsak, her iki rengin bir karışımını alır ve rengimiz sarı olur. Bu 3 renk bileşeni göz önüne alındığında, 16 milyondan fazla farklı renk üretebiliriz.

#version 330 core
out vec4 FragColor;

void main()
{
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
} 

Parça gölgelendirici yalnızca bir çıktı değişkeni gerektirir ve bu, kendimizin hesaplaması gereken son renk çıktısını tanımlayan dört boyutlu bir vektördür. Çıktı değerlerini burada FragColor olarak adlandırdığımız out anahtar sözcüğü ile bildirebiliriz. Sonra renk çıktısına 1.0 alfa değeri olan turuncu bir renk atarız (1.0 tamamen saydam olduğu anlamına gelir).

Bir parça gölgelendiriciyi derleme işlemi, köşe noktası gölgelendiriciye benzer; ancak bu sefer gölgelendirici türü olarak GL_FRAGMENT_SHADER sabitini kullanıyoruz:

unsigned int fragmentShader;
fragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragmentShader, 1, &fragmentShaderSource, NULL);
glCompileShader(fragmentShader);

Her iki gölgelendirici de derlenmiştir ve yapılacak tek şey, her iki gölgelendirici nesnesini oluşturma için kullanabileceğimiz bir gölgelendirici programına (ing. shader program) bağlamaktır. Burada da derleme hataları olup olmadığını kontrol ettiğimizden emin olmalıyız.

Gölgelendirici programı

Gölgelendirici program nesnesi, birden çok gölgelendiricinin birleştirilen son bağlantılı sürümüdür. Son derlenmiş gölgelendiricileri kullanmak için, bunları bir gölgelendirici program nesnesine bağlamalıyız ve ardından nesneleri oluştururken bu gölgelendirici programını etkinleştirmeliyiz. Oluşturulan çağrıları gerçekleştirdiğimizde, aktif gölgelendirici programının gölgelendiricileri kullanılacaktır.

Gölgelendiriciler bir programa bağlanırken, her gölgelendiricinin çıktısı bir sonraki gölgelendiricinin girdisine bağlanır. Ayrıca, çıktılarınız ve girdileriniz uyuşmuyorsa bağlama hatalarıyla (ing. linking error) karşılaşırsınız.

Bir program nesnesi oluşturmak oldukça kolaydır:

unsigned int shaderProgram;
shaderProgram = glCreateProgram();

glCreateProgram işlevi bir program oluşturur ve id referansını yeni oluşturulan program nesnesine döndürür. Artık, önceden derlenmiş gölgelendiricileri program nesnesine eklememiz ve sonra bunları glLinkProgram ile bağlamamız gerekir:

glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

Kod kendini oldukça açıklıyor. Gölgelendiricileri programa ekliyoruz ve glLinkProgram aracılığıyla bağlıyoruz.

Gölgelendirici derlemesi gibi, gölgelendirici programını bağlamanın başarısız olup olmadığını da kontrol edebilir ve ilgili log bilgisini alabiliriz. Ancak, glGetShaderiv ve glGetShaderInfoLog kullanmak yerine şu an aşağıdaki yapıyı kullanıyoruz:

glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if(!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    ...
}

Sonuç, argüman olarak yeni oluşturulan program nesnesiyle glUseProgram’ı çağırarak etkinleştirebileceğimiz bir program nesnesidir:

glUseProgram(shaderProgram);

glUseProgram’dan sonraki her gölgelendirici ve oluşturma çağrısı artık bu program nesnesini (ve gölgelendiricileri) kullanacaktır. Program nesnesine bağladıktan sonra gölgelendirici nesnelerini silmeyi unutmayın; çünkü artık onlara ihtiyacımız yok:

glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);  

Şu anda girdi olarak köşe nokta verisini GPU’ya gönderdik ve GPU’ya köşe nokta verisi ve parça gölgelendirici içindeki köşe noktası verilerinin nasıl işlenmesi gerektiğini bildirdik. Neredeyse sonuna geldik. OpenGL, bellekteki köşe nokta verilerini nasıl yorumlaması gerektiğini ve köşe nokta verisini köşe noktası gölgelendiricinin özelliklerine nasıl bağlaması gerektiğini henüz bilmiyor. OpenGL’e bunu nasıl yapacağını söyleyeceğiz.

Köşe nokta özniteliklerini bağlama (ing. Linking Vertex Attributes)

Köşe noktası gölgelendirici, istediğimiz herhangi bir girdiyi köşe noktası öznitelikleri biçiminde belirtmemize olanak tanır ve bu da büyük esneklik sağlarken, girdi verilerimizin hangi köşe noktası gölgelendiricide hangi köşe noktası özniteliğinin gittiğini manuel olarak belirtmemiz gerektiği anlamına gelir. Bu, OpenGL’in sahneleme işleminden önce köşe nokta verilerini nasıl yorumlaması gerektiğini belirtmemiz gerektiği anlamına gelir.

Köşe nokta arabellek verilerimiz aşağıdaki gibi biçimlendirilmiştir:

  • Konum verileri 32 bit (4 bayt) kayan nokta değerleri olarak depolanır.

  • Her konum bu değerlerden 3’ünden meydana gelmektedir.

  • Her 3 değer kümesi arasında boşluk (veya herhangi bir başka değer) yoktur. Değerler dizide sıkı bir şekilde paketlenmiştir.

  • Verilerdeki ilk değer, arabelleğin başındadır.

Bu bilgi ile OpenGL’e glVertexAttribPointer kullanarak köşe noktası verisini (her bir köşe noktası özniteliği için) nasıl yorumlaması gerektiğini söyleyebiliriz:

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

glVertexAttribPointer işlevinin oldukça az parametresi vardır, bu yüzden dikkatlice bunları inceleyelim:

  • İlk parametre hangi köşe noktası özniteliğini yapılandırmak istediğimizi belirtir. Köşe noktası gölgelendiricideki konum köşe noktası özniteliğinin yerleşimini (location = 0) belirlediğimizi unutmayın. Bu, köşe noktası özniteliğinin konumunu 0 olarak ayarlar ve verileri bu köşe noktası özniteliğine aktarmak istediğimizden, 0 değerini geçiririz.

  • Sonraki bağımsız değişken, köşe noktası özniteliğinin boyutunu belirtir. Köşe noktası özniteliği vec3 olarak nitelendirildiğinden 3 değerden oluşur.

  • Üçüncü argüman GL_FLOAT (GLSL’deki kayan nokta değerleri) veri türünü belirtir.

  • Bir sonraki argüman, verilerin normalize edilmesini isteyip istemediğimizi belirtir. Tamsayı veri türlerini (int, bayt) giriyorsak ve bunu GL_TRUE olarak ayarlamışsak, tamsayı verileri float değerine dönüştürüldüğünde 0 (veya işaretli veriler için -1) ve 1 olarak normalleştirilir. Bu durum bizim için uygun değil, bu yüzden GL_FALSE konumunda bırakacağız.

  • Beşinci argüman “stride” olarak bilinir ve bize ardışık köşe noktası öznitelikleri arasındaki boşluğu ifade eder. Bir sonraki konum verisi seti, float boyutunun tam olarak 3 katı olduğundan, bu değeri stride olarak belirleriz. Dizinin sıkı bir şekilde paketlendiğini bildiğimizden (sonraki köşe noktası özniteliği değeri arasında boşluk olmadığından) OpenGL’in stride’ı belirlemesine izin vermek için onu 0 olarak belirtebileceğimizi unutmayın (bu yalnızca değerler sıkıca paketlendiğinde çalışır). Ne kadar çok köşe noktası özniteliğine sahip olursak, her köşe noktası özniteliği arasındaki boşluğu dikkatlice tanımlamamız gerekir, ancak daha sonra bununla ilgili daha fazla örnek göreceğiz.

  • Son parametre void* türündedir ve bu nedenle garip bir biçim gerektirir. Bu, konum verilerinin arabellekte başladığı konumun ofsetidir. Konum verileri veri dizisinin başlangıcında olduğu için bu değer sadece 0’dır. Daha sonra bu parametreyi ayrıntılı olarak keşfedeceğiz.

Her köşe noktası özniteliği, verilerini bir VBO tarafından yönetilen bellekten alır ve bu glVertexAttribPointer çağrılırken mevcut durumda GL_ARRAY_BUFFER’a bağlı VBO tarafından belirlenir. Önceden tanımlanmış VBO halen glVertexAttribPointer köşe noktası özniteliği 0 çağrılmadan önce bağlı olduğundan artık köşe noktası verileri ile ilişkilendirilmiştir.

Artık OpenGL’in köşe noktası verilerini nasıl yorumlaması gerektiğini belirlediğimize göre, glEnableVertexAttribArray ile köşe noktası özniteliğini de etkinleştirmeliyiz; çünkü köşe noktası öznitelikleri varsayılan olarak devre dışıdır. Bu noktadan sonra her şeyi ayarlamış olduk. Köşe noktası verisini bir köşe noktası arabellek nesnesi kullanarak başlattık, bir köşe noktası ve bir parça gölgelendirici kurduk. OpenGL’e köşe nokta verisini köşe noktası gölgelendiricisinin köşe noktası özniteliklerine nasıl bağlayacağını söyledik. OpenGL’de bir nesne çizmenin yapısı şu şekildedir:

// 0. köşe noktaları dizimizi OpenGL'in kullanması için bir arabelleğe kopyalayın
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 1. sonra köşe noktası öznitelik işaretçilerini set edin
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  
// 2. bir nesneyi sahenelmek istediğinizde gölgelendirici programını kullanın
glUseProgram(shaderProgram);
// 3. şimdi nesneyi çizdirin
someOpenGLFunctionThatDrawsOurTriangle();   

Her nesne çizmek istediğimizde bu işlemi tekrarlamalıyız. Çok fazla görünmeyebilir, ancak 5’ten fazla köşe noktası özniteliğine ve belki de 100’den fazla farklı nesneye sahip olduğumuzu hayal edin. Uygun arabellek nesnelerini bağlamak ve bu nesnelerin her biri için tüm köşe noktası özniteliklerini yapılandırmak hantal bir işlem hâline gelir. Tüm bu durum yapılandırmalarını bir nesneye depolamanın ve bu nesneyi bağlamanın bir yolu olsaydı ne olurdu?

Köşe Nokta Dizisi Nesnesi (ing. Vertex Array Object)

Bir köşe nokta dizisi nesnesi (VAO olarak da bilinir) bir köşe nokta arabellek nesnesi gibi bağlanabilir ve bu noktadan sonraki herhangi bir köşe noktası özniteliği çağrısı VAO içinde saklanır. Bunun avantajı, köşe noktası özniteliği işaretleyicilerini yapılandırırken bu çağrıları yalnızca bir kez yapmanız ve nesneyi çizmek istediğimizde, karşılık gelen VAO’yu bağlayabilmemizdir. Bu, farklı köşe noktası verileri ve öznitelik yapılandırmaları arasında geçiş yapmayı farklı bir VAO bağlamak kadar kolaylaştırır. Yeni ayarladığımız tüm durum VAO içinde saklanır.

Çekirdek profil, VAO kullanmamızı gerektiriyor. Bu nedenle köşe noktası girdilerimizle ne yapacağımızı biliyor. Bir VAO’yu bağlayamazsak, OpenGL büyük olasılıkla bir şey çizmeyi reddeder.

Bir köşe noktası dizisi nesnesi aşağıdakileri depolar:

  • glEnableVertexAttribArray ya da glDisableVertexAttribArray çağrıları

  • glVertexAttribPointer aracılığıyla yapılan köşe noktası öznitelik yapılandırmaları

  • glVertexAttribPointer çağrıları vasıtasıyla köşe noktası öznitelikleri ilişkilendirilmiş VBO’lar.

Bir VAO oluşturma aşamaları VBO oluşturma aşamalarına benzerdir:

unsigned int VAO;
glGenVertexArrays(1, &VAO);  

Bir VAO kullanmak için yapmanız gereken tek şey VAO’yu glBindVertexArray kullanarak bağlamaktır. Bu noktadan sonra, ilgili VBO’ları ve öznitelik işaretleyicilerini bağlamalı/ yapılandırmalı ve daha sonra kullanmak üzere VAO’nun bağını serbest bırakmalıyız. Bir nesne çizmek istediğimiz anda, nesneyi çizmeden önce VAO’yu tercih edilen ayarlarla bağlarız. Kod üzerindekii görünümü:

// ..:: Initialization code (done once (unless your object frequently changes)) :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. then set our vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  
  
[...]

// ..:: Drawing code (in render loop) :: ..
// 4. draw the object
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
someOpenGLFunctionThatDrawsOurTriangle();   

Şimdiye kadar yaptığımız her şey, köşe noktası öznitelik yapılandırmamızı depolayan ve hangi VBO’nun kullanılacağını gösteren bir VAO’ya yol açtı. Genellikle çizmek istediğiniz birden fazla nesneniz olduğunda, önce tüm VAO’ları (ve böylece gerekli VBO ve öznitelik işaretçileri) oluşturur/yapılandırır ve daha sonra kullanılmak üzere saklarsınız. Nesnelerimizden birini çizmek istediğimiz anda, karşılık gelen VAO’yu alır, bağlar, sonra nesneyi çizer ve VAO’nun bağını serbest bırakırız.

Hepimizin Beklediği Üçgen

Seçtiğimiz nesneleri çizmek için, OpenGL bize şu andaki aktif gölgelendiriciyi, daha önce tanımlanmış köşe noktası öznitelik yapılandırmasını ve VBO’nun köşe noktası verilerini (dolaylı olarak VAO üzerinden) kullanarak ilkeleri çizen glDrawArrays işlevini sunmaktadır.

glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 3);

glDrawArrays işlevi ilk argümanı olarak çizmek istediğimiz OpenGL ilkeli türünü almaktadır. Başlangıçta bir üçgen çizmek istediğimizi söylediğimden GL_TRIANGLES’i aktarıyoruz. İkinci argüman çizmek istediğimiz köşe nokta dizisinin başlangıç dizinini belirtir; bunu sadece 0’da bırakıyoruz. Son argüman, kaç tane köşe çizmek istediğimizi belirtir, bu da 3’tür (verilerimizden tam olarak 3 köşe uzunluğunda ve sadece 1 üçgen oluştururuz).

Şimdi kodu derlemeye çalışın ve herhangi bir hata oluşursa geriye doğru inceleyerek gidin. Uygulamanız derlendiğinde, aşağıdaki sonucu görmelisiniz:

Programın tam kaynak kodunu buradan bulabilirsiniz.

Çıktınız aynı görünmüyorsa, muhtemelen yanlış bir şey yaptınız, bu yüzden tüm kaynak kodu kontrol edin, bir şey kaçırıp kaçırmadığınızı görün veya yorumlar bölümünde sorun.

Öğe Arabellek Nesneleri (ing. Element Buffer Objects)

Köşe noktaları oluştururken tartışmak istediğimiz son bir şey var ve bu EBO olarak kısaltılmış öğe arabellek nesneleri. Öğe arabellek nesnelerinin nasıl çalıştığını açıklamak için en iyi örnek: Bir üçgen yerine bir dikdörtgen çizmek istiyoruz ve iki üçgen kullanarak bir dikdörtgen çizdireceğiz (OpenGL esas olarak üçgenlerle çalışmaktadır). Bu durum, aşağıdaki köşe noktası kümesini oluşturur:

float vertices[] = {
    // ilk üçgen
     0.5f,  0.5f, 0.0f,  // sağ üst
     0.5f, -0.5f, 0.0f,  // sağ alt
    -0.5f,  0.5f, 0.0f,  // sol üst 
    // ikinci üçgen
     0.5f, -0.5f, 0.0f,  // sağ alt
    -0.5f, -0.5f, 0.0f,  // sol alt
    -0.5f,  0.5f, 0.0f   // sol üst
}; 

Gördüğünüz gibi, belirtilen köşe noktalarında bir miktar çakışma var. Sağ alt ve sol üst iki kez belirtiliyor. Aynı dikdörtgen 6 yerine sadece 4 köşe ile de belirtilebileceğinden bu %50’lik bir yük anlamında gelmektedir. Daha iyi bir çözüm olarak, yalnızca benzersiz köşeleri saklamak ve daha sonra bu köşeleri çizmek istediğimiz sırayı belirtmek olabilir. Bu durumda, dikdörtgen için sadece 4 köşeyi saklamamız ve daha sonra hangi sırayla belirtmemiz gerekir. OpenGL bize böyle bir özellik sağlamış olsaydı harika olmaz mıydı?

Neyse ki, öğe arabellek nesneleri tam olarak böyle çalışır. EBO, tıpkı bir köşe noktası arabellek nesnesi gibi, OpenGL’in hangi köşeleri çizmek gerektiğine karar vermek için kullandığı indeksleri depolayan bir arabellektir. Bu indeksli çizim, sorunumuzun çözümüdür. Başlamak için önce benzersiz köşeleri ve bunları dikdörtgen olarak çizmek için indisleri belirtmeliyiz:

float vertices[] = {
     0.5f,  0.5f, 0.0f,  // sağ üst
     0.5f, -0.5f, 0.0f,  // sağ alt
    -0.5f, -0.5f, 0.0f,  // sol alt
    -0.5f,  0.5f, 0.0f   // sol üst
};
unsigned int indices[] = {  // indekslerin 0'dan başladığına dikkat edin
    0, 1, 3,   // ilk üçgen
    1, 2, 3    // ikinci üçgen
};  

İndeksleri kullanırken 6 yerine 4 köşeye ihtiyacımız olduğunu görebilirsiniz. Sonra öğre arabellek nesnesini yaratmamız gerekiyor:

unsigned int EBO;
glGenBuffers(1, &EBO);

VBO’ya benzer şekilde EBO’yu bağlarız ve indeksleri glBufferData ile arabellek içerisine kopyalarız. Ayrıca, tıpkı VBO gibi, bu aramaları arabellek türü olarak GL_ELEMENT_ARRAY_BUFFER belirtmekle birlikte, bir bağlama ve bir bağ çözme görevleri arasında yapmak istiyoruz.

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW); 

Arabellek hedefi olarak GL_ELEMENT_ARRAY_BUFFER verdiğimizi unutmayın. Yapılacak son şey, üçgenleri bir indeks arabelleğinden oluşturmak istediğimizi belirtmek için glDrawArrays çağrısını glDrawElements ile değiştirmektir. glDrawElements kullanırken, şu anda bağlı olan öğe arabellek nesnesinde sağlanan indeksleri kullanarak çizim yapacağız:

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

İlk argüman, glDrawArrays’e benzer şekildedir ve çizmek istediğimiz modu belirtir. İkinci argüman çizmek istediğimiz öğe sayısıdır. Toplam 6 köşe noktası çizmek istediğimiz için 6 indeks belirledik. Üçüncü argüman GL_UNSIGNED_INT türündedir ve indekslerin türüdür. Son argüman, EBO’da bir ofset belirlememize izin veriyor (veya bir indeks dizisine geçebilmemize izin veriyor, ancak öğe arabellek nesnelerini kullanmıyorsanız) ama bunu 0’da bırakacağız.

glDrawElements işlevi, indekslerini mevcut GL_ELEMENT_ARRAY_BUFFER hedefine bağlı EBO’dan alır. Bu, bir nesneyi her zaman hantal görünen indekslerle oluşturmak istediğimizde ilgili EBO’yu da bağlamamız gerektiği anlamına gelir. Bir köşe nokta dizisi nesnesinin ayrıca öğe arabellek nesne bağlarını izlemesine sebep olur. Bir VAO bağlıyken o anda bağlı olan öğe arabellek nesnesi, VAO’nun öğe arabellek nesnesi olarak saklanır. Bir VAO’ya bağlanma EBO’yu da otomatik olarak bağlar.

Bir VAO, hedef GL_ELEMENT_ARRAY_BUFFER olduğunda glBindBuffer çağrılarını saklar. Bu aynı zamanda onun bağı serbest bırakma çağrılarını sakladığı anlamına gelir. Bu nedenle VAO’nun bağını serbest bırakmadan önce öğe dizi arabelleğinin bağını serbest bırakmadığınızdan emin olun, aksi takdirde EBO yapılandırılmaz.

Ortaya çıkan ilklendirme ve çizim kodu şimdi şöyle görünüyor:

// ..:: Initialization code :: ..
// 1. bind Vertex Array Object
glBindVertexArray(VAO);
// 2. copy our vertices array in a vertex buffer for OpenGL to use
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
// 3. copy our index array in a element buffer for OpenGL to use
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// 4. then set the vertex attributes pointers
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);  

[...]
  
// ..:: Drawing code (in render loop) :: ..
glUseProgram(shaderProgram);
glBindVertexArray(VAO);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0)
glBindVertexArray(0);

Programı çalıştırmak, aşağıda gibi bir görüntü vermelidir. Sol görüntü tanıdık görünmeli ve doğru görüntü telkafes modunda çizilen dikdörtgendir. Telkafes, dikdörtgenin gerçekten iki üçgenden oluştuğunu gösterir.

Telkafes (ing. wireframe) Modu

Üçgenlerinizi telkafes modunda çizmek için, OpenGL’in ilkelleri glPolygonMode(GL_FRONT_AND_BACK, GL_LINE) üzerinden nasıl çizeceğini yapılandırabilirsiniz. İlk argüman bunu tüm üçgenlerin önüne ve arkasına uygulamak istediğimizi söylüyor ve ikinci argüman bize bunları çizgi olarak çizmemizi söylüyor. Sonraki çizim çağrıları, glPolygonMode(GL_FRONT_AND_BACK, GL_FILL) kullanarak varsayılana geri dönene kadar üçgenleri telkafes modunda oluşturur.

Herhangi bir hatanız varsa, geriye doğru çalışın ve bir şey kaçırıp kaçırmadığınızı görün. Ayrıca, kaynak kodun tamamını burada bulabilirsiniz.

Bir üçgen veya dikdörtgen çizmeyi başardıysanız, Modern OpenGL’in en zor kısımlarından birini geçmeyi başardınız. İlk üçgeninizi çizmeden önce büyük bir bilgi birikimi gerektiğinden bu zor bir bölümdü. Neyse ki, şimdi bu engeli aştık ve yaklaşan eğitseller umarım daha kolay olacaktır.

Ek Kaynaklar

Alıştırmalar

Tartışılan kavramları gerçekten iyi anlaak için birkaç alıştırma verildi. Neler olup bittiğini iyi bir şekilde kavradığınızdan emin olmak için bir sonraki konuya geçmeden önce bunlar üzerinde çalışmanız önerilir.

  1. Verilerinize daha fazla köşe ekleyin ve glDrawArrays kullanarak yan yana 2 üçgen çizmeye çalışın: çözüm.

  2. Şimdi iki farklı VAO ve VBO kullanarak aynı 2 üçgeni oluşturun: çözüm.

  3. İkinci programın sarı rengi veren farklı bir parça gölgelendiriciyi kullandığı iki gölgelendirici program oluşturun; birinin sarı renk çıkardığı her iki üçgeni de yeniden çizin:çözüm

Orijinal Kaynak: Hello Triangle

Çeviri: Nezihe Sözen Furkan Onder

Last updated