눈팅하는 게임개발자 블로그
OpenGL Advanced 5-8 Advanced GLSL 본문
원문 사이트
learnopengl.com/Advanced-OpenGL/Advanced-GLSL
LearnOpenGL - Advanced GLSL
Advanced GLSL Advanced-OpenGL/Advanced-GLSL This chapter won't really show you super advanced cool new features that give an enormous boost to your scene's visual quality. This chapter goes more or less into some interesting aspects of GLSL and some nice t
learnopengl.com
번역 사이트
heinleinsgame.tistory.com/33?category=757483
[Learn OpenGL 번역] 5-8. 고급 OpenGL - 고급 GLSL
고급 GLSL 고급 OpenGL/고급 GLSL 이번 강좌는 여러분의 scene에 대한 시각적 효과를 증대시킬 아주 멋진 기능을 소개하지는 않을 것입니다. 이 강좌는 GLSL의 흥미로운 부분을 소개할 것이고 앞으로 도
heinleinsgame.tistory.com
Advanced GLSL
이번 강좌는 Scene의 시각적 효과를 증대시킬 만한 멋진 기능 보다는
GLSL의 흥미로운 부분에 대해 짚어볼 것이고 앞으로 도움이 될만한 멋진 트릭들도 다뤄볼 것이다.
기본적으로 OpenGL 응용 프로그램을 만들 때 알면 좋은 기능과 이를 더 쉽게 만들어줄 기능들이다.
흥미로운 내장 변수들과 shader의 입력과 출력을 구성하는 새로운 방법, uniform buffer objects라고 불리는 아주
유용한 도구를 다룰 것이다.
GLSL's built-in variables
Shader는 현재 shader 밖의 다른 소스의 데이터가 필요한 경우 데이터를 전달받아야 한다.
이를 vertex attributes, uniforms, sampler로서 전달하고는 했다.
이외에도 GLSL에는 추가적인 여러가지 내장 변수들이 존재한다.
gl_접두사가 붙어있고 이는 데이터를 모으거나 작성하는 등의 추가적인 의미를 가지고 있다.
이미 강좌에서 두 개의 내장 변수를 보았는데, vertex shader의 출력 벡터인
gl_Position과 fragment shader의 gl_FragCoord가 바로 그것이다.
흥미로운 내장 입력 출력 변수들을 다룰 것이고 이들이 어떤 이점을 주는 지에 대해서도 알아볼 것이다.
GLSL에 존재하는 모든 내장 변수들을 다루지는 않을 것이다.
Vertex shader variables
지금까지 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 사이의 거리인 clp-space 위치의 z값과 동일하게 설정하는 것이다.
이 점 크기는 viewer와 멀리 떨어진 vertex 일수록 더 커질 것이다.
결과는 크게 그려진 점일수록 viewer와 멀리 떨어져 있다는 것이다.
Vertex마다 점의 크기를 변하게 하여 흥미로운 기술인 particle을 만들어 낼 수도 있다.
gl_VertexID
gl_Position과 gl_PointSize는 출력 변수이다.
이들을 작성함으로써 결과에 영향을 줄 수 있다.
출력 변수 말고 입력 변수로서 gl_VertexID라는 흥미로운 변수가 있다.
이 정수 변수인 gl_VertexID는 현재 그리고 있는 vertex의 ID를 가지고 있다.
(glDrawElements 함수를 사용하여) Indexed rendering을 할 때 이 변수는 처음 렌더링 명령이
시작될 때부터 지금까지 처리된 vertex의 갯수를 가지고 있다.
지금은 별 도움이 안될 지라도 알고 있으면 도움이 될 것이다.
Fragment shader 변수
Fragment shader 내부에서는 일부 흥미로운 변수들을 확인할 수 있다.
GLSL은 gl_FragCoord와 gl_FrontFacing이라고 불리는 입력 변수들을 제공한다.
gl_FragCoord
depth testing에 대해 다룰 때 gl_FragCoord를 사용해본 적이 있다.
gl_FragCoord 벡터가 특정 fragment의 깊이 값과 동일하기 때문이다.
또한 이 벡터의 x, y 요소를 사용하여 흥미로운 효과를 낼 수 있다.
이 gl_FragCoord의 x, y 요소는 fragment의 window-space 좌표이다. 이 좌표는 좌측 하단부터 시작한다.
glViewport 함수를 이용하여 윈도우 창을 800 x 600으로 설정했으므로 이 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보다 작다는 것은 화면의 왼쪽에 있다는 것을 의미하고
이 오브젝트에 다른 컬러를 줄 수도 있다.
이제 두 개의 완전히 다른 fragment shader 결과를 화면 양쪽에 보여줄 수 있게 되었다.
이는 예를 들어 다른 조명 기술들을 테스팅할 때 아주 유용하다.
gl_FrontFacing
FragmentShader의 또다른 흥미로운 입력 변수는 gl_FrontFacing 변수이다.
Face culling 강좌에서 OpenGL은 면이 전면인지 후면인지 winding 순서에 따라 판별할 수 있다고 했다.
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);
}
컨테이너의 내부를 보면 다른 텍스처가 입혀져 있는 것을 볼 수 있다.
만약 face culling을 활성화시키면 컨테이너의 내부가 보이지 않기 때문에 gl_FrontFacing이 아무 의미가 없음을 유념해야 한다.
gl_FragDepth
glFragCoord는 현재 fragment의 window-space 좌표와 depth 값을 얻을 수 있는 입력 변수이지만
read-only 변수이다. 이 fragment의 window-space 좌표에 영향을 줄수가 없지만 사실 fragment의 depth 값을
설정하는 것은 가능하다.
GLSL은 gl-FragDepth라는 출력 변수를 제공해주는데 이 변수는 shader 내에서 fragment의 depth 값을 설정할 때
사용할 수 있다.
Shader 내부에서 실제로 depth 값을 설정하기 위해 간단히 이 출력 변수에 0.0과 1.0 사이의 값을 작성하면 된다.
gl_FragDepth = 0.0; // 이 fragment는 이제 0.0의 depth 값을 가집니다
Shader에서 gl_FragDepth에 값을 작성하지 않으면 이 변수는 자동으로 gl_FragCoord.z 값을 취한다.
하지만 직접 depth 값을 설정하는 데에는 단점이 존재한다. OpenGL이 모든 early 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으로 greater나 less를 지정함으로써 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의 선언은 block이 입력 block인지 출력 block인지에 따라 in, out 키워드를 사용하는 것만 제외하면
struct를 선언하는 것과 매우 비슷하다.
#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)은 원하는 대로 정할 수 있다.
이는 vertex shader에서는 출력 변수들을 가지고 있어서 vs_out이라는 이름을 사용하는 것이
부자연스럽지는 않지만 Fragment shader에서도 같은 이름을 사용해야 한다.
Fragment shader에서는 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 object를 생성하고 구성하는 데에는 약간의 작업이 필요하다.
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라고 불리고 두 개의 4 x 4 행렬을 저장하는 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 layout과 함께 GLSL은 변수들의 순서를 그대로 유지한 채로 최적화를 위해 uniform 변수들을 다시 위치시킬 수 있다.
각 uniform 변수들이 가질 offset을 모르기 때문에 uniform buffer를 정확히 어떻게 채워야 할지 모른다.
이 정보를 glGetUniformindices같은 함수를 사용해 확인할 수 있지만 이는 이 강좌의 범위를 벗어난다.
Shared layout이 일부 공간을 절약하는 최적화를 제공해주는 반면 각 uniform 변수들에 대한 offset을 알아야 하고
이는 많은 작업을 요구한다. 하지만 일반적으로는 shared layout을 사용하지 않고 std140 layout을 사용한다.
이 std140 layout은 각 변수 타입에 대해 룰에 따라 저마다의 offst을 명시하여 메모리 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 규칙 |
스칼라(int, bool 등) | 각 스칼라는 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 (std140)코드를 추가함으로써 OpenGL에게 이 uniform block은
std140 layout을 사용한다는 것을 알려준다.
buffer를 채우기 전에 offset에 대해서 알아보는 다른 방법이 두가지 존재한다.
이미 shared layout은 확인했고 다른 나머지 layout은 packed layout 이다.
이 packed layout을 사용할 때는 layout이 서로 다른 program 간 같은 값이 유지되리라는 보장이 없다.
이는 컴파일러가 uniform 변수들을 uniform block에 상관없이 최적화할 수 있도록 하기 때문이다.
Using uniform buffer
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에 연결한다. 효과적으로 서로 매칭이 되는 것이다.
이를 다이어그램으로 표현하면 다음과 같다.
보면 알 수 있듯이 여러 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 지정을 사용하여 glGetUniformBlockIndex와 glUniformBlockBinding 함수를 사용하지 않아도 되게 해준다. 다음 코드는 Lights uniform block의 binding point를 명확하게 설정한다. layout(std140, binding = 2) uniform Lights { ... }; |
그런 다음 또한 uniform buffer object를 동일한 binding point에 바인딩해야 하고
이는 glBindBuffersBase나 glBindBufferRange 함수를 통해 수행될 수 있다.
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 파라미터는 다를 것이다.
A simple example
이제 실제 유용한 예제를 살펴보자면, 이전의 강좌들을 살펴보면 지속적으로 3가지의 행렬을 사용해왔다.
projection, view, model 행렬이다. 이 행렬들 중에서 오직 model 행렬만이 자주 변경된다.
만약 이런 동일한 행렬의 모음을 사용하는 여러 shader를 가지고 있다면 uniform buffer object를 사용하는 것이 좋다.
projection, view 행렬을 Matrices라고 불리는 uniform block에 저장할 것이다.
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 호출을 줄일 수 있었다.
결과는 다음과 같다.
각 큐브는 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를 사용할 수 있다.
'공부한거 > OpenGL' 카테고리의 다른 글
OpenGL Advanced 5-10 Instancing (0) | 2020.12.12 |
---|---|
OpenGL Advanced 5-9 Geometry Shader (0) | 2020.12.11 |
OpenGL Advanced 5-7 Advanced Data (0) | 2020.12.10 |
OpenGL Advanced 5-5 Framebuffers (0) | 2020.12.10 |
OpenGL Advanced 5-4 Face culling (0) | 2020.12.10 |