link : https://learnopengl.com/Advanced-OpenGL/Advanced-GLSL
Advanced GLSL
이 튜토리얼에서는 장면의 시각적 품질을 크게 향상시키는 고급 기능을 소개하지 않는다.
이 튜토리얼은 GLSL의 흥미로운면과 미래의 노력에 도움이 될만한 좋은 트릭에 다소간 차이가 있다.
기본적으로 GLSL과 함께 OpenGL 응용 프로그램을 만들 때 더 쉽게 익숙해질 수 있는 기능이 있습니다.
흥미로운 내장 변수, 쉐이더의 입출력을 구성하는 새로운 방법, 균일 버퍼 객체라는 매우 유용한 도구에 대해 살펴 보겠습니다.
GLSL's built-in variables
쉐이더는 최소한이지만 현재 쉐이더 밖의 다른 소스의 데이터가 필요한 경우 데이터를 전달해야한다.
우리는 정점 속성, 유니폼, 샘플러를 통해 이를 수행하는 방법을 배웠다. 그러나 gl_ 접두사가 붙은 GLSL에 의해 정의 된 몇가지
추가 변수가 있는데, 이는 우리에게 데이터 수집 및 쓰기를 위한 추가 수단을 제공한다. 지금까지 튜토리얼에서 이미 두 가지를 보았다.
gl_position은 정점 쉐이더의 출력 벡터이고, 조각 쉐이더에서는 gl_FragCoord이다.
GLSL에 내장된 몇 가지 흥미로운 기본 제공 입력 변수와 출력 변수에 대해 설명하고, 이것이 우리에게 어떻게 도움이 되는지 설명한다.
우리가 GLSL에 존재하는 모든 내장 변수를 논의하지 않기 때문에 모든 내장 변수를 보고 싶다면 OpenGl의 wiki를 확인할 수 있다.
Vertex shader variables
이미 우리는 정점 쉐이더의 클립 공간 출력 위치 벡터인 gl_Position을 보았다. 화면에 무엇인가를 렌더링하고 싶다면
정점 쉐이더에서 gl_Position을 설정하는 것은 엄격한 요구 사항이다. 우리가 전에 보지 못한 것은 없다.
gl_PointSize
우리가 선택할 수 있는 렌더링 프리미티브 중 하나는 GL_POINTS이다. 이 경우 각 단일 꼭지점은 프리미티브이며 점으로 렌더링된다.
OpenGL은 glPointSize 함수를 통해 렌더링되는 점의 크기를 설정할 수도 있지만 이 값을 정점 쉐이더에 적용 할 수도 있다.
GLSL에 의해 정의된 출력 변수는 점의 너비와 높이를 픽셀로 설정 할 수 있는 float 변수인 gl_PointSize라고 한다.
정점 쉐이더에서 포인트의 크기를 기술함으로써 정점 당 이 포인트 값에 영향을 줄 수 있다.
정점 쉐이더의 포인트 크기에 영향을 주는 것은 기본적으로 비활성화 되어 있다. 하지만 이것을 활성화하려면 OpenGL의
GL_PROGRAM_POINT_SIZE:
glEnable (GL_PROGRAM_POINT_SIZE);
포인트 크기에 영향을 주는 간단한 예는 포인트 크기를 클립 공간 위치의 z 값과 동일하게 설정하는 것이다. 이 값은 뷰어에 대한 정점의
거리와 같습니다. 그러면 관측점인 꼭지점에서 멀리 떨어져 있는 지점 크기가 증가해야한다.
void main()
{
gl_Position = projection * view * model * vec4(aPos, 1.0);
gl_PointSize = gl_Position.z;
}
그 결과, 우리가 그려 놓은 점들이 더 멀리 떨어지면 커지게된다:
정점 당 포인터 크기를 변화시키는 것이 입자 생성과 같은 기술에 대해 흥미롭다는 것을 상상할 수 있다.
gl_VertexID
gl_Position과 gl_PointSize는 그 값이 정점 쉐이더에서 출력으로 읽혀지기 때문에 출력 변수이다. 우리는 그들에게 글쓰기로
결과에 영향을 줄 수 있다. 정점 쉐이더는 또한 gl_VertexID라 불리는 흥미로운 입력 변수를 읽을 수 있따.
정수 변수 gl_VertexID는 그리는 정점의 현재 ID를 유지한다. glDrawElements로 인덱스 렌더링을 할 때 이 변수는 우리가 그리는
정점의 현재 인덱스를 유지한다. (glDrawArrays를 통해) 인덱스 없이 드로잉 할 때, 이 변수는 렌더 호출의 시작 이후 현재 처리된
정점의 번호를 유지한다.
지금은 별로 유용하지 않지만, 이와 같은 정보에 액세스 할 수 있다는 것을 알고 있는 것이 좋다.
Fragment shader variables
조각 쉐이더 내에서 우리는 또한 흥미있는 변수에 접근 할 수 있다. GLSL은 gl_FragCoord와 gl_FrontFacing이라는 흥미로운 입력 변수를 제공한다.
gl_FragCoord
gl_FragCoord 벡터의 z 구성 요소가 특정 조각의 깊이 값과 같기 때문에 깊이 테스트에 대한 토론에서 gl_FragCoord를 두 번 보았다.
그러나 흥미롱누 효과를 내기 위해 벡터의 x 및 y 구성 요소를 사용할 수도 있다.
gl_FragCoord의 x 및 y 구성 요소는 윈도우의 왼쪽 하단에서 시작한다. 우리는 조각의 윈도우 공간 좌표가 0~800 x값과 0~600 y값을 갖도록
glViewport로 지정했다.
조각 쉐이더를 사용해 조각의 윈도우 좌표를 기반으로 다른 색상 값을 계산할 수 있다. gl_FragCoord 변수의 일반적인 용도는 기술 데모에서
일반적으로 볼 수 있는 여러 조각 계산의 시각적 출력을 비교하는 것이다. 예를 들어, 한 출력을 창의 왼쪽으로 렌더링하고, 다른 출력을
창의 오른쪽으로 렌더링해 화면을 두 개로 나눌 수 있다. 조각의 창 좌표를 기반으로 다른 색상을 출력하는 조각 쉐이더 예제는 다음과 같다:
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);
}
윈도우의 왼쪽과 오른쪽의 색상이 다르게 나타난다.
이제 완전히 다른 두 가지 조각 쉐이더 결과를 계산하고 각 조각 쉐이더 결과를 창의 다른면에 표시 할 수 있다.
이것은 예를 들어 다른 조명 기법을 테스트하는데 적합하다.
gl_FrontFacing
조각 쉐이더의 또 다른 흥미로운 입력 변수는 gl_FrontFacing 변수이다. face culling 튜토리얼에서 우리는 OpenGL이 정점의 와인딩
순서로 인해 face가 앞면인지 뒷면인지 파악할 수 있다고 언급했다. GL_FACE_CULL을 사용해 컬링을 사용하지 않으면
gl_FrontFacing 변수는 현재 조각이 앞면 또는 뒷면의 일부인지 여부를 알려준다. 그런 다음 예를 들어 앞면에 다른 색상을 계산하기로
결정할 수 있다.
gl_FrontFacing 변수는 조각이 정면의 일부일 경우 true이고 그렇지 않으면 false를 반환한다. 우리는 예를 들어 큐브를 이 방식으로
내부와 외부에 다른 텍스처로 만들 수 있다:
#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을 사용하면 컨테이너 안의 face를 볼 수 없으므로 gl_FrontFacing을 사용하면 무의미하다.
gl_FragDepth
입력 변수 gl_FragCoord는 윈도우 공간 좌표를 읽고 현재 조각의 깊이 값을 얻을 수 있는 입력 변수이지만 읽기 전용 변수이다.
조각의 윈도우 공간 좌표에 영향을 줄 수는 없지만 실제로 조각의 깊이 값을 설정할 수 있다. GLSL은 gl_FragDepth라는 출력 변수를
사용해 쉐이더 내에서 조각의 깊이 값을 설정할 수 있다.
쉐이더에서 깊이 값을 실제로 설정하려면 간단히 출력 변수에 0.0에서 1.0 사이의 float 값을 쓴다:
gl_FragDepth = 0.0; // this fragment now has a depth value of 0.0
쉐이더가 gl_FragDepth에 값을 쓰지 않으면 변수는 자동으로 gl_FragCoord.z에서 값을 가져온다.
그러나 OpenGL은 조각 쉐이더에서 gl_FragDepth에 쓰는 즉시 깊이 테스트 튜토리얼에서 논의된대로 초기의 모든 깊이 테스트를
비활성화 하기 때문에 자체 값으로 깊이 값을 설정하는 것이 큰 단점이 된다. 조각 쉐이더는 이 깊이 값을 완전히 바꿀 수 있기
때문에 조각 쉐이더를 실행하기 전에 조각이 가질 깊이 값을 OpenGL이 알 수 없으므로 비활성화되어 있다.
gl_FragDepth에 쓰려면 이 성능 저하를 고려해야한다. 그러나 OpenGL 4.2에서, 깊이 조건을 가진 조각 쉐이더의 맨 위에 있는
gl_FragDepth 변수를 다시 선언함으로써 양측을 중재할 수 있다:
layout (depth_<condition>) out float gl_FragDepth;
이 조건에는 다음 값을 사용할 수 있다:
Condition | Description |
---|---|
any | The default value. Early depth testing is disabled and you lose most performance. |
greater | You can only make the depth value larger compared to gl_FragCoord.z . |
less | You can only make the depth value smaller compared to gl_FragCoord.z . |
unchanged | If you write to gl_FragDepth , you will write exactly gl_FragCoord.z . |
depth 조건을 greater 혹은 less로 지정하면 OpenGL은 단편의 깊이 값보다 크거나 큰 깊이 값만 작성한다는 가정을 할 수 있다.
이 방법으로 OpenGL은 깊이 값이 단편의 깊이 값보다 작은 경우에도 초기 테스트를 수행 할 수 있다.
조각 쉐이더에서 깊이 값을 증가시키지만 초기 깊이 테스트 중 일부를 보존하려는 예제는 아래의 조각 쉐이더에 나와있다:
#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
지금까지 정점에서 조각 쉐이ㅓㄷ로 데이터를 보내고 싶을 때마다 매칭되는 input/output 변수들을 선언했었다.
하나의 쉐이더에서 다른 쉐이더로 데이터를 보내는 가장 쉬운 방법은 한 번에 하나씩 선언하는 것이다. 그러나 응용 프로그램이
커지면 배열 또는 구조체를 포함 할 수 있는 여러 변수들을 보내고 싶을 것이다.
이러한 변수들을 체계화 할 수 있도록 GLSL은 Interface block을 제공해 변수들을 그룹화 할 수 있다. 이러한 Interface 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이라는 인터페이스 블록을 선언해 다음 쉐이더로 보낼 모든 출력 변수를 그룹화했다.
이것은 사소한 일례이지만 쉐이더의 input/output을 구성하는데 도움이 될 것이다. 또한, 쉐이더 input/output을 배열로 그룹화하고
다음 튜토리얼에서 geometry 쉐이더에 대해 설명할 때 유용하다.
그런 다음 조각 쉐이더인 다음 쉐이더에서 입력 인터페이스 블록을 선언해야한다. 조각 쉐이더에서는 블록 이름이 동일해야하지만
인스턴스 이름(VS_OUT을 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);
}
두 인터페이스 블록 이름이 동일하면 해당 입력 및 출력이 서로 일치한다. 이는 코드를 구성하는 데 유용한 기능이며 기하 구조 쉐이더와
같은 특정 쉐이더 단계를 통과 할 때 유용하다.
Uniform buffer objects
우리는 꽤 오랫동안 OpenGL을 사용해왔고, 꽤 멋진 트릭을 배웠다. 하지만 몇 가지 성가심도 있었다. 예를 들어, 쉐이더를 두 개 이상 사용하는
경우 쉐이더마다 대다수가 똑같은 유니폼 변수를 게속 설정해야했다.
OpenGL은 uniform 버퍼 객체라는 도구를 제공한다. 이 도구를 사용하면 여러 쉐이더 프로그램에서 동일하게 유지되는 전역 균일 변수 세트를
선언할 수 있다. 균일한 버퍼 객체를 사용할 때는 관련 유니폼을 한 번만 설정해야한다. 균일한 버퍼 객체를 만들고 구성하려면 약간의 작업이
필요하다.
균일한 버퍼 객체는 다른 버퍼와 마찬가지로 버퍼이기 때문에 glGenBuffers를 통해 버퍼를 만들 수 있고, GL_UNIFORM_BUFFER 타겟에 바인드해
모든 관련 유니폼 데이터를 버퍼에 저장할 수 있다. 균일한 버퍼 객체에 대한 데이터를 저장하는 방법에 대해서는 일정한 규칙이 있으며
나중에 이에 대해 알아보겠다. 먼저 간단한 정점 쉐이더를 사용해 projection 및 view 매트릭스를 균일한 블록에 저장한다:
#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);
}
대부분의 샘플에서 우리는 projection을 설정하고 유니폼 매트릭스를 볼 때마다 우리가 사용하는 각 쉐이더에 대한 반복 렌더링을
렌더링했다. 이것은 균일한 버퍼 객체가 유용해지는 완벽한 예이다. 이제는 이 행렬을 한 번만 저장하면되므로 유용하다.
여기서 우리는 두 개의 4x4 행렬을 저장하는 Matrices라고 하는 유니폼 블록을 선언했다. 균일한 블록의 변수는 블록 이름을 접두사로
사용하지 않고 직접 액세스 할 수 있다. 그런 다음 이 행렬 값을 OpenGL 코드 어딘가에 버퍼에 저장하고, 이 균일 블록을 선언한 각 쉐이더는
행렬에 액세스 할 수 있다.
layout(std14) 이 무엇을 의미하는지 궁금할 것이다. 이것이 말하는 것은 현재 정의된 uniform 블록이 content를 위해 특정 메모리 레이아웃을
사용한다는 것이다. 이 statement는 균일한 블록 레이아웃을 설정한다.
Uniform block layout
uniform block의 내용은 기본적으로 예약된 메모리 조각 일뿐인 버퍼 객체에 저장된다. 이 메모리 조각은 어떤 종류의 데이터를 보유하고
있는지에 대한 정보가 없으므로 메모리의 어떤 부분이 쉐이더의 어떤 균일 변수에 해당하는지 OpenGL에 알려야한다.
쉐이더에서 다음 uniform block을 상상해보아라:
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};
우리가 알아야 할 것은 각 변수의 크기와 offset이다. 따라서 버퍼에 해당 순서대로 배치 할 수 있다. 각 요소의 크기는 OpenGL에 명시되어 있으며
C++ 데이터 유형에 직접 해당한다. 벡터와 행렬은 수레 배열이다. OpenGL에서 명확하게 언급하지 않은 것은 변수 사이의 간격이다.
이렇게 하면 하드웨어가 변수를 적합한 위치에 배치 할 수 있다. 일부 하드웨어는 예를 들어 float에 인접한 vec3를 배치 할 수 있다.
모든 하드웨어가 이것을 처리 할 수 있는 것은 아니며 float를 추가하기 전에 vec3를 4개의 부동 소수점 배열에 채운다.
훌륭한 기능이지만 우리에게는 불편하다.
기본적으로 GLSL은 shared layout이라고 하는 균일한 메모리 레이아웃을 사용한다. offset은 하드웨어에 의해 정의되고 여러 프로그램 간에
일관되게 공유되기 때문에 공유된다. 공유 레이아웃을 사용하면 GLSL은 변수의 순서가 그대로 유지되는 한 최적화를 위해 유니폼 변수를
재배치 할 수 있다. 각 유니폼 변수가 상쇄되는 위치를 알 수 없기 때문에 유티폼 버퍼를 정확하게 채우는 방법을 알지 못한다.
glGenUniformIndices와 같은 함수로 이 정보를 쿼리 할 수 있지만 이 튜토리얼에서는 다루지 않는다.
shared layout은 우리에게 공간 절약 최적화를 제공하지만 많은 개별적인 유니폼 변수에 대해 각 offset을 쿼리해야한다.
이는 많은 작업으로 이어진다. 그러나 일반적인 방법은 공유 레이아웃을 사용하지 않고, std140 레이아웃을 사용하는 것이다.
std140 layout은 일련의 규칙에 의해 관리되는 각각의 오프셋을 명시함으로써 각 변수 유형에 대한 메모리 레이아웃을 명시적으로 나타낸다.
이것은 명시적으로 언급되었기 때문에 각 변수에 대한 오프셋을 수동으로 계산할 수 있다.
각 변수는 일정한 블록 내에서 변수가 취하는 공간과 동일한 기본 정렬을 가진다. 이 기본 정렬은 std140 레이아웃 규칙을 사용해 계산된다.
그런 다음 각 변수에 대해 정렬된 오프셋을 계산한다. 이 오프셋은 블록의 시작부터 변수의 바이트 오프셋이다. 변수의 정렬된 바이트 오프셋은
기본 정렬의 배수와 같아야한다.
정확한 레이아웃 룰은 OpenGL의 유니폼 버퍼 사양에서 찾을 수 있지만, 아래에서 가장 일반적인 규칙을 나열한다.
int, float, bool과 같은 GLSL의 각 변수 유형은 4바이트의 각 entity가 N으로 표시되는 4바이트 수량으로 정의된다.
Type | Layout rule |
---|---|
Scalar e.g. | Each scalar has a base alignment of N. |
Vector | Either 2N or 4N. This means that a |
Array of scalars or vectors | Each element has a base alignment equal to that of a |
Matrices | Stored as a large array of column vectors, where each of those vectors has a base alignment of |
Struct | Equal to the computed size of its elements according to the previous rules, but padded to a multiple of the size of a |
대부분의 OpenGL 사양과 마찬가지로 예제를 통해 이해하기도 쉽다. 이전에 소개한 ExampleBlock이라는 균일 블록을 가져와서
std140 레이아웃을 사용해 각 멤버에 대해 정렬된 오프셋을 계산해보자:
layout (std140) uniform ExampleBlock
{
// base alignment // aligned offset
float value; // 4 // 0
vec3 vector; // 16 // 16 (must be multiple of 16 so 4->16)
mat4 matrix; // 16 // 32 (column 0)
// 16 // 48 (column 1)
// 16 // 64 (column 2)
// 16 // 80 (column 3)
float values[3]; // 16 // 96 (values[0])
// 16 // 112 (values[1])
// 16 // 128 (values[2])
bool boolean; // 4 // 144
int integer; // 4 // 148
};
연습으로 오프셋 값을 직접 계산해 이 테이블과 비교해보아라. 계산된 오프셋 값을 사용해 std140 레이아웃의 규칙에 따라
glBufferSubData와 같은 함수를 사용해 각 오프셋에서 변수 데이터로 버퍼를 채울 수 있다. 가장 효율적인 것은 아니지만 std140 레이아웃은
이 균일한 블록을 선언한 각 프로그램에서 메모리 레이아웃이 동일하게 유지된다는 것을 보장한다.
균일한 블록의 정의 이전에 명령문 layout (std140)을 추가함으로써 OpenGL에게 이 uniform 블록이 std140 레이아웃을 사용한다는 것을
알 수 있다. 버퍼를 채우기 전에 각 오프셋을 쿼리해야하는 두 가지 레이아웃 중에서 선택할 수 있다. 공유 레이아웃과 나머지 레이아웃이
이미 포장되어 있다. pack된 레이아웃을 사용할 때 컴파일러가 쉐이더마다 다를 수 있는 균일한 블록에서 균일한 변수를 최적화 할 수
있으므로 프로그램간에 레이아웃이 동일하게 유지된다는 보장이 없다.
Using uniform buffers
쉐이더에서 균일한 블록을 정의하고 메모리 레이아웃을 지정하는 방법에 대해 논의했지만 아직 실제로 사용하는 방법은 논의하지 않았다.
먼저 glGenBuffers를 통해 수행되는 균일한 버퍼 객체를 생성해야한다. 일단 버퍼 객체가 있으면 GL_UNIFORM_BUFFER 타겟에 바인드하고
glBufferData를 호출해 충분한 메모리를 할당한다:
unsigned int uboExampleBlock;
glGenBuffers (1, &uboExampleBlock);
glBindBuffer (GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData (GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // allocate 150 bytes of memory
glBindBuffer (GL_UNIFORM_BUFFER, 0);
이제 우리는 버퍼에 데이터를 업데이트하거나 삽입하려고 할 때마다 uboExampleBlock에 바인딩하고 glBufferSubData를 사용해
메모리를 업데이트한다. 이 균일 버퍼를 한 번만 업데이트하면 된다. 이 버퍼를 사용하는 모든 쉐이더는 이제 업데이트된 데이터를 사용한다.
하지만, OpenGL은 유니폼 버퍼가 어떤 유니폼 블록에 해당하는지 알 수 있나?
OpenGL 문법에는 uniform 버퍼를 연결할 수 있는 여러 개의 바인딩 포인트가 정의되어 있다. 균일한 버퍼를 만들고 나면 해당 바인딩 포인트 중
하나에 링크하고, 쉐이더의 균일 블록을 동일한 바인딩 포인트에 연결해 서로 효과적으로 연결한다. 다음 다이어그램에서는 이를 보여준다:
보시다시피 여러 개의 균일한 버퍼를 다른 바인딩 포인터에 바인딩 할 수 있다. 쉐이더 A와 쉐이더 B는 모두 동일한 바인딩 포인트 0에
연결된 균일한 블록을 가지고 있기 때문에 균일 블록은 uboMatrices에서 발견되는 균일한 데이터를 공유한다. 두 쉐이더 모두 동일한
Matrices uniform 블록을 정의해야한다는 요구 사항이 있었다.
uniform 블록을 특정 바인딩 포인트로 설정하기 위해 프로그램 객체를 첫 번째 인수로 사용하는 glUniformBlockBinding을 호출한다.
균일 블록 인덱스는 쉐이더에서 정의된 균일 블록의 위치 인덱스이다. 이것은 프로그램 객체와 uniform 블록의 이름을 받아들이는
glGetUniformBlockIndex를 호출해 검색 할 수 있다. 다이어그램에서 다음과 같이 바인딩 포인트 2에 라이트 균일 블록을 설정할 수 있다:
unsigned int lights_index = glGetUniformBlockIndex (shaderA.ID, "Lights");
glUniform BlockBinding (shaderA.ID, lights_index, 2);
각 쉐이더에 대해 이 프로세스를 반복해야한다는 점에 유의해라.
.
layout(std140, binding = 2) uniform Lights { ... };
그런 다음 균일 버퍼 객체를 동일한 바인딩 포인트에 바인딩해야한다. 이 작업은 glBindBufferBase 또는 glBindBufferRange를 사용해 할 수 있다.
glBindBuffer Base (GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// or
glBindBuffer Range (GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
glBindbufferBase 함수는 타겟, 바인딩 포인트 인덱스 및 uniform 버퍼 객체를 인자로 기대한다. 이 함수는 uboExampleBlock을
바인딩 포인트 2에 연결하며 바인딩 포인트의 양측에 있는 이 지점을 연결한다. 여분의 오프셋 및 크기 매개 변수를 예상하는
glBindBufferRange 함수를 사용할 수도 있다. 이 방법으로 균일 버퍼의 특정 범위만 바인딩 포인트에 바인딩 할 수 있다.
glBindBufferRange를 사용하면 단일 균일 버퍼 객체에 링크된 여러 개의 다른 균일한 블록을 가질 수 있다.
모든 것이 설정되었으므로 균일 버퍼에 데이터를 추가 할 수 있다. glBufferSubData를 사용할 때마다 모든 데이터를 단일 바이트 배열로
추가하거나 버퍼의 일부를 업데이트 할 수 있다. uniform 변수 boolean을 업데이트하기 위해 다음과 같이 uniform 버퍼 객체를
업데이트 할 수 있다:
glBindBuffer (GL_UNIFORM_BUFFER, uboExampleBlock);
int b = true; // bools in GLSL are represented as 4 bytes, so we store it in an integer
glBufferSubData (GL_UNIFORM_BUFFER, 144, 4, &b);
glBindBuffer (GL_UNIFORM_BUFFER, 0);
그리고 동일한 절차는 uniform 블록 내부의 다른 모든 uniform 변수에 적용되지만 범위 인수는 다르다.
A simple example
이제 균일한 버퍼 객체를 사용하는 유용한 예를 설명해보겠다. 앞의 모든 코드 샘플을 다시 보면 우리는 계속해서 3개의 행렬을 사용하고 있다:
projection, view, model 행렬. 모든 행렬 중에서 model 행렬만 자주 변경된다. 이 같은 행 집합을 사용하는 쉐이더가 여러 개인 경우
uniform 버퍼 객체를 사용하는 것이 좋다.
Matrix라는 균일한 블록에 projeciton 및 view 행렬을 저장한다. 모델 매트릭스는 쉐이더 간에 꽤 자주 변경되는 경향이 있으므로 모델 매트릭스를
거기에 저장하지 않을 것이다. 그래서 우리는 균일한 버퍼 오브젝트로부터 이익을 얻지 않을 것이다.
#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 레이아웃의 균일한 블록을 사용한다는 점을 제외하고는 별로 특별하지 않다. 샘플 애플리케이션에서 수행 할 작업은 4개의 큐브를
표시하는 것이다. 여기서 각 큐브는 다른 쉐이더 프로그램을 사용해 표시할 것이다. 4개의 쉐이더 프로그램은 모두 동일한 정점 쉐이더를
사용하지만 쉐이더마다 다른 하나의 색상들만을 출력하는 다른 조각 쉐이더를 사용할 것이다.
먼저 정점 쉐이더의 균일 블록을 바인딩 포인트 0과 동일하게 설정한다. 각 쉐이더에 대해 이 작업을 수행해야 한다.
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");
glUniform BlockBinding (shaderRed.ID, uniformBlockIndexRed, 0);
glUniform BlockBinding (shaderGreen.ID, uniformBlockIndexGreen, 0);
glUniform BlockBinding (shaderBlue.ID, uniformBlockIndexBlue, 0);
glUniform BlockBinding (shaderYellow.ID, uniformBlockIndexYellow, 0);
다음으로 실제 균일 버퍼 객체를 만들고 버퍼를 바인딩 포인트 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);
glBindBuffer Range (GL_UNIFORM_BUFFER, 0, uboMatrices, 0, 2 * sizeof(glm::mat4));
우선 glm::mat4의 2배 크기의 버퍼를 위한 충분한 메모리를 할당한다. GLM의 행렬 타입의 크기는 GLSL의 mat4와 직접적으로 일치한다.
그런 다음 버퍼의 특정 범위를 바인딩 포인트 0에 연결한다.
이제 남은 것은 실제로 버퍼를 채우는 것이다. 우리가 투영 행렬의 시야 값을 일정하게 유지하면 우리의 응용 프로그램에서 한 번만
정의하면 된다. 즉, 이 값을 버퍼에 한 번만 삽입하면 된다. 이미 버퍼 객체에 충분한 메모리를 할당했기 때문에 glBufferSubData를 사용해
게임 루프에 들어가기 전에 투영 행렬을 저장할 수 있다:
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);
여기서 우리는 균일 버퍼의 첫 번째 절반을 투영 행렬과 함께 저장한다. 객체를 렌더링 할 때마다 렌더링 반복을 하기 전에 버퍼의 나머지
절반을 뷰 행렬로 업데이트한다:
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);
그리고 그것은 균일한 버퍼 객체를 원한 것이다. matrix uniform block이 포함된 정점 쉐이더에는 이제 uboMatrices에 저장된 데이터가 포함된다.
이제 4개의 쉐이더를 사용해 4개의 큐브를 그려야한다면 투영과 뷰 매트릭스는 동일하게 유지되어야한다:
glBindVertexArray (cubeVAO);
shaderRed.use();
glm::mat4 model;
model = glm::translate (model, glm::vec3(-0.75f, 0.75f, 0.0f)); // move top-left
shaderRed.setMat4("model", model);
glDrawArrays (GL_TRIANGLES, 0, 36);
// ... draw Green Cube
// ... draw Blue Cube
// ... draw Yellow Cube
우리가 설정해야 할 유일한 유니폼은 모델 유니폼이다. 이와 같은 시나리오에서 균일한 버퍼 객체를 사용하면 쉐이더당 꽤 균일한
호출을 할 필요가 없다. 결과는 다음과 같다:
각 큐브는 모델 행렬을 변경해 윈도우의 한쪽으로 이동하며 다른 조각 쉐이더로 인해 오브젝트 색상이 다르다. 이것은 균일한 버퍼 객체를
사용할 수 있는 비교적 간단한 시나리오이지만 큰 렌더링 응용 프로그램에서는 수백 가지 이상의 쉐이더 프로그램이 활성화 될 수 있다.
균일한 버퍼 객체가 실제로 빛나기 시작하는 곳이다.
균일 버퍼 객체는 단일 유니폼에 비해 몇 가지 장점이 있다.
1) 한 번에 많은 유니폼을 설정하는 것이 한 번에 하나의 유니폼을 여러 개 설정하는 것보다 빠르다.
2) 동일한 쉐이더를 여러 쉐이더에서 변경하려는 경우 균일한 버퍼에서 유니폼을 변경하는 것이 훨씬 쉽다.
3) 균일한 버퍼 객체를 사용하는 쉐이더에서 더 많은 유니폼을 사용할 수 있다.
OpenGL은 GL_MAX_VERTEX_UNIFORM_COMPONENTS로 질의 할 수 있는 균일한 데이터의 양을 제한한다.
균일한 버퍼 객체를 사용하면 이 한계가 훨씬 더 커진다. 따라서 최대 유니폼에 도달 할 때마다 항상 균일한 버퍼 객체를 사용하는게 좋다.
** ** ** ** ** ** ** ** ** **
GLSL 문법 중 구조체같은 부분을 배웠다. 프로젝트를 진행할 때 이 방법을 사용해야 효율이 높을 것 같으니 그때 다시 보도록하자.
'Game > Graphics' 카테고리의 다른 글
Learn OpenGL - Advanced OpenGL : Instancing (0) | 2018.09.10 |
---|---|
Learn OpenGL - Advanced OpenGL : Geometry Shader (0) | 2018.09.09 |
LearnOpenGL - Advanced OpenGL : Advanced Data (0) | 2018.09.07 |
Learn OpenGL - Advanced OpenGL : Cubemaps (0) | 2018.09.06 |
Learn OpenGL - Advanced OpenGL : Framebuffers (0) | 2018.09.04 |