REAL-TIME RENDERING

유니폼(uniform) 본문

Season 1/OpenGL

유니폼(uniform)

minseoklee 2015. 9. 18. 20:25

vertex attribute: 데이터를 정점 셰이더에 넘긴다

interface block: 한 셰이더 스테이지에서 다른 스테이지로 데이터를 넘긴다

uniform: 애플리케이션에서 한 셰이더 스테이지로 데이터를 넘긴다

 

유니폼은 default block uniform과 uniform block으로 나뉜다. uniform block에서는 값이 버퍼 개체 안에 저장된다.

 

유니폼은 항상 상수이며 셰이더가 값을 할당할 수 없다. 단 유니폼 선언 시의 할당만은 가능하다.

 

유니폼 선언 예시

layout(location = 17) uniform vec4 myUniform;

// layout 한정자는 생략 가능하며 생략시 자동으로 설정된다.

 

유니폼의 location 찾기

GLint glGetUniformLocation(GLuint program, const GLchar* name)

활성화된 프로그램(active program)이 아니더라도 사용 가능하다.

 

선언했지만 쓰지 않는 유니폼은 최적화 과정에서 없어질 수 있다.

 

유니폼의 값 설정하기

glUniform[1/2/3/4][f/i/ui](location, ...value1, value2, ..., valueN) // value의 개수는 1/2/3/4에 일치

 

uniform float time; // glUniform1f(location, value)

uniform int index; // glUniform1i(location, value)

uniform vec4 color; // glUniform4f(location, v1, v2, v3, v4)

uniform bool flag; // glUniform1i(location, value)

 

유니폼의 원소들을 따로따로 넘기지 않고 하나의 배열으로 넘길 때는 glUniform*v()를 사용한다.

 

uniform vec4 color; // GLfloat vcolor[4]; glUniform4fv(location, 1, vcolor);

uniform vec4 color[2]; // GLfloat vcolor[4][2]; glUniform4fv(location, 2, vcolor);

 

glUniform*() 호출 시 주의할 점
개고통 받아서 빨간 색으로 강조했다 진심 빡친다

어떤 프로그램 객체의 이름을 pid라 하자. glUseProgram(pid)을 호출하면 pid가 active program이 되는데, glUniform으로 시작하는 유니폼 설정 함수들은 오직 active program에 대해서만 작동한다. glUseProgram(0)을 호출해서 active program이 없거나, active program이 아닌 프로그램에 대해 glUniform*()을 호출하면 1282(Invalid Operation) 에러가 발생한다. (glGetError()로 확인 가능. 확인할 때는 glUniform*() 호출 바로 다음 줄에서 glGetError()를 호출할 것)

 

그리고 OpenGL 4.1부터 지원하는 program pipeline 기능을 쓸 경우 여러 프로그램을 동시에 쓸 수 있게 되는데, 이 때 active program은 glActiveShaderProgram(pipeline, program) 호출에 의해 결정된다. 예를 들어 파이프라인을 만들어 다음과 같이 두 프로그램을 추가했다고 치자.

 

glGenProgramPipelines(1, &programPipeline);

glUseProgram(0); // 파이프라인을 쓸 때는 반드시 호출해야 한다
glBindProgramPipeline(programPipeline);

 

glUseProgramStages(pipeline, GL_VERTEX_SHADER_BIT, vertexProgram);
glUseProgramStages(pipeline, GL_FRAGMENT_SHADER_BIT, fragmentProgram);

 

이제 vertexProgram의 mvpTransform 유니폼과 fragmentProgram의 color 유니폼에 값을 넣으려면 반드시 이렇게 해야 한다.

 

// vertexProgram을 active program으로 만든 후 유니폼 설정

glActiveShaderProgram(pipeline, vertexProgram);
glUniformMatrix4fv(glGetUniformLocation(vertexProgram, "mvpTransform"), 1, false, 행렬값);

 

// fragmentProgram을 active program으로 만든 후 유니폼 설정

glActiveShaderProgram(pipeline, fragmentProgram);
glUniform3fv(glGetUniformLocation(fragmentProgram, "color"), 1, 색상값);


유니폼의 배열

// 셰이더
uniform vec3 colors[4]; // 배열 크기는 반드시 상수


// 애플리케이션

GLfloat colors[3 * 4]; // vec3가 4개 -> 3 * 4

glUniform3fv(glGetUniformLocation(program, "colors"), 4, colors); // 4 = 배열의 크기


변수로 유니폼 배열 인덱싱하기는 불가능

// 셰이더

for(int i=0; i<4; i++){

  color += colors[i]; // 불가능!! 여기를 참조할 것.

}


행렬 유니폼

glUniformMatrix[2/3/4][f/d]v(location, count, transpose, matrix)

transpose가 GL_FALSE면 column-major order, GL_TRUE면 row-major order

 

 

유니폼 블록

복잡한 셰이더에 많은 상수가 필요할 때 일일이 glUniform*()를 호출하는 것은 비효율적이다.

유니폼들을 하나의 유니폼 블록으로 묶어서 블록 전체를 하나의 버퍼 객체에 저장할 있다.

지금까지의 유니폼들은 default block에 들어가며 이 default block은 셰이더의 전역 스코프 내에 선언되어 있다.

 

유니폼 블록의 예시

// TransformBlock은 이름, transform은 인스턴스

// 인스턴스가 여러 개 필요할 경우 transform[3]; 처럼 배열로 선언

uniform TransformBlock {

  float scale;

  vec3 translation;

  float rotation[3];

  mat4 proj_mat;

} transform;

 

버퍼 개체에 데이터를 채우려면 glBufferData()나 glMapBuffer() 같은 함수를 써야 한다. 지금은 유니폼 버퍼 개체(UBO)에 유니폼 데이터를 채워야 한다. 버퍼 내에 데이터가 저장되는 방식(standard layout 또는 shared layout)에 따라 채우는 방법이 다르다.

 

standard layout

- 애플리케이션에서 데이터를 버퍼로 바로 복사할 수 있고 블록의 멤버들의 위치를 확정할 수 있다.

- glBufferData() 또는 glMapBuffer()가 블록 멤버 사이에 빈 공간을 남길 수 있다.

 

shared layout

- 수행능력에 최적이라 판단되는 데이터 배치를 GL이 결정한다.

- standard layout이 아니라 shared layout이 기본 레이아웃이다.

- 왜 shared layout인가?

  - 똑같은 유니폼 블록 선언 = 프로그램간 같은 데이터 배치 = 임의 프로그램에서 동일한 버퍼 개체 사용 가능

 

standard layout 사용법

유니폼 블록을 선언할 때 std140이라는 layout 한정자를 붙인다.

 

layout(std140) uniform TransformBlock {

  // members

} transform;

 

각 멤버는 버퍼 내 일정 공간을 차지한다.

유니폼 블록 내의 배열이 반드시 조밀하게 배치되는 것은 아니다. 블록 내에 float 배열을 선언하고 C 배열을 바로 복사하면 안된다.

 

standard layout에서 멤버들의 오프셋을 결정하는 규칙

1. 버퍼 내에서 N바이트를 차지하는 타입은 N의 배수인 바이트에서 시작한다. 즉 int, float, bool 등 32비트 즉 4바이트를 차지하는 타입은 4의 배수인 바이트에서 시작한다.

2. N바이트 타입의 2원소 벡터는 2N의 배수 바이트에서 시작한다.

3. N바이트 타입의 3, 4원소 벡터는 4N의 배수 바이트에서 시작한다.

4. 스칼라 또는 벡터의 배열에서 각 멤버는 위와 같은 규칙에 의해 위치가 정해지지만, vec4의 정렬 방식을 따라간다.

    이는 vec4 이외의 타입의 배열 (그리고 Nx4 행렬)이 조밀하게 배치되지 않으며 원소간에 공백이 생긴다는 것을 뜻한다.

5. 행렬은 벡터의 짧은 배열처럼 취급되고 행렬의 배열은 벡터의 매우 긴 배열로 취급된다.

6. 구조체와 구조체의 배열은 크기가 가장 큰 멤버에게 필요한 경계에서 시작하며 vec4의 크기로 반올림된다.

모든 규칙은 https://www.opengl.org/registry/specs/ARB/uniform_buffer_object.txt 참조

 

shared layout을 사용할시

static const GLchar * names = { "TransformBlock.scale", "TransformBlock.translation", "TransformBlock.rotation" };

GLuint indices[3];

glGetUniformIndices(program, 3, names, indices);

glGetActiveUniformsiv(program, count, indices, pname, params); // pname: GL_UNIFORM_[OFFSET/ARRAY_STRIDE/MATRIX_STRIDE]

 

unsigned char *buffer = (unsigned char*)malloc(4096);

*((float*)(buffer + uniformOffsets[0])) = 3.0f; // TransformBlock.scale (float)

((float*)(buffer + uniformOffsets[1]))[0] = 1.0f; // TransformBlock.translation (vec3)

((float*)(buffer + uniformOffsets[1]))[2] = 2.0f;

((float*)(buffer + uniformOffsets[1]))[3] = 3.0f;

 

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];

}

 

column-major matrix: 각 column을 벡터 취급.

row-major matrix: 각 row를 벡터 취급.

 

블록 유니폼 내의 행렬 멤버

const GLfloat matrix[] = { ... };

for(int i=0; i<4; i++){

  GLuint offset = uniformOffsets[3] + matrixStrides[3] * i;

  for(int j=0; j<4; j++){

    *((float*)(buffer + offset)) = matrix[i*4 + j];

    offset += sizeof(GLfloat);

  }

}

 

어렵다

그러니까 standard layout을 쓰자

 

 

UBO를 유니폼 블록에 바인딩하기

버퍼를 유니폼 블록에 바인딩하는 절차

1. 유니폼 블록에 바인딩 포인트를 할당한다

2. 이 바인딩 포인트에 버퍼를 바인딩한다

-> 버퍼 바인딩은 놔두고 다른 프로그램으로 교체하면 새 프로그램에 자동적으로 유니폼이 채워진다

 

유니폼 블록에 바인딩 포인트 할당하기

1. 애플리케이션에서 할당

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

    유니폼 블록의 인덱스는 다음 함수로 얻어온다

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

2. 셰이더에서 지정

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

  이 경우 glUniformBlockBinding()을 호출할 필요가 없다

 

바인딩 포인트에 버퍼 바인딩하기

glBindBufferBase(GL_UNIFORM_BUFFER, index, buffer);

0 Comments
댓글쓰기 폼