Notice
Recent Posts
Recent Comments
Link
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
Archives
Today
Total
관리 메뉴

게임공장

[Learn OpenGL 번역] 2-5. 시작하기 - Shaders 본문

OpenGL

[Learn OpenGL 번역] 2-5. 시작하기 - Shaders

짱승_ 2018. 7. 15. 19:57

Shaders

시작하기/Shaders

  Hello Triangle 강좌에서 언급했듯이 shader는 GPU에서 동작하는 작은 프로그램입니다. 이 프로그램들은 그래픽 파이프라인의 특정 부분을 각자 맡아서 실행합니다. 기본적인 의미에서 shader는 입력값을 출력값으로 변환시키는 프로그램 그 이상도 이하도 아닙니다. 또한 shader는 아주 독립적인 프로그램이기 때문에 서로 의사소통할 수 없습니다. 유일한 의사소통은 그들의 입력값과 출력값을 통해서 하는 것뿐입니다.


  이전의 강좌에서 shader를 간단히 알아보았고 그들을 적절히 사용하는 방법을 알아보았습니다. 우리는 이제 shader에 대해 설명하고 특히 OpenGL Shading Language에 대해서 보다 일반적인 방식으로 설명할 것입니다.

GLSL

  Shader는 C언어와 비슷하기 생긴 GLSL로 작성됩니다. GLSL은 그래픽과 함께 쓰일 수 있도록 만들어졌고 특히 vector와 matrix를 조작하는데에 유용한 기능들을 가지고 있습니다.


  Shader는 항상 버전 선언으로부터 시작합니다. 그 다음으로는 입력 변수와 출력 변수들이 나오게 됩니다. 그리고 uniform이 나오고 그 후엔 main 함수로 감싸져 있습니다. Shader의 시작하는 지점은 main 함수부터입니다. 여기에서 모든 입력 변수를 처리하고 출력 변수로 결과를 출력합니다. 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()
{
  // 입력 값을 처리하고 그래픽 작업을 합니다.
  ...
  // 처리된 것을 출력 변수로 출력합니다.
  out_variable_name = weird_stuff_we_processed;
}

  Vertex shader에 대해서 이야기할 때 각각의 입력 변수는 vertex attribute라고 부릅니다. 하드웨어에 의해 제한되어 vertex attribute를 선언할 수 있는 최대 갯수가 정해져 있습니다. OpenGL은 4가지의 요소를 가진 vertex attribute를 최소 16개까지는 보장하고 있습니다. 하지만 어떠한 하드웨어는 더 많이 허용할 수 도 있습니다. 그에 대한 정보는 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은 다른 프로그래밍 언어와 마찬가지로 사용할 변수의 유형읠 지정하는 데이터 타입을 가지고 있습니다. GLSL은 C언어와 같은 언어에서 볼 수 있는 기본적인 타입들의 대부분을 가지고 있습니다(int, float, double, uint, bool). 또한 GLSL 두 가지의 컨테이너 타입도 있습니다. 이 타입들은 강좌를 진행하는 동안 아주 많이 사용될 것이며 vector, matrices가 있습니다. Matrices에 관해서는 나중의 강좌에서 다루도록 하겠습니다.

Vectors

  GLSL의 Vector는 1 ~ 4가지의 요소(언급한 모든 기본적인 타입의 요소들)를 가진 컨테이너 입니다. 다음과 같은 형식을 취합니다(n은 요소의 갯수를 나타냅니다).


  • vecn: n개의 float 타입 요소를 가지는 기본적인 vector
  • bvecn: n개의 boolean 타입 요소를 가지는 vector
  • ivecn: n개의 integer 타입 요소를 가지는 vector.
  • uvecn: n개의 unsigned integer 타입 요소를 가지는 vector
  • dvecn: n개의 double 타입 요소를 가지는 vector

  float 타입이 대부분의 작업에서 충분하기 때문에 우리는 대부분 기본적인 vecn을 사용할 것입니다.


  Vector의 요소들은 vec.x 를 통해 접근할 수 있습니다. x는 vector의 첫 번째 요소입니다. 첫 번째, 두 번째, 세 번째, 네 번째 요소들을 각각 접근하기 위해 .x, .y, .z, .w를 사용할 수 있습니다. 또한 GLSL에서 컬러는 rgba를 사용하여 나타낼 수 있고 텍스쳐 좌표는 stpq를 사용하여 나타낼 수 있습니다.


  Vector 데이터 타입으로 swizzling이라고 불리는 흥미롭고 유연한 요소 선택을 할 수 있습니다. Swizzling은 다음과 같은 문법을 따릅니다.


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

  새로운 vector(같은 타입의)를 생성하기 위해 원래의 vector가 가지고 있는 요소들로 최대 4개의 문자의 모든 조합을 사용할 수 있습니다. 예를 들어 vec2.z는 액세스할 수 없습니다. 또한 vector를 다른 vector의 생산자의 파라미터로 넘길 수 있습니다. 그렇게 하면 필요한 파라미터의 수가 적어집니다.


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

  따라서 Vactor는 모든 유형의 입출력에 사용할 수 있는 유연한 데이터타입입니다. 강좌 전체에 걸쳐서 vector를 창의적으로 관리할 수 있는 방법에 대한 많은 예제들을 볼 수 있을것입니다.

입력과 출력

  Shader는 그 자체만으로 멋진 작은 프로그램이지만 전체의 일부분이기 때문에 각각의 shader 마다 입력과 출력이 필요합니다. 그래서 우리는 그것들을 돌아가면서 옮길 수 있습니다. 그렇기에 GLSL은 inout 키워드를 정의합니다. 각 shader는 이 키워드들로 입력과 출력을 지정하고 출력 변수가 그들이 넘겨질 다음 shader의 어떤 입력 변수와 맞는지 지정할 수 있습니다. vertex shader와 fragment shader는 약간 다릅니다.


  Vertex shader는 일정한 형태의 입력을 받아야 합니다. 그렇지 않으면 효과가 없을 것입니다. vertex shader는 입력에 대해서 좀 다릅니다. vertex 데이터를 곧바로 입력으로 받습니다. vertex 데이터가 어떻게 구성되어 있는지 정의하기 위해 location 메타데이터와 함께 입력 변수를 지정함으로써 CPU에 vertex attribute를 구성할 수 있습니다. 우리는 이를 이전의 강좌에서 layout (loaction = 0) 코드를 통해 보았습니다. 따라서 vertex shader는 입력에 관한 별도의 layout 명시를 요구하므로 우리는 그것을 vertex 데이터와 연결할 수 있습니다.

  또한 layout (location = 0) 명시자를 사용하지 않고 glGetAttribLocation 함수를 통해 attribute의 location은 물어볼 수 있지만 vertex shader에 직접 설정하는 것을 권장합니다. 이는 이해하기 쉽고 다른 작업을 하지 않아도 되도록 합니다.

  또 다른 예외는 fragment shader는 최종 출력 컬러를 생성해야 하기 때문에 vec4 타입의 컬러 출력 변수를 필요로 한다는 것입니다. fragment shader에서 출력 컬러를 지정하는 것이 실패하였다면 OpenGL은 오브젝트를 검정색(혹은 하얀색)으로 렌더링할 것입니다.


  그래서 데이터를 한 shader에서 다른 shader롤 넘기고 싶다면 보내는 shader에서 출력을 선언해야하고 마찬가지로 받는 shader에서 입력을 선언해야 합니다. 양쪽의 타입과 이름이 같다면 OpenGL은 그 변수들을 연결시켜 shader 간에 데이터를 보낼 수 있도록 합니다(program 객체의 연결이 완료되면). 실제로는 어떻게 동작하는 보여주기 위해 이전의 튜토리얼의 shader를 vertex shader가 컬러를 결정할 수 있도록 수정하겠습니다.

Vertex shader

#version 330 core
layout (location = 0) in vec3 aPos; // 위치 변수는 attribute position 0을 가집니다.
  
out vec4 vertexColor; // fragment shader를 위한 컬러 출력을 지정

void main()
{
    gl_Position = vec4(aPos, 1.0); // vec4의 생성자에 vec3를 직접적으로 주었습니다.
    vertexColor = vec4(0.5, 0.0, 0.0, 1.0); // 출력 변수에 짙은 빨간색을 설정했습니다.
}
Fragment shader

#version 330 core
out vec4 FragColor;
  
in vec4 vertexColor; // Vertex Shader로 부터 받은 입력 변수(똑같은 이름, 똑같은 타입)

void main()
{
    FragColor = vertexColor;
} 

  Vertex shader에 vec4 타입의 출력 변수 vertexColor를 선언한 것을 볼 수 있습니다. 마찬가지로 fragment shader에서도 vertexColor 입력 변수를 선언하였습니다. 둘 다 이름고 타입이 같기 때문에 fragment shader의 vertexColor 변수는 vertex shader의 vertexColor 변수와 연결되었습니다. Vertex shader에서 짙은 빨간색으로 설정하였기 때문에 결과 fragment도 짙은 빨간색이 되었습니다. 밑의 이미지는 결과 화면입니다.



  이것입니다! 우리는 막 vertex shader에서 fragment shader로 데이터를 보냈습니다. 조금 더 추가하여 응용 프로그램에서 fragment shader로 컬러를 보낼 수 있는 지 알아봅시다.

Uniforms

Uniforms은 CPU위의 응용 프로그램에서 GPU위의 shader로 데이터를 전달하는 다른 방법입니다. 하지만 uniform은 vertex attribute와 비교하면 약간 어렵습니다. 우선 uniform global입니다. Global은 uniform은 shader program 객체이서 고유한 변수이고 shader program의 모든 단계의 모든 shader에서 접근 가능합니다. uniform 변수에 어떤 값을 설정하든 리셋을 하거나 업데이트를 하기 전까지 그 값을 계속 유지하고 있습니다.


  GLSL에서 uniform을 선언하기 위해 shader에 타입, 이름과 함께 uniform 키워드를 추가하기만 하면됩니다. 그 후부터 새로 선언된 uniform 변수를 사용할 수 있습니다. 이번에 uniform을 통해 삼각형의 컬러를 설정할 수 있는지 알아봅시다.


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

void main()
{
    FragColor = ourColor;
}   

  Fragment shader에 vec4 ourColor uniform을 선언했고 fragment의 출력 컬러를 uniform 값으로 설정하였습니다. uniform은 전역(Global) 변수이기 때문에 우리가 원하는 모든 shader에서 선언이 가능하므로 fragment shader에서 무엇인가를 얻기위해 다시 vertex shader로 갈 필요가 없습니다. 우리는 Vertex shader에서 이 uniform을 사용하지 않을 것이므로 vertex shader에 정의할 필요 없습니다.

  GLSL 코드에서 어떠한 곳에서 사용되지 않는 uniform을 선언했다면 컴파일러는 컴파일된 버전에서 자동으로 그 변수를 삭제하여 몇가지 오류를 일으킬 것입니다. 이것을 명심하십시오!

  현재 uniform은 비어 있습니다. 우리는 아직 uniform에 어떠한 데이터도 삽입하지 않았습니다. 이제 해봅시다. 먼저 shader에서 uniform attribute의 index/location을 찾아야 합니다. uniform의 index/location을 알아내기만 하면 값을 수정할 수 있습니다. 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 함수를 사용하여 outColor uniform의 location을 확인합니다. 이 함수에 shader program과 우리가 찾기를 원하는 uniform의 이름을 제공합니다. glGetUniformLocation 함수가 -1를 리턴한다면 location을 찾지 못한것입니다. 마지막으로 glUniform 함수를 사용하여 uniform 변수의 값을 설정할 수 있습니다. uniform location을 찾는 일은 여러분이 shader program을 사용하는 것을 요구하지 않지만 값을 수정할 때는 program을 사용하는 것을 요구 합니다(glUseProgram 함수를 호출함으로써). 현재 활성화된 shader program의 uniform의 값을 설정하기 때문입니다.

  OpenGL은 C 라이브러리가 핵심이기 때문에 타입 오버로딩을 지원하지 않습니다. 그래서 함수가 다른 유형으로 호출될 수 있는 곳마다 OpenGL은 다른 유형의 새로운 함수를 정의합니다. gLUniform 함수가 가장 좋은 예입니다. 이 함수는 특정한 접미사를 가지고 있고 이 접미사는 여러분이 설정하고 싶은 uniform의 타입을 나타냅니다. 가능한 몇 가지의 접미사는 다음과 같습니다.


  • f: 이 함수는 float 타입의 값을 원합니다.
  • i: 이 함수는 int 타입의 값을 원합니다.
  • ui: 이 함수는 unsigned int 타입의 값을 원합니다.
  • 3f: 이 함수는 3개의 float 타입의 값을 원합니다.
  • fv: 이 함수는float 타입의 vector/배열을 원합니다.
  OpenGL의 옵션을 설정할 때마다 간단히 여러분의 타입에 해당하는 오버로드된 함수를 선택하세요. 우리의 경우이는 4개의 float 타입의 값을 설정하기 원하므로 glUniform4f 함수를 통해 설정합니다(또한 fv도 사용할 수 있습니다).

  이제 uniform 변수에 값을 설정하는 방법을 알았으니 이들을 렌더링을 위해 사용할 수 있습니다. 컬러가 점점 바뀌는 것을 원한다면 이 uniform을 게임 루프가 돌때마다 수정해주면 됩니다. 그렇지 않으면 삼각형은 하나의 컬러를 유지하게 될 것입니다. 그래서 우리는 greenValue를 계산하고 루프가 돌때마다 uniform의 값을 수정합니다.


while(!glfwWindowShouldClose(window))
{
    // 입력
    processInput(window);

    // 렌더링
    // colorbuffer 비우기
    glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT);

    // shader를 활성화
    glUseProgram(shaderProgram);
  
    // uniform 컬러 수정
    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);
  
    // 버퍼 교체, IO 이벤트 처리
    glfwSwapBuffers(window);
    glfwPollEvents();
}

  이 코드는 이전 코드를 비교적 간단하게 적용한 것입니다. 이번에는 루프가 돌때마다 삼각형을 그리기 전에 uniform 값을 수정합니다. uniform 정확하게 수정하였다면 삼각형의 색이 녹색에서 검정색으로, 검정색에서 녹색으로 변하는 것을 볼 수 있습니다.



  문제가 있다면 여기에서 전체 소스 코드를 확인하세요.


  보시다시피 uniform은 루프가 돌때마다 변할 수 있는 attribute들을 세팅하는 데나 응용 프로그램과 shader 사이에서 데이터를 상호교환하는 데에 유용한 도구입니다. 하지만 각 vertex마다 색을 설정하고 싶다면 어떨까요? 이 경우에는 우리가 가진 vertex의 수만큼 많은 uniform들을 선언해야 합니다. 좀더 좋은 해법은 vertex attribute에 더 많은 데이터를 추가하는 것입니다.

Attributes 추가!

  우리는 이전 강좌에서 VBO를 채우고 vertex attribute pointer를 구성하여 VAO에 저장하는 방법을 보았습니다. 이번에는 vertex 데이터에 컬러 데이터를 추가해보겠습니다. 우리는 vertices 배열에 3개의 float 타입의 컬러 데이터를 추가할 것입니다. 삼각형의 각각의 모서리에 빨강, 초록, 파랑 색을 할당합니다.


float vertices[] = {
    // 위치              // 컬러
     0.5f, -0.5f, 0.0f,  1.0f, 0.0f, 0.0f,   // 우측 하단
    -0.5f, -0.5f, 0.0f,  0.0f, 1.0f, 0.0f,   // 좌측 하단
     0.0f,  0.5f, 0.0f,  0.0f, 0.0f, 1.0f    // 위 
};    

  이제 vertex shader에 보낼 추가 데이터가 있기 때문에 vertex attribute 입력으로 컬러 값도 받을 수 있도록 vertex shader를 조정해야 합니다. aColor attribute의 location을 layout 명시자를 이용하여 1로 설정해야합니다.


#version 330 core
layout (location = 0) in vec3 aPos;   // 위치 변수는 attribute position 0을 가집니다.
layout (location = 1) in vec3 aColor; // 컬러 변수는 attribute position 1을 가집니다.
  
out vec3 ourColor; // 컬러를 fragment shader로 출력

void main()
{
    gl_Position = vec4(aPos, 1.0);
    ourColor = aColor; // vertex data로부터 가져오 컬러 입력을 ourColor에 설정
}       

  fragment의 컬러를 위해 uniform을 쓸 필요없지만 이제 ourColor 출력 변수를 사용해야하기 때문에 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 메모리의 수정된 데이터는 다음과 같습니다.


Interleaved data of position and color within VBO to be configured wtih <function id='30'>glVertexAttribPointer</function>

  현재 layout을 알고 있다면 glVertexAttribPointer 함수를 사용하여 vertex 형식을 수정할 수 있습니다.


// 위치 attribute
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
// 컬러 attribute
glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3* sizeof(float)));
glEnableVertexAttribArray(1);

  glVertexAttribPointer 함수의 처음 몇개의 파라미터는 비교적 간단합니다. 이번에는 attribute location 1의 vertex attribute를 구성합니다. 컬러 값은 float 타입 3개의 크기를 가지고 정규화 하지 않습니다.


  우리는 이제 2개의 vertex attribute를 가지고 있기 때문에 stride 값을 다시 계산해야 합니다. 데이터 배열의 다음 attribute 값(다음 위치 vector의 x 요소)을 받기위해 float 타입 크기를 6 번 오른쪽으로 이동해야 하고 그 6개 중의 3개는 위치 값이고 나머지 3개는 컬러 값입니다. 이로인해 stride 값은 float 크기의 6배입니다(= 24 바이트).
  또한 이번에는 offset를 지정해야 합니다. 각각의 vertex에 대하여 위치 vertex attribute는 첫 번째이므로 offset을 0으로 설정합니다. color attribute는 위치 데이터 다음부터 시작하므로 offset은 3 * sizeof(float) 입니다(= 12 바이트).


  프로그램을 실행하면 다음과 같은 화면이 나와야 합니다.



  문제가 생긴다면 여기에서 전체 소스 코드를 확인하세요.


  이미지가 정확히 여러분이 원하던 것이 아닐 수도 있습니다. 우리는 오직 3가지의 컬러를 제공하고 큰 컬러 팔레트를 제공한 것이 아니기에 지금은 이렇게 보이는 것입니다. 이 모든 결과는 fragment shader의 fragment inerpolation라고 불립니다. 삼각형을 렌더링할 때 rasterization 단계는 일반적으로 원래 지정된 vertex들 보다 많은 fragment를 생성합니다. 그런 다음 rasterizer는 그들이 삼각형의 어느 부분을 맡고 있는지에 기반하여 각 fragment의 위치를 결정합니다.
  이 위치들을 기반으로 하여 fragment shader의 모든 입력 변수를 보간(interpolate)합니다. 예를 들어 위의 점이 녹색이고 아래 점이 파란색인 선이 있다고 합시다. fragment shader가 선의 70% 위치에서 실행되면 그 결과 생성되는 컬럽 입력 attribute는 녹색과 파란색의 선형 조합이 됩니다. 정확히 말하자면 30%의 파란색과 70% 녹색.


  이는 정확히 삼각형에서 일어나는 일입니다. 3개의 vertex와 3개의 컬러를 가지고 있고 fragment shader가 픽셀들 사이의 컬러들을 보간한 약 50.000개의 fragment를 포함한다고 판단합니다. 색을 잘 살펴보면 모든 것을 이해할 수 있을 것입니다. 빨간색에서 파란색으로 바뀔 때 먼저 자주색으로 변한 후 마지막으로 파란색으로 변합니다. Fragment interpolation은 fragment shader의 모든 입력 attribute에 적용됩니다.

우리만의 shader 클래스

  shader를 작성하고 컴파일하고 관리하는 것은 꽤 번거롭습니다. shader 과목의 마지막으로써 디스크에서 shader를 읽고 컴파일하고 그들을 연결한 후 오류를 확인하는 shader class 생성 하여 작업을 쉽게 만들 것입니다. 이는 지금까지 학습한 내용들을 어떻게 유용한 추상 객체에 캡슐화 할 것이지에 대한 아이디어를 제공합니다.


  학습 목적과 이식성을 위해 헤더 파일에 전체적으로 shader class를 생성할 것입니다. 필요한 include들을 추가하고 class 구조를 정의하면서 시작하겠습니다.


#ifndef SHADER_H
#define SHADER_H

#include <glad/glad.h> // 필요한 모든 OpenGL의 헤더파일을 가져오기 위해 glad를 포함합니다.
  
#include <string>
#include <fstream>
#include <sstream>
#include <iostream>
  

class Shader
{
public:
    // program ID
    unsigned int ID;
  
    // 생성자는 shader를 읽고 생성합니다.
    Shader(const GLchar* vertexPath, const GLchar* fragmentPath);
    // shader를 활성화하고 사용합니다.
    void use();
    // 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
  헤더 파일의 맨 위에 여러가지 preprocess directives를 사용했습니다. 이를 사용하여 shader 헤더를 여러 파일이 포함했더라도 아직 포함되지 않은 경우에만 이 헤더 파일을 포함하고 컴파일하도록 컴파일러에게 알립니다. 이는 연결 충돌을 예방할 수 있습니다.

  Shader class는 shader program의 ID를 가지고 있습니다. 생성자는 디스크에 간단한 텍스트 파일로 저장할 수 있는 vertex shader와 fragment shader의 소스 코드 파일의 경로를 요구합니다. 별도의 것을 추가하기 위해 여러가지 유틸리티 함수를 추가합니다. use 함수는 shader program을 활성화하고 모든 set... 함수들은 uniform location을 확인하고 값을 설정합니다.

파일 읽기

  우리는 파일의 내용을 여러가지 string 객체에 읽기 위해 C++ filestream들을 사용합니다.


Shader(const char* vertexPath, const char* fragmentPath)
{
    // 1. 파일 경로를 통해 vertex/fragment shader 소스 코드를 검색합니다.
    std::string vertexCode;
    std::string fragmentCode;
    std::ifstream vShaderFile;
    std::ifstream fShaderFile;
    // ifstream 객체들이 예외를 던질 수 있도록 합니다.
    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;
        // stream에 파일의 버퍼 내용을 읽기
        vShaderStream << vShaderFile.rdbuf();
        fShaderStream << fShaderFile.rdbuf();		
        // 파일 핸들러 닫기
        vShaderFile.close();
        fShaderFile.close();
        // stream을 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를 컴파일하고 링크해야 합니다. 디버깅할 때 유용하도록 컴파일/링킹이 실패했는지 확인하고 실패 했다면 compile-time 오류들을 출력한다는 것을 알아두세요(결국 여러분은 이 오류 로그들이 필요할 것입니다).


// 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;
}
  
// program 내부에서 shader들이 링크 완료되었다면 이제 필요 없으므로 shader들을 삭제
glDeleteShader(vertex);
glDeleteShader(fragment);

  use 함수는 간단합니다.


void use() 
{ 
    glUseProgram(ID);
}  

  모든 uniform 세팅 함수들도 마찬가지입니다.


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를 수정하세요: 해답
  2. vertex shader 에서 uniform을 통해 수평 offset을 지정하고 이 offset 값을 이용하여 삼각형을 화면의 오른쪽으로 옮기세요: 해답
  3. out 키워드를 이용하여 fragment shader로 vertex 위치를 보내고 fragment의 색을 이 vertex 위치와 같게 설정하세요(vertex의 위치조차도 삼각형을 통해 어떻게 보간되는지 보세요). 만약 여러분이 이것을 할 수 있다면 다음 질문에 대답해보세요: 왜 삼각형의 왼쪽 하단이 검은색일까요?: 해답



출처 : https://learnopengl.com, Joey de Vries

반응형