[OpenGL ES] 셰이더 다루기

Park Sejin·2022년 9월 3일
0
post-thumbnail

이번 장에서는 정말로 윈도우에 삼각형을 그려볼 것입니다. 이전 장의 렌더링 파이프라인에서 버텍스 셰이더와 프래그먼트 셰이더는 프로그래밍이 가능하다고 했습니다. 바로 이 셰이더들을 활용하여 윈도우에 삼각형을 그릴 것입니다. 물론 원래는 버텍스 셰이더의 이전 단계인 버텍스 버퍼, 버텍스 어레이 단계에서 삼각형의 좌표나 색상 정보들을 설정하여 렌더링 파이프라인에 보낸 다음, 그 정보를 버텍스 셰이더가 넘겨 받아 필요한 작업을 수행하는 것이 맞습니다. 하지만 버텍스 버퍼, 버텍스 어레이 단계를 잠시 건너뛰고 직접 버텍스 셰이더에 삼각형의 좌표를 설정하여 보다 간단하게 삼각형을 그려보겠습니다.

셰이더 생성

먼저 셰이더를 다루기 위해서 Shader 클래스를 만듭니다.
Shader 클래스의 생성자는 파라미터로 셰이더의 경로를 받습니다. 그리고 생성자에서 createGraphicsPipeline 멤버함수를 호출합니다. createGraphicsPipeline 멤버함수에서 셰이더를 사용하기 위해 필요한 모든 작업을 수행합니다. 그 중에서 셰이더를 생성하는 부분에 대해서 살펴보겠습니다. 셰이더 경로를 파라미터로 넣고 createShader 함수를 호출하면 아래의 코드가 실행됩니다.

코드

// SCOP/Base/src/Shader.cpp

...

GLuint Shader::createShader(GLenum shaderType, const std::string &source) {
    // 셰이더 생성 
    GLuint shader = glCreateShader(shaderType);
    if (!shader) {
        spdlog::error("Fail to create a shader.");
        throw std::runtime_error("Fail to create a shader.");
    }

    const char *data = source.data();
    // 셰이더에 소스 코드 추가
    GL_TEST(glShaderSource(shader, 1, &data, nullptr));
    // 셰이더 컴파일
    GL_TEST(glCompileShader(shader));

    GLint status;
    GL_TEST(glGetShaderiv(shader, GL_COMPILE_STATUS, &status));

    if (!status) {
        GLint length;
        GL_TEST(glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length));

        if (length) {
            std::string log(length, '\n');
            GL_TEST(glGetShaderInfoLog(shader, length, nullptr, log.data()));
            spdlog::error("{}.", log);
        }

        GL_TEST(glDeleteShader(shader));
        throw std::runtime_error("Fail to create a shader.");
    }
    return shader;
}

...

셰이더를 사용하기 위해서는 다음 그림과 같은 순서의 작업이 필요합니다.

shader1

  1. 셰이더를 생성하기 위해서는 glCreateShader 함수를 호출합니다.
  2. 생성된 셰이더에 소스 코드를 추가하기 위해서 glShaderSource 함수를 호출합니다.
  3. 셰이더를 컴파일하기 위해서 glCompileShader 함수를 호출합니다.

프로그램 생성

shader2

셰이더를 생성하여 바로 사용할 수 있는 것이 아닙니다. 셰이더를 어태치할 프로그램이 필요합니다. 프로그램은 셰이더를 어태치할 수 있는 객체입니다. 위의 그림과 같은 순서의 작업이 필요합니다.

  1. 프로그램을 생성하기 위해서 glCreateProgram 함수를 호출합니다.
  2. glAttachShader 함수를 호출하여 프로그램에 셰이더를 어태치시킵니다.
    • 셰이더를 어태치시킬 시점에서 셰이더가 컴파일되어 있지 않아도 됩니다.
  3. glLinkProgram 함수를 호출하여 어플리케이션에 프로그램을 링킹시킵니다.
    • 이 단계를 수행하기 위해서는 셰이더가 반드시 컴파일 되어있어야 합니다.
  4. glUseProgram 함수를 호출하여 링크한 프로그램을 실제로 로드하고 사용합니다.
    • 원하는만큼 프로그램을 링크하고 사용할 준비를 할 수 있지만, 하나의 프로그램만 활성화할 수 있습니다.

코드

// SCOP/Base/src/Shader.cpp

...

GLuint Shader::createGraphicsPipeline(const std::array<std::filesystem::path, 2> &paths) {
    // 프로그램 생성
    GLuint program = glCreateProgram();
    if (!program) {
        spdlog::error("Fail to create a program.");
        throw std::runtime_error("Fail to create a program.");
    }

    const GLuint shaders[] = {
        createShader(paths[0]),
        createShader(paths[1])
    };

    // 셰이더 어태치
    GL_TEST(glAttachShader(program, shaders[0]));
    GL_TEST(glAttachShader(program, shaders[1]));
    // 프로그램 링킹
    GL_TEST(glLinkProgram(program));

    GLint status;
    GL_TEST(glGetProgramiv(program, GL_LINK_STATUS, &status));

    if (!status) {
        GLint length;
        GL_TEST(glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length));

        if (length) {
            std::string log(length, '\n');
            GL_TEST(glGetProgramInfoLog(program, length, nullptr, log.data()));
            spdlog::error("Err to link a program.\n{}.", log);
        }

        GL_TEST(glDeleteProgram(program));
        throw std::runtime_error("Fail to create a program.");
    }

    // 셰이더 파괴
    GL_TEST(glDeleteShader(shaders[0]));
    GL_TEST(glDeleteShader(shaders[1]));

    return program;
}

링크 이후에 셰이더의 코드가 수정될 경우, 수정 사항을 적용하려면 프로그램을 다시 링크해야 합니다.

셰이더 작성

버텍스 셰이더

vertexshader

버텍스 셰이더는 각 버텍스에서 실행됩니다. 그리고 버텍스 셰이더는 입력 버텍스 당 하나의 출력 버텍스를 내보냅니다. 위 그림과 같이 입력 버텍스는 사용자가 정의한 입력 속성(input attribute) 를 여러개 가질 수 있습니다. 입력 속성에는 위치, 법선 벡터, 텍스쳐 좌표 등이 있습니다. 위 그림에는 없지만 gl_VertexID 라는 입력 속성이 존재합니다. gl_VertexID 는 현재 버텍스의 인덱스를 저장하고 있습니다. 또한 버텍스 셰이더는 uniform 변수 에 접근할 수 있습니다. uniform 변수 는 모든 버텍스에서 읽기 전용 전역변수처럼 동작합니다.
버텍스 셰이더에서 입력속성들에 대한 계산을 수행한 후 출력 속성으로 버텍스를 출력합니다. 버텍스 셰이더에는 gl_Positiongl_Pointsize 가 빌트인 출력 속성으로 정의 되어있습니다. 일반적으로 gl_Position 변수를 사용합니다. 버텍스 셰이더가 실행되면 로컬 좌표계의 버텍스가 클립 좌표계의 버텍스 동차좌표로 좌표 변환되어 gl_Position 에 저장됩니다. 또한 여러 개의 사용자 정의 출력 속성을 사용할 수 있습니다.

// glsl 3.0 버전을 사용합니다.
#version 300 es

// 사용자 정의 출력 속성 정의합니다.
out vec3 vert_color;

// c언어와 유사한 방식으로 구조체를 선언할 수도 있습니다.
struct Vertex {
    vec3 position;
    vec3 color;
};

// 버텍스 셰이더의 출력은 main함수에서 계산됩니다.
void main() {
    // Vertex 타입을 원소로 가지는 배열을 선언합니다.
    Vertex vertices[3];
    // 삼각형의 각 버텍스의 좌표와 색상을 입력합니다.
    vertices[0] = Vertex(vec3(-0.5, -0.5, 0.0), vec3(1.0, 0.0, 0.0));
    vertices[1] = Vertex(vec3( 0.5, -0.5, 0.0), vec3(0.0, 1.0, 0.0));
    vertices[2] = Vertex(vec3( 0.0,  0.5, 0.0), vec3(0.0, 0.0, 1.0));

    // gl_Position 빌트인 출력 속성에 동차좌표를 입력합니다.
    gl_Position = vec4(vertices[gl_VertexID].position, 1.0);
    // vert_color 사용자 정의 출력 속성에 색상을 입력합니다.
    vert_color = vertices[gl_VertexID].color;
}

프레그먼트 셰이더

fragmentshader

레스터라이제이션 단계에서 레스터화된 프레그먼트가 프레그먼트 셰이더로 하나씩 입력됩니다.

#version 300 es
precision mediump float;

// 사용자 정의 입력 속성으로 색상을 받습니다.
// 픽셀의 위치에 맞게 보간된 색상을 입력받습니다.
in vec3 vert_color;

// 출력 속성으로 frag_color을 선언하고 location 0에 바인딩합니다.
layout(location = 0) out vec4 frag_color;

void main() {
    // 해당 픽셀의 색상을 출력 속성으로 입력합니다.
    frag_color = vec4(vert_color, 1.0);
}

셰이더 파괴

glDeleteShader 함수를 호출하여 셰이더를 파괴합니다.
glDeleteProgram 함수를 호출하여 프로그램을 파괴합니다.

결과

screenshot

참고

0개의 댓글