본문 바로가기
OSS

OpenGL ES 3.0 스터디 4주차 공부자료

by mazayong 2023. 8. 14.

++이 자료는 OSS 스터디팀에서 공동작업한 문서이며, 추후 깃헙 레포 주소를 추가할 예정입니다.

CH 04. Shaders and Programs

Created: August 8, 2023 12:04 PM

초록

ch02 에서는 하나의 삼각형을 그리는 간단한 프로그래밍 예제를 소개했다.

이번장에서는 두 shader objects (vertex, fragment) 와 하나의 program object 를 이용해 삼각형을 그린다.

이 장에서는 shader 를 생성하고, 컴파일하고, program obj 로 링크하는 과정의 전부를 다룬다.

그 중 vertex shader 와 fagment shader 의 작성은 추후에 알아보고, 아래의 주제에 집중한다.

  • Shader, Program Object 란 무엇인지.
  • Shader 의 생성과 컴파일
  • Program 의 생성과 링킹
  • uniform 의 생성(setting)과 접근(getting)
  • attributes 의 생성(setting)과 접근(getting)
  • Shader 컴파일러와 program binaries

Shader and Programs

shader를 이용해 랜더링을 하기 위해서는 shader objectprogram object 라는 가장 근본적인 두 타입의 객체가있습니다.

shader obj 와 program obj 는 C compiler 와 linker 의 관계라고 생각할 수 있다.

C complier 는 object code 를 생성하고( ex .obj, .o)

그 뒤 C linker 는 obj file 들을 엮어 실행 가능한 프로그램을 만든다.

비슷한 일련의 과정이 OpenGL 에서 또한 일어나는데,

shader obj는 하나의 shader 를 담는 객체이고, 이는 object form 으로 컴파일된다.

컴파일 된 이후 program obj에 부착 될 수 있다?(attached 를 뭐라 번역해야하지)

program obj 는 여러개의 shader obj 를 가질수 있다.

desktop Opengl 은 여러개의 shader 를 가질수 있지만, ES 에서는 ‘딱 하나의 vertex shader’ 와 ‘ 딱 하나의 fragment shader obj‘ 를 가질 수 있다. (적어서도, 더 많아서도 안됨)

program obj 는 링킹 이후 최종 실행가능한 결과물을 만들어내고, 이게 렌더에 사용된다.

일반적으로 다음과 같은 6개의 과정을 통해 linked shader object 가 만들어진다

  1. vertex shader obj 와 fragment shader obj 를 생성한다.
  2. 각각의 shader obj 에 소스코드를 첨부 (attach)
  3. shader obj 를 컴파일
  4. program obj 를 생성
  5. 컴파일된 shader obj 를 program obj 에 첨부
  6. program obj 를 링크

모든 과정이 오류없이 종료되면, GL 에게 생성된 프로그램을 통해 화면을 그리도록 요청 할 수 있다.

Creating and Compiling a Shader

  1. shader obj 로 작업하기 위한 첫번째 단계는 “생성”하는 것이다.

→ type 을 가지고 새로운 vertex shader 또는 fragment shader를 생성할 수 있다.

GLuint glCreateShader(GLenum type)
-------------------------------------------------------------
type: GL_VERTEX_SHADER, GL_FRGMENT_SHADER 둘중에 하나.

gl3.h 에 다음과 같이 정의되어있다.
#define GL_FRAGMENT_SHADER                0x8B30
#define GL_VERTEX_SHADER                  0x8B31

→ shader 를 다 쓰고 나면 아래 함수를 호출하여 지울 수 있다. 단, shader 가 program object 에 연결되어 있다면, 즉각적으로 shader가 지워지지 않는다. Shader는 지워질것이라고 내부적으로 표시만 되고, 그리고 해당 shader가 더이상 어떠한 program object에 참조가 안되고 있을때 지워질 것이다.

void glDeleteShader(GLuint shader)
-------------------------------------------------------------
shader: 삭제할 쉐이더의 identifier
gl3.h 에 다음과 같이 정의되어있다.
GL_APICALL void GL_APIENTRY glDeleteShader (GLuint shader);
  1. 생성한 후에는, shader source code 를 제공하기
void glShaderSource(
    GLuint shader, 
    GLsizei count, 
    const GLchr*const *string, 
    const GLint *length,
)
-------------------------------------------------------------
shader: 소스를 추가할 쉐이더의 identifier 
count: 소스 문자열 배열의 length. 여러개의 string source 를 제공할 수 있지만, 
    main entrypoint는 오직 하나여야 한다.
string: C에는 문자열이라는 타입이 없기 때문에, 이를 const char* 로 표현한다.
우리는 문자열의 배열을 전달해줄 꺼기 때문에, (const char* = String) const * 가 되는것.
length: 각 문자열의 길이를 담고있는 배열
  1. 쉐이더에 소스를 특정해 준 뒤엔 컴파일을 진행해야한다.
void glCompileShader(GLuint shader)

shader: 컴파일할 쉐이더의 identifier 

아마 여러분들은 컴파일을 하고 나면, 컴파일 에러가 나는지, 잘 동작 하는지 알고 싶을 것이다.

→ 그때 필요한 것은 glGetShaderiv()

void glGetShaderiv(GLint shader, GLenum pname, GLint *params)
---------------------------------------------------------------
shader: 상태를 알아볼 쉐이더의 identifier 
pname: 
    GL_COMPILE_STATUS : 컴파일 성공하면 params 가 true, 아니면 false 
    GL_DELETE_STATUS : 
    GL_INFO_LOG_LENGTH : 컴파일 glGetShaderInfoLog()를 사용하여 에러 로그를 확인가능
    GL_SHADER_SOURCE_LENGTH : 
    GL_SHADER_TYPE : vertex 인지 filament 인지
params: 
  • filament 코드에서 glGetShaderiv() 함수 활용한 곳을 가져와봤습니다.
      bool ShaderCompilerService::checkProgramStatus(program_token_t const& token) noexcept {
    
          SYSTRACE_CALL();
    
          assert_invariant(token->gl.program);
    
          GLint **status**;
          glGetProgramiv(token->gl.program, GL_LINK_STATUS, &status);
          if (UTILS_LIKELY(status == GL_TRUE)) {
              return true;
          }
    
          // only if the link fails, we check the compilation status
          UTILS_NOUNROLL
          for (size_t i = 0; i < Program::SHADER_TYPE_COUNT; i++) {
              const ShaderStage type = static_cast<ShaderStage>(i);
              const GLuint shader = token->gl.shaders[i];
              if (shader) {
                  **glGetShaderiv**(shader, GL_COMPILE_STATUS, &**status**);
                  if (**status** != GL_TRUE) {
                      logCompilationError(slog.e, type,
                              token->name.c_str_safe(), shader, token->shaderSourceCode[i]);
                  }
                  glDetachShader(token->gl.program, shader);
                  glDeleteShader(shader);
                  token->gl.shaders[i] = 0;
              }
          }
          // log the link error as well
          logProgramLinkError(slog.e, token->name.c_str_safe(), token->gl.program);
          glDeleteProgram(token->gl.program);
          token->gl.program = 0;
          return false;
      }
  • 활용한 코드는 아래처럼 void 함수지만,
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    params인 status 를 통해 value 값을 확인 할 수 있네요!

만약 위 과정에서, error 가 났다면 해당 로그를 확인하고 싶을 거다. 아래 내장 함수를 통해 확인 가능하다.

void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog)
---------------------------------------------------------------
shader: info log 를 가져올 수 있는 shader
maxLength: info log 를 저장할 수 있는 버퍼 사이즈
length: log 길이, NULL 가능
infoLog: 
  • 마찬가지로 filament 코드에서 사용한 부분 들고 왔습니다. 이 부분은 컴파일타임에서 error 를 확인하는 util function 인 것 같습니다.
  • glGetShaderInfoLog()* 를 통해
  • UTILS_NOINLINE void logCompilationError(io::ostream& out, ShaderStage shaderType, const char* name, GLuint shaderId, UTILS_UNUSED_IN_RELEASE CString const& sourceCode) noexcept { auto to_string = [](ShaderStage type) -> const char* { switch (type) { case ShaderStage::VERTEX: return "vertex"; case ShaderStage::FRAGMENT: return "fragment"; case ShaderStage::COMPUTE: return "compute"; } }; { // scope for the temporary string storage GLint length = 0; glGetShaderiv(shaderId, **GL_INFO_LOG_LENGTH**, &length); CString infoLog(length); **glGetShaderInfoLog**(shaderId, length, nullptr, infoLog.data()); out << "Compilation error in " << to_string(shaderType) << " shader \"" << name << "\":\n" << "\"" << infoLog.c_str() << "\"" << io::endl; } #ifndef NDEBUG std::string_view const shader{ sourceCode.data(), sourceCode.size() }; size_t lc = 1; size_t start = 0; std::string line; while (true) { size_t const end = shader.find('\n', start); if (end == std::string::npos) { line = shader.substr(start); } else { line = shader.substr(start, end - start); } out << lc++ << ": " << line.c_str() << '\n'; if (end == std::string::npos) { break; } start = end + 1; } out << io::endl; #endif }

Creating and linking a Program

program obj = container obj (shader obj에 부착되고, 최종 실행 프로그램 링크)

  1. program obj 생성.

리턴값 : program obj에 대한 handle을 반환한다.

GLuint glCreateProgram()

program 삭제를 위해서는 glDeleteProgram을 사용한다.

void glDeleteProgram(GLuint program)
---------------------------------------------------------------
program: 삭제하기 위한 program obj handle
  1. shader에 program obj 부착하기

모든 program obj에 vertex shader obj, fragment shader obj가 각각 1개만 연결되는 조건만 만족하면 shader는 compile 여부, souce code 여부와 별개로 부착 가능하다.

void glAttach(GLuint program, GLuint shader)
---------------------------------------------------------------
program : program obj handle
shader : program obj에 부착하기 위한 shader obj handle

부착된 shader를 분리

void glDetachShader(GLuint program, GLuint shader)
---------------------------------------------------------------
program : program obj handle
shader : program obj에 부착하기 위한 shader obj handle
  1. shader에 program 링크

glLinkProgram()

void glLinkProgram(GLuint program)
---------------------------------------------------------------
program : program obj handle

(linker이 확인하는 조건 중 일부는 vertex shader, fragment shader 설명 후에 이해 가능한 요소들 포함되어 있음.)

linker이 성공적 link를 위해 확인하는 조건

  • fragment shader가 사용하는 모든 vertex shader 출력 변수가 vertex shader에 의해 작성되었는지, 동일한 유형으로 선언되었는지 확인한다.
  • vertex shader, fragment shader 모두에서 선언된 모든 uniform, uniform buffer type 일치 여부를 확인한다.
  • 최종 프로그램이 구현 조건에 일치하는지 확인한다.(속성, uniform, 입력 및 출력 shader 변수)

→ 해당 조건을 확인하기 위한 함수가 glGetProgramiv()

void glGetProgramiv(GLuint program, GLenum pname, GLint *params)
-------------------------------------------------------------
program : 정보를 가져오기 위한 program obj handle
pname : 
    GL_ACTIVE_ATTRIBUTES : vertex shader의 활성된 속성 수
    GL_ACTIVE_ATTRIBUTE_MAX_LENGTH : 가장 큰 속성 이름의 최대 길이
    GL_ACTIVE_UNIFORM_BLOCK : 활성 uniform 포함하는 프로그램의 uniform block 수
    GL_ACTIVE_UNIFORM_BLOCK_MAX_LENGTH : 활성 uniform 포함하는 프로그램의 uniform block 이름 최대 길이
    GL_ACTIVE_UNIFORMS : 활성된 uniform의 수
    GL_ACTIVE_UNIFORM_MAX_LENGTH : 가장 큰 uniform 이름의 최대 길이
    GL_ATTACHED_SHADERS : 부착된 shader 수
    GL_DELETE_STATUS : program obj 삭제 표시 여부 확인
    GL_INFO_LOG_LENGTH : 쿼리할 수 있는 정보 로그 길이(program obj가 저장)
    GL_LINK_STATUS : 링크 성공 여부 확인
    GL_PROGRAM_BINARY_RETRIEVABLE_HINT : binary retrievable hint가 프로그램에 활성화되어있는지 여부 판단.
    GL_TRANSFORM_FEEDBACK_BUFFER_MODE : GL_SEPERATE_ATTRIBS / GL_INTERLEAVED_ATTRIBS 반환. (transform feedback이 활성화된 경우 버퍼 모드)
    GL_TRANSFORM_FEEDBACK_VARYINGS : 프로그램에 대한 쿼리는 각각 프로그램에 대한 transform feedback 모드에서 캡쳐할 출력 변수의 수
    GL_TRANSFORM_FEEDBACK_VARYING_MAX_LENGTH : 프로그램에 대한 쿼리는 각각 프로그램에 대한 transform feedback 모드에서 캡쳐할 출력 변수의 최대 길이
    GL_VALIDATE_STATUS : 유효성 검사 작업의 상태
params : 해당 함수의 결과가 담긴 integer storage 위치를 가리키는 포인터

(transform feedback : 8장에서 설명)

링킹 후, 프로그램 정보 로그에서 정보를 가져온다. (shader에서 정보 가져오는 것과 유사)

void glGetProgramInfoLog(GLuint program, GLsizei maxLength, GLsizei *length, GLchar *infoLog)
---------------------------------------------------------------
program : 정보를 가져올 program obj의 handle
maxLength : 로그 저장을 위해 필요한 버퍼의 크기
length : 기록된 정보 로그의 길이(null문자 제외, null값일 수 있음.)
infoLog : 정보 로그를 저장할 문자 버퍼의 포인터

프로그램의 유효성 검사(성공적인 링크 여부)

성공적인 링크가 보장되지 않을 경우를 위해 필요하다.

  • 응용 프로그램이 유효한 텍스쳐 단위를 sampler에 바인딩하지 않을 경우.

(link time에는 파악할 수 없지만, draw time에는 파악 가능하다.)

void glValidateProgram(GLuint program)
---------------------------------------------------------------
program : 유효성 검사를 진행할 program obj의 handle

해당 검사 결과는 glGetProgramiv() 의 GL_VALIDATE_STATUS로 확인 가능하다.

(이 함수는 디버깅 목적으로만 사용할 수도 있으나, 느린 작업이어서 모든 렌더링 작업에 사용할 필요는 없다. 또한 해당 함수는 응용 프로그램이 성공적으로 렌더링되면 사용하지 않아도 된다.)

  1. program obj 활성화
void glUseProgram(GLuint program)
---------------------------------------------------------------
program : 활성화할 program obj의 handle

Uniforms and Attributes

링크된 프로그램 개체(linked program object)가 있으면 해당 개체에 대해 수행할 수 있는 여러 쿼리가 있다. 먼저 프로그램에서 활성상태의 유니폼(active uniforms)들에 대해 알아봐야 할 것이다. 유니폼(uniform)은 다음 장인 shading language에서 더 자세히 설명하겠지만, 응용 프로그램이 OpenGL ES 3.0 API를 통해 셰이더에 전달하는 읽기 전용 상수 값(read-only constant values)을 저장하는 변수이다.

  • What is the Uniform??
    Untitled
    예를 들어, 모델-뷰-프로젝션 행렬, 빛의 위치, 머터리얼 색상 등이 uniform으로 정의될 수 있습니다.
  • Uniform은 CPU에서 GPU로 데이터를 전달 할 수 있는 방법입니다. 또한 ‘전역(global)변수’ 로서 값을 업데이트하거나 바꾸지 않는이상 유지됩니다. 이를 통해 동일한 쉐이더 프로그램을 사용하면서 다양한 객체나 픽셀에 일관된 값을 적용할 수 있게 됩니다. 쉐이더 내에서 uniform 변수에 접근하고 값을 변경하기 위해서는 프로그램이 실행되기 전에 CPU에서 그 값을 설정해 주어야 합니다. 간단히 말해, uniform은 쉐이더 간에 공유되는 변하지 않는 값으로, 쉐이더의 렌더링 결과에 일관성을 제공하고 다양한 그래픽 요소를 조절하는 데 사용됩니다.
  • https://www.khronos.org/opengl/wiki/Uniform_(GLSL)

1개의 변수들을 지칭하는 uniform들을 모으면 uniform block이 된다. uniform block은 2가지 형태의 블록으로 나누어 진다. 첫 번째는 named uniform block으로, 셰이더 코드에서 다음과 같은 형태로 선언된다.

//  다음 예제에서는 세 개의 uniform(matViewProj, matNormal 및 matTexGen)을 포함하는 이름이 TransformBlock인 named uniform block을 선언합니다.
uniform TransformBlock
{
 mat4 matViewProj;
 mat3 matNormal;
 mat3 matTexGen;
};
  • 해당 block안에 있는 uniform 값들은 uniform buffer object에 매핑된다(자세한 내용은 다음에 설명).
  • named uniform block에는 1개의 uniform block index가 할당됨.

두번째 카테고리는 default uniform block으로 셰이더 코드에서 다음과 같은 형태로 선언된다.

uniform mat4 matViewProj;
uniform mat3 matNormal;
uniform mat3 matTexGen;
  • named uniform block **외부에서 선언된 uniform들에 대한 기본적인 uniform block이다.
  • named uniform block과 달리 default uniform block에는 이름과 uniform block index가 없다.

default uniform block안에 존재하는 uniform 변수들은 (활성상태의 경우) 링크 과정에서 uniform location값을 부여받는다. 일종의 id라고 생각하면 되고 나중에 이 uniform location값을 활용해 변수값을 초기화 할 것이다.

만약 uniform 변수가 vertex shader와 fragment shader 각각에 둘다 활용이 된다면 각 셰이더 안에서 같은 type을 가지고 있어야 한다. 그래야 해당 uniform의 값이 같을것이다.

named uniform block안의 uniform들이 활성상태이면서 array나 matrix type이면 offset과 strides를 추가할것이다.

Getting and Setting Uniforms

먼저 program object안의 활성상태인 uniform 변수들의 갯수를 알아내려면 glGetProgramiv 함수를 GL_ACTIVE_UNIFORMS 파라미터를 넣어 호출하면 된다. 그 목록들은 program object의 셰이더 코드에 존재하는 named uniform blocks, default block uniforms안의 활성화된 uniform들의 갯수이다. 활성화된 uniform이라는 개념이 많이 언급되는데, 이것은 program에 의해 실제로 쓰이는가의 유무를 나타내는 개념이다. 즉, shader code에 선언만 해놓고 사용해지 않는다면 활성화된 uniform이 아닌것이다(링커가 optimize를 통해 변수를 날려버린다). 또한, 나중에 uniform값을 initialize 하기 위해 갯수 uniform 변수들의 이름을 알아내야 하는데, program object안의 uniform 변수들의 이름중 가장 큰 이름의 길이를 glGetProgramiv 함수에 GL_ACTIVE_UNIFORM_MAX_LENGTH 파라미터를 넣어 호출해 알아낼 수 있다.

일단 이렇게 활성화된 uniform 변수들의 갯수와 이름의 최대 길이값을 알아 내었으면 해당 uniform 변수의 각각의 보다 세부적인 속성값들에 대해 알아낼 수 있다. 이때 사용하는 함수가 glGetActiveUniform, glGetActiveUniformsiv 함수들이다.

// uniform 변수의 이름, type, size의 갯수를 알아내는 함수
void glGetActiveUniform(GLuint program, GLuint index,
                                                GLsizei bufSize, GLsizei *length,
                                                GLint *size, GLenum *type,
                                                GLchar *name)
---------------------------------------------------------------
program : program obj handle
index : 쿼리할 uniform index
bufSize : name array의 문자 수
length : null이 아닌 경우 name array에 기록된 문자 수로 기록
size : 쿼리되는 uniform 변수가 배열인 경우, 프로그램에서 사용되는 최대 배열요소(+1)로 설정. 배열이 아닐 경우 1로 설정.
type : uniform type
    GL_FLOAT, GL_FLOAT_VEC2, GL_FLOAT_VEC3,
    GL_FLOAT_VEC4, GL_INT, GL_INT_VEC2, GL_INT_VEC3,
    GL_INT_VEC4, GL_UNSIGNED_INT,
    GL_UNSIGNED_INT_VEC2, GL_UNSIGNED_INT_VEC3,
    GL_UNSIGNED_INT_VEC4, GL_BOOL, GL_BOOL_VEC2,
    GL_BOOL_VEC3, GL_BOOL_VEC4, GL_FLOAT_MAT2,
    GL_FLOAT_MAT3, GL_FLOAT_MAT4, GL_FLOAT_MAT2x3,
    GL_FLOAT_MAT2x4, GL_FLOAT_MAT3x2, GL_FLOAT_MAT3x4,
    GL_FLOAT_MAT4x2, GL_FLOAT_MAT4x3, GL_SAMPLER_2D,
    GL_SAMPLER_3D, GL_SAMPLER_CUBE,
    GL_SAMPLER_2D_SHADOW, GL_SAMPLER_2D_ARRAY,
    GL_SAMPLER_2D_ARRAY_SHADOW,
    GL_SAMPLER_CUBE_SHADOW, GL_INT_SAMPLER_2D,
    GL_INT_SAMPLER_3D, GL_INT_SAMPLER_CUBE,
    GL_INT_SAMPLER_2D_ARRAY,
    GL_UNSIGNED_INT_SAMPLER_2D,
    GL_UNSIGNED_INT_SAMPLER_3D,
    GL_UNSIGNED_INT_SAMPLER_CUBE,
    GL_UNSIGNED_INT_SAMPLER_2D_ARRAY
*/
name : buffer size 문자 수(null로 끝남)
// 보다 세부적인 property를 알아내고 싶을때 사용하는 함수
void glGetActiveUniformsiv(GLuint program, GLsizei count,
                                                        const GLuint *indices,
                                                        GLenum pname, GLint *params)
---------------------------------------------------------------
program : program obj handle
count : indices 배열의 요소 수
indices : uniform indices의 목록
pname : params의 요소에 기록될 uniform indices 각각의 uniform 속성
    GL_UNIFORM_TYPE, GL_UNIFORM_SIZE,
    GL_UNIFORM_NAME_LENGTH, GL_UNIFORM_BLOCK_INDEX,
    GL_UNIFORM_OFFSET, GL_UNIFORM_ARRAY_STRIDE,
    GL_UNIFORM_MATRIX_STRIDE, GL_UNIFORM_IS_ROW_MAJOR
params: uniform indices의 각 uniform에 해당하는 pname으로 지정된 결과
*/

glGetActiveUniform 을 사용하면 uniform 변수의 거의 모든 속성들을 알아낼 수 있다. name, type, array 타입 유무와 더불어 array크기를 size 변수를 통해 알 수 있다.

  1. uniform name을 glGetUniformLocation 함수에 인자로 넣어 통해 uniform location값을 알아낼 수 있다. uniform location값은 program object안의 uniform이 위치하고 있는곳을 식별하기 위한 id라고 생각하면 된다. (named uniform block안의 uniform들은 location 값을 부여받지 않는다는 것에 유의하자)
  2. uniform location은 uniform변수의 value를 loading하는 glUniform1f 함수에 인자로 쓰인다.
GLint glGetUniformLocation(GLuint program,
                                                        const GLchar* name)
---------------------------------------------------------------
program : program obj handle
name : 위치를 가져올 uniform 이름

위 함수는 name인자에 해당하는 uniform location을 반환한다. 만약 uniform변수가 active uniform이 아닐시 -1이 return된다. 일단 이렇게 uniform location, type, array size 세가지를 알아내었다면 uniform변수에 구체적인 값을 load할 준비가 끝난것이다. loading하는 함수는 type에 따라 다양하게 존재한다.

void glUniform1f(GLint location, GLfloat x)
void glUniform1fv(GLint location, GLsizei count, 
const GLfloat* value)
void glUniform1i(GLint location, GLint x)
void glUniform1iv(GLint location, GLsizei count,
const GLint* value)
void glUniform1ui(GLint location, GLuint x)
void glUniform1uiv(GLint location, GLsizei count, 
const GLuint* value)
void glUniform2f(GLint location, GLfloat x, GLfloat y)
void glUniform2fv(GLint location, GLsizei count,
const GLfloat* value)
void glUniform2i(GLint location, GLint x, GLint y)
void glUniform2iv(GLint location, GLsizei count,
const GLint* value)
void glUniform2ui(GLint location, GLuint x, GLuint y)
void glUniform2uiv(GLint location, GLsizei count,
const GLuint* value)
void glUniform3f(GLint location, GLfloat x, GLfloat y, 
GLfloat z)
void glUniform3fv(GLint location, GLsizei count, 
const GLfloat* value)
void glUniform3i(GLint location, GLint x, GLint y, 
GLint z)
void glUniform3iv(GLint location, GLsizei count, 
const GLint* value)
void glUniform3ui(GLint location, GLuint x, GLuint y, 
GLuint z)
void glUniform3uiv(GLint location, GLsizei count, 
const GLuint* value)
void glUniform4f(GLint location, GLfloat x, GLfloat y, 
GLfloat z, GLfloat w);
void glUniform4fv(GLint location, GLsizei count, 
const GLfloat* value)
void glUniform4i(GLint location, GLint x, GLint y, 
GLint z, GLint w)
void glUniform4iv(GLint location, GLsizei count, 
const GLint* value)
void glUniform4ui(GLint location, GLuint x, GLuint y, 
GLuint z, GLuint w)
void glUniform4uiv(GLint location, GLsizei count, 
const GLuint* value)
void glUniformMatrix2fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix3fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix4fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix2x3fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix3x2fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix2x4fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix4x2fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix3x4fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
void glUniformMatrix4x3fv(GLint location, GLsizei count, 
GLboolean transpose, 
const GLfloat* value)
---------------------------------------------------------------
location : 값을 로드할 uniform의 위치
count : 로드할 배열 요소의 수(벡터 명령의 경우) 또는 수정할 행렬의 수(행렬 명령의 경우)
transpose : 행렬 명령의겨우, 행렬이 열 주요 순서(GL_FALSE)인지 행 주요 순서(GL_TRUE)인지 지정한다.
x, y, z, w : 업데이트된 uniform 값
value : count elements의 배열에 대한 포인터

/ / 이 일련의 과정들(location정보를 알아내어 glUniformMatrix3f를 call 하기까지를 좀 더 간단히 요약하고 location 정보가 왜 필요한지, location 정보는 어떠한 것인지 풀어서 설명하는것이 의견으로 나옴)

uniform변수에 값들을 로딩하는 function 들은 대부분 자기 설명적이다. glGetActiveUnifom 함수가 return한 type 에 따라 호출되어야 하는 함수가 결정된다. 예컨대 type이 GL_FLOAT_VEC4 이라면 glUniform4fglUniform4fv 함수가 호출되어야 한다. 만약 반환된 size가 1보다 크다면 array형태를 초기화 할 수 있는glUniform4fv 함수를 호출하여 편리하게 array값들을 한번에 load할수 있다. 만약 uniform변수가 array가 아니라면 glUniform4f와 glUniform4fv 함수 중 아무것이나 사용할 수 있을것이다.

한가지 주목할 점은 glUniform* 함수들은 program object를 파라미터로 넣지 않는다는 것인데, 그 이유는 glUniform함수는 glUseProgram함수 call을 통해 현재 bounding되어있는 program에 항시 적용되기 때문이다. uniform 변수 값들은 program object에 보관되어 있다. 한번 uniform 변수 값을 program object에 할당한다면 그 값은 다른 program object가 active한 상태가 되더라도 그대로 program object에 남아있다. 한마디로 uniform 값들은 *program object에 local 범위이다.

다음 코드는 이때까지 설명한 uniform들의 정보들을 알아내는 코드이다.

GLint maxUniformLen;
GLint numUniforms;
char * uniformName;
GLint index;

glGetProgramiv(progObj, GL_ACTIVE_UNIFORMS, & numUniforms);
glGetProgramiv(progObj, GL_ACTIVE_UNIFORM_MAX_LENGTH, &maxUniformLen);

uniformName = malloc(sizeof(char) * maxUniformLen);

for (index = 0; index < numUniforms; index++) {
    GLint size;
    GLenum type;
    GLint location;
    // Get the uniform info
    glGetActiveUniform(progObj, index, maxUniformLen, NULL,
                                     &size, & type, uniformName);
    // Get the uniform location
    location = glGetUniformLocation(progObj, uniformName);
    switch (type) {
    case GL_FLOAT:
        //
        break;
    case GL_FLOAT_VEC2:
        //
        break;
    case GL_FLOAT_VEC3:
        //
        break;
    case GL_FLOAT_VEC4:
        //
        break;
    case GL_INT:
        //
        break;
        // ... Check for all the types ...
    default:
        // Unknown type
        break;
    }
}

Uniform Buffer Objects

// 이하 내용을 전부 지운다..?

위의 방식대로 uniform location방식을 알아내어 uniform 값들을 loading하는 방식은 각 shader마다, program마다 공유가 안되는 불편함이 있다. buffer object를 사용하여 uniform data를 저장하고 shader, program마다 공유가 가능하게 할 수 있는데, 이러한 buffer object를 uniform buffer objects라고 한다. uniform buffer object를 사용하면 (uniform들을 저장하는 default uniform block size이외에도) 버퍼에 의해 uniform들을 저장할수 있는 공간이 늘어날 수 있다는 장점이 있다.

Uniform buffer object안의 uniform data를 update하기 위해 glBufferData, glBufferSubData, glMapBufferRange, glUnmapBuffer 함수들을 사용하여 buffer object의 내용물을 할당/변경할 수 있다. (이 명령들은 Chapter 6의 “Vertex Attributes, Vertex Arrays, and Buffer Object” 부분에서 더 자세히 다룸) 위에서 다루었던 glUniform*함수는 사용하지 않는다.

Uniform buffer object안의 uniform들은 다음과 같이 memory에 상주한다.

  • bool, int, uint, float와 같은 타입은 지정된 주소(specified offset)에 uint-typed, int-typed, uint-typed, float-typed로 저장이 된다.
  • 💡 여기서 uint-typed, int-typed, uint-typed, float-typed는 GLSL 타입을 의미함.
  • bool, int, uint, float와 같은 기본 타입으로 이루어진 vector들은 지정된 주소(specified offset)의 연속된 공간에 백터 요소들이 순서대로 낮은주소에서 높은주소에 배정이 된다.
  • CxR크기의 열 기준 행렬(Column-major matrices)은 float 타입의 R개의 원소를 가지고 있는 열백터가 C개 모인 array로 취급된다. 유사하게, RxC크기의 행 기준 행렬(row-major matrices)은 float 타입 C개의 원소를 가지고 있는 행백터가 R개 모인 array로 취급된다. 열백터나 행백터는 연속적으로 배정되지만, (컴파일러의) 구현체에 따라 gap을 가지고 저장될 수 있다. 이 백터간의 offset은 column stride 또는 row stride라고 불리며 glGetActiveUniformsiv 함수에GL_UNIFORM_MATRIX_STRIDE 인자를 넣어 확인할 수 있다.
  • scalars, vectors, matrices요소로 이루어진 array 타입은 memory상에서 요소 순서대로 저장이 된다. 첫번째 array element가 가장 낮은 주소에 배치가 된다. 이때 각 element 간의 offset은 array stride 라고 불리고 glGetActiveUniformsiv 함수에 GL_UNIFORM_ARRAY_STRIDE인자를 넣어 조회할 수 있다.

std140 uniform block layout을 사용하지 않는 한, uniform buffer object에 값을 채우기 위해 program object에 byte offset과 stride 정보를 물어봐야 한다는 단점이 있다.

std140 layout은 명백한 구조(explicit layout)를 OpenGL ES 3.0 specification에 정의함으로써 변수들이 특정 위치들에(guarantees packing behavior)들어가는 것이 보장된다. 따라서 std140 layout을 사용한다면 서로 다른 OpenGL 3.0 구현체에서 uniform block을 공유할 수 있게된다. (다른 packing format들은 std140 보다 더 빽빽하게 채워넣을 수 있을수도 있다!)

다음 예시는 LightBlock이라는 named uniform block을 std140 layout으로 정의한 코드이다.

layout (std140) uniform LightBlock
{
 vec3 lightDirection;
 vec4 lightPosition;
};

std140 layout은 다음의 명세로 정의되어 있다.(OpenGL ES 3.0 specification을 개작한것)

  1. scalar 변수—기본 크기는 scalar의 크기다. 예컨대, sizeof(GLint).
  2. 2개의 원소가 있는 vector— 기본 크기는 원소크기의 2배이다.
  3. 3 또는 4개의 원소가 있는 vector —기본크기는 원소크기의 4배이다.
  4. scalar, 또는 vector의 array—기본 크기와 array stride는 한개의 원소가 있는 array의 크기와 stride에 맞춰진다. 전체 array는 vec4 크기의 배수로 패딩된다.
  5. CxR크기의 열기준 matrix는 R개의 요소가 있는 C개의 백터로 저장된다.
  6. CxR 크기의 열기준 matrix가 M개 있는 array: R개의 요소로 이루어진 MxC개의 백터로 저장이 된다.
  7. CxR크기의 행기준 matrix는 C개의 요소가 있는 R개의 백터로 저장된다.
  8. CxR 크기의 행기준 matrix가 M개 있는 array: C개의 요소로 이루어진 MxR개의 백터로 저장이 된다.
  9. structure: 크기와 시작점은 이전의 규칙에 의하여 정해진다. structure의 크기는 vec4크기의 배수로 패딩된다.
  10. S 개의 structure가 있는 array: 크기는 array의 원소의 크기에 의하여 계산된다. array 원소의 크기는 9번 rule에 의하여 계산되어 진다.

uniform location이 default uniform block의 uniform을 참조하는데에 쓰이는것 처럼 uniform block index는 uniform block을 참조하는데 쓰인다. glGetUniformBlockIndex 함수를 통해 uniform block index를 알아낼 수 있다.

GLuint glGetUniformBlockIndex(GLuint program,const GLchar *blockName)
/*
program: handle to the program object
blockName: the name of the uniform block for which to get the index
*/

uniform block index로 부터 uniform block의 세부사항을 glGetActiveUniformBlockName 함수(이름을 알아낼 수 있다), glGetActiveUniformBlockiv(다양한 속성값 조회할 수 있다)함수를 통해 조회 가능하다.

void glGetActiveUniformBlockName(GLuint program,
                                                                    GLuint index,
                                                                    GLsizei bufSize,
                                                                    GLsizei *length,
                                                                    GLchar *blockName)
/*
    program: handle to the program object
    index: the uniform block index to be queried
    bufSize: the number of characters in the name array
    length: if not NULL, will be written with the number of characters 
    written into the name array (less the null terminator)
    blockName: will be written with the name of the uniform up to 
                        bufSize number of characters; this will be a nullterminated strin
*/
void glGetActiveUniformBlockiv(GLuint program, 
                                                            GLuint index,
                                                            GLenum pname, 
                                                            GLint *params)
/*
    program: handle to the program object
    index: the uniform block index to be queried
    pname: property of the uniform block index to be written into 
    params: can be
                    GL_UNIFORM_BLOCK_BINDING
                    GL_UNIFORM_BLOCK_DATA_SIZE
                    GL_UNIFORM_BLOCK_NAME_LENGTH
                    GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS
                    GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES
                    GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER
                    GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER
                    params will be written with the result specified by pname
*/
  • GL_UNIFORM_BLOCK_BINDING은 마지막의 buffer binging point를 return한다.
  • GL_UNIFORM_BLOCKL_DATA_SIZE는 uniform block의 모든 unifom들을 담을 buffer object의 최소 크기를 return한다.
  • GL_UNIFORM_BLOCK_NAME_LENGTH는 uniform block의 널문자를 포함한 이름의 총 길이를 return한다.
  • GL_UNIFORM_BLOCK_ACTIVE_UNIFORMS는 uniform block안의 active한 uniform 갯수를 return한다
  • GL_UNIFORM_BLOCK_ACTIVE_UNIFORM_INDICES는 uniform block안의 active한 uniform들의 index들을 return한다.
  • GL_UNIFORM_BLOCK_REFERENCED_BY_VERTEX_SHADER는 uniform block이 program object안의 vertex shaer에 의해 참조되고 있는지의 유무를 나타내는 boolean값을 return한다.
  • GL_UNIFORM_BLOCK_REFERENCED_BY_FRAGMENT_SHADER는 uniform block이 program object안의 fragment shaer에 의해 참조되고 있는지의 유무를 나타내는 boolean값을 return한다.

Uniform block index를 가지고 있다면 이 값을 glUniformBlockBinding 함수에 인자로 넣어 uniform block을 특정 binding point에 연결할 수 있다.

void glUniformBlockBinding(GLuint program,
                                                        GLuint blockIndex,
                                                        GLuint blockBinding)
/*
    program: handle to the program object
    blockIndex: index of the uniform block
    blockBinding: uniform buffer object binding point
*/

uniform block은 binding point에 연결했으니, uniform buffer object도 연결해주어야 한다.

void glBindBufferRange(GLenum target, GLuint index,
                                                GLuint buffer, GLintptr offset,
                                                GLsizeiptr size)
void glBindBufferBase(GLenum target, GLuint index,
                                            GLuint buffer)

/*
target: must be GL_UNIFORM_BUFFER or
                GL_TRANSFORM_FEEDBACK_BUFFER
index: the binding index
buffer: the handle to the buffer object
offset: a starting offset in bytes into the buffer object
                (glBindBufferRange only)
size: the amount of data in bytes that can be read from or written 
            to the buffer object (glBindBufferRange only)
*/

uniform block을 프로그래밍할때 다음과 같은 제약사항이 있다는것에 주목하자:

  • vertex, fragment shader가 최대로 사용할 수 있는 active uniform block갯수는 정해져 있으며, glGetIntergerv 함수에 GL_MAX_VERTEX_UNIFORM_BLOCKS 이나 GL_MAX_FRAGMENT_UNIFORM_BLOCKS 을 인자로 넣어 호출하여 알 수 있다. 하지만 최소 지원 갯수는 12개여서 어떠한 구현체들도 최소 12개는 지원한다.
  • program안의 모든 shader에서 사용할 수 있는 combined active uniform block 갯수는 정해져 있으며, glGetIntergerv 함수에 GL_MAX_COMBINED_UNIFORM_BLOCKS 인자를 넣어 호출하여 알 수 있다. 구현체마다 보장된 최소 지원갯수는 24이다.
  • uniform buffer마다 사용할 수 있는 최대 용량은 glGetInterger64v 함수에 GL_MAX_UNIFORM_BLOCK_SIZE인자를 넣어 알 수 있다. 구현체마다 보장된 최소 용량은 16KB이다.

이 규칙들을 지키지 않는다면 program object는 링킹되지 않을것이다.

다음 코드는 앞전에 기술한 LightTransform named uniform block에 대해 실제로 uniform buffer object를 설정하는 코드이다.

GLuint blockId, bufferId;
GLint blockSize;
GLuint bindingPoint = 1;
GLfloat lightData[] =
{
 // lightDirection (padded to vec4 based on std140 rule)
 1.0f, 0.0f, 0.0f, 0.0f,
 // lightPosition
 0.0f, 0.0f, 0.0f, 1.0f
};

// Retrieve the uniform block index
blockId = glGetUniformBlockIndex ( program, “LightBlock” );

// Associate the uniform block index with a binding point
glUniformBlockBinding ( program, blockId, bindingPoint );

// Get the size of lightData; alternatively,
// we can calculate it using sizeof(lightData) in this example
glGetActiveUniformBlockiv ( program, blockId,
                                                        GL_UNIFORM_BLOCK_DATA_SIZE,
                                                        &blockSize );

// Create and fill a buffer object
glGenBuffers ( 1, &bufferId );
glBindBuffer ( GL_UNIFORM_BUFFER, bufferId );
glBufferData ( GL_UNIFORM_BUFFER, blockSize, lightData,
GL_DYNAMIC_DRAW);

// Bind the buffer object to the uniform block binding point
glBindBufferBase ( GL_UNIFORM_BUFFER, bindingPoint, buffer );

Getting and Setting Attributes

program object의 uniform 정보들을 질의하는것처럼 program object의 vertex 정보들 역시 조회 가능하다(조회해야 할 것이다!). 질의 쿼리들은 uniform 쿼리들과 유사하다. active attributes를 GL_ACTIVE_ATTRIBUTES 쿼리를 활용해 알아낼 수 있다. 또한 attribute의 properties를 glGetActiveAttrib 명령을 통해 알아낼 수 있다. 그리고 나서 vertex array를 설정하여 vertex attributes를 value와 함께 로드하기 위한 일련의 루틴을 사용할 수 있다.

하지만, vertex attributes를 설정하는것은 primitives와 vertex shader에 대한 이해가 조금 더 필요하다. 대신 쳅터 6장에서(“Vertex Attributes, Vertex Arrays, and Buffer Objects”) vertex attribute와 vertex arrays에 대하여 1장을 할애하여 설명한다. 만약 어떻게 vertex attribute정보를 알아내고 싶다면 챕터 6장으로 건너뛰어 Vertex Attribute Variables 구간을 참고해라.

Shader Compiler

shader 코드 : 대부분의 컴파일된 언어(Abstract Syntax Tree)와 같이 중간 표현으로 구문 분석된다.

구문 분석 이후, 컴파일러는 추상 표현을 기계 명령어로 변환해야 한다.

하드웨어를 위해 컴파일러는 데드 코드 제거, 지속적인 propagation과 같은 최적화의 모든 작업을 수행하기 위해 CPU 시간과 메모리를 사용해야 한다.

OpenGL ES 3.0 구현 과정에서 온라일 셰이더 컴파일을 지원해야 한다.(컴파일 결과 : GL_TRUE를 만족하는 glGetBooleanv를 사용하여 검색된 GL_SHADER_COMPILER의 값)

glShaderSource를 사용하여 shader 지정shader compilation의 리소스 영향을 완화시킬 수 있다. 이 함수는 리소스를 해제할 수 있도록 shader compiler로 구현을 완료했다는 힌트를 제공한다.

아래 함수를 이용하여 shader을 컴파일할 경우, 구현 과정에서 컴파일러에 대한 리소스를 재할당해야 한다.

void glReleaseShaderCompiler(void)
shader compiler에서 사용하는 리소스를 해제할 수 있다는 힌트를 제공한다.
힌트일 뿐이므로 일부 구현 과정에서는 해당 함수에 대한 호출을 무시할 수 있다.

Program Binaries

Program Binaries

  • 완전한 컴파일 및 링크 프로그램의 binary 표현.
  • 이후 다시 사용할 수 있도록 파일 시스템에 저장할 수 있으므로 온라인 컴파일 비용을 피할 수 있어서 유용하다.
  • 구현 과정에서 shader 소스 코드를 배포할 필요가 없도록 하기 위해 사용할 수도 있다.
void glGetProgramBinary(GLuint program, GLsizei bufSize, GLsizei *length, GLenum binaryFormat, GLvoid *binary)
---------------------------------------------------------------
program : program obj handle
bufSize : binary 형식으로 최대로 쓸 수 있는 바이트 수
length : binary 데이터의 바이트 수
binaryFormat : 벤더별 binary 형식 토큰
binary : shader 컴파일러에서 생성된 binary 데이터에 대한 포인터.

program binary 검색 후, 해당 결과를 파일 시스템에 저장하거나 glProgramBinary를 사용해 program binary를 구현으로 다시 로드 가능하다.

void glProgramBinary(GLuint program, GLenum binaryFormat, const GLvoid *binary, GLsizei length)
---------------------------------------------------------------
program : program obj handle
binaryFormat : 벤더별 binary 형식 토큰
binary : shader 컴파일러에서 생성된 binary 데이터에 대한 포인터
length : binary 데이터의 바이트 수

OpenGL ES 사양은 특정 binary 형식을 요구하지 않는다.

저장된 program binary의 호환성을 확인하기 위해 glProgramBinary()를 호출 후 glGetProgramiv를 통해 GL_LINK_STATUS를 쿼리한다.

호환되지 않는 경우, shader 소스 코드를 다시 컴파일해야 한다.