컴퓨터 공학/OpenGL

[최신 OpenGL 시작하기 #1_5] 셰이더 기초 (Shader)

James Kim_ 2025. 3. 6. 15:08

Shader

이전 장에 삼각형 만들기에서 언급했듯이, shader는 GPU에 존재하는 작은 프로그램입니다. 이 프로그램들은 graphics pipeline의 특정 부분에서 실행됩니다. 기본적으로 shader는 input을 output으로 변환하는 프로그램에 불과합니다. 그러므로 Shader 간 유일한 통신은 input과 output을 통해서만 이루어집니다.

 

이번 장에서는 OpenGL Shading Language에 대해 좀 더 일반적인 방식으로 설명하겠습니다.

GLSL

Shader는 C와 유사한 언어인 GLSL로 작성됩니다. GLSL은 그래픽 사용에 맞게 조정되었으며 vector 및 matrix 조작에 특화된 유용한 기능을 포함하고 있습니다.

 

Shader는 항상 버전 선언으로 시작하며, 그 다음에 input 및 output 변수, uniform 목록과 main 함수가 옵니다. 각 shader의 진입점은 main 함수이며, 여기서 input 변수를 처리하고 결과를 output 변수에 저장합니다. Uniform는 곧 설명하겠습니다.

 

Shader는 다음과 같은 구조를 가집니다:

#version version_number
in type in_variable_name;
in type in_variable_name;

out type out_variable_name;
  
uniform type uniform_name;
  
void main()
{
  // input을 처리하고 그래픽 관련 작업 수행
  ...
  // 처리된 결과를 output 변수에 저장
  out_variable_name = weird_stuff_we_processed;
}

 

특히 vertex shader에 대해 이야기할 때, 각 input 변수는 vertex attribute라고도 합니다. 하드웨어에 의해 제한되도록 vertex attribute의 최대 개수를 선언 할수 있는데요. OpenGL은 항상 최소 16개의 4-component vertex attribute를 사용할 수 있도록 보장하지만, 일부 하드웨어는 더 많이 사용할 수 있고, GL_MAX_VERTEX_ATTRIBS를 조회하여 확인할 수 있습니다:

int nrAttributes;
glGetIntegerv(GL_MAX_VERTEX_ATTRIBS, &nrAttributes);
std::cout << "Maximum nr of vertex attributes supported: " << nrAttributes << std::endl;

 

대부분 목적에 맞게 최솟값 16을 반환합니다.

Types

GLSL은 다른 프로그래밍 언어와 마찬가지로 어떤 종류의 변수로 작업할 것인지 지정하는data type이 있습니다. GLSL은 C와 같은 기본 type을 가지고 있습니다: int, float, double, uint 및 bool 등.. GLSL은 또한 많이 사용될 2개의 container type인 vector와 matrix를 제공합니다. Matrix는 나중 장에서 논의하겠습니다.

Vector

GLSL의 vector는 앞서 언급한 기본 type 중 하나를 위한 2, 3 또는 4 component container입니다. 다음과 같은 형태를 취할 수 있습니다(n은 component의 수를 나타냅니다):

  • vecn: n개의 float로 구성된 기본 vector
  • bvecn: n개의 boolean으로 구성된 vector
  • ivecn: n개의 정수로 구성된 vector
  • uvecn: n개의 부호 없는 정수로 구성된 vector
  • dvecn: n개의 double component로 구성된 vector

대부분의 경우 우리는 float가 대부분의 목적에 충분하기 때문에 기본 vecn을 사용할 것입니다.

 

Vector의 component는 vec.x와 같이 접근할 수 있으며,  x는 vector의 첫 번째 component입니다. .x, .y, .z 및 .w를 사용하여 각각 첫 번째, 두 번째, 세 번째 및 네 번째 component에 접근할 수 있습니다. GLSL은 또한 색상을 위한 .rgba나 texture 좌표를 위한 .stpq를 사용하여 동일한 component에 접근할 수 있게 합니다.

 

Vector data type은 swizzling이라고 불리는  component 을 사용하는데,  Swizzling을 사용하면 다음과 같은 구문을 사용할 수 있습니다:

vec2 someVec;
vec4 differentVec = someVec.xyxx;
vec3 anotherVec = differentVec.zyw;
vec4 otherVec = someVec.xxxx + anotherVec.yxzy;

 

원래 vector가 해당 component를 가지고 있는 한, 최대 4개의 문자 조합을 사용하여 (동일한 type의) 새 vector를 만들 수 있습니다. 예를 들어, vec2의 .z component에 접근하는 것은 허용되지 않습니다. 또한 다른 vector 생성자 호출에 vector를 인수로 전달하여 필요한 인수 수를 줄일 수도 있습니다:

vec2 vect = vec2(0.5, 0.7);
vec4 result = vec4(vect, 0.0, 0.0);
vec4 otherResult = vec4(result.xyz, 1.0);

 

따라서 vector는 우리가 모든 종류의 input과 output에 사용할 수 있는 유연한 data type입니다. 책 전체에서 우리가 어떻게 창의적으로 vector를 관리할 수 있는지에 대한 많은 예를 볼 수 있을 것입니다.

Ins and outs

Shader는 개별 shader에 input과 output을 가지고 있어 정보를 이동시킬 수 있며 동작합니다. GLSL은 그 목적을 위해 특별히 in과 out 키워드를 정의했습니다. 각 shader는 이러한 키워드를 사용하여 input과 output을 지정할 수 있으며, output 변수가 다음 shader 단계의 input 변수와 일치하는 경우 둘은 연결됩니다. 그러나 vertex shader와 fragment shader는 조금 다릅니다.

 

Vertex shader는 vertex 데이터로부터 직접 input는 구조입니다. Vertex 데이터가 어떻게 구성되어 있는지 정의하기 위해 CPU에서 vertex attribute를 구성할 수 있도록 location metadata를 가진 input 변수를 지정합니다. 이것은 이전 장에서 본 layout (location = 0)입니다. 따라서 vertex shader는 vertex 데이터와 연결할 수 있도록 input에 대한 추가 layout 지정이 필요합니다.

layout (location = 0) 지정자를 생략하고 OpenGL 코드에서 glGetAttribLocation을 통해 attribute 위치를 쿼리하는 것도 가능하지만, vertex shader에서 설정하는 것이 더 좋습니다. 이해하기 쉽고 사용자(및 OpenGL)에게 약간의 작업을 절약할 수 있습니다.

 

다른 예외는 fragment shader가 vec4 color output 변수를 필요로 한다는 것입니다. Fragment shader는 최종 output 색상을 생성해야 하기 때문입니다. Fragment shader에서 output 색상을 지정하지 않으면, 해당 fragment의 color buffer output은 정의되지 않은 상태가 됩니다(일반적으로 OpenGL은 이를 검은색이나 흰색으로 렌더링합니다).

 

따라서 한 shader에서 다른 shader로 데이터를 보내려면 보내는 shader에서 output을 선언하고 받는 shader에서 유사한 input을 선언해야 합니다. 양쪽의 type과 이름이 같을 때 OpenGL은 이러한 변수를 연결하고 shader 간에 데이터를 보낼 수 있게 됩니다(이는 program object를 link할 때 수행됩니다). 이것이 실제로 어떻게 작동하는지 보여주기 위해 이전 장의 shader를 변경하여 vertex shader가 fragment shader의 색상을 결정하도록 해보겠습니다.

 

Vertex shader:

#version 330 core
layout (location = 0) in vec3 aPos; // position 변수는 attribute position 0을 가집니다
  
out vec4 vertexColor; // fragment shader에 color output 지정

void main()
{
    gl_Position = vec4(aPos, 1.0); // vec3를 vec4의 생성자에 직접 제공하는 방법을 보세요
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // output 변수를 진한 빨간색으로 설정합니다
}

 

Fragment shader:

#version 330 core
out vec4 FragColor;
  
in vec4 vertexColor; // vertex shader에서 온 input 변수 (동일한 이름과 type)

void main()
{
    FragColor = vertexColor;
} 

 

우리는 vertex shader에서 설정하는 vec4 output으로 vertexColor 변수를 선언했고, fragment shader에서 유사한 vertexColor input을 선언했습니다. 둘 다 동일한 type과 이름을 가지고 있기 때문에 fragment shader의 vertexColor는 vertex shader의 vertexColor에 연결됩니다. Vertex shader에서 색상을 진한 빨간색으로 설정했기 때문에 결과 fragment도 진한 빨간색이어야 합니다.

 

성공했습니다! 방금 vertex shader에서 fragment shader로 값을 전송하는 데 성공했습니다. 이제 조금 더 재미있게 만들어보고 애플리케이션에서 fragment shader로 색상을 보낼 수 있는지 확인해봅시다!

Uniform

Uniform은 CPU의 애플리케이션에서 GPU의 shader로 데이터를 전달하는 또 다른 방법입니다. 그러나 uniform은 vertex attribute와 2가지 차이가 있습니다. 첫째, uniform은 전역적(global)입니다. 전역적이라는 것은 uniform 변수가 shader program object마다 고유하며 shader program의 모든 단계에서 어떤 shader든 접근할 수 있다는 의미입니다. 둘째, uniform 값을 설정하면 재설정하거나 업데이트할 때까지 그 값을 유지합니다.

 

GLSL에서 uniform을 선언하려면 단순히 uniform 키워드를 type과 이름과 함께 shader에 추가하면 됩니다. 그 시점부터 shader에서 새로 선언된 uniform을 사용할 수 있습니다. 이번에는 uniform을 통해 삼각형의 색상을 설정해보겠습니다:

#version 330 core
out vec4 FragColor;
  
uniform vec4 ourColor; // 이 변수는 OpenGL 코드에서 설정합니다.

void main()
{
    FragColor = ourColor;
}   

Fragment shader에 uniform vec4 ourColor를 선언하고 fragment의 output 색상을 이 uniform 값의 내용으로 설정했습니다. Uniform은 전역 변수이므로 원하는 어떤 shader 단계에서도 정의할 수 있습니다. 따라서 vertex shader를 다시 거쳐 fragment shader에 무언가를 전달할 필요가 없습니다. Vertex shader에서 이 uniform을 사용하지 않으므로 거기에 정의할 필요가 없습니다.

GLSL 코드내에서 사용되지 않는 uniform을 선언하면 컴파일러는 컴파일된 버전에서 변수를 자동으로 제거하는데, 이는 여러 가지 혼란스러운 오류의 원인이 됩니다. 이 점을 명심하세요!

 

현재 uniform은 비어 있습니다. 아직 uniform에 데이터를 추가하지 않았으므로 그렇게 해보겠습니다. 먼저 shader에서 uniform attribute의 인덱스/위치를 찾아야 합니다. Uniform의 인덱스/위치를 얻으면 그 값을 업데이트할 수 있습니다. Fragment shader에 단일 색상을 전달하는 대신, 시간이 지남에 따라 색상이 점차 변하도록 해보겠습니다:

float timeValue = glfwGetTime();
float greenValue = (sin(timeValue) / 2.0f) + 0.5f;
int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
glUseProgram(shaderProgram);
glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

먼저, glfwGetTime()을 통해 초 단위로 실행 시간을 검색합니다. 그런 다음 sin 함수를 사용하여 0.0-1.0 범위에서 색상을 변화시키고 결과를 greenValue에 저장합니다.

 

그 다음 glGetUniformLocation을 사용하여 ourColor uniform의 위치를 쿼리합니다. 쿼리 함수에 shader program과 uniform의 이름(위치를 검색하려는)을 제공합니다. glGetUniformLocation이 -1을 반환하면 위치를 찾을 수 없는 것입니다. 마지막으로 glUniform4f 함수를 사용하여 uniform 값을 설정할 수 있습니다. Uniform 위치를 찾는 것은 먼저 shader program을 사용할 필요가 없지만, uniform을 업데이트하려면 먼저 program을 사용해야 합니다(glUseProgram 호출). 이는 현재 활성화된 shader program에 uniform을 설정하기 때문입니다.

OpenGL은 핵심적으로 C 라이브러리이기 때문에 함수 오버로딩에 대한 기본 지원이 없습니다. 따라서 함수가 다른 type으로 호출될 수 있는 경우 OpenGL은 필요한 각 type에 대해 새로운 함수를 정의합니다. glUniform은 이에 대한 완벽한 예입니다. 이 함수는 설정하려는 uniform의 type에 따라 특정 접미사를 필요로 합니다. 가능한 몇 가지 접미사는 다음과 같습니다:

  • f: 함수는 값으로 float를 예상합니다.
  • i: 함수는 값으로 int를 예상합니다.
  • ui: 함수는 값으로 unsigned int를 예상합니다.
  • 3f: 함수는 값으로 3개의 float를 예상합니다.
  • fv: 함수는 값으로 float vector/array를 예상합니다.

OpenGL의 옵션을 구성하려면 type에 해당하는 오버로드된 함수를 선택하세요. 우리의 경우 uniform의 4개의 float를 개별적으로 설정하려고 하므로 glUniform4f를 통해 데이터를 전달합니다(fv 버전도 사용할 수 있었습니다).

 

이제 uniform 변수의 값을 설정하는 방법을 알았으므로 렌더링에 사용할 수 있습니다. 색상이 점차 변하도록 하려면 uniform을 매 프레임마다 업데이트해야 합니다. 그렇지 않으면 한 번만 설정할 경우 삼각형은 단일 단색을 유지할 것입니다. 따라서 greenValue를 계산하고 각 렌더링 반복마다 uniform을 업데이트합니다:

while(!glfwWindowShouldClose(window))
{
    // input
    processInput(window);

    // render
    // color buffer 지우기
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // shader를 활성화해야 합니다
    glUseProgram(shaderProgram);
  
    // uniform color 업데이트
    float timeValue = glfwGetTime();
    float greenValue = sin(timeValue) / 2.0f + 0.5f;
    int vertexColorLocation = glGetUniformLocation(shaderProgram, "ourColor");
    glUniform4f(vertexColorLocation, 0.0f, greenValue, 0.0f, 1.0f);

    // 이제 삼각형 렌더링
    glBindVertexArray(VAO);
    glDrawArrays(GL_TRIANGLES, 0, 3);
  
    // buffer 교체 및 IO 이벤트 폴링
    glfwSwapBuffers(window);
    glfwPollEvents();
}

이 코드는 이전 코드의 비교적 간단한 적용입니다. 이번에는 삼각형을 그리기 전에 매 프레임마다 uniform 값을 업데이트합니다. Uniform을 올바르게 업데이트하면 삼각형의 색상이 녹색에서 검은색으로, 다시 녹색으로 점차 변하는 것을 볼 수 있습니다.

보시다시피, uniform은 매 프레임마다 변경될 수 있는 attribute를 설정하거나 애플리케이션과 shader 사이에 데이터를 교환하는 데 유용한 도구입니다. 그러나 각 vertex에 대해 색상을 설정하려면 어떻게 해야 할까요? 그런 경우 vertex 수만큼 많은 uniform을 선언해야 할 것입니다. 더 나은 해결책은 vertex attribute에 더 많은 데이터를 포함하는 것이며, 앞으로 해야될 일입니다.

더 많은 속성 ( More attributes )!

이전 장에서 우리는 어떻게 VBO를 채우고, vertex attribute pointer를 구성하고, 모든 것을 VAO에 저장하는지 보았습니다. 이번에는 vertex 데이터에 color 데이터도 추가하고 싶습니다. Color 데이터를 3개의 float로 vertices 배열에 추가할 것입니다. 삼각형의 각 모서리에 빨간색, 녹색, 파란색을 각각 할당합니다:

float vertices[] = {
    // positions         // colors
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // bottom right
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // bottom left
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // top 
};    

이제 vertex shader로 보낼 데이터가 더 많아졌기 때문에, vertex attribute input으로 color 값도 받을 수 있도록 vertex shader를 조정해야 합니다. Layout 지정자를 사용하여 aColor attribute의 위치를 1로 설정한 것에 주목하세요:

#version 330 core
layout (location = 0) in vec3 aPos;   // position 변수는 attribute position 0을 가집니다
layout (location = 1) in vec3 aColor; // color 변수는 attribute position 1을 가집니다
  
out vec3 ourColor; // fragment shader로 color output

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; // vertex 데이터에서 얻은 input color로 ourColor를 설정합니다
}       

Fragment의 color에 더 이상 uniform을 사용하지 않고 이제 ourColor output 변수를 사용하므로 fragment shader도 변경해야 합니다:

#version 330 core
out vec4 FragColor;  
in vec3 ourColor;
  
void main()
{
    FragColor = vec4(ourColor, 1.0);
}

다른 vertex attribute를 추가하고 VBO의 메모리를 업데이트했기 때문에 vertex attribute pointer를 다시 구성해야 합니다.

VBO 메모리에서 업데이트된 데이터를 표현하면 다음과 같습니다:

현재 Vertex Buffer Object(VBO) 내부

현재 layout을 알고 있으므로 glVertexAttribPointer로 vertex 형식을 업데이트할 수 있습니다:  

// position attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// color attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);

glVertexAttribPointer의 처음 몇 가지 인수는 상대적으로 간단합니다. 이번에는 attribute 위치 1에서 vertex attribute를 구성하고 있습니다. Color 값의 크기는 3개의 float이며 값을 정규화하지 않습니다.

 

이제 두 개의 vertex attribute가 있으므로 stride 값을 다시 계산해야 합니다. 데이터 배열에서 다음 attribute 값(예: position vector의 다음 x component)을 얻으려면 오른쪽으로 6개의 float를 이동해야 합니다. 세 개는 position 값용이고 세 개는 color 값용입니다. 이렇게 하면 float 크기의 6배(= 24바이트)의 stride 값이 됩니다.

 

또한 이번에는 offset을 지정해야 합니다. 각 vertex에 대해 position vertex attribute가 먼저 오므로 0의 offset을 선언합니다. Color attribute는 position 데이터 이후에 시작하므로 offset은 바이트 단위로 3 * sizeof(float)(= 12바이트)입니다.

 

애플리케이션을 실행하면 다음과 같은 이미지가 나타나야 합니다:

 

 

잘 안된다면 여기 소스코드를 참고해보세요.

 

이미지는 우리가 예상했던 것과 정확히 일치하지 않을 수 있습니다. 우리는 3개의 color만 제공했는데, 지금 보이는 엄청난 color palette가 있습니다. 이것은 모두 fragment shader에서 발생하는 fragment interpolation의 결과입니다. 삼각형을 렌더링할 때 rasterization 단계는 일반적으로 원래 지정된 vertex보다 더 많은 fragment를 생성합니다. Rasterizer는 그 후 삼각형 모양에서 어디에 위치하는지에 따라 각 fragment의 위치를 결정합니다.

이러한 위치에 기반하여, 모든 fragment shader의 input 변수를 보간합니다. 예를 들어, 상단 점이 녹색이고 하단 점이 파란색인 선이 있다고 가정해봅시다. Fragment shader가 선의 70% 위치에 있는 fragment에서 실행된다면, 결과 color input attribute는 녹색과 파란색의 선형 조합일 것입니다. 더 정확히 말하면: 30% 파란색과 70% 녹색입니다.

 

이것이 바로 삼각형에서 나타나는데요. 우리는 3개의 vertex와 따라서 3개의 color를 가지고 있으며, 삼각형의 pixel로 판단하면 아마도 약 50000개의 fragment를 포함하고 있을 것입니다. 여기서 fragment shader가 이 pixel 간의 color를 보간했습니다. Color를 자세히 살펴보면 모든 것이 이해됩니다: 빨간색에서 파란색으로 가는 것은 먼저 보라색이 되고 그 다음 파란색이 됩니다. Fragment interpolation은 fragment shader의 모든 input attribute에 적용됩니다.

우리만의 shader class

Shader를 작성, 컴파일 및 관리하는 것은 꽤 번거로울 수 있습니다. Shader 주제의 마지막 마무리로 disk에서 shader를 읽고, 컴파일하고 link하며, 오류를 확인하고 사용하기 쉬운 shader class를 만들어 우리의 삶을 조금 더 쉽게 만들 것입니다. 이는 또한 지금까지 배운 지식 중 일부를 유용한 추상 객체로 캡슐화하는 방법에 대한 아이디어를 제공합니다.

 

주로 학습 목적과 휴대성을 위해 shader class를 header 파일에 전부 만들 것입니다. 필요한 include를 추가하고 class 구조를 정의하여 시작해 보겠습니다:

#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h> // include glad to get all the required OpenGL headers
  
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
  
  
class Shader
{
public:
    // program ID
    unsigned int ID;
  
    // constructor는 shader를 읽고 빌드합니다
    Shader(const char* vertexPath, const char* fragmentPath);
    // shader를 사용/활성화합니다
    void use();
    // utility uniform 함수들
    void setBool(const std::string &name, bool value) const;  
    void setInt(const std::string &name, int value) const;   
    void setFloat(const std::string &name, float value) const;
};
  
#endif

 

Header 파일의 맨 위에 여러 전처리기 지시문을 사용했습니다. 이러한 코드 라인을 사용하면 여러 파일이 shader header를 포함하더라도 이 header 파일이 아직 포함되지 않은 경우에만 포함하고 컴파일하도록 컴파일러에 알려줍니다. 이는 링크 충돌을 방지합니다.

 

Shader class는 shader program의 ID를 보유합니다. 그 생성자는 각각 vertex와 fragment shader의 소스 코드의 파일 경로를 필요로 하며, 이는 단순한 텍스트 파일로 disk에 저장할 수 있습니다. 추가적으로 몇 가지 유틸리티 함수도 추가합니다: use는 shader program을 활성화하고, 모든 set... 함수는 uniform 위치를 쿼리하고 그 값을 설정합니다.

파일에서 읽기

C++ filestream을 사용하여 파일 내용을 여러 string 객체로 읽습니다:

Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. filePath에서 vertex/fragment 소스 코드 검색
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // ifstream 객체가 예외를 throw할 수 있도록 함:
    vShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions (std::ifstream::failbit | std::ifstream::badbit);
    try 
    {
        // 파일 열기
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // 파일의 버퍼 내용을 스트림으로 읽기
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();		
        // 파일 핸들러 닫기
        vShaderFile.close();
        fShaderFile.close();
        // 스트림을 string으로 변환
        vertexCode   = vShaderStream.str();
        fragmentCode = fShaderStream.str();		
    }
    catch(std::ifstream::failure e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESFULLY_READ" << std::endl;
    }
    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = fragmentCode.c_str();
    [...]

다음으로 shader를 컴파일하고 link해야 합니다. 컴파일/링크가 실패했는지도 검토하고 실패했다면 컴파일 타임 오류를 출력한다는 점에 유의하세요. 이는 디버깅할 때 매우 유용합니다(결국에는 이러한 오류 로그가 필요할 것입니다):

// 2. shader 컴파일
unsigned int vertex, fragment;
int success;
char infoLog[512];
   
// vertex Shader
vertex = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex, 1, &vShaderCode, NULL);
glCompileShader(vertex);
// 컴파일 오류가 있다면 출력
glGetShaderiv(vertex, GL_COMPILE_STATUS, &success);
if(!success)
{
    glGetShaderInfoLog(vertex, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
};
  
// Fragment Shader에 대해서도 비슷하게 진행
[...]
  
// shader Program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glLinkProgram(ID);
// 링크 오류가 있다면 출력
glGetProgramiv(ID, GL_LINK_STATUS, &success);
if(!success)
{
    glGetProgramInfoLog(ID, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::PROGRAM::LINKING_FAILED\n" << infoLog << std::endl;
}
  
// shader가 이제 우리 program에 link되었으므로 더 이상 필요하지 않아 삭제
glDeleteShader(vertex);
glDeleteShader(fragment);

Use 함수는 간단합니다:

void use() 
{ 
    glUseProgram(ID);
}  

Uniform setter 함수들도 마찬가지입니다:

void setBool(const std::string &name, bool value) const
{         
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value); 
}
void setInt(const std::string &name, int value) const
{ 
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value); 
}
void setFloat(const std::string &name, float value) const
{ 
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value); 
} 

이제 완성된 shader class가 있습니다. Shader class를 사용하는 것은 꽤 쉽습니다. 한 번 shader 객체를 생성하고 그 시점부터 간단히 사용하기 시작합니다:

Shader ourShader("path/to/shaders/shader.vs", "path/to/shaders/shader.fs");
[...]
while(...)
{
    ourShader.use();
    ourShader.setFloat("someUniform", 1.0f);
    DrawStuff();
}

여기서 우리는 vertex와 fragment shader 소스 코드를 shader.vs와 shader.fs라는 두 파일에 저장했습니다. shader 파일의 이름은 원하는 대로 지을 수 있습니다. 개인적으로는 .vs와 .fs 확장자가 꽤 직관적이라고 생각합니다.

 

새로 만든 shader class를 사용하는 소스 코드를 여기에서 찾을 수 있습니다. shader 파일 경로를 클릭하면 shader의 소스 코드를 찾을 수 있습니다.

연습문제

  1. Vertex shader를 조정하여 삼각형이 뒤집히도록 만드세요: solution.
  2. Uniform을 통해 수평 offset을 지정하고 이 offset 값을 사용하여 vertex shader에서 삼각형을 화면 오른쪽으로 이동시키세요: solution.
  3. Out 키워드를 사용하여 vertex position을 fragment shader로 출력하고 fragment의 color를 이 vertex position과 동일하게 설정하세요(삼각형 전체에서 vertex position 값이 어떻게 보간되는지 확인하세요). 이것을 성공적으로 수행한 후 다음 질문에 답해보세요: 왜 삼각형의 왼쪽 아래 부분이 검은색인가요?: solution.

출처: https://learnopengl.com/Getting-started/Shaders

생각하기

 

저같은경우 현재 아래와 같은 폴더 구조입니다.

OpenGL
 ┣ include
 ┃ ┗ Shader
 ┃ ┃ ┗ shader.h
 ┣ src
 ┃ ┣ glad.c
 ┃ ┣ shader.cpp
 ┃ ┣ shader.fs
 ┃ ┗ shader.vs
 ┣ main.cpp

Shader.h 코드

#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h> // include glad to get all the required OpenGL headers

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


class Shader
{
public:
    // the program ID
    unsigned int ID;

    // constructor reads and builds the shader
    Shader(const char* vertexPath, const char* fragmentPath);
    // use/activate the shader
    void use();
    // utility uniform functions
    void setBool(const std::string& name, bool value) const;
    void setInt(const std::string& name, int value) const;
    void setFloat(const std::string& name, float value) const;

private:
    void checkCompileErrors(unsigned int shader, std::string type);

};

#endif

Shader.cpp 코드

#include <Shader/shader.h>

Shader::Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. retrieve the vertex/fragment source code from filePath
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // ensure ifstream objects can throw exceptions:
    vShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
    fShaderFile.exceptions(std::ifstream::failbit | std::ifstream::badbit);
    try
    {
        // open files
        vShaderFile.open(vertexPath);
        fShaderFile.open(fragmentPath);
        std::stringstream vShaderStream, fShaderStream;
        // read file's buffer contents into streams
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();
        // close file handlers
        vShaderFile.close();
        fShaderFile.close();
        // convert stream into string
        vertexCode = vShaderStream.str();
        fragmentCode = fShaderStream.str();
    }
    catch (std::ifstream::failure& e)
    {
        std::cout << "ERROR::SHADER::FILE_NOT_SUCCESSFULLY_READ: " << e.what() << std::endl;
    }
    const char* vShaderCode = vertexCode.c_str();
    const char* fShaderCode = fragmentCode.c_str();
    // 2. compile shaders
    unsigned int vertex, fragment;
    // vertex shader
    vertex = glCreateShader(GL_VERTEX_SHADER);
    glShaderSource(vertex, 1, &vShaderCode, NULL);
    glCompileShader(vertex);
    checkCompileErrors(vertex, "VERTEX");
    // fragment Shader
    fragment = glCreateShader(GL_FRAGMENT_SHADER);
    glShaderSource(fragment, 1, &fShaderCode, NULL);
    glCompileShader(fragment);
    checkCompileErrors(fragment, "FRAGMENT");
    // shader Program
    ID = glCreateProgram();
    glAttachShader(ID, vertex);
    glAttachShader(ID, fragment);
    glLinkProgram(ID);
    checkCompileErrors(ID, "PROGRAM");
    // delete the shaders as they're linked into our program now and no longer necessary
    glDeleteShader(vertex);
    glDeleteShader(fragment);
}

void Shader::use()
{
    glUseProgram(ID);
}

void Shader::setBool(const std::string& name, bool value) const
{
    glUniform1i(glGetUniformLocation(ID, name.c_str()), (int)value);
}

void Shader::setInt(const std::string& name, int value) const
{
    glUniform1i(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::setFloat(const std::string& name, float value) const
{
    glUniform1f(glGetUniformLocation(ID, name.c_str()), value);
}

void Shader::checkCompileErrors(unsigned int shader, std::string type)
{
    int success;
    char infoLog[1024];
    if (type != "PROGRAM")
    {
        glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
        if (!success)
        {
            glGetShaderInfoLog(shader, 1024, NULL, infoLog);
            std::cout << "ERROR::SHADER_COMPILATION_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
        }
    }
    else
    {
        glGetProgramiv(shader, GL_LINK_STATUS, &success);
        if (!success)
        {
            glGetProgramInfoLog(shader, 1024, NULL, infoLog);
            std::cout << "ERROR::PROGRAM_LINKING_ERROR of type: " << type << "\n" << infoLog << "\n -- --------------------------------------------------- -- " << std::endl;
        }
    }
}

 

shader.fs (속성> 항목형식 > 텍스트로 변경)

#version 330 core
out vec4 FragColor;

in vec3 ourColor;

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

shader.vs (속성> 항목형식 > 텍스트로 변경)

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

out vec3 ourColor;

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor;
}

 

이번 포스팅에서는 OpenGL Shader의 특징과 관련 예시를 살펴봤습니다.

 

유용하고 가치있는 정보를 드리려고 노력합니다.
많은 응원 부탁드리며, 피드백은 언제나 환영입니다 :)