Ana içeriğe geç

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:

Grafik İşlem Hattı

  1. 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.
  2. 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.
  3. Ş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.
  4. 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.
  5. 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.
  6. 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.
  7. 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:

NDC Koordinat Sistemi

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:

const char *fragmentShaderSource = "#version 330 core
"
    "out vec4 FragColor;
"
    "void main()
"
    "{
"
    "   FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
"
    "}
";

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

Shader Programı Oluşturma ve Bağlama

Yazdığımız bu iki shader'ı bir araya getirip tek bir Shader Program Nesnesi (Shader Program Object) içinde bağlamalıyız (Link):

unsigned int shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);

// Bağlama hatası kontrolü:
glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
if (!success) {
    glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
    std::cout << "HATA::SHADER::PROGRAM::BAGLAMA_BASARISIZ
" << infoLog << std::endl;
}

// Program bağlandıktan sonra bağımsız shader nesnelerine ihtiyacımız kalmaz, silebiliriz:
glDeleteShader(vertexShader);
glDeleteShader(fragmentShader);

Tepe Noktası Niteliklerini Bağlama (Vertex Attributes)

GPU belleğindeki veriler sadece ham baytlardan ibarettir. OpenGL bu baytların ne anlama geldiğini (nerede koordinat, nerede renk var) kendi kendine bilemez.

Bellek yapımızı OpenGL'e tam olarak tarif etmeliyiz:

Vertex Nitelik Yapısı

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
  • 0: Vertex Shader'daki location = 0 girdisine karşılık gelir.
  • 3: Her tepe noktasının 3 bileşeni vardır (vec3: x, y, z).
  • GL_FLOAT: Veri tipimiz ondalıklı sayıdır (float).
  • GL_FALSE: Verilerin normalize edilmesini istemiyoruz.
  • 3 * sizeof(float): Adım (Stride) — Bir sonraki tepe noktasına geçmek için bellekte kaç bayt atlanacağını belirtir.
  • (void*)0: Öteleme (Offset) — Verinin tamponun neresinden başladığını belirtir (en baştan, yani 0).

VAO: Hayat Kurtaran Vertex Array Object

Her çizim yapacağımız zaman bu VBO'ları tekrar tekrar bağlamak ve nitelik işaretçilerini (glVertexAttribPointer) baştan tanımlamak zorunda kalsaydık kodlarımız çorbaya dönerdi.

İşte VAO (Vertex Array Object) tam olarak bu ayarları hafızasında saklayan bir "kasetçalar" gibidir:

VAO Yapısı

Bir kez VAO oluşturup içine VBO'muzu ve nitelik işaretçilerimizi kaydederiz. Çizim yapacağımız zaman sadece o VAO'yu bağlamak (glBindVertexArray) yeterli olur!

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

// 1. VAO'yu bağla
glBindVertexArray(VAO);

// 2. VBO'yu bağla ve veriyi kopyala
glBindBuffer(GL_ARRAY_BUFFER, VBO);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

// 3. Tepe noktası niteliklerini ayarla
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);

İlk Üçgenimizi Çiziyoruz! 🎨

Artık render döngümüzün içine şu 3 satırı ekleyerek ilk üçgenimizi ekrana çizebiliriz:

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

Programı çalıştırdığınızda (F5), karşınızda bilgisayar grafiklerinin kutsal kasesi duruyor olacak:

İlk Üçgen


EBO: Bir Dörtgen Çizmek (Element Buffer Objects)

Peki bir üçgen yerine bir dikdörtgen çizmek istersek ne yaparız? İki adet üçgeni yan yana getiririz!

Normalde iki üçgen için 6 tepe noktası gerekir; fakat bu iki üçgen 2 ortak tepe noktasını paylaşır. 4 nokta tanımlamak varken neden 6 nokta tanımlayıp bellek israf edelim?

İşte EBO (Element Buffer Object - İndeks Tamponu) tam olarak bunu çözer:

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[] = {
    0, 1, 3,  // İlk üçgen
    1, 2, 3   // İkinci üçgen
};

EBO'yu oluşturup bağlarız:

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

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

Ve render döngüsünde glDrawArrays yerine glDrawElements çağırırız:

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

Tebrikler! Artık indekslenmiş verilerle mükemmel bir dikdörtgen çizdiniz:

Dikdörtgen Çizimi


Alıştırmalar 🏋️

Öğrendiklerinizi pekiştirmek için şu 3 harika alıştırmayı mutlaka deneyin:

  1. glDrawArrays kullanarak yan yana duran iki adet üçgen çizin.
  2. Aynı iki üçgeni, her bir üçgen için farklı VAO ve VBO kullanarak çizmeyi deneyin.
  3. İki farklı Fragment Shader yazarak üçgenlerden birinin sarı, diğerinin turuncu renkte çizilmesini sağlayın!

Sırada GLSL dilinin derinliklerine ineceğimiz ve renk geçişleriyle büyüleyici efektler yaratacağımız Bölüm 5: "Shader'lar (Gölgelendiriciler)" var!

👉 Sonraki Bölüm: Shader'lar (Shaders)