Merhaba Üçgen! (Hello Triangle) 🔺
OpenGL'de her şey üç boyutlu uzaydadır; ancak ekranımız veya penceremiz piksellerden oluşan iki boyutlu bir dizidir. Bu yüzden OpenGL'in en temel görevi, üç boyutlu tüm koordinatları ekranınızdaki iki boyutlu renkli piksellere dönüştürmektir.
Üç boyutlu koordinatları iki boyutlu piksellere dönüştürme süreci OpenGL'in Grafik İşlem Hattı (Graphics Pipeline) tarafından yönetilir. Grafik işlem hattı temelde iki büyük bölüme ayrılabilir: 1. 3B koordinatlarınızı 2B ekran koordinatlarına dönüştürmek, 2. Bu 2B koordinatları penceremizdeki gerçek renkli piksellere dönüştürmek.
Grafik İşlem Hattı (The Graphics Pipeline)
Grafik işlem hattı, girdi olarak bir dizi 3B koordinat alır ve bunları ekranınızdaki renkli piksellere dönüştürür. Bu işlem hattı; her biri bir öncekinin çıktısını girdi olarak alan son derece hızlı, özelleşmiş ve birbirini izleyen aşamalara bölünmüştür.
Bu aşamaların çoğu doğrudan ekran kartı (GPU) üzerinde paralel olarak çalışır. Modern ekran kartları, işlem hattının her aşamasında binlerce küçük programı aynı anda yürütebilecek binlerce küçük işlem çekirdeğine sahiptir. GPU üzerinde çalışan bu küçük programlara Gölgelendirici (Shader) denir.
Aşağıdaki diyagramda grafik işlem hattının tüm aşamalarını soyut bir biçimde görebilirsiniz. Mavi renkli aşamalar, kendi özel shader programlarımızı yazıp GPU'ya enjekte edebileceğimiz programlanabilir aşamaları temsil eder:

- Tepe Noktası Verisi (Vertex Data): İşlem hattına girdi olarak 3B koordinatlardan oluşan bir dizi tepe noktası (Vertex) veririz. Tepe noktası sadece bir 3B konum değil, aynı zamanda renk veya doku koordinatları gibi ek verileri de taşıyabilir.
- Tepe Noktası Gölgelendiricisi (Vertex Shader): İlk programlanabilir aşamadır. Tek bir tepe noktasını girdi alır. Ana görevi, 3B koordinatları başka koordinatlara dönüştürmek ve tepe noktası nitelikleri üzerinde temel işlemler yapmaktır.
- Şekil Birleştirme (Primitive Assembly): Tepe noktalarını alıp belirlenen ilkel şekle (örneğin üçgenler için
GL_TRIANGLES) göre birbirine bağlar. - Geometri Gölgelendiricisi (Geometry Shader - İsteğe Bağlı): Yeni tepe noktaları üreterek mevcut şekillerden yepyeni şekiller (örneğin bir noktadan bir patlama efekti ya da küp) oluşturabilir.
- Rasterizasyon (Rasterization Stage): İlkel şekilleri ekrandaki piksellerle eşleştirir ve Parçacıkları (Fragments) üretir. Bir parça (fragment), OpenGL'in tek bir pikseli boyamak için ihtiyaç duyduğu tüm veridir.
- Parça Gölgelendiricisi (Fragment Shader): İkinci ve en çok seveceğimiz programlanabilir aşamadır! Ana görevi, bir pikselin nihai rengini hesaplamaktır. İleri seviye ışıklandırma, gölgeler, yansımalar ve renk geçişleri hep bu aşamada hesaplanır.
- Testler ve Karıştırma (Tests & Blending): Derinlik testi (Depth test), şablon testi (Stencil test) ve alfa karıştırma (Blending) yapılarak pikselin görünür olup olmadığına karar verilir ve nihai renk ekran tamponuna yazılır.
Modern OpenGL'de en azından kendi Vertex Shader ve Fragment Shader programlarımızı yazmamız zorunludur (GPU'da varsayılan bir shader yoktur).
Tepe Noktası Girdisi (Vertex Input)
Ekrana bir üçgen çizmek için 3 adet tepe noktası (Vertex) tanımlamamız gerekir. OpenGL'de koordinatlar yalnızca x, y ve z değerleri -1.0 ile 1.0 arasında olduğunda ekranda görünür. Bu aralığa Normalleştirilmiş Cihaz Koordinatları (Normalized Device Coordinates - NDC) denir:

Hemen 3 tepe noktasından oluşan üçgen verimizi tanımlayalım (z ekseni 0.0 olsun, derinlik şimdilik yok):
float vertices[] = {
-0.5f, -0.5f, 0.0f, // Sol alt
0.5f, -0.5f, 0.0f, // Sağ alt
0.0f, 0.5f, 0.0f // Tepe orta
};
VBO: Veriyi GPU Belleğine Göndermek
Bu koordinat dizisi şu an bilgisayarımızın ana belleğindedir (RAM). Ekran kartımızın (GPU) bu noktalara ışık hızında erişebilmesi için bu veriyi GPU belleğine (VRAM) transfer etmeliyiz.
GPU belleğinde bu veriyi saklayan nesnelere VBO (Vertex Buffer Object - Tepe Noktası Tampon Nesnesi) denir.
unsigned int VBO;
glGenBuffers(1, &VBO); // 1. Tampon nesnesi oluştur ve ID al
// 2. Tamponu GL_ARRAY_BUFFER yuvasına bağla
glBindBuffer(GL_ARRAY_BUFFER, VBO);
// 3. Veriyi GPU belleğine kopyala
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBufferData fonksiyonunun 4. parametresi ekran kartına bu veriyi nasıl kullanacağımızın ipucunu verir:
* GL_STATIC_DRAW: Veri neredeyse hiç değişmeyecek (bizim üçgenimiz gibi sabit geometriler).
* GL_DYNAMIC_DRAW: Veri sık sık değişecek.
* GL_STREAM_DRAW: Veri her çizimde değişecek.
İlk Gölgelendiricimiz: Vertex Shader
Artık verimiz GPU belleğinde bekliyor. Şimdi onu işleyecek ilk shader programımızı GLSL (OpenGL Shading Language) dilinde yazalım:
#version 330 core
layout (location = 0) in vec3 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}
#version 330 core: OpenGL 3.3 Core profilini kullandığımızı belirtir.layout (location = 0) in vec3 aPos: 0 numaralı girdi yuvasından 3 boyutlu bir vektör (vec3) beklediğimizi söyler.gl_Position: Vertex Shader'ın dahili çıktı değişkenidir; tepe noktasının ekrandaki nihai 4B homojen koordinatıdır.
C++ kodumuzda bu shader kodunu bir C karakter dizisi (const char*) olarak saklayıp çalışma zamanında derleriz:
const char *vertexShaderSource = "#version 330 core
"
"layout (location = 0) in vec3 aPos;
"
"void main()
"
"{
"
" gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
"
"} ";
unsigned int vertexShader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);
// Derleme hatası kontrolü:
int success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
std::cout << "HATA::SHADER::VERTEX::DERLEME_BASARISIZ
" << infoLog << std::endl;
}
İkinci Gölgelendiricimiz: Fragment Shader
Üçgenimizin tepe noktaları belirlendi; peki bu üçgen hangi renkte boyanacak? İşte burada Fragment Shader devreye girer. Üçgenimizi göz alıcı bir turuncu renkle (1.0, 0.5, 0.2) boyayalım:
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f); // Turuncu (RGBA)
}
Bunu da C++ tarafında derliyoruz: