Notice
Recent Posts
Recent Comments
Link
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Archives
Today
Total
관리 메뉴

게임공장

[Learn OpenGL 번역] 5-8. 고급 OpenGL - 고급 GLSL 본문

OpenGL

[Learn OpenGL 번역] 5-8. 고급 OpenGL - 고급 GLSL

짱승_ 2018. 8. 18. 16:13

고급 GLSL

고급 OpenGL/고급 GLSL

  이번 강좌는 여러분의 scene에 대한 시각적 효과를 증대시킬 아주 멋진 기능을 소개하지는 않을 것입니다. 이 강좌는 GLSL의 흥미로운 부분을 소개할 것이고 앞으로 도움이 될만한 멋진 트릭들도 소개할 것입니다. 기본적으로 OpenGL 응용 프로그램을 만들 때 알면 좋은 기능과 여러분의 삶을 쉽게 만들어 줄 기능들입니다.


  우리는 흥미로운 내장 변수들과 shader의 입력과 출력을 구성하는 새로운 방법, uniform buffer objects라고 불리는 아주 유용한 도구를 다룰 것입니다.

GLSL의 내장 변수

  Shader는 혀재 shader 밖의 다른 소스의 데이터가 필요한 경우 데이터를 전달해야 합니다. 우리는 이를 vertex attributes, uniforms, sampler 에서 볼 수 있습니다. 하지만 GLSL에는 추가적인 여러가지 내장 변수들을 가지고 있습니다. gl_ 접두사가 붙어있고 이는 데이터를 모으거나 작성하는 추가적인 의미를 가지고 있습니다. 우리는 이미 강좌에서 두 개의 내장 변수를 보았습니다. vertex shader의 출력 벡터인 gl_Position과 fragment shader의 gl_FragCoord가 바로 그것입니다.


  우리는 흥미로운 내장 입력 출력 변수들을 다룰 것이고 이들이 우리에게 어떠한 이점을 주는지에 대해서도 알아볼 것입니다. 우리는 GLSL에 존재하는 모든 내장 변수들을 다루지는 않을 것이므로 모든 내장 변수들을 보고 싶다면 OpenGL의 wiki에서 확인해보세요.

Vertex shader 변수

  우리는 이미 vertex shader의 clip-space 출력 위치 벡터인 gl_Position을 보았습니다. Vertex shader에서 gl_Position을 설정하는 것은 화면에 무엇이든 렌더링하기 위해 필수적인 요구사항입니다. 우리가 이를 수행하기 전에는 아무것도 볼 수 없습니다.

gl_PointSize

  기초 도형을 렌더링할 때 우리는 GL_POINTS를 선택할 수 있습니다. 이 것은 하나의 vertex가 점으로 렌더링되는 것입니다. 우리는 이 점의 크기를 OpenGL의 glPointSize 함수를 사용하여 설정할 수 있습니다. 하지만 우리는 또한 이 값을 vertex shader에서도 영향을 줄 수 있습니다.


  gl_PointSize라고 불리는 GLSL의 출력 내장 변수는 float 타입 변수이고 점의 너비와 높이를 픽셀 단위로 설정할 수 있습니다. Vertex shader에서 점의 크기를 설정하면 vertex마다의 점 값을 설정할 수 있습니다.


  Vertex shader에서 점의 크기를 수정하는 것은 기본값으로는 비활성화되어 있습니다. 하지만 활성화시키고자 한다면 OpenGL의 GL_PROGRAM_POINT_SIZE를 활성화시켜야 합니다.


glEnable(GL_PROGRAM_POINT_SIZE);  

  이 것의 간단한 예제는 점의 크기를 viewer와 vertex 사이의 거리인 clip-space 위치의 z 값과 동일하게 설정하는 것입니다. 이 점 크기는 viewer와 멀리 떨어진 vertex일수록 더 커질 것입니다.


void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    gl_PointSize = gl_Position.z;    
}  

  결과는 크게 그려진 점일수록 우리와 멀리 떨어져있다는 것입니다.


Points in OpenGL drawn with their gl_PointSize influenced in the vertex shader

  Vertex 마다 점의 크기를 변하게 하여 흥미로운 기술인 particle을 만들어 낼 수도 있습니다.

gl_VertexID

  gl_Positiongl_PointSize출력 변수입니다. 우리는 이들을 작성함으로써 결과에 영향을 줄 수 있습니다. 이 vertex shader는 또한 흥미로운 입력 변수를 제공합니다. gl_VertexID가 바로 그것입니다.


  이 정수 변수인 gl_VertexID는 우리가 지금 그리고 있는 vertex의 ID를 가지고 있습니다. (glDrawElements 함수를 사용하여) Indexed rendering을 할 때 이 변수는 처음 렌더링 명령이 시작될 때부터 지금까지 처리된 vertex의 갯수를 가지고 있습니다.


  지금은 별 도움이 안될지라도 알고 있으면 도움이 될 것입니다.

Fragment shader 변수

  Fragment shader 내부에서 우리는 일부 흥미로운 변수들을 볼 수 있습니다. GLSL은 gl_FragCoordgl_FrontFacing이라고 불리는 흥미로운 입력 변수들을 제공합니다.

gl_FragCoord

  우리는 depth testing을 다룰 때에 gl_FragCoord 변수를 본 적이 있었습니다. gl_FragCoord 벡터가 특정 fragment의 깊이 값과 동일하기 때문이었죠. 하지만 우리는 또한 이 벡터의 x, y 요소를 사용하여 흥미로운 효과를 낼 수 있습니다.


  이 gl_FragCoord의 x, y 요소는 fragment의 window-space 좌표입니다. 이 좌표는 좌측 하단부터 시작하죠. 우리는 glViewport 함수를 이용하여 윈도우 창을 800x600으로 설정했으므로 이 fragment의 window-space 좌표는 x 값은 0에서 800사이, y 값은 0에서 600 사이의 값을 가질 것입니다.


  이 fragment shader를 사용하여 우리는 fragment의 윈도우 좌표를 기반으로 다른 컬러 값을 계산할 수 있습니다. gl_FragCoord 변수가 자주 쓰이는 용도는 다른 fragment 연산과 시각적 효과를 비교하기 위해 쓰입니다. 마치 테크 데모처럼 말이죠. 예를 들어 우리는 하나의 출력은 화면 왼쪽에 렌더링하고 다른 출력은 화면 오른쪽에 렌더링하여 분할된 화면을 만들 수 있습니다. 아래와 같이 fragment의 윈도우 좌표에 따라 다른 출력을 줄수가 있습니다.


void main()
{             
    if(gl_FragCoord.x < 400)
        FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    else
        FragColor = vec4(0.0, 1.0, 0.0, 1.0);        
}  

  윈도우 창의 너비가 800이기 때문에 픽셀의 x 좌표가 400보다 작다는 것은 화면의 왼쪽에 있다는 것을 의미하고 따라서 우리는 이 오브젝트에 다른 컬러를 줄 수 있습니다.


Cube in OpenGL drawn with 2 colors using gl_FragCoord

  우리는 이제 두개의 완전히 다른 fragment shader 결과를 화면 양쪽에 보여줄 수 있게 되었습니다. 이는 예를 들어서 다른 조명 기술들을 테스팅할 때 아주 유용합니다.

gl_FrontFacing

  FragmentShader의 또다른 흥미로운 입력 변수는 gl_FrontFacing 변수입니다. Face culling 강좌에서 OpenGL은 면이 전면인지 후면인지 winding 순서에 따라 판별할 수 있다고 하였습니다. 만약 face culling을 사용하지 않는다면 gl_FrontFacing 변수가 우리에게 현재 fragment가 전면인지 후면인지 알려줍니다. 그러면 우리는 예를 들어 전면에만 다른 컬러를 줄 수가 있습니다.


  gl_FrontFacing 변수는 이 fragment가 전면이면 true, 후면이면 false를 가지는 bool 타입 변수입니다. 예를 들어 우리는 안쪽과 바깥쪽에 다른 텍스처를 입힌 큐브를 만들 수 있습니다.


#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D frontTexture;
uniform sampler2D backTexture;

void main()
{             
    if(gl_FrontFacing)
        FragColor = texture(frontTexture, TexCoords);
    else
        FragColor = texture(backTexture, TexCoords);
}  

  컨테이너의 내부를 보면 다른 텍스처가 입혀져 있는 것을 볼 수 있습니다.


OpenGL container using two different textures via gl_FrontFacing

  만약 face culling을 활성화시키면 컨테이너의 내부가 보이지 않기 때문에 gl_FrontFacing이 아무 의미가 없음을 알아두세요.

gl_FragDepth

  gl_FragCoord는 현재 fragment의 window-space 좌표와 depth 값을 얻을 수 있는 입력 변수이지만 read-only 변수입니다. 이 fragment의 window-space 좌표에 영향을 줄수가 없지만 사실 fragment의 depth 값을 설정하는 것은 가능합니다. GLSL은 gl_FragDepth라는 출력 변수를 제공해주는데 이 변수는 shader 내에서 fragment의 depth 값을 설정할 때 사용할 수 있습니다.


  Shader 내부에서 실제로 depth 값을 설정하기 위해 우리는 간단히 이 출력 변수에 0.01.0 사이의 float 타입의 값을 작성하면 됩니다.


gl_FragDepth = 0.0; // 이 fragment는 이제 0.0의 depth 값을 가집니다

  Shader에서 gl_FragDepth에 값을 작성하지 않으면 이 변수는 자동으로 gl_FragCoord.z 값을 취합니다.


  하지만 우리가 직접 depth 값을 설정하는 데에는 단점이 존재합니다. OpenGL이 모든 early depth testing (depth testing 강좌에서 다루었던) 을 비활성화하기 때문입니다. OpenGL이 fragment shader 가 실행되기 전에 이 fragment가 어떠한 depth 값을 가질지 알 수 없기 때문에 비활성화됩니다.


  gl_FragDepth에 작성함으로써 여러분은 이러한 성능적인 패널티를 고려해야합니다. 하지만 OpenGL 4.2버전 부터 우리는 fragment shader의 시작지점에 depth condition과 함께 gl_FragDepth를 재정의함으로써 둘 사이를 조정할 수 있습니다.



layout (depth_<condition>) out float gl_FragDepth;

  이 condition은 다음과 같은 값들을 취할 수 있습니다.


Condition 설명
any 기본값. Early depth testing이 비활성화되고 대부분의 성능을 잃게됩니다.
greater 오직 gl_FragCoord.z 값보다 큰 depth 값만을 작성할 수 있습니다.
less 오직 gl_FragCoord.z 값보다 작은 depth 값만을 작성할 수 있습니다.
unchanged gl_FragDepth를 작성하면 정확히 gl_FragCoord.z의 값을 작성합니다.

  Depth condition으로 greaterless를 지정함으로써 OpenGL은 여러분이 오직 fragment의 depth 값보다 크거나 작은 값만을 작성할 수 있도록 합니다. 이 방법으로 OpenGL은 early depth test를 fragment의 depth 값보다 작거나 큰 값에 대해서 수행할 수 있습니다.


  Fragment shader에서 depth 값을 증가시키지만 일부 early depth testing을 수행하는 예제는 아래와 같습니다.


#version 420 core // note the GLSL version!
out vec4 FragColor;
layout (depth_greater) out float gl_FragDepth;

void main()
{             
    FragColor = vec4(1.0);
    gl_FragDepth = gl_FragCoord.z + 0.1;
}  

  이 기능은 오직 OpenGL 4.2 버전이상부터 사용가능하다는 것을 알아두세요.

Interface blocks

  지금 까지 vertex shader 에서 fragment shader로 데이터를 보내고 싶을때마다 우리는 여러 입력/출력 변수들을 선언했었습니다. 이들을 동시에 선언하는 것은 한 shader에서 다른 shader로 데이터를 보낼 때 가장 쉬운 방법이지만 여러분이 보낼 데이터가 많아질 수록 응용 프로그램은 커지게 됩니다.


  이 변수들을 묶을 수 있도록 GLSL은 interface blocks라고 불리는 것을 제공해줍니다. 이 것은 이러한 변수들을 같이 그룹화시킬 수 있도록 해줍니다. 이런 interface block의 선언은 struct 선언과 매우 비슷합니다. block이 입력 block인지 출력 block인지에 따라 in, out 키워드를 사용하는 것만 제외하면 말이죠.


#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

out VS_OUT
{
    vec2 TexCoords;
} vs_out;

void main()
{
    gl_Position = projection * view * model * vec4(aPos, 1.0);    
    vs_out.TexCoords = aTexCoords;
}  

  이번에는 vs_out라고 불리는 interface block을 선언했습니다. 이 것은 우리가 다음 shader로 넘겨주고 싶은 모든 출력 변수들을 서로 묶어줍니다. 이 것은 아주 간단한 예제이지만 여러분은 이 것이 shader의 입력/출력을 체계화해줄 수 있다는 것을 알아두세요. 이는 또한 다음 강좌에서 볼 shader의 입력/출력을 배열로 묶어줄 때도 유용합니다.


  그런 다음 또한 다음 shader인 fragment shader에서도 입력 interface block을 선언해야 합니다. 이 block 이름 (VS_OUT) 은 fragment shader에서도 동일한 이름을 가져야 합니다. 하지만 instance 이름 (vertex shader에서 vs_out) 은 여러분이 원하는대로 정할 수 있습니다. 입력 변수들을 가지고 있는데 vs_out과 같은 혼란스러운 이름을 정하지는 마세요.


#version 330 core
out vec4 FragColor;

in VS_OUT
{
    vec2 TexCoords;
} fs_in;

uniform sampler2D texture;

void main()
{             
    FragColor = texture(texture, fs_in.TexCoords);   
} 

  두 interface block의 이름이 동일기만하면 해당 입력과 출력은 서로 연결됩니다. 이는 여러분의 코드를 체계화해주고 geometry shader와 같은 특정 shader 단계를 거칠 때 유용합니다.

Uniform buffer objects

  우리는 OpenGL을 잠시동안 사용해왔고 일부 멋진 트릭들을 배웠지만 몹시 성가심니다. 예를 들어 하나 이상의 shader를 사용할 때 우리는 계속해서 각 shader 에서 정확히 동일한 값을 가지는 uniform 들을 설정해야 합니다.


  OpenGL은 uniform buffer objects라고 불리는 도구를 제공해줍니다. 이 것은 여러 shader program에 걸쳐 동일하게 남아있는 전역 uniform 변수의 모음을 선언할 수 있도록 합니다. Uniform buffer objects를 사용할 때 우리는 연관된 uniform 들을 오직 한번에 설정해야 합니다. 우리는 여전히 shader마다 수작업으로 uniform들을 설정해야 합니다. Uniform buffer obejct를 생성하고 구성하는 데에 약간의 작업이 필요합니다.


  Uniform buffer object는 다른 buffer들과 같은 buffer이기 때문에 우리는 glGenBuffers 함수를 통해 생성할 수 있고 GL_UNIFORM_BUFFER에 바인딩할 수 있으며 모든 연관 uniform 데이터들을 buffer에 저장할 수 있습니다. Uniform buffer object를 위한 데이터가 저장되는 방법에 대해 특정 규칙들이 존재하지만 우리는 나중에 알아볼 것입니다. 먼저, 간단한 vertex shader를 사용하고 projection, view 행렬을 uniform block이라고 불리는 곳에 저장할 것입니다.


#version 330 core
layout (location = 0) in vec3 aPos;

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

uniform mat4 model;

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

  대부분의 우리 예제들에서 우리가 사용할 각 shader에 반복문이 돌때마다 projection, view 행렬을 설정합니다. 이 것은 uniform buffer object의 유용함을 보여줄 훌륭한 예제입니다. 지금 오직 이 행렬들을 하나로 저장하기만 했지만 말이죠.


  여기에서 우리는 Matrices라고 불리고 두 개의 4x4 행렬을 저장하는 uniform block을 선언하였습니다. Uniform block에 들어있는 변수들은 접두사로 block 이름을 쓰지 않아도 직접 사용할 수 있습니다. 그런 다음 OpenGL 코드의 어딘가에서 우리는 이 행렬 변수들을 buffer에 저장합니다. 그러면 이 uniform block을 선언한 각 shader에서는 이 행렬들을 사용할 수 있습니다.


  여러분은 아마 지금 layout (std140) 가 무슨 의미인지 궁금할 것입니다. 이 것은 현재 정의된 uniform block은 지정된 메모리 layout을 사용한다는 뜻입니다. 이 코드는 uniform block layout을 설정합니다.

Uniform block layout

  Uniform block의 내용은 기본적으로 메모리를 예약해 놓은 것 이상도 아닌 buffer 객체에 저장됩니다. 이 메모리 조각이 어떠한 유형의 데이터를 가지고 있는지에 대한 정보를 가지고 있지 않기 때문에 우리는 OpenGL에게 메모리의 어떠한 부분이 어떠한 shader의 어떠한 uniform 변수에 해당하는 지 알려주어야 합니다.


  Shader에서 다음과 같은 uniform block이 선언되어있다고 생각해보세요.


layout (std140) uniform ExampleBlock
{
    float value;
    vec3  vector;
    mat4  matrix;
    float values[3];
    bool  boolean;
    int   integer;
};  

  우리가 알고 싶은 것은 크기(바이트로)와 각 변수들의 offset(block의 시작위치로부터)이므로 우리는 이들을 buffer에 각자의 순서로 위치시킬 수 있습니다. 각 요소의 크기는 명확히 OpenGL에 명시되어있고 직접적으로 C++ 데이터 타입에 해당합니다. 벡터와 행렬은 float의 (커다란) 배열입니다. OpenGL이 명확히 명시하지 않는 것은 변수들 사이의 간격(spacing)입니다. 이는 하드웨어가 변수들을 적합하다고 생각하는 곳에 위치시키도록 합니다. 예를 들어 일부 하드웨어는 vec3를 인접하는 float에 위치시킵니다. 모든 하드웨어가 이를 조작할 수 있는 것은 아니고 vec3 타입을 4개의 float 배열에 추가할 수도 있습니다. 좋은 기능이지만 불편합니다.


  기본적으로 GLSL은 shared layout 이라고 불리는 uniform 메모리 layout을 사용합니다. 이들은 계속해서 여러 program에서 공유(shared)됩니다. Shared layout과 함께 GLSL은 변수들의 순서를 그대로 유지한 채로 최적화를 위해 uniform 변수들을 다시 위치시킬 수 있습니다. 우리는 각 uniform 변수들이 가질 offset을 모르기 때문에 uniform buffer를 정확히 어떻게 채워야 할지 모릅니다. 우리는 이 정보를 glGetUniformindices 같은 함수를 사용하여 확인할 수 있지만 이 강좌의 범위에서 벗어납니다.


  Shared layout이 일부 공간을 절약하는 최적화를 제공해주는 반면 우리는 각 uniform 변수들에 대한 offset들을 알아야하고 이는 많은 작업을 요구합니다. 하지만 일반적으로는 shared layout을 사용하지 않고 std140 layout을 사용합니다. 이 std140 layout은 각 변수 타입에 대해 룰에 따라 저마다의 offset을 명시하여 메모리 layout을 분명하게 명시합니다. 이 것이 명확하게 명시하기 때문에 우리는 수작업으로 각 변수들의 offset을 알아낼 수 있습니다.


  각 변수는 uniform block 내에서 변수가 가질수 있는 공간(여백을 포함)인 base alignment를 가집니다. 이 base alignment는 std140 layout 규칙을 사용하여 계산됩니다. 그런 다음 각 변수에 대해 우리는 block의 시작으로 부터 해당 변수까지의 바이트 offset인 aligned offset을 계산합니다. 이 aligned 바이트 offset은 base alignment의 배수입니다.

  정확한 layout 규칙은 OpenGL uniform buffer 스펙 여기에서 확인할 수 있습니다. 하지만 우리는 가장 많이 쓰이는 규칙들만 아래에 나타내었습니다. int, float, bool과 같은 GLSL의 각 변수 타입들은 4 바이트의 타입으로 정의됩니다. 4 바이트의 각 요소들은 N으로 표시했습니다.


타입 Layout 규칙
스칼라(예. intbool 등) 각 스칼라는 N의 base alignment를 가지고 있습니다.
벡터 2N 아니면 4N. 이는 vec3가 4N의 base alignment를 가진다는 것을 의미합니다.
스칼라나 벡터의 배열 각 요소들은 vec4와 동일한 base alignment를 가집니다.
행렬 벡터의 큰 배열로서 저장됩니다. 각 벡터들은 vec4의 base alignment를 가집니다.
Struct 각 요소들이 위의 규칙에 따라 계산된 크기와 동일합니다. 하지만 vec4의 크기의 배수로 채워져있습니다.

  OpenGL의 설명서의 대부분의 것처럼 이는 예제를 통해서 이해하기 쉽습니다. 우리는 ExampleBlock라고 불리는 uniform block을 사용하고 std140 layout을 사용하여 각 멤버들의 aligned offset을 계산합니다.


layout (std140) uniform ExampleBlock
{
                     // base alignment  // aligned offset
    float value;     // 4               // 0 
    vec3 vector;     // 16              // 16  (16의 배수여야하므로 4->16)
    mat4 matrix;     // 16              // 32  (0 열)
                     // 16              // 48  (1 열)
                     // 16              // 64  (2 열)
                     // 16              // 80  (3 열)
    float values[3]; // 16              // 96  (values[0])
                     // 16              // 112 (values[1])
                     // 16              // 128 (values[2])
    bool boolean;    // 4               // 144
    int integer;     // 4               // 148
}; 

  연습삼아, 여러분 스스로 offset 값을 계산해보세요. 그리고 이 표와 비교해보세요. 이 std140 layout 규칙에 따라 계산된 offset 값들과 함께 우리는 glBufferSubData 함수를 사용하여 변수 데이터를 각 offset에 채울 수 있습니다. 대부분이 효율적이지 않지만 std140 layout은 memory layout이 각 program에 대해서 정의되어진 uniform block의 형태를 유지한다는 장점이 있습니다.


  Uniform block을 정의하기 전에 layout (std14) 코드를 추가함으로써 우리는 OpenGL에게 이 uniform block은 std140 layout을 사용한다라는 것을 알려줍니다. Buffer를 채우기 전에 offset에 대해서 알아보는 다른 방법이 두가지 존재합니다. 우리는 이미 shared layout을 보았고 다른 나머지 layout은 packed입니다. 이 packed layout을 사용할 때는 layout이 program 같에 동일하게 유지되지 않습니다. 이는 컴파일러가 uniform 변수들을 uniform block에 상관없이 최적화할 수 있도록 하기 때문이죠.

Uniform buffers 사용

  Shader에서의 uniform block 선언과 메모리 layout 지정에 대해서 다루었지만 실제로 어떻게 사용하는지는 아직 다루지 않았습니다.


  먼저 glGenBuffers 함수를 통해 uniform buffer object를 생성해야 합니다. buffer object를 가지게되면 이 것을 GL_UNIFORM_BUFFER 타겟에 바인딩하고 glBufferData 함수를 호출하여 충분한 메모리를 할당해줍니다.


unsigned int uboExampleBlock;
glGenBuffers(1, &uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // 152 바이트 메모리 할당
glBindBuffer(GL_UNIFORM_BUFFER, 0);

  이제 buffer에 데이터를 집어넣거나 수정하고 싶을때마다 uboExampleBlock을 바인딩하고 glBufferSubData 함수를 하용하여 메모리를 수정합니다. 우리는 오직 이 buffer를 한번 수정하면 이 buffer를 사용하는 모든 shader들은 이 수정된 데이터를 사용하게 됩니다. 하지만 OpenGL이 어떠한 uniform block이 어떠한 uniform buffer에 해당하는 어떻게 알 수 있을까요?


  OpenGL에는 우리가 uniform buffer를 연결시킬 수 있는 곳에 정의된 binding points가 존재합니다. 우리가 uniform buffer를 생성하여 우리는 이 binding points들 중 하나에 연결을 하고 또한 이를 shader의 uniform block을 동일한 binding point에 연결합니다. 효과적으로 서로 연결이 되는 것이지요. 다음 다이어그램이 이를 설명합니다.


Diagram of uniform binding points in OpenGL

  보시다시피 우리는 여러 uniform buffer들을 여러 binding point에 바인딩할 수 있습니다. shader A와 shader B 둘 다 동일한 binding point 0에 연결되어있기 때문에 그들의 uniform block은 uboMatrices의 동일한 uniform data를 공유합니다. 요구되어야할 사항은 두 개의 shader 모두 같은 Matrices uniform block을 정의해야 된다는 것이지요.


  특정 binding point에 uniform block을 연결하기 위해 우리는 glUniformBlockBinding 함수를 호출합니다. 이 함수는 첫 번째 파라미터로 program object를 받아들이고 uniform block index와 연결할 binding point를 받아들입니다. 이 uniform block index란 shader에 정의된 uniform block의 location index입니다. 이는 glGetUniformBlockIndex 함수를 통해 얻을 수 있습니다. 이 함수는 program object를 받아들이고 uniform block의 이름을 받아들입니다. 우리는 Lights uniform block을 binding point 2에 설정할 수 있습니다.


unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");   
glUniformBlockBinding(shaderA.ID, lights_index, 2);

  우리는 이를 shader에 반복해야한다는 것을 알아두세요.

  OpenGL 4.2버전부터 uniform block의 binding point를 shader에 명확히 저장하는게 가능해졌습니다. 이는 또다른 layout 지정를 사용하여 glGetUniformBlockIndexglUniformBlockBinding 함수를 사용하지 않아도 되게 해줍니다. 다음 코드는 Lights uniform block의 binding point를 명확하게 설정합니다.

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

  그런 다음 또한 우리는 uniform buffer object를 동일한 binding point에 바인딩해야하고 이는 glBindBufferBaseglBindBufferRange 함수를 통해 수행될 수 있습니다.


glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock); 
// 또는
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);

  glBindbufferBase 함수는 target, binding point index, uniform buffer object를 파라미터로 받습니다. 이 함수는 uboExampleBlock을 binding point 2에 연결시키고 이 이후부터 binding point의 양쪽은 연결되어집니다. 또한 여러분은 glBindBufferRange 함수를 사용할수도 있습니다. 이 함수는 추가적으로 offset과 크기 파라미터를 받습니다. 이 방법으로 여러분은 오직 지정한 uniform buffer의 영역만 binding point에 바인딩할 수 있습니다. glBindBufferRange 함수를 사용하여 여러 다른 uniform block들을 하나의 uniform buffer object에 연결시킬 수 있습니다.


  이제 모든 것이 세팅되었으므로 우리는 uniform buffer에 데이터를 추가할 수 있습니다. 우리는 모든 데이터를 하나의 바이트 배열로 추가하거나 glBufferSubData 함수를 사용하여 buffer의 특정 부분을 수정할 수 있습니다. Uniform 변수 boolean을 수정하기 위해 우리는 다음과 같이 uniform buffer object를 수정할 수 있습니다.


glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // GLSL에서 bool은 4바이트로 표현되므로 integer 타입으로 저장합니다
glBufferSubData(GL_UNIFORM_BUFFER, 144, 4, &b); 
glBindBuffer(GL_UNIFORM_BUFFER, 0);

  그리고 같은 과정을 uniform block 내부의 모든 다른 uniform 변수들에 적용할 수 있습니다. 하지만 range 파라미터는 다를 것입니다.

간단한 예제

  이제 실제 유용한 예제를 설명드리겠습니다. 이전의 강좌들을 살펴보면 우리는 계속해서 3가지의 행렬을 사용해왔습니다. projection, view, model 행렬 말이죠. 이 행렬들 중에서 오직 model 행렬만 자주 변경됩니다. 만약 우리가 이러한 동일한 행렬의 모음을 사용하는 여러 shader를 가지고 있다면 uniform buffer object를 사용하는 것이 좋습니다.


  우리는 projection, view 행렬을 Matrices라고 불리는 uniform block에 저장할 것입니다. Model 행렬을 제외할 것입니다. Model 행렬은 shader 사이에서 자주 변경되므로 uniform buffer object를 사용하는 이점이 없기 때문입니다.


#version 330 core
layout (location = 0) in vec3 aPos;

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

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

  여기에 특별한 것은 없습니다. std140 layout을 사용하는 uniform block을 사용한다는 점만 제외하고 말이죠. 우리가 해야할 일은 4개의 큐브를 그리는 것인데 각 큐브는 다른 shader program을 사용합니다. 각 4 shader program은 동일한 vertex shader를 사용하지만 shader마다 다른 오직 하나의 색만 출력하는 다른 fragment shader를 사용합니다.


  먼주 우리는 vertex shader의 uniform block을 binding point 0으로 설정합니다. 이를 각 shader에 대해 수행해야한다는 것을 알아두세요.


unsigned int uniformBlockIndexRed    = glGetUniformBlockIndex(shaderRed.ID, "Matrices");
unsigned int uniformBlockIndexGreen  = glGetUniformBlockIndex(shaderGreen.ID, "Matrices");
unsigned int uniformBlockIndexBlue   = glGetUniformBlockIndex(shaderBlue.ID, "Matrices");
unsigned int uniformBlockIndexYellow = glGetUniformBlockIndex(shaderYellow.ID, "Matrices");  
  
glUniformBlockBinding(shaderRed.ID,    uniformBlockIndexRed, 0);
glUniformBlockBinding(shaderGreen.ID,  uniformBlockIndexGreen, 0);
glUniformBlockBinding(shaderBlue.ID,   uniformBlockIndexBlue, 0);
glUniformBlockBinding(shaderYellow.ID, uniformBlockIndexYellow, 0);

  그 다음 실제 uniform buffer object를 생성하고 binding point 0에 바인딩합니다.


unsigned int uboMatrices
glGenBuffers(1, &uboMatrices);
  
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferData(GL_UNIFORM_BUFFER, 2 * sizeof(glm::mat4), NULL, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
  
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));

  먼저 buffer에 충분한 메모리를 할당합니다. 이 메모리는 glm::mat4 두 개분의 크기입니다. GLM의 행렬 타입 크기는 GLSL의 mat4와 직접적으로 해당됩니다. 그런 다음 우리는 buffer의 특정 범위를 지정하고 이 경우에는 buffer 전체입니다. 그리고 binding point 0에 바인딩합니다.


  이제 남은 해야할 일들은 실제로 buffer를 채우는 것입니다. 우리가 field of view 값을 상수로 유지시키고 싶다면 (카메라 줌이 없다) 우리는 오직 이 것을 한번만 정의하기만 하면 됩니다. 이는 이 데이터를 buffer에 한번만 삽입하면 되다는 의미입니다. 우리는 buffer object에 충분한 메모리를 할당하였기 때문에 glBufferSubData 함수를 사용하여 게임 루프에 들어가기 전에 projection 행렬을 저장할 수 있습니다.


glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)width/(float)height, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);  

  여기에서 우리는 uniform buffer의 처음 반절 공간에 projection 행렬을 저장합니다. 각 렌더링 루프에서 오브젝트를 그리기 전에 우리는 buffer의 두 번째 공간에 view 행렬을 넣습니다.


glm::mat4 view = camera.GetViewMatrix();	       
glBindBuffer(GL_UNIFORM_BUFFER, uboMatrices);
glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);  

  그리고 이제 됬습니다. Matrices uniform block을 가지고 있는 각 vertex shader는 이제 uboMatrices에 저장되어 있는 데이터를 가지고 있을 것입니다. 그리고 이제 4개의 다른 shader들을 사용하여 4개의 큐브를 그려보면 이들의 projection, view 행렬이 동일하게 유지되는 것을 알 수 있을 것입니다.


glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate(model, glm::vec3(-0.75f, 0.75f, 0.0f));	// 좌측 상단으로 이동
shaderRed.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);        
// ... 녹색 큐브 그리기
// ... 파란색 큐브 그리기
// ... 노란색 큐브 그리기	  

  우리가 설정해야할 uniform은 오직 model uniform입니다. Uniform buffer object를 사용하여 shader마다의 약간의 uniform 호출을 줄일 수 있었습니다. 결과는 다음과 같습니다.


Image of 4 cubes with their uniforms set via OpenGL's uniform buffer objects

  각 큐브는 model 행렬에 의해 화면의 한쪽으로 이동되어졌고 서로 다른 fragment shader 때문에 각자 색이 달라졌습니다. 이는 비교적 간단한 예제이지만 거대한 렌더링 응용 프로그램은 수백개의 shader program을 가질수 있습니다. 이 경우에는 uniform buffer object가 빛을 발하기 시작할 것입니다.


  여기에서 전체 소스 코드를 확인할 수 있습니다.


  Uniform buffer object는 여러가지 장점을 가지고 있습니다. 첫 째, 많은 uniform들을 한번에 설정하는 것은 하나하나 설정하는 것보다 빠릅니다. 둘 째, 여러 shader에 걸쳐있는 동일한 uniform을 수정하고 싶을 때 uniform buffer에 있는 uniform을 한번 수정하기가 쉽습니다. 직접적으로 드러나지 않은 마지막 장점은 uniform buffer object를 사용하여 shader에서 아주 많은 uniform들을 사용할 수 있다는 것입니다. OpenGL은 관리할 수 있는 uniform의 갯수에 제한이 있습니다. 이는 GL_MAX_VERTEX_UNIFORM_COMPONENTS를 사용하여 확인할 수 있습니다. Uniform buffer object를 사용할 때 이 제한은 아주 높아집니다. 그래서 uniform 갯수의 한계치에 닿았을 때(예를 들어 스켈레톤 애니메이션)마다 여러분은 항상 uniform buffer object를 사용할 수 있습니다.



출처 : https://learnopengl.com, Joey de Vries

반응형