포스트

컴퓨터그래픽스 5. 그래픽스 API는 어떻게 사용하는가

컴퓨터그래픽스 5. 그래픽스 API는 어떻게 사용하는가

그래픽스 API는 어떻게 사용하는가?

보통 그래픽스 API에서 사용자가 해야할 것은 다음과 같다.

  1. 데이터 만들기 : VRAM에 데이터 공간 (Buffer) 만들기.
  2. 데이터 보내기 / 업데이트하기 : 만든 공간에 데이터 보내거나 업데이트하기
  3. Setup Parameters : 셰이더 프로그램 선택, 렌더링 옵션을 설정한다.
  4. 그리기 명령!

데이터 공간 만들고, 데이터를 보내고, 업데이트하고, 렌더링 상태를 설정하고, 그리면 된다.

만들 수 있는 데이터 공간 (Buffer)은 어떤게 있는가?

많이 사용하는 것들만 알아보자. 모든 Buffer는 1차원 Queue를 사용한다.

  1. Vertex Buffer (VBO) : Vertex의 모든 속성을 여기에다 넣는다. 가장 기본은 Vertex의 좌표고, 텍스처 좌표 (U, V)나 법선 벡터도 여기에 넣는다. 쉐이더에 몇번 변수에 몇개씩 묶어서 어떤 타입으로 보낼건지 설정해야 한다.
  2. Index Buffer (IBO) : 정점 버퍼에 있는 점의 연관 관계를 정한다. 어떤 도형을 만들건지, 몇개의 Vertex씩 묶을건지 설정해야 한다.
  3. Uniform Buffer (UBO) : 셰이더에 전체적으로 사용하는 유니폼 변수를 여기에다 넣는다. 어떤 쉐이더 변수와 연결할지 설정해야 한다.
  4. Texture Buffer (TBO) : Texture를 넣는 버퍼가 아니다. 아주 큰 데이터를 보낼 때 사용한다.
  5. Frame Buffer (FBO) : 이미 만들어져 있는 프레임 버퍼를 가져다가 설정할 수 있다. 또는 내가 프레임 버퍼를 만들 수도 있다.
  6. Render Buffer (RBO) : 몰라

Buffer Object에 데이터를 일자로 집어넣는데, 데이터 구분을 어떻게 하냐?

예를들어 (2, 5, 1) 좌표를 갖고 (0.5, 0.3) 텍스처 좌표를 갖는 점과 (3, 0, 0), (0.2, 0.3) 좌표를 갖는 점 두개가 있다고 치자. Vertex Buffer에 넣을 땐 다음과 같이 들어간다.

\[\to2, 5, 1, 0.5, 0.3, 3, 0, 0, 0.2, 0.3, \dots\]

어디서부터 어디까지가 Vertex 하나에 대한 정보고, 그중 앞에 3개가 Position 정보고, 뒤에 2개가 Texture Coordinate 정보라는 것을 사전에 정의해둬야 한다. 즉, VBO를 어떻게 해석해야 하는지 설명서를 작성해서 GPU에게 알려줘야 한다.

그 설명서를 Attribute Pointers라고 부른다. VBO의 경우 Vertex Attribute Pointers이다. OpenGL에선 glVertexAttribPointer() 함수를 사용해 설명서를 만들 수 있다. 사용법은 다음과 같다.

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
// 예시: 위치 속성(location = 0) 설명서 만들기
glVertexAttribPointer(
    0,         // 속성 번호표 (location = 0)
    3,         // 한 정점당 몇 개 요소? (vec3니까 3개: x, y, z)
    GL_FLOAT,  // 데이터 타입은 뭐냐? (float)
    GL_FALSE,  // 정규화 할거냐? (보통 float는 안 함)
    sizeof(Vertex), // 다음 정점 데이터까지 몇 바이트 건너뛰어야 하냐? (Stride)
                   // Vertex 구조체 크기 (만약 위치, 색깔, 텍스처 좌표 다 있다면)
                   // 만약 위치 데이터만 VBO에 있다면 0 또는 sizeof(float)*3
    (void*)offsetof(Vertex, Position) // 이 속성 데이터는 정점 데이터 시작부터
                                     // 몇 바이트 뒤에 있냐? (Offset)
                                     // 보통 첫 속성은 (void*)0
);

// 예시: 색깔 속성(location = 1) 설명서 만들기
glVertexAttribPointer(
    1,         // 속성 번호표 (location = 1)
    4,         // vec4니까 4개 (R, G, B, A)
    GL_FLOAT,  // 데이터 타입 (float)
    GL_FALSE,  // 정규화 안 함
    sizeof(Vertex), // Stride (위랑 같음)
    (void*)offsetof(Vertex, Color) // Offset (위치 데이터 뒤에 있음)
);

// 텍스처 좌표(location = 2)도 비슷하게...
glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex), (void*)offsetof(Vertex, TexCoords));

그럼 텍스쳐는 어떻게 GPU에 넘겨주는가?

Texture Object를 따로 만든다. 하나의 텍스처는 하나의 Texture Object에 넣는다. 그렇다면, 쉐이더는 어떻게 알고 VRAM에 있는 특정 Texture Object를 가져다 쓰는가? 결론적으론, 셰이더 내 텍스쳐 변수와 텍스처 오브젝트를 Draw하기 직전에 직접 연결해줘야 한다.

셰이더에서는 uniform sampler2D와 같이 텍스쳐 변수를 선언한다. CPU에서 텍스처 객체를 만들고 데이터를 올린다. 이후에는 그리기 직전마다 텍스처 변수와 텍스처 객체를 연결해주는 작업을 일일히 해주면 된다. 어떻게?

사용할 텍스쳐 유닛 (텍스쳐 채널이라 생각하라)을 활성화 시키고, 사용할 텍스처 객체를 연결한다. 필요한 텍스처를 모두 바인딩했다면, 셰이더의 몇번 변수는 몇번 유닛을 사용하라고 지정한다. 셰이더의 변수 인덱스는 변수 이름을 통해 가져올 수 있다. 이러면 쉐이더는 텍스처 데이터를 받아 사용할 수 있다. 이 과정을 Draw Loop에서 매번 해주면 된다.

왜 매번 해야되는가? 만약 쉐이더 프로그램 한개만 쓰고, 사용하는 텍스처도 바뀌지 않으면 위 과정을 최초 한번만 해도 되는가? 이론상으론 그렇다. 하지만 매번 Draw Loop 하기 전에 설정해주는 것을 권장한다. 보통 쉐이더 프로그램은 한개만 사용하지 않고 여러개를 사용한다. 만약 직므은 한개만 사용하더라도 나중에 여러개를 사용할 수도 있다. 그 경우 코드를 갈아 엎어야 하므로, 텍스처 유닛을 유연하게 사용하려면 (쉐이더 프로그램 활성화 -> ... -> Draw) 이 사이 과정에서 매번 수행하는 것을 권장한다.