Advanced GLSL
- 흥미로운 내장 변수들과 shader의 입력과 출력을 구성하는 새로운 방법, uniform buffer objects라고 불리는 유용한 도구를 다룰 것이다.
GLSL’s built-in variables
- 셰이더는 극도로 파이프라인되어 있다.
- shader 밖의 다른 곳의 데이터가 필요한 경우, 데이터를 전달해야 함.
- vertex attributes, uniforms, sampler
GLSL은 추가적인 여러 내장 변수들을 가짐.
gl_
접두사가 붙어있음.- 데이터를 모으거나 작성하는 추가적인 의미를 가짐
- 앞에서 본
gl_Position
과gl_FragCoord
같은것들이 이에 해당.
- 여기에서 모든 내장 변수를 볼 수 있다.
Vertex shader variables
- Vertex shader의 clip-space 출력 위치 벡터인
gl_Position
은 렌더링하기위해 필수적인 변수이다.
gl_PointSize
기초 도형을 렌더링할 때,
GL_POINTS
를 선택할 수 있다.- 하나의 vertex가 점으로 렌더링되는것.
- 이 점의 크기를
glPointSize
함수를 사용하여 설정할 수 있다. - 하지만 우리는 이 값을 vertex shader에서도 영향을 줄 수 있다.
gl_PointSize
의 GLSL 출력 내장 변수는 float 타입 변수이며, 너비와 높이를 픽셀 단위로 설정할 수 있다.- Vertex shader에서 점의 크기를 설정하면, vertex 마다 점의 값을 설정 가능.
- 기본적으로 점 크기 수정은 비활성화
- OpenGL에서
GL_PROGRAM_POINT_SIZE
를 활성화 시켜야함1
glEnable(GL_PROGRAM_POINT_SIZE);
- OpenGL에서
간단한 예제는 점의 크기를 viewer와 vertex 사이의 거리인 clip-space 위치의 z값과 동일하게 설정하는 것.
- 이 점의 크기는 viewr와 멀리 떨어진 vertex일수록 더 커진다.
1 2 3 4 5
void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); gl_PointSize = gl_Position.z; }
- 결과는 다음과 같다. (큰 점이 더 멀리있음)
- 이를 사용하여 particle을 만들어낼 수 있다.
gl_VertexID
gl_Position
과gl_PointSize
는 출력 변수- 결과에 영향을 줌
gl_VertexID
는 입력 변수- 정수 변수로, 지금 그리고 있는 vertex의 ID를 가짐.
- 인덱스된 렌더링(
glDrawElements
)을 수행할 때, 이 변수는 우리가 그리는 정점의 현재 인덱스를 보유한다. - 인덱스 없이 그릴때 (
glDrawArrays
) 이 변수는, 렌더 호출이 시작된 이후 현재 처리된 꼭짓점의 번호를 보유한다.(처리된 vertex 개수)
Fragment shader variables
gl_FragCoord
gl_FragCoord 벡터의 z요소가 특정 fragment의 깊이값과 동일
- 이 벡터의 x, y 요소를 사용해보자
x,y 요소는 fragment의 window-space 좌표이다.
- 이 좌표는 좌측 하단부터 시작한다.
glViewport
함수로 800x600을 설정하면, x값은 0~800, y값은 0~600 사이의 값을 가짐.
이 fragment shader를 사용하여 fragment의 윈도우 좌표를 기반으로 다른 컬러 값을 계산할 수 있다.
gl_FragCoord
변수가 자주 쓰이는 용도는 다른 fragment 연산과 시각적 효과를 비교하기 위해 쓰인다.- ex) 하나의 출력은 화면 왼쪽에 렌더링하고, 다른 출력은 화면에 오른족에 렌더링하여 분할된 화면을 만들 수 있다.
1
2
3
4
5
6
7
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);
}
- 이제 다른 fragment shader 결과를 화면 양쪽에 보여줄 수 있게 되었다.
- 이는 다른 조명 기술들을 테스팅할 때 유용하다.
gl_FrontFacing
Face culling을 사용하지 않는다면,
gl_FrontFacing
변수로 전면인지 후면인지 알려준다.- 이를 사용해 전면에만 다른 컬러 설정 가능
gl_FrontFacing
변수는 이 fragment가 전면이면true
후면이면false
를 가지는 bool 타입 변수이다.- 예를 들어 안과 밖이 다른 텍스처를 입힌 큐브를 만들 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
#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
gl_FragCoord
는 read-only 변수이다.하지만, fragment의 depth 값은 설정 가능하다.
gl_FragDepth
변수로 shader내에서 depth를 설정할 수 있다.
1
gl_FragDepth = 0.0; // this fragment now has a depth value of 0.0
Shader에서 이 값을 작성하지 않으면, 자동으로
gl_FragCoord.z
값으로 설정됨- 이 설정은 early depth testing 을 비활성화 시킨다.
- OpenGL이 fragment shader가 실행되기 전에 이 fragment 가 어떤 depth 값을 가질지 알 수 없기 때문.
gl_FragDepth
는 이와같은 성능 패널티를 고려해야함.- 하지만
OpenGL4.2
부터는 fragment shader 시작지점에 depth condition과 함께,gl_FragDepth
를 재정의함으로써 둘 사이를 조정할 수 있다.
1
layout (depth_<condition>) out float gl_FragDepth;
- 하지만
condition
값은 다음 값들을 취할 수 있다.
Condition | Description |
---|---|
any | The default value. Early depth testing is disabled. |
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. |
greater
나less
를 지정하여, 오직 fragment의 depth 값보다 크거나 작은 값만을 작성할 수 있도록 할 수 있다.- 이 방법으로, early depth test를 fragment의 depth 값보다 작거나 큰 값에 대해서 수행할 수 있다.
depth 값을 증가시키지만, 일부 early depth testing을 수행하는 예제는 아래와 같다.
1
2
3
4
5
6
7
8
9
10
#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;
}
Interface blocks
vertex 에서 frag로 데이터 보내고 싶을때, 여러 입력/출력 변수들을 선언했었다.
- 보낼 데이터가 많을 수 있음
이 변수들을 묶을 수 있도록, GLSL은
interface blocks
라고 불리는 것을 제공한다.- 이 역시 in, out 키워드를 사용한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#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;
}
이를 통해 입/출력을 체계화해줄 수 있다.
frag에서도 interface block을 선언해보자
- 이름은 동일해야함.
- 하지만 instance 이름은 원하는대로 지정 가능.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#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
shader를 사용할 때, 계속해서 각 shader에서 정확히 동일한 값을 가지는 uniform들을 설정해야함.
UBO는 여러 shader program에 걸쳐 동일하게 유지되는 전역 uniform 변수의 모음을 선언할 수 있도록 한다.
UBO를 사용할 때 고정 GPU 메모리에서 관련 uniform을 한 번만 설정한다.
- 셰이더별로 고유한 uniform을 수동으로 설정해야 한다.
UBO는 다른 버퍼들과 같은 버퍼
glGenBuffers()
로 생성GL_UNIFORM_BUFFER
에 바인딩- 모든 연관 uniform 데이터들을 버퍼에 저장가능.
먼저, 간단한 vertex shader에서 projection, view 행렬을 uniform block에 저장할 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#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
,view
행렬을 설정하므로, 이를 하나로 저장한것.layout (std140)
의 의미: 현재 정의된 uniform block이 특정한 메모리 layout을 사용한다는 뜻.- 이 코드는 uniform block layout을 설정한다.
Uniform block layout
- uniform block 내용은 버퍼 객체에 저장되며, 이는 사실상 글로벌 GPU 메모리의 예약된 부분.
- 이 메모리 조각에는 어떤 종류의 데이터가 있는지에 대한 정보가 없음
- 메모리의 어떤 부분이 셰이더의 uniform 변수에 해당하는지 OpenGL에 알려줘야함.
- shader에 아래와 같은 uniform block 이 선언되어 있다고 생각해보자
1
2
3
4
5
6
7
8
9
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};
여기서 알아야하는것은 크기(바이트)와 변수들의 offset(block의 시작위치로부터), 그래야 이들을 buffer에 각자의 순서로 위치시킬 수 있다.
- 크기: OpenGL에 명시되어 있음, C++ 데이터 타입에 해당
- 변수 사이의 간격(spacing)은 명확히 명시되어 있지 않음.
- 이는 하드웨어가 변수들을 적합하다고 생각하는 곳에 위치시키기 때문.
- ex)일부 하드웨어는 vec3을 float와 인접하게 위치시킨다. 모든 하드웨어가 이렇게 처리할 수 있는것이 아니며, float을 추가하기 전에 vec3을 float[4] 로 만들어 채운다.
기본적으로 GLSL은
shared layout
이라는 uniform 메모리 layout을 사용한다.(offset이 하드웨어에 의해 정의되면, 여러 프로그램 간에 일관되게 공유되기 때문)- 이를 사용하면, 변수의 순서가 그대로 유지되는 한, GLSL이 최적화를 위해 uniform 변수의 위치를 변경할 수 있다.
- 우리는 각 uniform 변수들이 가질 offset을 모르기 때문에, uniform buffer를 정확히 채우는 방법을 모른다.
- 이 정보를
glGetUniformindices
같은 함수를 사용하여 쿼리할 수 있지만, 이 접근 방법은 이 챕터에 맞지 않다.
shared layout
이 일부 공간을 절약하는 최적화를 주지만, 각 변수들에 대한 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의 배수이다.
- 이 base alignment는
- 정확한 layout 규칙은 여기에서 확인할 수 있다.
- 가장 많이 쓰이는 규칙들은 아래 표에서 찾을 수 있다.
- int, float, bool 과 같은 GLSL 변수 타입들은 4바이트 타입으로 정의된다. 4바이트의 각 요소들은
N
으로 표시됨.
Type | Layout rule |
---|---|
Scalar e.g. int or bool | Each scalar has a base alignment of N. |
Vector | Either 2N or 4N. This means that a vec3 has a base alignment of 4N. |
Array of scalars or vectors | Each element has a base alignment equal to that of a vec4. |
Matrices | Stored as a large array of column vectors, where each of those vectors has a base alignment of vec4. |
Struct | Equal to the computed size of its elements according to the previous rules, but padded to a multiple of the size of a vec4. |
std140 layout
예제는 다음과 같다. (각 멤버들의 aligned offset 계산)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
layout (std140) uniform ExampleBlock
{
// base alignment // aligned offset
float value; // 4 // 0
vec3 vector; // 16 // 16 (offset 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
};
이렇게 계산된 offset 값들과 함께,
glBufferSubData
함수를 사용하여 변수 데이터를 각 offset에 채울 수 있다.- std140 layout은 memory layout이 각 program에 대해서 정의되어진 uniform block 형태를 유지한다는 장점이 있다.
- uniform block을 정의하기전
layout (std140)
을 추가함으로, OpenGL에게 이 uniform block이 이를 사용한다라는것을 알려준다. - 버퍼를 채우기전에 offset을 쿼리하는 다른 방법이 두가지 존재
shared
layout 는 위에서 보았음.packed
layout: 컴파일러가 셰이더마다 다를 수 있는 uniform block에서 uniform 변수를 최적화할 수 있기 때문에 layout이 프로그램 간에 동일하게 유지된다는 보장은 없음(공유되지 않음).
Using uniform buffers
- 먼저
glGenBuffers
를 사용하여 UBO를 생성한다.- buffer object를 가지게되면, 이것을
GL_UNIFORM_BUFFER
타겟에 바인딩하고,glBufferData
함수를 호출하여 충분한 메모리를 할당해준다.
- buffer object를 가지게되면, 이것을
1
2
3
4
5
unsigned int uboExampleBlock;
glGenBuffers(1, &uboExampleBlock);
glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock);
glBufferData(GL_UNIFORM_BUFFER, 152, NULL, GL_STATIC_DRAW); // allocate 152 bytes of memory
glBindBuffer(GL_UNIFORM_BUFFER, 0);
이제 buffer에 데이터를 집어넣거나 수정하고 싶을 경우,
uboExampleBlock
을 바인딩하고glBufferSubData
함수를 사용하여 메모리를 수정한다.- 우리는 오직 이 buffer를 한번 수정하면 이 버퍼를 사용하는 모든 셰이더들은 이 수정된 데이터를 사용하게 된다.
OpenGL이 어떠한 uniform block이 어떠한 uniform buffer에 해당하는지 어떻게 아는지?
- OpenGL에는 uniform buffer를 연결시킬 수 있는곳에 정의된 binding points 가 존재
- uniform buffer가 일단 생성되면, 이 binding points들 중 하나에 연결을 하고 또한 이를 shader의 uniform block을 동일한 binding point에 연결한다.
- 위 이미지에서 볼 수 있듯이, 여러 버퍼들을 여러 binding point에 바인딩 가능하다.
- uniform block이 동일한 uniform data를 공유할 수 있다.(위 이미지에서 binding point
0
부분) 이 때 요구사항은 두 셰이더가 모두 같은 uniform block을 정의해야한다는 것.
glUniformBlockBinding: shader <-> binding point
- 특정 binding point에 uniform block을 연결하기 위해 우리는
glUniform BlockBinding
함수를 호출한다.- 파라미터1:
program object
- 파라미터2:
uniform block index
- 파라미터3:
binding point
- 파라미터1:
- uniform block index: shader에 정의된 uniform block의 location index
glGetUniformBlockIndex
함수를 통해 얻을 수 있다.- 이 함수는 program object를 받아들이고, uniform block의 이름을 받는다.
- 아래는 binding point를
2
로 설정한것.
1
2
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_index, 2);
- 이를 각 shader에 반복해야한다.
OpenGL 4.2 부터 binding point를 shader에 명확히 저장하는게 가능해짐. 이는 또 다른 layout 지정을 사용하여
glGetUniformBlockIndex
와glUniformBlockBinding
함수를 사용하지 않아도 되게 해준다.
아래 코드는 binding point를 명확하게 설정한다.
layout(std140, binding = 2) uniform Lights { ... };
glBindBufferBase: binding point <-> UBO
- 그런 다음, UBO를 동일한 binding point에 바인딩해야하고 이는
glBindBufferBase
나glBindBufferRange
함수를 통해 수행될 수 있다.
1
2
3
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// or
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
glBindBufferRange
- 이 함수는 추가적으로 offset 과 size를 받는다.
- 이는 지정한 uniform buffer의 영역만 binding point에 바인딩 가능
- 여러 다른 uniform block들을 하나의 ubo에 연결 가능하다.
데이터 추가
- 이제 모든 것이 세팅되었으므로, uniform buffer에 데이터를 추가할 수 있다.
- 모든 데이터를 하나의 바이트 배열로 추가하거나
glBufferSubData
함수를 사용하여 buffer의 특정 부분을 수정할 수 있다. - uniform 변수 boolean을 수정하기 위해 다음과 같이 ubo를 수정할 수 있다.
- 모든 데이터를 하나의 바이트 배열로 추가하거나
1
2
3
4
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 block 내부의 모든 다른 uniform 변수들에 적용 가능하지만, range 파라미터는 다를 것이다.
A simple example
계속해서 사용해왔던 3가지 행렬 projection, view, model
- 이 행렬 중 오직 model 행렬만 자주 변경됨
- 동일한 행렬의 모음을 사용하는 여러 shader를 가지고 있다면, ubo를 사용하는 것이 좋다.
projection, view 를 matrics라는 uniform block에 저장해보자
- model은 자주 변경되므로, 이점이 없음
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#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);
}
4개의 큐브를 그리고, 각자 다른 shader program을 사용할것임.
- vertex shader는 같지만, shader마다 다른 오직 하나의 색만 출력하는 다른 fragment shader를 사용한다.
먼저, vertex shader의 uniform block을 binding point
0
으로 설정(각 shader에 대해)
1
2
3
4
5
6
7
8
9
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);
- 그다음 ubo를 생성하고 binding point
0
에 바인딩한다.
1
2
3
4
5
6
7
8
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));
적절히 메모리를 설정, 그리고 binding point를 0으로 설정
그리고 buffer를 채우자
- field of view 값을 상수로 유지(카메라 줌이 없으면)하고 싶다면, 한번만 정의하면 됨.(한번만 버퍼에 삽입)
- buffer object에 충분한 메모리를 할당했기 때문에
glBufferSubData
함수를 사용하여 게임 루프에 들어가기 전에 projection 행렬을 저장할 수 있다
1
2
3
4
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);
- 각 렌더링 루프에서 오브젝트를 그리기 전에, buffer의 두 번째 공간에 view 행렬을 넣는다.
1
2
3
4
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);
- 그리고 matrics uniform block을 가지고 있는 각 vertex shader는 이제 uboMatrix에 저장된 데이터를 가지고 있다.
- 그러므로 4개의 큐브를 그리면, 동일하게 projection, view 행렬이 유지된다.
1
2
3
4
5
6
7
8
9
glBindVertexArray(cubeVAO);
shaderRed.use();
glm::mat4 model = glm::mat4(1.0f);
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
- 설정하는 uniform은 model 행렬뿐이므로, 이전보다 약간의 uniform 호출을 줄였다.
- UBO 의 장점
- 한번에 uniform들을 설정하는 것은 하나 하나 설정하는 것보다 빨,ㅁ
- 여러 shader에 걸쳐있는 동일한 uniform을 수정하고 싶은 경우, 한번에 수정하기 쉬움.
- 아주 많은 uniform들을 사용할 수 있음. (OpenGL은 관리할 수 있는 uniform의 개수가 제한되어 있음.
GL_MAX_VERTEX_UNIFORM_COMPONENTS
를 사용하여 확인 가능)- uniform 개수의 한계치에 닿았을 때(ex. 스켈레톤 애니메이션)마다 ubo를 사용하면 좋다.