En kaba tabirle OpenGL; nokta, çizgi ve yüzey çizip bunu renklendirmenize olanak tanıyan bir API’dir. Başka bir deyişle GPU’yu kullanabilmemiz için sunulmuş bir arayüzdür. Bu arayüzü kullanabilmemiz için grafik kartı üreticisinin (veya başka bir kişinin), driver yazıp OpenGL’in bize sunmuş olduğu fonksiyonların kodlaması gerekmektedir. Göreceğiniz üzere OpenGL aynı zamanda bir “specification” yani tanımlamadır. Khronos grup OpenGL üzerinde olması gereken fonksiyonları tanımlar ve driver yazan kişiler bu tanımlamaya göre kodlamasını yapar.

OpenGL’de iki temel mod bulunmaktadır. Bunlar core ve immediate mod’lardır. “immediate mode” OpenGL’in ilk çıktığında var olan kullanımı çok kolay olan modudur. Çizgi çiz, nokta koy, rengini ayarla gibi komutlarla çok basit ve hızlı bir şekilde bir uygulama yapılabilir. Lakin bu basitlik büyük bir dezavantajı da yanında getiriyor. Soyutlama arttıkça, donanımdan uzaklaştıkça kontrolü ve verimi kaybediyoruz. Bu sebepten dolayı OpenGL versiyon 3.2 de immediate mode kullanımdan kaldırıldı ve onun yerine core mode’a geçildi. Core mode, çok daha zordur lakin çok daha fazla olanak sunduğu için yapılabilecek şeyler çok daha fazladır.

Daha da günümüze gelirsek artık Vulkan’ı görürüz. OpenGL 2016’da deprecated edildi. Artık yeni bir versiyonu çıkmayacak. Onun yerine çok daha fazla seçenek sunan ama bir o kadar da karmaşık Vulkan API si bulunmakta. Vulkan’ı öğrenmek için OpenGL önşart olmasa da öğrenilmesi tavsiye edilir.

Bir grafik API öğrenmek, oyun yapmayı öğrenmek değildir ya da bir CAD programını yapmayı öğrenmek değildir. Sadece onların bir parçasını öğrenmektir. Geriye kalanlar ise apayrı bir dünyadır. Bu sebepten dolayı eğer hızlı bir şekilde oyun yapmayı düşünüyorsanız OpenGL sizin için çok da uygun olmayacaktır. Onun yerine Unreal, Unity, Godot vb bir oyun motoru kullanabilirsiniz.


Pencere

OpenGL’de bir görsel oluşturmadan önce o görseli koyabileceğimiz bir pencereye ihtiyacımız var. Bildiğiniz üzere her işletim sisteminde pencere oluşturmak için farklı yollar izlenmekte. GLUT, SDL, SFML ve GLFW gibi kütüphaneler bu işlemi kolaylaştırıyor. Bu tutorial’da, GLFW’yi kullanacağız. Bunun dışında bir tane daha kütüphaneye ihtiyacımız olacak. GLAD, kütüphanesiyle OpenGL fonksiyon pointerlarını yükleyeceğiz. Daha yalın bir ifadeyle pencere çizdirmek için GLFW’ye; OpenGL fonksiyonlarını kullanabilmek için GLAD’a ihtiyacımız olacak. Bu kütüphaneleri en kolay kurulumu VCPKG veya alternatif bir paket manager ile yüklemektir. Diğer yolu için:

https://learnopengl.com/Getting-started/Creating-a-window

// LOGL.cpp : This file contains the 'main' function. Program execution begins and ends there.
//

#include <iostream>

#include <glad/glad.h> // GLAD, GLFW den önce include edilmeli!!!
#include <GLFW/glfw3.h>

// pencerenin boyutu değiştiğinde çalışacak fonksiyonu yazıyoruz
void yeni_boyut(GLFWwindow* window, int width, int height) {
    glViewport(0, 0, width, height);
}


int main(){
    // GLFW yi initialize ediyoruz. 
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); 
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 
    //glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);

    // Pencere oluşturuyoruz 
    GLFWwindow* window = glfwCreateWindow(800, 600, "LearnOpenGL", NULL, NULL);
    if (window == NULL){
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }
    //Hangi pencere çizileceğini seçiyoruz. 
    // Şuanlık sadece tek penceremiz var ama birden fazla penceremiz de olabilirdi
    glfwMakeContextCurrent(window);
    // GLAD'ı initialize etmeden önce mutlaka bunu yapmalıyız. 


    // GLAD'ı initialize ediyoruz. Herhangi bir OpenGL komutunu kullanmadan önce bunu yapmalıyız
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }


    // Penceremizin boyutu kadar bir opengl viewportu oluşturuyoruz 
    // https://registry.khronos.org/OpenGL-Refpages/gl4/html/glViewport.xhtml
    glViewport(0, 0, 800, 600); 

    // Penceremiz boyut değiştirdiğinde çağrılacak fonksiyoun bağlıyoruz. 
    glfwSetFramebufferSizeCallback(window, yeni_boyut);

    //Render loop 
    while (!glfwWindowShouldClose(window)){ 
        glfwSwapBuffers(window); // Double buffering 
        glfwPollEvents(); 

        // ekranı temizleme rengi 
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT); // neyi temizleyeceğimiz 
        // color dışında depth stencil gibi farklı seçenekler de bulunmakta. 
    }

    // Pencereyi yok eder. Bir alt satırda glfwTerminate fonksiyonunu çağıracağımız için aslında buna gerek yoktur ama pratik olması için ekledim. 
    glfwDestroyWindow(window);
    
    // GLFW ile ilgili her şeyi yok eder. 
    glfwTerminate();
    return 0; 
}

double buffering

Grafiklerin ekranda pürüzsüz bir şekilde hareket edebilmesi için en az iki tane buffer’a ihtiyacımız var. Biz buna iki resim diyelim ya da iki kağıt. Kağıtlardan biri ekranda gösterilirken arka tarafta öteki tarafa resmimizi çizeriz. İşlem bittiğinde ekranda gösterilen kağıtla, çizim yaptığımız kağıdın yerlerini değiştiririz(glfwSwapBuffers(window);). Böyle yeni resim ekrana yansır. Bu sefer eski resimi tamamen sileriz (glClear(GL_COLOR_BUFFER_BIT);) sildikten sonra çizim yapıp yine yer değiştiririz ve bu döngü bu şekilde devam eder.

Refactoring

Kodumuzu biraz düzenliyelim

#include <iostream>

#include <glad/glad.h> 
#include <GLFW/glfw3.h>

using namespace std;

void yeni_boyut(GLFWwindow* window, int width, int height);

void init_glfw();
void init_glad();
GLFWwindow* createWindow();
void termination();


GLFWwindow* window;
int WIDTH = 800;
int HEIGHT = 600;
string title = "OpenGL Window";

int main(){
    init_glfw();
    window = createWindow();
    init_glad();
    glViewport(0, 0, WIDTH, HEIGHT);
    glfwSetFramebufferSizeCallback(window, yeni_boyut);


    //Render loop 
    while (!glfwWindowShouldClose(window)){ 
        glfwSwapBuffers(window); 
        glfwPollEvents(); 

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT); 
    }

    termination();
}


void yeni_boyut(GLFWwindow* window, int width, int height){
    glViewport(0, 0, width, height);
}

void init_glfw() {
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
}
void init_glad() {
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        exit(EXIT_FAILURE);
    }
}
GLFWwindow* createWindow() {
    GLFWwindow* window = glfwCreateWindow(WIDTH, HEIGHT, title.c_str(), NULL, NULL);
    if (window == NULL) {
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        exit(EXIT_FAILURE);
    }
    glfwMakeContextCurrent(window);
    return window;
}

void termination() {
    glfwDestroyWindow(window);
    glfwTerminate();
}

Input

Girdileri handle etmek için bir fonksiyon ekleyelim

void processInput(GLFWwindow* window) {
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
        glfwSetWindowShouldClose(window, true);
}


int main(){

    //Render loop 
    while (!glfwWindowShouldClose(window)){ 
        processInput(window);

        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT); 

        glfwPollEvents(); 
        glfwSwapBuffers(window); 
    }
}

İlk Üçgen

Vertex

Uzaydaki bir noktanın tüm verisine vertex denir. Vertex sadece bir pozisyondan ibaret değildir! Rengi, normali, UV haritasında nereye denk geldiği gibi farklı verileri içerebilir. Bu verilerin hangilerini, ne şekilde koyacağımız bize kalmış.

Rendering Pipeline

Vertex’leri GPU ya attığımızda belirli aşamalardan geçerek ekrana basılıyor. Bu aşamaların herbirinde ufak bir program çalışıyor. Bu çalışan programa shader diyoruz. Bu aşamaların tümüne de rendering pipeline. Shaderlardan hepsine müdahale edemesek de bazılarını kodlayabiliyoruz. Şuanlık bizim kodlamamız gereken iki ader shader bulunmakta. Bunlar vertex shader ve fragment shader‘dır.

  1. Vertexlerin konumu belirle
  2. Vertexleri birleştirerek, istenilen geometriyi oluştur. (misal bir üçgen)
  3. Geometrinin ekranda karşılık geldiği pikselleri belirle
  4. Belirlenen pikselleri istenilen renge boya

https://www.khronos.org/opengl/wiki/Rendering_Pipeline_Overview

Shader

Shader’lar, OpenGL Shading Language (GLSL) denilen C benzeri bir dilde yazılır. Çalışma esnasında derlenir. Yapmamız gereken aşamalar

  1. Dosyadan shader kodunu oku
  2. Shader’ı derle
  3. Derlenen shaderları, bir program oluşturup linkle

Projemizin bulunduğu dizine iki adet dosya oluşturuyoruz. Bunları vs.glsl ve fs.glsl diye adlandırabiliriz.

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

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

Vertex shaderımızda noktaların konumlarını belirleyeceğiz.

  • 1. satırda versiyonu belirtiyoruz
  • 2. satırda shadera gelecek verinin ne şekilde olacağını belirtiyoruz. Şuanlık 3 floattan oluşan bir adet vec3 değişkenimiz olacak.
  • void main() bizim ana fonksiyonumuz
  • gl_Position ise vertexin konumu. 3D bir uzayda 4D bir vector kullandık. 4. boyut ileri aşamalarda perspektif için kullanılacak
#version 330 core
out vec4 FragColor;

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

Fragment shaderda ise pikselimizin rengini belirliyoruz. Şuanlık tek renk yaptık.

NDC

OpenGL’de ekranda gösterilecek uzayımızın boyutu x,y ve z aksislerinde -1 ile 1 aralığındadır. Bunun dışında kalan alana gösterilmiyor. Bizler çeşitli matematik işlemleri(linear cebir) yaparak gerçek hayatta kullandığımız boyutları bu uzayın boyutlarına indirgiyoruz.

State Machine

OpenGL, bir state machine’dir. Yani belli durumları vardır ve bu durumlara göre çalışır. Belirli flaglar ve slotlar vardır. Örneğin saydamlık için bir flag vardır siz bu flag’ı true yaptığınızda saydamlığı aktif hale getirmiş olursunuz. GPU da bir buffer oluşturursunuz ve bu bufferın bir numarası olur (ID değeri) bu numarayı data slotuna bağlarsanız shader onu kullanır. Onun yerine başka bir buffer ID si verirseniz onu kullanır.

İlk başta kulağa yabancı gelse de aslında kodlama yaparken bizler de kimi zaman bu tip şeyler yapabiliyoruz. Örneğin aşağıdaki gibi:

class Sayac{
  int sayac_degeri=0;
  int artim_miktari=1;
  
public:
  void set_artim_miktari(int miktar){
    artim_miktari = miktar; 
  }
  
  int say(){
    sayac_degeri += artim_miktari;
    return sayac_degeri;
  }
};

VAO, VBO, Vertex Attribute

VAO

Vertex Array Object, tek başına hiçbir işimize yaramaz. Diğer bufferları tutmak için kullandığımız bir array AMA programın çalışabilmesi için en az 1 tane olmalıdır.

A Vertex Array Object (VAO) is an object which contains one or more Vertex Buffer Objects and is designed to store the information for a complete rendered object. In our example this is a diamond consisting of four vertices as well as a color for each vertex.

https://www.khronos.org/opengl/wiki/Tutorial2:_VAOs,_VBOs,_Vertex_and_Fragment_Shaders_(C_/_SDL)

VBO

Vertex data’mızı tutacağımız buffer’dır

A Vertex Buffer Object (VBO) is a memory buffer in the high speed memory of your video card designed to hold information about vertices. In our example we have two VBOs, one that describes the coordinates of our vertices and another that describes the color associated with each vertex. VBOs can also store information such as normals, texcoords, indicies, etc.

https://www.khronos.org/opengl/wiki/Tutorial2:_VAOs,_VBOs,_Vertex_and_Fragment_Shaders_(C_/_SDL)

Vertex Attribute

Vertex’imin nasıl bir yapıya sahip olduğunu belirtmemiz gerekmekte. Biz GPU ya sadece bir demet byte gönderiyoruz. Bu byte’ların ne anlama geldiğini belirtmemiz gerekmekte.

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);
  • index: şablondaki kaçıncı değişkeni(niteliği/ attribute’ü) modifiye edeceğimizi seçiyoruz
  • size: bu değişkenin kaç komponentten oluştuğu. Örneğin vec3 -> 3 komponentten oluşurken vec4-> 4 komponenten oluşur
  • type: tipi
  • normalized: otomatik olarak normalize etsin mi etmesin mi?
  • stride: vertex’imizin byte cinsinden boyutu
  • pointer: Değişkenin başlangıçtan itibaren byte cinsinden ofseti.

https://registry.khronos.org/OpenGL-Refpages/gl4/html/glVertexAttribPointer.xhtml


#include <iostream>
#include <fstream>
#include <sstream>

#include <glad/glad.h> 
#include <GLFW/glfw3.h>

using namespace std;

//Eski kısımlar

void yeni_boyut(GLFWwindow* window, int width, int height);
void init_glfw();
void init_glad();
GLFWwindow* createWindow();
void termination();
void processInput(GLFWwindow* window);
GLFWwindow* window;
int WIDTH = 800;
int HEIGHT = 600;
string title = "OpenGL Window";

// Yeni eklediklerimiz 

// Vertex of triangle
float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

string read_file(string file_name);
unsigned int compile_shader(string shaderCode, unsigned int shaderType);
void link_program(unsigned int vertexShader, unsigned int fragmentShader);
void bind_buffers();



int main(){
    init_glfw();
    window = createWindow();
    init_glad();
    glViewport(0, 0, WIDTH, HEIGHT);
    glfwSetFramebufferSizeCallback(window, yeni_boyut);

    // Shader 
    string vsSource = read_file("vs.glsl");
    string fsSource = read_file("fs.glsl");
    auto vs = compile_shader(vsSource, GL_VERTEX_SHADER);
    auto fs = compile_shader(fsSource, GL_FRAGMENT_SHADER);
    cout << vs << endl << fs << endl;
    link_program(vs, fs);
    
    // Buffers 
    bind_buffers();

    //Render loop 
    //...
}

string read_file(string file_name){
    std::stringstream ss;
    std::ifstream myFile; 

    myFile.open(file_name);
    ss << myFile.rdbuf();
    myFile.close();

    return ss.str();
}

unsigned int compile_shader(string shaderCode, unsigned int shaderType){
    unsigned int myShader = glCreateShader(shaderType);

    const char* shaderSource = shaderCode.c_str();
    glShaderSource(myShader, 1, &shaderSource, NULL);
    glCompileShader(myShader);

    // check for shader compile errors
    int success;
    char infoLog[512];
    glGetShaderiv(myShader, GL_COMPILE_STATUS, &success);

    if (!success) {
        glGetShaderInfoLog(myShader, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::" << (shaderType == GL_VERTEX_SHADER ? "VERTEX" : "FRAGMENT") << "::COMPILATION_FAILED\n" << infoLog << std::endl;
    }

    return myShader;
}

void link_program(unsigned int vertexShader, unsigned int fragmentShader){
    // link shaders
    unsigned int shaderProgram = glCreateProgram();
    glAttachShader(shaderProgram, vertexShader);
    glAttachShader(shaderProgram, fragmentShader);
    glLinkProgram(shaderProgram);

    // check for linking errors
    int success;
    char infoLog[512];
    glGetProgramiv(shaderProgram, GL_LINK_STATUS, &success);
    if (!success) {
        glGetProgramInfoLog(shaderProgram, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
    }

    // linkleme işleminden sonra shaderları silebiliriz 
    glDeleteShader(vertexShader);
    glDeleteShader(fragmentShader);

    // linklenen programı kullanıma alıyoruz 
    glUseProgram(shaderProgram);
}

void bind_buffers(){
    unsigned int VBO, VAO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);

    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

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



Comments

Leave a Reply

Your email address will not be published. Required fields are marked *