개발/OpenGL / / 2022. 3. 8. 20:58

OpenGL 공부일지 - OpenGL Super Bible 유니폼 - 1

반응형

Uniforms

storage는 아니지만, uniform은 쉐이더로 데이터를 전달하고 애플리케이션과 연결하는 중요한 방법이다. 

버텍스 속성을 써서 버텍스 쉐이더로 데이터를 전달하는 것, 인터페이스 블록으로 스토리지 간 데이터를 전달하는 방법을 알아보았었다. 유니폼을 통해서는 애플리케이션이 직접 쉐이더 스테이지로 데이터를 전달할 수 있다. 

선언하는 방식은 디폴트 블록에 선언하는 것과 유니폼 블록에 선언하는 두 가지 방법이 있다.

Default Block Uniforms

속성의 경우 위치, surface normal, 텍스처 좌표 등 버텍스별로 필요한 데이터인 것과는 반대로, 유니폼은 전체 프리미티브 배치를 렌더링하는 동안 또는 그 후에도 남아 데이터를 쉐이더로 전달한다. 

일반적인 유니폼은 변환 행렬이다. 변환 행렬은 버텍스 쉐이더에서 버텍스 위치나 벡터 등을 변환할 때 사용한다. 어떤 쉐이더 변수라도 유니폼으로 선언할 수 있고, 유니폼은 어떤 쉐이더 스테이지든 들어갈 수 있다.

 

uniform float fTime;
uniform int iIndex;
uniform vec4 vColorValue;
uniform mat4 mvpMatrix;

 

 

위 코드와같이 uniform이라는 키워드를 변수 앞에 붙여 유니폼을 만들 수 있다.

uniform int answer = 42;

본래 항상 상수이기 때문에 쉐이더 코드에서 할당할 수 없지만, 위와 같이 선언시 디폴트값을 대입할 수 있다. 

동일한 유니폼을 여러 쉐이더 스테이지에서 선언해도, 모든 스테이지는 동일한 유니폼 값을 가진다.

Arranging Your Uniform

쉐이더 컴파일 후 프로그램 객체에 링크된 다음에는 OpenGL에 정의된 여러 함수로 값을 설정한다. (쉐이더에서 정한 값이 아닌 다른 값이다). 버텍스 속성처럼 이 함수들은 프로그램 객체 내 위치(location)를 통해 유니폼을 참조한다.

location layout 지시어를 통해 유니폼의 위치를 쉐이더 코드 내에서 정하는 것도 가능하다.

layout (location = 17) uniform vec4 myUniform;

Opengl은 유니폼의 위치를 쉐이더 코드에서 정한 값으로 설정하려고 시도한다. 

 

유니폼을 이한 location layout 지시어와 vertex shader 입력을 위한 위치 레이아웃 지시어 둘 다 쉐이더에서 유니폼의 위치를 정하지 않으면 opengl에 자동으로 할당한다. 위 코드에서는 17로 location을 지정하였지만, 만약 정하지 않았다면,

GLint glGetUniformLocation(GLuint program, const GLchar* name);

위 함수를 통해 어떤 location이 할당되었는지를 알 수 있다.

program으로 지정한 프로그램에서 name 변수에 대해 location을 부호 있는 정수로 반환한다.

GLint iLocation = glGetUniformLocation(myProgram, "vColorValue");

위와 같은 코드로 vColorValue의 유니폼 location을 얻을 수 있다. 하지만 location을 안다면 굳이 함수를 사용하지 않는 게 좋다.

-1이 return된다면, 유니폼 이름이 프로그램에 없다는 것이다.

쉐이더가 정상적으로 컴파일되어도, attach된 쉐이더 중 유니폼을 직접 사용하는 쉐이더가 없다면 유니폼의 이름은 프로그램에서 사라질 수 있다는 점을 유의하다.

 쉐이더 코드에서 명시적으로 위치를 할당해주는 경우에도 그렇다. 유니폼 변수의 최적화에 대해서는 걱정할 필요가 없다. 하지만 유니폼 변수를 선언하고 사용하지 않는 다면 컴파일러가 없애버린다. 

Setting Uniforms

opengl은 쉐이딩 언어 및 API에서는 모든 데이터를 전달할 수 있도록 많은 데이터 타입을 지원한다.

유니폼 값 설정을 위한 많은 함수도 지원한다. 단일 스칼라나 벡터 타입의 경우 glUniform*() 함수의 통해 설정할 수 있다. glUniform*함수는 오버라이딩 되지 않고 이름으로 구분하는 여러 종류가 있다.

 

layout (location = 0) uniform float fTime;
layout (location = 1) uniform int iIndex;
layout (location = 2) uniform vec4 vColorValue;
layout (location = 3) uniform bool bSomeFlag;

이러한 코드가 있다고 하였을 때, 쉐이더에서 이 값들을 찾고 설정하려면

glUseProgram(myShader);
glUniform1f(0, 45.2f);
glUniform1i(1, 42);
glUniform4f(2, 1.0f, 0.0f, 0.0f, 1.0f);
glUniform1i(3, GL_FALSE);

glUniform의 다양한 버전이 있는데, 위 코드에서는 정수 버전을 사용하여 bool값을 설정하였다. bool형은 0.0으로 설정하여 false값으로 할 수 있으며, 0 이외에는 모두 true이다.

Setting Uniform Arrays

glUniform*함수는 배열 형태로 들어있는 값의 포인터도 인자로 가질 수 있는 다양한 함수가 있다.

 

uniform vec4 vColor;

4요소 유니폼을 선언하고

GLfloat vColor[4] = { 1.0f, 1.0f, 1.0f, 1.0f };

요소들의 값을 배열로 나타낸다.

glUniform4fv(iColorLocation, 1, vColor);

uniform 함수를 이용해서 쉐이더로 전달한다.

 

uniform vec4 vColors[2];

위 코드처럼 색상 값을 배열로 선언하면, 

GLfloat vColors[4][2] = { { 1.0f, 1.0f, 1.0f, 1.0f } ,
	{ 1.0f, 0.0f, 0.0f, 1.0f } };
...
glUniform4fv(iColorLocation, 2, vColors);

이러한 식으로 정의하고 전달하는 것이 가능하다.

GLfloat fValue = 45.2f;
glUniform1fv(iLocation, 1, &fValue);

 

 

이러한 방식으로 부동소수점 유니폼을 설정하는 것도 가능하다.

Setting Matrix Uniforms

쉐이더 행렬 데이터 타입은 함수 종류가 적다. 함수 이름은 glUniformMatrix*의 형태이며, 전부 count라는 인자를 가지는데, 이것은 포인터 인자가 가리키는 행렬의 개수이다. transpose 인자는 GL_FLASE로 설정할 경우 행렬이 열우선으로 저장된 것으로 보고, GL_TRUE는 쉐이더로 복사될 때 행렬이 전치된다. 행렬 라이브러리를 사용하는 경우 이러한 transpose변경이 유용할 것이다.

 

Uniform Blocks

쉐이더를 작성하면서 유니폼으로 다 하면 비효율적이다. glUniform*을 매번 호출하는 성능 저하를 막고 유니폼 업데이트 작업을 단순히 하기 위해 opengl에서는 유니폼 블록으로 그룹화하는 과정이 있다. 버퍼 객체는 그대로 바인딩을 변경하거나덮어쓰면 유니폼 그룹을 빠르게 설정가능하다. 이러한 기능을 uniform buffer object(UBO)라고 부른다. 지금까지는 디폴트 유니폼 블록이었지만, 이제는 이름 있는 것을 하나 이상 생성해야한다.

uniform TransformBlock
{
    float scale; // Global scale to apply to everything
    vec3 translation; // Translation in X, Y, and Z
    float rotation[3]; // Rotation around X, Y, and Z axes
    mat4 projection_matrix; // A generalized projection matrix to apply
    // after scale and rotate
} transform;

위 코드는 Transform이라는 이름의 유니폼 블록을 선언하는 코드이다. transform이라는 인스턴스도 생성한다. 

쉐이더 안에서 transform 인스턴스를 이용해서 블록의 멤버를 참조할 수 있다. 

 

Building Uniform Blocks

named uniform block을 통해 쉐이더 내에서 접근한 데이터는 버퍼 객체에 저장된다. 버퍼 객체는 애플리케이션에서 두 가지의 방법으로 채울 수 있다.

하나는 데이터 레이아웃의 의존하는 방법이다. 안전하지만 편의성에 비해 성능손해가 있다.

다른 방법은 opengl에게 데이터의 위치를 결정하게 하는 방법이다. 효율적으로 쉐이더를 만들 수 있으모 위치를 애플리케이션이 알아야한다. 이 방식은 유니폼 버퍼의 저장된 데이터가 공유된 레이아웃 형태로 정렬된다.

 

layout(std140) uniform TransformBlock
{
    float scale; // Global scale to apply to everything
    vec3 translation; // Translation in X, Y, and Z
    float rotation[3]; // Rotation around X, Y, and Z axes
    mat4 projection_matrix; // A generalized projection matrix to
    // apply after scale and rotate
} transform;

위 방식은 표준 레이아웃 방식이다. std140으로 유니폼이 선언되면 버퍼에 그만큼의 공간을 차지한다.

 

std140 레이아웃과 C++ 컴파일러의 규칙에는 차이가 있다. 유니폼의 경우 C의 배열 데이터와 달리 패킹되지 않는다.

layout(std140) uniform TransformBlock
{
    // Member base alignment offset aligned offset
    float scale; // 4 0 0
    vec3 translation; // 16 4 16
    float rotation[3]; // 16 28 32 (rotation[0])
    // 48 (rotation[1])
    // 64 (rotation[2])
    mat4 projection_matrix; // 16 80 80 (column 0)
    // 96 (column 1)
    // 112 (column 2)
    // 128 (column 3)
} transform;

위 코드는 유니폼 블록 TrnasformBlock의 offset의 예제이다.

void glGetUniformIndices(GLuint program,
    GLsizei uniformCount,
    const GLchar ** uniformNames,
    GLuint * uniformIndices);

정말 공유 레이아웃을 사용하고 싶다면 오프셋을 알아내기 위해 위 함수로 유니폼 블록 멤버의 인덱스를 얻는다.

static const GLchar * uniformNames[4] =
{
    "TransformBlock.scale",
    "TransformBlock.translation",
    "TransformBlock.rotation",
    "TransformBlock.projection_matrix"
};
GLuint uniformIndices[4];

glGetUniformIndices(program, 4, uniformNames, uniformIndices);

위의 방법처럼 유니폼 블록 멤버의 인덱스를 얻어온 후 

void glGetActiveUniformsiv(GLuint program,
    GLsizei uniformCount,
    const GLuint * uniformIndices,
    GLenum pname,
    GLint * params);

위 함수로 유니폼 블록 멤버에 대한 정보를 가져온다.

GLint uniformOffsets[4];
GLint arrayStrides[4];
GLint matrixStrides[4];
glGetActiveUniformsiv(program, 4, uniformIndices,
	GL_UNIFORM_OFFSET, uniformOffsets);
glGetActiveUniformsiv(program, 4, uniformIndices,
	GL_UNIFORM_ARRAY_STRIDE, arrayStrides);
glGetActiveUniformsiv(program, 4, uniformIndices,
	GL_UNIFORM_MATRIX_STRIDE, matrixStrides);

즉 위와 같이 코드를 실행하면 uniformOffsets에 transformblock의 블록 멤버에 대한 오프셋, attayStrides로 배열 멤버들의 스트라이드를 담고, matrixStrides는 행렬 멤버들의 스트라이드를 담는다.

glGetActiveUniformsiv를 통해 유니폼 인자를 질의할 때 pname값에 따라 아양한 타입을 가져올 수 있다.

 

// Allocate some memory for our buffer (don't forget to free it later)
unsigned char * buffer = (unsigned char *)malloc(4096);

// We know that TransformBlock.scale is at uniformOffsets[0] bytes
// into the block, so we can offset our buffer pointer by that and
// store the scale there.
*((float *)(buffer + uniformOffsets[0])) = 3.0f;

유니폼의 scale을 단일 부동소수점 설정으로 저장한다.

// Put three consecutive GLfloat values in memory to update a vec3
((float *)(buffer + uniformOffsets[1]))[0] = 1.0f;
((float *)(buffer + uniformOffsets[1]))[1] = 2.0f;
((float *)(buffer + uniformOffsets[1]))[2] = 3.0f;

다음으로 데이터를 초기화한다. 벡터의 첫 요소에 대한 위치를 찾아 그 위치부터 세 개의 부동소수점값을 입력하는 코드이다.

// TransformBlock.rotations[0] is at uniformOffsets[2] bytes into
// the buffer. Each element of the array is at a multiple of
// arrayStrides[2] bytes past that.

const GLfloat rotations[] = { 30.0f, 40.0f, 60.0f };
unsigned int offset = uniformOffsets[2];

for (int n = 0; n < 3; n++)
{
    *((float *)(buffer + offset)) = rotations[n];
    offset += arrayStrides[2];
}

그 다음으로 3요소 배열과 공유 레이아웃을 사용하여 rotation 배열을 처리하는 과정이다.

// The first column of TransformBlock.projection_matrix is at
// uniformOffsets[3] bytes into the buffer. The columns are
// spaced matrixStride[3] bytes apart and are essentially vec4s.
// This is the source matrix - remember, it's column major.
const GLfloat matrix[] =
{
    1.0f, 2.0f, 3.0f, 4.0f,
    9.0f, 8.0f, 7.0f, 6.0f,
    2.0f, 4.0f, 6.0f, 8.0f,
    1.0f, 3.0f, 5.0f, 7.0f
};

for (int i = 0; i < 4; i++)
{
	GLuint offset = uniformOffsets[3] + matrixStride[3] * i;
    for (j = 0; j < 4; j++)
    {
        *((float *)(buffer + offset)) = matrix[i * 4 + j];
        offset += sizeof(GLfloat);
    }
}

마지막으로 위 코드로 TransformBlock.projection_matrix에 대한 데이터를 설정한다. 

이런 식으로 레이아웃에 상관없이 오프셋과 스트라이드를 질의할 수 있다. 하지만 공유 레이아웃에서는 이 방식 뿐이다.

 

하나의 프로그램이 사용할 수 있는 유니폼 블록의 최대 개수에는 제한이 있기 때문에, glGetIntegerv에 GL_MAX_UNIFORM_BUFFERS인자를 전달하여 확인할 수 있다.

GL_MAX_VERTEX_UNIFORM_BUFFERS, GL_MAX_GEOMETRY_UNIFORM_BUFFERS, GL_MAX_TESS_CONTROL_UNIFORM_BUFFERS, GL_MAX_TESS_EVALUATION_UNIFORM_BUFFERS,  GL_MAX_FRAGMENT_UNIFORM_BUFFERS 등으 인자로 다양한 값을 확인할 수도 있다.

 

GLuint glGetUniformBlockIndex(GLuint program,
	const GLchar * uniformBlockName);

위 함수는 유니폼 블록의 인덱스를 반환한다. 예제에서 uniformBlockName은 TransformBlock이다.

버퍼를 유니폼 블록에 바인딩하기 위해서 유니폼 블록을 바인딩 포인트에 할당한 뒤, 바인딩 포인트에 버퍼를 바인딩하면 버퍼가 유니폼 블록에 연결되는 식이다. 

void glUniformBlockBinding(GLuint program,
    GLuint uniformBlockIndex,
    GLuint uniformBlockBinding);

위와 같은 함수를 호출하여 유니폼 블록에 바인딩 포인트를 할당한다.

layout(std140, binding = 2) uniform TransformBlock
{
...
} transform;

위와 같은 방식으로 유니폼 블록의 인덱스를 쉐이더 코드 안에서 직접 지정할 수도 있다. binding 키워드로 레이아웃 지시어를 지정하고, transformblock을 바인딩에 할당한다.

이 방식은 glUniformBlockBinding을 하지않아도 되고, 애플리케이션에서 블록 인덱스를 몰라도 된다.

glBindBufferBase(GL_UNIFORM_BUFFER, index, buffer);

프로그램의 유니폼 블록에 바인딩 포인트를 할당하면, 위 함수를 통해 해당 바인딩 포인트에 버퍼를 바인딩하여 버퍼상의 데이터를 유니폼 블록에 보이게할 수 있다.

index의 경우 glUniformBlockBinding에 uniformBlockBinding으로 지정한 값과 같아야 한다. 

 

유니폼 블록 인덱스와 바인딩 포인트를 연결하는 그림을 참고해보자.

Harry, Bob, Susan이라는 세 개의 유니폼 블록이 있고, 세 개의 버퍼 객체 A,B,C가 있다.

Harry는 바인딩 포인트 1에 연결되어있으므로 버퍼 C에서 데이터를 받는다. 같은 방식으로 다른 유니폼 블록도 동작한다. 

// Get the indices of the uniform blocks using glGetUniformBlockIndex
GLuint harry_index = glGetUniformBlockIndex(program, "Harry");
GLuint bob_index = glGetUniformBlockIndex(program, "Bob");
GLuint susan_index = glGetUniformBlockIndex(program, "Susan");

// Assign buffer bindings to uniform blocks, using their indices
glUniformBlockBinding(program, harry_index, 1);
glUniformBlockBinding(program, bob_index, 3);
glUniformBlockBinding(program, susan_index, 0);

// Bind buffers to the binding points
// Binding 0, buffer B, Susan's data
glBindBufferBase(GL_UNIFORM_BUFFER, 0, buffer_b);
// Binding 1, buffer C, Harry's data
glBindBufferBase(GL_UNIFORM_BUFFER, 1, buffer_c);
// Note that we skipped binding 2
// Binding 3, buffer A, Bob's data
glBindBufferBase(GL_UNIFORM_BUFFER, 3, buffer_a);

위에서 본 그림과 같이 유니폼 블록을 바인딩 포인트에 연결하는 코드이다. 

layout (binding = 1) uniform Harry
{
	// ...
};
layout (binding = 3) uniform Bob
{
	// ...
};
layout (binding = 0) uniform Susan
{
	// ...
};

만약 위와 같이 쉐이더 코드 안에서 binding 레이아웃 지시어를 사용해서 바인딩을 설정하면 glUniformBlockBinding함수를 호출하지 않아도 된다.

이 방식은 애플리케이션이 opengl에 대해 호출하는 함수가 더 적고, 애플리케이션이 이름을 몰라도 특정 바인딩 포인트에 유니폼 블록 연결이 가능하다.

 

유니폼 블록은 보통 임시 상태와 고정 상태를 분리할 때 사용하곤 한다. 

유니폼 블록을 사용하는 이점은 유니폼 블록은 크기가 커도 된다는 것이다. GL_MAX_UNIFORM_BLOCK_SIZE glGetIntegerv함수를 호출하면 유니폼 블록의 최대 크기를 얻을 수 있다. opengl은 최소 64kb를 보장하고, 하나의 프로그램이 최소 14개 참조가 가능하다. 

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