쉐이더 스테이지에서 상수값으로 균일하게 계속 사용할 수 있는 Uniform에 대해 알아보자.
디폴트 블록에 선언하는 방식과 유니폼 블록에 선언하는 방식이 존재한다.
쉐이더 내에 변수 선언 시, uniform 키워드를 넣어주면 유니폼을 선언할 수 있다.
디폴트 블록 유니폼(Default Block Uniforms)은 OpenGL에서 셰이더가 사용하는 변수를 관리하는 중요한 개념이다. OpenGL의 유니폼(Uniform)은 셰이더 프로그램에서 일정 기간 동안 바뀌지 않는 변수들로, CPU에서 셰이더로 데이터를 전달할 때 주로 사용되는데, 디폴트 블록은 이러한 유니폼 변수들이 특별한 구조체나 레이아웃 없이 선언된 경우를 의미한다.
디폴트 블록은 셰이더 프로그램에서 특별한 선언 없이 선언된 유니폼 변수들이 속하는 기본적인 유니폼 블록. 즉, 특별히 유니폼 블록(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은 디폴트 블록에 속한 유니폼들인 셈이다.
glUniform* 함수로 CPU에서 값을 전달받는다.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));
따라서, 유니폼이 많을 경우 glUniform* 함수로 각각의 유니폼을 매번 업데이트하는 것은 비효율적이다.
그래서 우리는 유니폼 블록을 활용하여 유니폼의 업데이트를 진행하게된다.
유니폼 블록은 여러 유니폼 변수를 그룹화한 것으로, 이를 통해 OpenGL에서 데이터를 보다 효율적으로 처리할 수 있습니다. 유니폼 블록은 유니폼 버퍼 객체(UBO, Uniform Buffer Object)와 연계하여 사용되며, 한 번에 많은 데이터를 GPU로 전달할 수 있습니다.
예를 들어, 모델, 뷰, 투영 행렬을 유니폼 블록으로 그룹화할 수 있습니다.
셰이더 코드에서 유니폼 블록은 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이라는 세 개의 유니폼 변수가 포함됨.
유니폼 블록을 선언할 때 layout (std140)을 사용한다. std140은 유니폼 변수가 GPU 메모리에서 어떻게 배치될지 정의하는 표준 레이아웃이다. std140 레이아웃을 사용하면 CPU에서 유니폼 데이터를 정확히 메모리에 맞춰 보낼 수 있다.
유니폼 블록을 사용하기 위해서는 유니폼 버퍼 객체(UBO)를 생성하고 데이터를 전송해야함
UBO 생성:
GLuint ubo;
glGenBuffers(1, &ubo);
glBindBuffer(GL_UNIFORM_BUFFER, ubo);
glBufferData(GL_UNIFORM_BUFFER, sizeof(float) * 16 * 3, nullptr, GL_STATIC_DRAW);
셰이더 프로그램에 UBO 바인딩: 셰이더 프로그램에서 유니폼 블록을 바인딩합니다.
GLuint blockIndex = glGetUniformBlockIndex(shaderProgram, "Matrices");
glUniformBlockBinding(shaderProgram, blockIndex, 0);
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);
UBO를 셰이더와 연결: UBO를 특정 바인딩 포인트에 연결합니다.
glBindBufferBase(GL_UNIFORM_BUFFER, 0, ubo);
간단한 렌더링을 진행하는 게 아니라면 UBO를 통한 유니폼 블록을 사용하게 될 것 같다. 프로젝트 규모가 커질수록 효율적인 방식이 될 것이다.