To Paint a World

OpenGL - 4. Shaders 본문

3D Engine/OpenGL

OpenGL - 4. Shaders

Polariche 2025. 1. 6. 12:35

Writing Shaders

쉐이더는 정점 데이터를 입력받아 GPU 에서 병렬 연산을 처리하는 프로그램이다. 

쉐이더의 종류로는 정점 데이터를 변환해주는 Vertex Shader 와, 픽셀 데이터를 가공하여 픽셀 색상을 출력하는 Fragment Shader 가 있다.

  • Vertex Shader
  • Fragment Shader

렌더링 파이프라인은 정점 데이터를 모으고, 쉐이더를 거쳐 데이터를 가공하고, 기하 연산을 처리하여 어떤 도형이 최종적으로는 스크린에 출력될지 결정하는 역할을 한다.

 

Vertex Shader

Vertex Shader 는 한 개의 정점 데이터 (position, color, ... 등의 attribute들) 를 입력받고, 일괄적인 연산을 수행하여 출력하는 쉐이더이다.

 

Vertex Shader 의 가장 흔한 목표는 각 정점에 트랜스폼 (로컬 -> 월드 좌표계 변환 등) 을 적용하는 것이다. 가공된 좌표는 gl_Position 이란 built-in 변수 값에 배정하면 된다.

 

#version 330 
layout (location = 0) in vec3 position;
void main()
{
    gl_Position = vec4(position, 1.0);
}

 

Fragment Shader

Vertex Shader 를 거친 정점 데이터는 Rasterization 이란 과정을 통해 화면 내 픽셀로 변환된다.

이러한 픽셀은 Fragment Shader 로 보내진다. Fragment Shader 는 픽셀의 위치 값, attribute 등을 처리하여 화면에 출력할 색상을 반환한다.

 

#version 330 core
out vec4 color;
void main()
{
    color = vec4(0.8F, 0.5f, 0.2f, 0.8F);
}

 

 

Shader Class

쉐이더 코드를 작성했으면, 이를 불러오고 컴파일하여 OpenGL 프로그램에 부착해서 사용할 수 있다.

아래 Shader 클래스는 그러한 기능을 수행하기 위한 함수들을 정의한다.

#pragma once
#include <GL/glew.h>
#include <GLFW/glfw3.h>

class Shader {
private:
    unsigned int _id;
    bool deleted = 0;

public:
    Shader(GLenum shaderType, const GLchar* source);
    ~Shader();

    void Attach(unsigned int programId);
    void Delete();
};

 

 

Creating a Shader

쉐이더를 생성하여 그 이름을 _id 변수에 저장하고, 경로로부터 쉐이더 코드를 불러오고 컴파일한다.

Shader::Shader(GLenum shaderType, const GLchar* source) {
    _id = glCreateShader(shaderType);
    glShaderSource(_id , 1, &source, NULL);
    glCompileShader(_id);
    
    // Check for compile time errors
    GLint success;
    GLchar infoLog[512];
    glGetShaderiv(_id, GL_COMPILE_STATUS, &success);
    if (!success)
    {
        glGetShaderInfoLog(_id, 512, NULL, infoLog);
        std::cout << "ERROR::SHADER::COMPILATION_FAILED\n" << infoLog << std::endl;
    }
}

 

Attach

쉐이더를 OpenGL 프로그램에 부착한다.

void Shader::Attach(unsigned int programId) {
    glAttachShader(programId, _id);
}

 

 

Delete

쉐이더를 삭제한다.

void Shader::Delete() {
    if (deleted)
        return;
    glDeleteShader(_id);
    deleted = true;
}

 

 

 

Shader Initialization

GLApp 에서 쉐이더를 불러오는 InitShaders 함수와, 쉐이더 프로그램의 이름인 shaderProgram 변수, 각각 vertex shader 와 fragment shader 에 해당하는 Shader 오브젝트를 선언한다.

class GLApp {
// ...

protected:
    bool InitShaders(const char*  vertexPath, const char* fragPath);

protected:
    unsigned int shaderProgram;
    Shader* vertexShader;
    Shader* fragmentShader;
    
};

 

 

쉐이더를 불러오기에 앞서, 쉐이더를 사용하려면 OpenGL 3+ 버전을 지정해야 한다.

bool GLApp::InitOpenGL() {

    if (!glfwInit())
        return false;
    
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    
    // glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 2);
    // glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 1);
    
    
    // ...
    
}

 

 

InitShaders 함수에서 vertex shader 와 fragment shader 의 경로를 받아와서, 프로그램에 부착한다.

bool GLApp::InitShaders(const char*  vertexPath, const char* fragPath) {
    shaderProgram = glCreateProgram();

    std::string vertexShaderSource = load_text(vertexPath);
    if (!vertexShaderSource.empty()) {
        vertexShader = new Shader(GL_VERTEX_SHADER, vertexShaderSource.c_str());
        vertexShader->Attach(shaderProgram);
        vertexShader->Delete();
    }

    std::string fragmentShaderSource = load_text(fragPath);
    if (!fragmentShaderSource.empty()) {
        fragmentShader = new Shader(GL_FRAGMENT_SHADER, fragmentShaderSource.c_str());
        fragmentShader->Attach(shaderProgram);
        fragmentShader->Delete();
    }

    glLinkProgram(shaderProgram);
    

    return true;
}

 

 

 

Using Shaders

MainApp 의 Initialize 함수에서 쉐이더를 불러온다.

bool MainApp::Initialize() {
    // init GLApp functionalities
    GLApp::InitOpenGL();
    GLApp::InitShaders("../src/Scenes/tutorials/shaders/vert.vert", 
                        "../src/Scenes/tutorials/shaders/frag.frag");
    GLApp::InitCallbacks();
    
    // ...
    
}

 

드로우 콜에서 도형에 대한 명령어를 호출하기에 앞서, glUseProgram 함수를 호출하여 쉐이더를 사용하도록 설정한다.

void MainApp::Draw() {
    // ...
    glUseProgram(shaderProgram);
    
    // ...
}

 

 

모든 구현이 성공적으로 이뤄졌으면, fragment shader 에서 지정한 색상으로 도형이 색칠된 것을 볼 수 있다.

'3D Engine > OpenGL' 카테고리의 다른 글

OpenGL - 5. Uniforms  (0) 2025.02.18
OpenGL - 3. Buffer Objects  (1) 2025.01.02
OpenGL - 2. Input Callbacks  (0) 2024.12.26
OpenGL - 1. Initialization  (3) 2024.12.26