개발 · 컴퓨터공학 / / 2022. 2. 22. 17:26

OpenGL 공부일지 - OpenGL Super Bible 그래픽스에서 중요한 파이프라인 2

728x90
반응형

Geometry Shaders

vertex stage와 tessellation stage의 다음단계인 geometry shader는 개념적으로는 마지막 shader stage이다.

primitive당 한 번 수행되고, primitive를 구성하는 모든 vertex에 대한 입력 vertex 데이터에 접근할 수 있다.

 또한 프로그래밍으로 파이프라인 내 데이터를 유일하게 조절할 수 있는 stage이다. 이는 tessellation shader도 가능하지만, patch에 대한 tessellation 레벨의 설정을 통해서만 가능하다.

geometry shader는 EmitVertex와 EndPrimitive함수를 통해 primitive assembly 및 rasterization으로 보내는 vertex를 생성할 수 있다.

geometry shader의 고유한 기능 중 하나는 파이프라인 중간에 primitive mode를 변경하는 것이다. 삼각형을 입력으로 설정하여 점이나 선으로 출력할 수 있다. 반대로 점들로부터 삼각형을 생성할 수도 있다.

 

// eg3-9.cpp
#version 450 core
layout(triangles) in;
layout(points, max_vertices = 3) out;
void main(void)
{
	int i;
	for (i = 0; i < gl_in.length(); i++)
	{
		gl_Position = gl_in[i].gl_Position;
		EmitVertex();
	}
}

위 geometry shader는 vertex를 확인가능하도록 점으로 변환하는 pass-through shader로 본다. 

첫 번째 레이아웃 지시어로 geometry shader가 삼각형을 입력으로 받고, 두 번째 레이아웃 지시어로 shader가 생성하는 점의 개수를 최대 3으로 설정한다. 

main에서는 gl_in 배열의 인덱스를 돌며 EmitVertex를 수행한다.

 삼각형을 처리하고 있기 때문에 vertex가 세 개이므로 배열의 길이는 3이다. geometry shader는 vertex shader의 출력과 유사하고, gl_Position을 vvertex의 위치를 설정하기 위해 사용하는 것도 유사하다.

 EmitVertex를 호출하면 geometry shader의 출력으로 점을 생성한다. geometry shader는 종료시 자동으로 EndPrimitive를 호출하여 직접 코드로 호출하지 않아도 된다.

 

#include <sb6.h>

class tessllatedgstri_app : public sb6::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Tessellation and Geometry Shaders";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    virtual void startup()
    {
        static const char * vs_source[] =
        {
            "#version 410 core                                                                  \n"
            "                                                                                   \n"
            "void main(void)                                                                    \n"
            "{                                                                                  \n"
            "    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),                   \n"
            "                                   vec4(-0.25, -0.25, 0.5, 1.0),                   \n"
            "                                   vec4( 0.25,  0.25, 0.5, 1.0));                  \n"
            "                                                                                   \n"
            "    gl_Position = vertices[gl_VertexID];                                           \n"
            "}                                                                                  \n"
        };

        static const char * tcs_source[] =
        {
            "#version 410 core                                                                  \n"
            "                                                                                   \n"
            "layout (vertices = 3) out;                                                         \n"
            "                                                                                   \n"
            "void main(void)                                                                    \n"
            "{                                                                                  \n"
            "    if (gl_InvocationID == 0)                                                      \n"
            "    {                                                                              \n"
            "        gl_TessLevelInner[0] = 5.0;                                                \n"
            "        gl_TessLevelOuter[0] = 5.0;                                                \n"
            "        gl_TessLevelOuter[1] = 5.0;                                                \n"
            "        gl_TessLevelOuter[2] = 5.0;                                                \n"
            "    }                                                                              \n"
            "    gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;      \n"
            "}                                                                                  \n"
        };

        static const char * tes_source[] =
        {
            "#version 410 core                                                                  \n"
            "                                                                                   \n"
            "layout (triangles, equal_spacing, cw) in;                                          \n"
            "                                                                                   \n"
            "void main(void)                                                                    \n"
            "{                                                                                  \n"
            "    gl_Position = (gl_TessCoord.x * gl_in[0].gl_Position) +                        \n"
            "                  (gl_TessCoord.y * gl_in[1].gl_Position) +                        \n"
            "                  (gl_TessCoord.z * gl_in[2].gl_Position);                         \n"
            "}                                                                                  \n"
        };

        static const char * gs_source[] =
        {
            "#version 410 core                                                                  \n"
            "                                                                                   \n"
            "layout (triangles) in;                                                             \n"
            "layout (points, max_vertices = 3) out;                                             \n"
            "                                                                                   \n"
            "void main(void)                                                                    \n"
            "{                                                                                  \n"
            "    int i;                                                                         \n"
            "                                                                                   \n"
            "    for (i = 0; i < gl_in.length(); i++)                                           \n"
            "    {                                                                              \n"
            "        gl_Position = gl_in[i].gl_Position;                                        \n"
            "        EmitVertex();                                                              \n"
            "    }                                                                              \n"
            "}                                                                                  \n"
        };

        static const char * fs_source[] =
        {
            "#version 410 core                                                 \n"
            "                                                                  \n"
            "out vec4 color;                                                   \n"
            "                                                                  \n"
            "void main(void)                                                   \n"
            "{                                                                 \n"
            "    color = vec4(0.0, 0.8, 1.0, 1.0);                             \n"
            "}                                                                 \n"
        };

        program = glCreateProgram();
        GLuint vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, 1, vs_source, NULL);
        glCompileShader(vs);

        GLuint tcs = glCreateShader(GL_TESS_CONTROL_SHADER);
        glShaderSource(tcs, 1, tcs_source, NULL);
        glCompileShader(tcs);

        GLuint tes = glCreateShader(GL_TESS_EVALUATION_SHADER);
        glShaderSource(tes, 1, tes_source, NULL);
        glCompileShader(tes);

        GLuint gs = glCreateShader(GL_GEOMETRY_SHADER);
        glShaderSource(gs, 1, gs_source, NULL);
        glCompileShader(gs);

        GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, 1, fs_source, NULL);
        glCompileShader(fs);

        glAttachShader(program, vs);
        glAttachShader(program, tcs);
        glAttachShader(program, tes);
        glAttachShader(program, gs);
        glAttachShader(program, fs);

        glLinkProgram(program);

        glDeleteShader(vs);
        glDeleteShader(tcs);
        glDeleteShader(tes);
        glDeleteShader(gs);
        glDeleteShader(fs);

        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
    }

    virtual void render(double currentTime)
    {
        static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
        glClearBufferfv(GL_COLOR, 0, green);

        glUseProgram(program);

        glPointSize(5.0f);

        glDrawArrays(GL_PATCHES, 0, 3);
    }

    virtual void shutdown()
    {
        glDeleteVertexArrays(1, &vao);
        glDeleteProgram(program);
    }

private:
    GLuint          program;
    GLuint          vao;
};

DECLARE_MAIN(tessllatedgstri_app)

 

(github 소스코드에서 해당 예제는 tessellatedgstri project이다)

 

Primitive Assembly, Clipping, and Rasterization

vertex shading, tessellation, geometry shadering 등의 파이프라인의 프론트엔드 작업이 수행된 후, 파이프라인의 fixed function이 다음 작업을 수행한다. 이때 vertex 형태로 받은 데이터를 픽셀로 변환하고, 색상을 결정한 후 화면에 출력한다. 

첫 번째 단계는 primitive assembly로 vertex들을 line과 삼각형으로 그룹화하는 일을 수행한다.

vertex들로 primitive가 구성되면 화면, 보통은 viewport에 clipping된다.

마지막으로 화면에 보이도록 primitive들은 fixed function의 서브시스템인 rasterizer로 전송된다. 이 과정은 어떤 픽셀들 위에다가 점, 선, 삼각형과 같이 primitive를 표시하는지 결정하고, 그 픽셀의 목록을 다음 staged인 fragment shading로 보낸다.

Clipping

vertex가 vertex shader 즉 파이프라인의 프론트엔드로부터 나오고 나서의 공간을 clip space라고 한다. 이는 위치를 나타내는 좌표계이다. 

 vertex, tessellation, geometry shader 등에서 gl_Position변수를 저장할 때 자료형이 vec4이었다. 이는 Homogeneous Coordinate(동차좌표)로 투영된 geometry에 사용되며, Cartesian space(직교 좌표계)보다 단순한 계산이 가능하다. homogeneous coordinate는 cartesian보다 하나의 요소가 추가되어 4개의 변수로 표현한다.

 

 프론트엔드 출력은 homogeneous로 진행되지만, clipping은 cartesian으로 처리된다. 따라서 homogeneous에서 cartesian으로 변환하기 위한 OpenGL에서의 perspective division이 수행된다. perspective division은 homogeneous 4요소를 마지막 요소 w로 나누는 작업이다. w를 1.0으로 설정하면 homogeneous space의 vertex를 cartesian으로 투영할 수 있다. 이전 포스팅과 앞 예제들은 gl_Position의 w값을 1.0으로 설정해왔기에 division 효과가 없었다.

 perspective division이후 normalized device space로 결과 위치가 나오는데, 이 영역안에 포함되는 geometry가 보이는 것이다. 이 보이는 영역 볼륨의 6면은 공간에서의 평면이고, 평면은 공간을 둘로 나누므로 평면 양쪽의 반쪽 공간을 half-space라고 한다.

 

다음 stage로 primitive를 보내기 전에 OpenGL에서 vertex들이 위에서 말한 볼륨의 어떤 평면에 놓여있는지 판단하고 clipping을 수행한다. 각 평면은 outside / inside를 가지는데 outside에 vertex가 있다면 버려진다. vertex중 일부가 outside에 있고, 일부는 inside에 있다면 특별한 처리가 필요하다.

Viewport Transformation

clipping이후 geometry의 vertex들은 x, y 값이 -1.0 ~ 1.0의 좌표값을 갖는다. z는 0.0 ~ 1.0의 값을 갖는다. 이것은 normalized device coordinates이다. 하지만 그려야하는 윈도우의 좌표는 좌하단 (0,0), 영역이 (w - 1, h - 1)인 좌표를 갖는다(w : 넓이 픽셀, h : 높이 픽셀). geometry를 윈도우로 넣기 위해서 OpenGL에서는 viewport transformation을 적용한다. 이는 scale과 offset을 이용해서 vertex를 normalized 좌표에서 윈도우 좌표로 이동시킨다.

 scale 및 offset은 viewport bound로 결정하는데 glViewport, glDepthRange함수를 호출함으로써 설정할 수 있다.

void glViewport(GLint x, GLint y, GLsizei width, GLsizei height);
void glDepthRange(GLdouble nearVal, GLdouble farVal);

변환 과정의 식은 위와 같다. 좌항의 x, y, z는 윈도우 공간에서 vertex의 좌표이고, 우항의 x, y, z는 입력된 0vertex의 normalized 좌표이다. p는 픽셀 단위의 viewport 넓이(x), 높이(y)이다. n, f는 각각 z좌표에서 near과 far 평면의 거리이다. o의 좌표는 viewport의 원점이다.

 

Culling

culling은 삼각형을 처리하기 전에 선택적으로 거쳐가는 stage이다. 삼각형이 정면 방향을 향하는지 그 반대 방향인지 결정하여 계속 그릴지 여부를 판단한다. view를 향한 방향이 정면, 반대가 후면이다. 이를 결정하기 위해서 OpenGL에서는 두 모서리의 외적으로 signed area를 계산한다.

x_w^i, y_w^i는 우니도우에서 삼각형의 i 번째 vertex 좌표이고, ⊕ 기호는 modular 연산이다. 

면적이 양의 값이면 삼각형은 정면, 음이면 후면이다. 이는 glFrontFace함수에서 dir인자를 GL_CW나 GL_CCW로 설정하여 조정할 수 있다. 이것을 winding order라고 하며 시계 혹은 반시계 방향은 vertex의 그려지는 순서이다. 기본적으로는 GL_CCW로 설정되어 반시계는 정면, 시계는 후면 방향으로 설정된다. GL_CW라면 반대일 것이다.

방향 설정에 따라 특정 방향의 삼각형을 무시할 수 있는데, glEnable의 cap 인자를 GL_CULL_FACE로 설정하여 culling을 설정하면 후면 방향의 삼각형을 그리지 않는다. 그리지 않을 삼각형의 방향을 변경하기 위해서는 glCullFace의 face인사를 GL_FRONT 혹은 GL_BACK 또는 GL_FRONT_BACK으로 설정하여 변경할 수 있다.

점이나 선은 기하적으로 면적이 없어 culling될 수 없다.

 

...

3D엔진을 다루다보면 mesh의 방향에 따라서 렌더링이 되지 않아 polygon normal 벡터와 관련된 언급이 나올 때가 있다. 그래픽 언어에서 이러한 culling의 단계에 의해서 발생하는 과정인가보다.

...

 

Rasterization

선이나 삼각형 등의 primitive가 어떤 fragment를 채울지 결정하는 작업이다. OpenGL에서는 병렬화 구현을 위해 삼각형에 대한 half-space–based 방식을 사용한다고 한다. 윈도우 좌표상에서 삼각형의 bounding box를 구하고 그 안의 fragment가 삼각형의 안쪽인지 바깥쪽인지 검사한다(...고 한다;;).

 fragment가 삼각형의 세 가장자리 모두에 대해 안쪽에 있으면 삼각형의 안쪽, 그렇지 않으면 바깥쪽이라고 간주한다.

Fragment Shaders

OpenGL의 그래픽스 파이프라인에서 마지막으로 프로그래밍 가능한 stage이다. 여기서 각 fragment의 색상을 결정하여 이후 framebuffer로 보내 최종 윈도우에 그려지도록 한다.

 rasterizer가 primitive를 처리하면, 색상이 입혀질 fragment가 fragment shader로 보내진다. 이 과정에서 상당히 많은 fragment를 생산하기 때문에 파이프라인의 작업량은 상당하다.

 

// eg3-10.cpp
#include <sb6.h>

class colorfromposition_app : public sb6::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - Simple Triangle";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    virtual void startup()
    {
        static const char* vs_source[] =
        {
            "#version 420 core                                                          \n"
            "                                                                           \n"
            "void main(void)                                                            \n"
            "{                                                                          \n"
            "    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),           \n"
            "                                   vec4(-0.25, -0.25, 0.5, 1.0),           \n"
            "                                   vec4( 0.25,  0.25, 0.5, 1.0));          \n"
            "                                                                           \n"
            "    gl_Position = vertices[gl_VertexID];                                   \n"
            "}                                                                          \n"
        };

        static const char* fs_source[] =
        {
            "#version 420 core                                                          \n"
            "                                                                           \n"
            "out vec4 color;                                                            \n"
            "                                                                           \n"
            "void main(void)                                                            \n"
            "{                                                                          \n"
            "    color = vec4(sin(gl_FragCoord.x * 0.25) * 0.5 + 0.5,                   \n"
            "                 cos(gl_FragCoord.y * 0.25) * 0.5 + 0.5,                   \n"
            "                 sin(gl_FragCoord.x * 0.15) * cos(gl_FragCoord.y * 0.1),  \n"
            "                 1.0);                                                     \n"
            "}                                                                          \n"
        };

        program = glCreateProgram();
        GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, 1, fs_source, NULL);
        glCompileShader(fs);

        GLuint vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, 1, vs_source, NULL);
        glCompileShader(vs);

        glAttachShader(program, vs);
        glAttachShader(program, fs);

        glLinkProgram(program);

        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
    }

    virtual void render(double currentTime)
    {
        static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
        glClearBufferfv(GL_COLOR, 0, green);

        glUseProgram(program);
        glDrawArrays(GL_TRIANGLES, 0, 3);
    }

    virtual void shutdown()
    {
        glDeleteVertexArrays(1, &vao);
        glDeleteProgram(program);
    }

private:
    GLuint          program;
    GLuint          vao;
};

DECLARE_MAIN(colorfromposition_app)

위 코드의 fragment shader에는 gL_FragCoord로부터 출력 색상을 구하는 쉐이더 코드가 있다. gl_FragCoord 변수는 fragment shader에서 사용하는 내장 변수 중 하나로, fragment shader에 대한 입력을 직접 정의할 수 있다. 이 입력은 rasterization 이전에 수행되는 shader에 의해 채워진다. 

(github 소스코드에서 해당 예제는 fragcolorfrompos project이다)

 

fragment shader의 입력은 다른 stage와의 다른 점이 있는데, OpenGL에서 입력값을 렌더링되는 primitive에 따라 interpolates한다.

 

// eg3-11and12.cpp
#include <sb6.h>

class colorfromposition_app : public sb6::application
{
    void init()
    {
        static const char title[] = "OpenGL SuperBible - vertex shader having output";

        sb6::application::init();

        memcpy(info.title, title, sizeof(title));
    }

    virtual void startup()
    {
        static const char* vs_source[] =
        {
            "#version 420 core                                                          \n"
            "                                                                           \n"
            "out vec4 vs_color; \n"
            "void main(void)                                                            \n"
            "{                                                                          \n"
            "    const vec4 vertices[] = vec4[](vec4( 0.25, -0.25, 0.5, 1.0),           \n"
            "                                   vec4(-0.25, -0.25, 0.5, 1.0),           \n"
            "                                   vec4( 0.25,  0.25, 0.5, 1.0));          \n"
            "    const vec4 colors[] = vec4[](vec4(1.0, 0.0, 0.0, 1.0),                 \n"
            "                                 vec4(0.0, 1.0, 0.0, 1.0),                 \n"
            "                                 vec4(0.0, 0.0, 1.0, 1.0));                \n"
            "                                                                           \n"
            "    gl_Position = vertices[gl_VertexID];                                   \n"
            "    vs_color = colors[gl_VertexID];                                        \n"
            "}                                                                          \n"
        };

        static const char* fs_source[] =
        {
            "#version 420 core                                                          \n"
            "                                                                           \n"
            "in vec4 vs_color;                                                          \n"
            "out vec4 color;                                                            \n"
            "                                                                           \n"
            "void main(void)                                                            \n"
            "{                                                                          \n"
            "    color = vs_color;                                                      \n"
            "}                                                                          \n"
        };

        program = glCreateProgram();
        GLuint fs = glCreateShader(GL_FRAGMENT_SHADER);
        glShaderSource(fs, 1, fs_source, NULL);
        glCompileShader(fs);

        GLuint vs = glCreateShader(GL_VERTEX_SHADER);
        glShaderSource(vs, 1, vs_source, NULL);
        glCompileShader(vs);

        glAttachShader(program, vs);
        glAttachShader(program, fs);

        glLinkProgram(program);

        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
    }

    virtual void render(double currentTime)
    {
        static const GLfloat green[] = { 0.0f, 0.25f, 0.0f, 1.0f };
        glClearBufferfv(GL_COLOR, 0, green);

        glUseProgram(program);
        glDrawArrays(GL_TRIANGLES, 0, 3);
    }

    virtual void shutdown()
    {
        glDeleteVertexArrays(1, &vao);
        glDeleteProgram(program);
    }

private:
    GLuint          program;
    GLuint          vao;
};

DECLARE_MAIN(colorfromposition_app)

위 코드의 vertex shader코드를 보면 두 번째 배열인 colors를 추가하였고, gl_VertexID를 이용해서 인덱스를 참조하고 있다. 해당 내용을 vs_color로 out에 저장한다.

fragment 코드를 보자.

vertex shader에서 입력한 vs_color를 받아 color로 사용하여 출력값으로 저장한다.

(github 소스코드에서 해당 예제는 fragcolorfrompos project이다)

Framebuffer Operations

 framebuffer는 OpenGL 그래픽스 파이프라인의 마지막 stage에 해당한다. 화면에 보이는 거나 색상 혹은 픽셀당 값을 저장하기 위한 추가적 메모리 영역에 해당한다. 보통 윈도우를 가리킨다. 

 윈도우 시스템에서 default framebuffer를 제공하지만, off-screen 영역에 렌더링하기 위해서는 직접 framebuffer를 제공할 수도 있다. framebuffer는 fragment shader가 어디에 written되는지 또는 그 형식에 대한 정보가 필요하다. 이러한 것들은 framebuffer object에 저장되고, 여기에 저장되지 않는 Pixel Operations 상태도 존재한다.

Pixel Operations

fragment shader가 출력을 생성한 후, 윈도우로 가기 전까지 fragment에서는 출력이 윈도우에 포함되는지 판별하는 일 등 과정이 필요하다. 

첫 번째로 사용자가 정의한 사각형에 대해 fragment를 테스트하는 scissor test가 일어난다. 

 다음으로 application에서 지정한 참조값과 stencil buffer의 내용을 비교하는 stencil test가 발생한다. stencil buffer는 각 픽셀당 하나의 값을 저장하고 원하는 방식으로 사용할 수 있다.

 stencil test 이후 다음 테스트로 depth test가 실행된다. depth test는 fragment의 z좌표와 depth buffer의 내용을 비교한다. depth buffer는 stencil buffer와 같이 메모리영역이고, 픽셀당 하나의 깊이 값을 가지는 framebuffer의 일부이다. 

 보통 depth buffer의 값은 0에서 1사이이다. 0이 가장 가까운 점, 1이 가장 먼 점을 의미한다. OpenGL에서는 depth buffer에 저장된 깊이 값과 fragment의 z좌표를 비교하여 이미 렌더링된 fragment보다 현재 fragment가 가까운지 확인하고, 기존 fragment의 값보다 작은(가까운) 경우 새 fragment를 렌더링한다.

 fragment의 색상은 framebuffer가 floating-point를 저장하는지, normalized되었는지, integer값인지에 따라 blending이나 logic operation stage로 전달된다. floating point이거나 normailzed integer값이면 blending으로 새 값을 계산하고, normalized 되지 않은 integer값이라면 logic operation을 이용하여 새 값을 계산하여 framebuffer에 저장한다.

Compute Shaders

OpenGL에서는 별도의 단일 stage 파이프라인으로 compute shader를 제공한다. 

 compute shader는 work item이라는 단일 구성 요소 작업으로 동작하고, 이 item들이 모여 local work group을 형성한다. 이 work group들의 집합이 OpenGL의 compute pipeline으로 전송되어 처리된다. compute shader는 특정 내장 변수 이외에 고정 입력이나 출력을 갖지 않고, 모든 작업은 shader에 의해 메모리에 저장되어 다음 파이프라인에서는 활용되지 않는다. 

// eg3-13.cpp
#version 450 core
layout (local_size_x = 32, local_size_y = 32) in;
void main(void)
{
// Do nothing
}

compute shader는 위와 같은 형태를 가졌다.

 위 코드는 local work group이 32*32개 work item이 됨을 표시하고, main에서는 작업하지 않는다.

 컴파일을 위해서는 GL_COMPUTE_SHADER 타입으로 shader 객체를 생성하고, GLSL코드를 glShaderSource로 attach한 후, glCompileShader로 컴파일, glAttachShader와 glLinkProgram으로 프로그램에 링크시키는 과정은 다른 shader와 비슷한 과정이다. 

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유