Computer Graphics/OpenGL

OpenGL 정리 - 22. GPU 데이터 관리 기법, UBO, Geometry Shader

surkim 2024. 10. 30. 11:41

1. OpenGL에서 VBO와 IBO 데이터 관리

1.1 VBO 데이터 관리 기법

  • glBufferData(): VBO에 데이터를 설정하는 기본 함수로, 메모리를 할당하고 데이터를 복사한다. nullptr을 인자로 넣으면 메모리 할당만 이루어지고 데이터 복사는 이루어지지 않는다. 이는 필요에 따라 이후 별도의 데이터 전송을 위해 미리 메모리를 할당할 때 유용하다.
  • glBufferSubData(): 일부 데이터만 갱신하고 싶을 때 사용하는 함수로, 기존 메모리의 특정 위치부터 필요한 데이터 크기만큼 복사하여 성능을 최적화할 수 있다. 예를 들어, 매 프레임마다 전체 데이터가 아닌 변경된 부분만 업데이트할 때 사용할 수 있다.
  • glMapBuffer(), glUnmapBuffer(): 데이터 포인터를 통해 버퍼 메모리에 직접 접근할 수 있게 해주는 함수로, glMapBuffer()로 메모리 포인터를 가져와 memcpy()를 사용해 데이터를 복사한 후, glUnmapBuffer()로 접근을 종료한다. 버퍼를 직접 수정할 때 유용하다.
  • glCopyBufferSubData(): 한 버퍼의 데이터를 다른 버퍼로 복사하는 함수로, glBindBuffer와 함께 GL_COPY_READ_BUFFERGL_COPY_WRITE_BUFFER의 두 버퍼를 설정하여 사용한다. 메모리 복사 시 성능을 최적화할 수 있다.

1.2 텍스처 데이터 갱신

  • glTexSubImage2D(): 이미 GPU 메모리가 할당된 2D 텍스처에 새로운 CPU 데이터를 업데이트하고 싶을 때 사용하는 함수이다. 특정 위치와 크기의 영역에 새로운 이미지를 복사하여 부분 갱신할 수 있어, 예를 들어 UI 요소나 애니메이션에서 특정 부분만 갱신할 때 효율적으로 사용할 수 있다.

2. Advanced GLSL - 내장 변수와 인터페이스 블록 활용

2.1 내장 변수 (Built-in Variables)

OpenGL의 GLSL에는 미리 정의된 여러 내장 변수들이 있어, 쉐이더 간의 데이터 전달이나 특정 기능을 구현하는 데 사용할 수 있다.

  • gl_Position: Vertex Shader의 결과물로, 정점의 위치 좌표를 저장한다.
  • gl_PointSize: 점을 그릴 때 점의 크기를 지정하는 변수로, GL_POINTS와 함께 활성화하여 사용한다.
  • gl_VertexID: 현재 쉐이더에서 처리 중인 정점의 인덱스. 인덱스를 기반으로 다양한 효과를 추가할 수 있다.
  • gl_FragCoord: 현재 프래그먼트(픽셀)의 화면상 위치를 나타내는 변수로, 화면 좌표에 따라 색상을 변화시키는 등의 효과에 활용된다.
  • gl_FrontFacing: 프래그먼트가 앞면인지 뒷면인지를 알 수 있는 변수로, 앞면과 뒷면을 다르게 렌더링할 때 유용하다.
  • gl_FragDepth: 픽셀의 깊이값을 설정할 수 있는 변수로, 수동으로 깊이값을 조정할 수 있다.

2.2 Interface Block을 통한 변수 그룹화

쉐이더의 Vertex Shader와 Fragment Shader 간의 데이터 전송 시 변수 연결이 일관되게 관리되어야 하는데, Interface Block을 사용하면 두 쉐이더 간에 일관된 변수 그룹을 선언하고 사용할 수 있다. 마치 구조체처럼 변수를 묶어 관리할 수 있으며, 코드의 가독성과 관리 효율성을 높인다.

Interface Block 예제 코드

#version 330 core // vs

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aTexCoords;

out VS_OUT {  // Vertex Shader에서 인터페이스 블록 정의
    vec2 TexCoords;
} vs_out;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
    vs_out.TexCoords = aTexCoords;  // 인터페이스 블록을 통해 텍스처 좌표를 전달
}


#version 330 core // fs

in VS_OUT {  // Fragment Shader에서 동일한 인터페이스 블록으로 변수 받기
    vec2 TexCoords;
} fs_in;

out vec4 FragColor;

uniform sampler2D texture;

void main() {
    FragColor = texture(texture, fs_in.TexCoords);  // 인터페이스 블록을 통해 받은 텍스처 좌표 사용
}

3. Uniform Buffer Object (UBO)

3.1 UBO란?

UBO(Uniform Buffer Object)는 OpenGL에서 여러 쉐이더 프로그램들이 공통 변수를 한 번에 공유할 수 있도록 해주는 버퍼다. 예를 들어, model, view, projection과 같은 변환 행렬은 여러 쉐이더에서 공통으로 사용되기 때문에 이를 UBO로 설정해 각 쉐이더마다 중복 설정할 필요 없이 공유할 수 있다.

3.2 UBO 생성 및 사용 방법

  1. UBO 생성 및 크기 설정: glGenBuffers로 UBO를 생성하고 사용할 데이터 크기를 glBufferData로 지정한다.
  2. UBO에 데이터 설정: 필요한 데이터를 glBufferSubData를 통해 입력한다.
  3. Shader에서 UBO 연결: 쉐이더 코드에서 Uniform Block을 정의하고 이를 UBO와 연결해 사용한다.
UBO 예제 코드

아래 예제는 Matrices라는 UBO를 생성하고, ProjectionView 행렬을 저장해 여러 쉐이더가 이를 공통으로 사용할 수 있도록 설정한 코드다.

// 1. UBO 생성 및 크기 설정
GLuint uboMatrices;
glGenBuffers(1, &uboMatrices);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), nullptr, GL_STATIC_DRAW);

// 2. UBO에 Projection, View 행렬 데이터 설정
glm::mat4 projection = glm::perspective(glm::radians(45.0f), aspectRatio, 0.1f, 100.0f);
glm::mat4 view = glm::lookAt(cameraPos, cameraTarget, cameraUp);

glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);

// 3. UBO를 바인딩하여 모든 쉐이더가 동일하게 사용할 수 있게 설정
glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboMatrices);
Shader (Uniform Block 정의 및 사용)
#version 330 core

layout(location = 0) in vec3 aPos;
layout(std140) uniform Matrices {
    mat4 projection;
    mat4 view;
};

uniform mat4 model;

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}

위 코드를 통해 모든 쉐이더가 동일한 Matrices UBO를 참조하며, 공통된 projectionview 행렬을 사용할 수 있다.


4. Geometry Shader 소개 및 사용 사례

4.1 Geometry Shader란?

Geometry Shader는 Vertex Shader와 Fragment Shader 사이에 위치하며, 특정 도형을 입력받아 새로운 도형으로 변환하거나 수정할 수 있는 쉐이더다. 예를 들어, 점을 선으로 연결하거나, 삼각형을 여러 조각으로 분해해 폭발 효과를 줄 수 있다.

4.2 Geometry Shader 설정 과정

  1. 입력 도형과 출력 도형 지정: Geometry Shader에서 입력받는 도형과 출력할 도형을 지정한다.
  2. EmitVertex()EndPrimitive() 사용: EmitVertex()는 현재 위치에 정점을 생성하고, EndPrimitive()는 생성된 정점들을 하나의 도형으로 묶어준다.
Geometry Shader 예제 코드

아래는 점(Point primitive)을 입력받아 선(line strip primitive)으로 변환하는 코드로, 점이 선으로 연결되는 효과를 구현한다.

#version 330 core

layout(points) in;                     
layout(line_strip, max_vertices = 2) out;

void main() {
    vec4 left = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
    vec4 right = gl_in[0].gl_Position + vec4(0.1, 0.0, 0.0, 0.0);

    gl_Position = left;
    EmitVertex();  

    gl_Position = right;
    EmitVertex();  

    EndPrimitive(); 
}

이 예제에서 EmitVertex()는 각 정점을 출력하고, EndPrimitive()는 현재의 도형을 마무리 짓는다. 이를 통해 점을 선으로 변환하여 다양한 그래픽 효과를 줄 수 있다.

4.3 Geometry Shader 활용 예시

  • Object Exploding: Geometry Shader를 통해 삼각형의 각 정점을 중심에서 바깥으로 확장시키면, 폭발하는 듯한 효과를 줄 수 있다.
  • Normal Vector 시각화: 각 삼각형의 법선 벡터를 따라 작은 선을 생성하여 물체 표면의 방향성을 시각화할 수 있다.