OpenGL Vertex Shader 버텍스(정점) 셰이더 [Learn OpenGL 쉬운 번역]

728x90
반응형
※ 본 포스팅은 learnopengl.com을 번역 및 가감한 포스팅이며, learnopengl에서는 번역 작업 참여를 적극 장려하고 있습니다!
아래 링크에서 원문을 확인할 수 있습니다.

 

Vertex input

화면에 그림을 그리려면 OpenGL에 vertex data부터 넘겨주어야합니다. OpenGL은 그래픽 라이브러리이기 때문에 3D 공간에서의 좌표를 다룹니다. 모든 공간을 그리는 건 아니고 x,y,z 세 축을 기준으로 -1.0~1.0 사이의 범위에 있는 좌표들을 처리하는 것입니다. 따라서 이 범위에 맞는 좌표만 그리는데 이 범위 좌표를 Normalized Device Coordinates(정규화 장치 좌표, NDC)라고 합니다. 범위를 나가면 보이지 않죠. 

 

삼각형을 렌더링하려면 세 개의 3D 위치를 가지는 정점을 지정해야합니다. 이것들을 opengl에서 볼 수 있는 좌표 영역인 normalized device coordinates로 정의해서 float 배열로 지정합니다.

float vertices[] = {
    -0.5f, -0.5f, 0.0f,
     0.5f, -0.5f, 0.0f,
     0.0f,  0.5f, 0.0f
};

OpenGL은 3D 공간에서 작업하는데, 2D 삼각형을 렌더링할 때는 z좌표를 0.0으로 설정해야 각 정점 깊이가 동일하여 2D로 보입니다. 

 

Normalized Device Coordinates (NDC)
vertex shader에서 정점 좌표를 처리한 후, 좌표는 x,y,z 값은 -1.0~1.0 사이의 값인 normalized device coordinates 공간에 위치해야합니다. NDC를 넘어가면 삭제되거나 clipping되어 화면에 띄우지 않습니다. 
일반적인 화면 좌표와 다르게 NDC에서는 y축이 위쪽이고, 그래프의 중앙이 (0,0)입니다. 최종적으로 모든 좌표는 NDC 범위 안에있어야하고, 그렇지 않으면 화면에 뿌리지 못합니다. 
glViewport를 통해서 데이터를 보내면 NCD좌표에서 screen-space coordinates 로 변환합니다. 이 변환을 viewport transform이라고 합니다. 이렇게 변환된 screen-space coordinates는 그 다음 fragment shader 입력으로 사용되기 위해 fragment로 변환됩니다. 

 

이렇게 vertex data를 정의했다면, 그래픽 파이프라인의 첫 번째 단계 vertex shader에 입력으로 보내야합니다. 그러려면 GPU 메모리를 생성해서 vertex data를 저장하고, OpenGL이 이 메모리를 읽는 방법을 정해주고, 저장한 데이터를 그래픽 카드로 전송하는 방식을 지정해야합니다. 상당히 결정해야할 것이 많습니다. 그러고 나서 vertex shader는 메모리에서 지정된 vertex data들을 처리합니다.

 

하나하나의 vertex data를 각각 메모리에 담아서 전송하기는 비효율적이니까 메모리에 한 번에 여러 vertex들을 담는 vertex buffer object VBO를 통해서 GPU에 여러 vertex 정보를 한 번에 전달합니다. 메모리가 충분하면 데이터를 쭉 유지할 수 있습니다. 참고로 CPU에서 그래픽 카드로 데이터를 전송하는게 상당히 느립니다. 그래서 한 전송 버퍼에 많은 데이터를 담는게 최적화 측면에서 유리합니다. 한 번에 buffer에 담아 그래픽카드 메모리로 옮기면 바로바로 vertex data들을 사용할 수 있겠죠. 

 

VBO와 같이 OpenGL에는 object의 종류가 더 있습니다. OpenGL의 object에는 고유 ID가 있고 glGenBuffers 함수로 ID를 생성합니다.

unsigned int VBO;
glGenBuffers(1, &VBO);

VBO의 buffer type은 GL_ARRAY_BUFFER입니다. OpenGL에서는 다양한 type의 buffer를 binding할 수 있는데, glBindBuffer를 사용해서 VBO에 binding하겠다고 설정합니다.

 

glBindBuffer(GL_ARRAY_BUFFER, VBO);

이제 buffer에 대한 함수는 binding 되어있는 VBO에 대해서 처리합니다. 그러니까 glBufferData 함수로 data를 VBO의 buffer에 복사해 넣게 됩니다. 

glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

개발자가 정의한 vertex data를 binding 되어있는 buffer로 복사하는 glBufferData의 첫 번째 인자는 복사하는 buffer의 유형입니다. VBO의 buffer type인 GL_ARRAY_BUFFER죠. 두 번째 인자는 buffer에 들어갈 data size입니다. 단위는 byte이기 때문에 sizeof 함수를 쓰면 됩니다. 세 번째 인자는 실제 data입니다. 

 

네 번째 인자는 좀 어려운데 그래픽 카드가 데이터를 관리하는 방법을 설정합니다. 

 

  • GL_STREAM_DRAW: 데이터가 고정으로 설정되고, GPU에 의한 최대 사용 횟수 한계가 있습니다.
  • GL_STATIC_DRAW: 데이터가 고정으로 설정되고, 여러 번 사용됩니다.
  • GL_DYNAMIC_DRAW: 데이터가 변경 가능으로 설정되고, 여러 번 사용됩니다.

buffer의 r/w 권한 설정이라고도 볼 수 있겠는데, 이걸 실제로 알맞게 사용하기는 쉽지 않습니다. 삼각형의 경우는 고정된 데이터를 여러 번 읽으므로 GL_STATIC_DRAW 정도가 되겠네요. 데이터가 자주 변경되면 dynamic으로 설정하는 등 상황에 맞게 대처하면 됩니다.

이렇게 구분하는 이유는 그래픽 카드의 메모리의 특성이 각 다르기 때문에 용도에 맞는 메모리에다가 저장하기 때문입니다.

 

여기까지 vertex data를 VBO에 저장해서 그래픽카드 메모리에 전달했습니다. 다음은 데이터를 처리하는 vertex shader를 작성하고 그 후 fragment shader를 작성하게 될 겁니다. 

 

Vertex shader

vertex shader는 프로그래밍 가능한 셰이더였죠. Opengl에서는 렌더링을 위해 최소 vertex, fragment shader를 설정해야한다고 했습니다. 이번에는 shader 코드를 작성해봅시다.

 

셰이더 언어는 opengl에서 GLSL(OpenGL Shading Language)를 사용합니다. vertex shader를 작성하면 shader를 컴파일해서 애플리케이션에서 사용가능한 형태로 만들어야합니다. 

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

void main()
{
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
}

 

GLSL은 C언어와 유사합니다. 코드는 먼저 버전 선언으로 시작하는데, opengl 3.3 이후부터 opengl 버전과 GLSL버전 번호는 같다고 합니다. opengl 3.3에서 glsl 330 core를 이용하고 4.2에서 420 버전을 사용합니다. 

 

in 키워드는 vertex shader의 input vertex attribute를 말합니다. 현재는 position 데이터만 가져오고 있습니다. vec3는 float 세개로 이루어진 vector type입니다. vector type은 가진 float 변수의 개수별로 vec1 vec2 vec3 vec4 네 가지가 있습니다. 그럼 코드에서는 aPos라는 이름의 vec3 타입 변수를 만든 겁니다.

layout (location = 0)이라는 것은 입력 변수의 위치를 설정하는 것인데 추후 설명합니다. 

 

Vector
그래픽스에서는 위치나 방향을 vector라는 수학적 개념으로 표현합니다. GLSL에서는 vec,x, vec.y, vec.z, vec.w의 4개의 값을 가지고 공간을 표현합니다. w는 위치 값은 아니고 homogeneous 라는 개념을 알아야 이해할 수 있습니다. perspective division에서 사용되는데, 이후 다루게 될 것입니다.

 

vertex shader에서는 position 데이터를 gl_Position 이라는 변수에 할당해야 출력할 수 있습니다. gl_Position은 vec4 타입이고, main함수가 끝나면 gl_Position의 값이 shader 출력 값이 됩니다. aPos는 vec3라서 vec4로 변환하기 위해 vec4 생성자로 값을 넣고 w에는 1.0이라는 값을 넣었습니다. (왜 1.0을 넣는지는 나중에)

 

지금은 vertex shader에서 받은 position을 그대로 출력만 합니다. 지금 입력한 데이터는 정규화 좌표인 NDC 범위 안에 있지 않습니다. 그래서 프로그램에서 영역 내 좌표로 변환해야합니다. 

 

Compiling a shader

vertex shader 코드를 가져와서 const 문자열로 저장해놓습니다.

const char *vertexShaderSource = "#version 330 core\n"
    "layout (location = 0) in vec3 aPos;\n"
    "void main()\n"
    "{\n"
    "   gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);\n"
    "}\0";

opengl에서 shader를 사용하기 위해 런타임에서 동적으로 컴파일을 해야됩니다. 먼저 shader object를 생성합시다. 

glCreateShader를 이용해서 shader를 생성하고 이걸 unsigned int 형식으로 shader ID를 저장합니다.

unsigned int vertexShader;
vertexShader = glCreateShader(GL_VERTEX_SHADER);

shader 생성시 shader type을 지정해주는데, GL_VERTEX_SHADER 즉 vertex shader를 생성하라고 전달해줍니다.

그 다음 shader 코드를 shader object에 할당해서 컴파일합니다.

glShaderSource(vertexShader, 1, &vertexShaderSource, NULL);
glCompileShader(vertexShader);

glShaderSource로 shader object에 코드를 할당합니다. 첫 번째 인수는 shader object의 ID, 두 번째로 전달할 문자열 개수(지금은 코드 하나이므로 1입니다), 세 번째로 소스 코드 문자열, 네 번째는 사용하지 않고 NULL로 합니다.

 

glCompileShader를 호출해서 컴파일이 성공했는지 보고 오류가 생기면 수정해야합니다. 컴파일 오류는 따로 확인하는 방법이 있습니다.
int  success;
char infoLog[512];
glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success);​

glGetShaderiv함수를 호출하면 성공 여부를 int로 담고, 오류 로그를 문자열 배열에 담습니다. 실패했는지 알려면 glGetShaderInfoLog로 log를 출력해야합니다.

if(!success)
{
    glGetShaderInfoLog(vertexShader, 512, NULL, infoLog);
    std::cout << "ERROR::SHADER::VERTEX::COMPILATION_FAILED\n" << infoLog << std::endl;
}

에러가 없으면 컴파일 성공입니다! 

다음 포스팅에서 fragment shader를 알아봅시다. 

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