[openGL] 유니폼

나우히즈·2024년 10월 4일

Graphics

목록 보기
6/17

서론

쉐이더 스테이지에서 상수값으로 균일하게 계속 사용할 수 있는 Uniform에 대해 알아보자.

디폴트 블록에 선언하는 방식과 유니폼 블록에 선언하는 방식이 존재한다.


디폴트 블록 유니폼

쉐이더 내에 변수 선언 시, uniform 키워드를 넣어주면 유니폼을 선언할 수 있다.

디폴트 블록 유니폼(Default Block Uniforms)은 OpenGL에서 셰이더가 사용하는 변수를 관리하는 중요한 개념이다. OpenGL의 유니폼(Uniform)은 셰이더 프로그램에서 일정 기간 동안 바뀌지 않는 변수들로, CPU에서 셰이더로 데이터를 전달할 때 주로 사용되는데, 디폴트 블록은 이러한 유니폼 변수들이 특별한 구조체나 레이아웃 없이 선언된 경우를 의미한다.

1. 디폴트 블록(Default Block)

디폴트 블록은 셰이더 프로그램에서 특별한 선언 없이 선언된 유니폼 변수들이 속하는 기본적인 유니폼 블록. 즉, 특별히 유니폼 블록(Uniform Block)으로 그룹화되지 않은 유니폼 변수들은 모두 디폴트 블록에 속하게 된다.

#version 330 core

uniform mat4 model;      // 디폴트 블록에 속한 유니폼
uniform mat4 view;       // 디폴트 블록에 속한 유니폼
uniform mat4 projection; // 디폴트 블록에 속한 유니폼

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

위 GLSL 코드에서 model, view, projection은 디폴트 블록에 속한 유니폼들인 셈이다.

2. 디폴트 블록 유니폼의 특징

  • 글로벌 범위에서 선언된 유니폼 변수들은 모두 디폴트 블록에 포함.
  • 디폴트 블록에 속한 유니폼들은 각각의 유니폼 변수에 대해 따로따로 값을 설정해야한다. 즉, 블록화된 유니폼처럼 한꺼번에 데이터를 설정할 수 없다.
  • 이러한 유니폼들은 주로 glUniform* 함수로 CPU에서 값을 전달받는다.

3. 디폴트 블록의 유니폼 설정

CPU에서 디폴트 블록에 있는 유니폼 변수들을 설정할 때는 OpenGL의 glUniform* 계열 함수들을 사용한다. 그 전에 프로그램에 해당 유니폼이 존재하는지 확인을 위해 glGetUniformLocation함수를 통해 위치를 확인한다.

// 셰이더 프로그램 생성
GLuint program = glCreateProgram(); 
glUseProgram(program);

// 유니폼 변수의 위치를 얻어옴
GLint modelLoc = glGetUniformLocation(program, "model");

// 유니폼에 값 전달
glm::mat4 model = glm::mat4(1.0f); // 모델 행렬
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

4. 유니폼 블록과 디폴트 블록의 차이점

  • 디폴트 블록: 모든 유니폼 변수를 하나씩 별도로 업데이트해야함. 각 유니폼은 개별적으로 관리됨.
  • 유니폼 블록: 여러 유니폼을 하나의 블록으로 그룹화. 유니폼 버퍼 객체(UBO, Uniform Buffer Object)를 사용하여 한 번에 많은 유니폼 데이터를 GPU로 전송할 수 있어 효율적.

따라서, 유니폼이 많을 경우 glUniform* 함수로 각각의 유니폼을 매번 업데이트하는 것은 비효율적이다.

그래서 우리는 유니폼 블록을 활용하여 유니폼의 업데이트를 진행하게된다.


유니폼 블록

1. 유니폼 블록이란?

유니폼 블록은 여러 유니폼 변수를 그룹화한 것으로, 이를 통해 OpenGL에서 데이터를 보다 효율적으로 처리할 수 있습니다. 유니폼 블록은 유니폼 버퍼 객체(UBO, Uniform Buffer Object)와 연계하여 사용되며, 한 번에 많은 데이터를 GPU로 전달할 수 있습니다.

예를 들어, 모델, 뷰, 투영 행렬을 유니폼 블록으로 그룹화할 수 있습니다.

2. 유니폼 블록 선언

셰이더 코드에서 유니폼 블록은 layout 키워드를 사용하여 선언됩니다. 예를 들어:

#version 330 core

layout (std140) uniform Matrices {
    mat4 model;
    mat4 view;
    mat4 projection;
};

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

위 코드에서 Matrices라는 이름의 유니폼 블록이 정의되었고, 이 블록에는 model, view, projection이라는 세 개의 유니폼 변수가 포함됨.

3. std140 레이아웃

유니폼 블록을 선언할 때 layout (std140)을 사용한다. std140유니폼 변수가 GPU 메모리에서 어떻게 배치될지 정의하는 표준 레이아웃이다. std140 레이아웃을 사용하면 CPU에서 유니폼 데이터를 정확히 메모리에 맞춰 보낼 수 있다.

4. UBO (Uniform Buffer Object) 사용

유니폼 블록을 사용하기 위해서는 유니폼 버퍼 객체(UBO)를 생성하고 데이터를 전송해야함

  1. UBO 생성:

    GLuint ubo;
    glGenBuffers(1, &ubo);
    glBindBuffer(GL_UNIFORM_BUFFER, ubo);
    glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * 16 * 3, nullptr, GL_STATIC_DRAW);
  2. 셰이더 프로그램에 UBO 바인딩: 셰이더 프로그램에서 유니폼 블록을 바인딩합니다.

    GLuint blockIndex = glGetUniformBlockIndex(shaderProgram, "Matrices");
    glUniformBlockBinding(shaderProgram, blockIndex, 0);
  3. UBO에 데이터 전송: CPU에서 UBO로 데이터를 전송합니다.

    glm::mat4 model = glm::mat4(1.0f);
    glm::mat4 view = glm::mat4(1.0f);
    glm::mat4 projection = glm::mat4(1.0f);
    
    glBindBuffer(GL_UNIFORM_BUFFER, ubo);
    glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(model));
    glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
    glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4) * 2, sizeof(glm::mat4), glm::value_ptr(projection));
    glBindBuffer(GL_UNIFORM_BUFFER, 0);
  4. UBO를 셰이더와 연결: UBO를 특정 바인딩 포인트에 연결합니다.

    glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo);

5. 유니폼 블록의 장점

  • 여러 유니폼 변수를 하나의 블록으로 묶어 한 번에 GPU로 데이터를 전달 가능.
  • 복잡한 장면에서 유니폼 변수를 효율적으로 관리.
  • 여러 셰이더 프로그램에서 동일한 UBO를 공유하여 데이터를 재사용.
  • OpenGL은 UBO를 통해 유니폼 블록 데이터를 효율적으로 캐싱하여 성능 향상.

정리

간단한 렌더링을 진행하는 게 아니라면 UBO를 통한 유니폼 블록을 사용하게 될 것 같다. 프로젝트 규모가 커질수록 효율적인 방식이 될 것이다.

0개의 댓글