Advanced GLSL
이 튜토리얼은 너에게 너의 scene의 비쥬얼 퀄리트를 엄청나게 상승시킬 매우 고급의, 멋지고, 새로운 특징을 보여주지 않을 것이다. 이 튜토리얼은 GLSL의 다소 흥미로운 측면을 다룰 것이고, 너의 나중의 노력에 도움을 줄 몇 가지 좋은 트릭을 다룬다. 기본적으로 GLSL와 함께 OpenGL 프로그램을 만들 때 너를 좀 더 편안하게 만들어주는 특징들과 알기 좋은 것들이다.
우리는 몇 가지 흥미로운 built-in variables 내장 변수들을 다루고, shader의 input과 output을 구성하는 새로운 방식과, uniform buffer objects라고 불려지는 유용한 도구를 다룰 것이다.
GLSL's built-in variables
쉐이더들은 최소한이다. 만약 현재 쉐이더 밖에서 어떤 다른 소스로부터 데이터가 필요하다면, 우리는 데이터를 전달해야만 할 것이다. 우리는 vertex attributes, uniforms and samplers를 통해서 이것을 하는 것을 배웠다. 그러나 GLSL에 의해 정의되는 gl_로 접두사가 붙는 몇 가지 추가 변수들이 있다. 이것은 우리에게 data를 모으고 또는 쓸 추가 수단을 제공한다. 우리는 이미 튜토리얼에서 그것들 두 가지를 보아왔다: gl_Position은 vertex shader의 output vector이고, fragment 쉐이더의 gl_FracCoord.
우리는 GLSL에 내장된 몇 가지 흥미로운 내장 input과 output 변수들을 다룰 것이다. 그리고 어떻게 그것들이 우리에게 혜택을 주는지 설명할 것이다. 우리는 GLSL에 존재하는 모든 내장 변수들을 다루지 않을 것이다. 그래서 만약 너가 모든 내장 변수가 궁금하다면 너는 OpenGL의 wiki를 확인할 수 있다.
Vertex shader variables
우리는 이미 vertex shader의 clip-space output position vector인 gl_Position을 봤다. vertex shader에서 gl_Position을 설정하는 것은 엄격한 요구사항이다. 만약 너가 스크린에 어떤 것을 렌더링하길 원한다면.
gl_PointSize
우리가 선택할 수 있는 render primitives중의 하나는 GL_POINTS 이다. 여기에서 각 single vertex는 primitive이고 한 점으로서 렌더링 된다. OpenGL의 glPointSize 함수를 통해 렌더링 될 점들의 크기를 설정하는 것이 가능하지만, 우리는 vertex shader에서 이 값에 영향을 또한 줄 수 있따.
GLSL에 의해 정의된 output variable은 gl_PointSize라고 불려지고, 이것은 float variable이다. 너는 그 점의 width와 height를 픽셀단위로 정할 수 있다. vertex shader에서 그 점의 사이즈를 묘사하여, 너는 vertex마다 이 점의 값에 영향을 줄 수 있다.
vertex shader에서 그 점의 사이즈에 영향을 미치는 것은 기본적으로 비활성화 되어있지만, 만약 너가 이것을 활성화 하길 원한다면, 너는 OpenGL의 GL_PROGRAM_POINT_SIZE를 활성화 해야만 할 것이다:
glEnable(GL_PROGRAM_POINT_SIZE);
그 점의 크기에 영향을 주는 간단한 예제는, clip-space position의 z값과 동일하게 point size를 설정하는 것이다. 그 z값은 viewer와 vertex의 거리와 같다. 그 점의 크기는 그러고나서 우리가 정점으로부터 멀어질 수록 증가해야만 한다.
#version 330 core layout (location = 0) in vec3 aPos; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); gl_PointSize = gl_Position.z; }
그 결과는 우리가 그리는 점들이 우리가 그것들로부터 멀리갈수록 더 커지게 렌더링 된다는 것이다:
(이것을 하게 하는데 조금 착오가 있었는데, 이것을 하려면 다음의 코드가 필요하다.
glEnable(GL_PROGRAM_POINT_SIZE); Shader glslShader = Shader("ShaderFolder/glsladvan.vs", "ShaderFolder/glsladvan.fs"); glslShader.loadShader(); unsigned int gl_quadVAO, gl_quadVBO; glGenVertexArrays(1, &gl_quadVAO); glGenBuffers(1, &gl_quadVBO); glBindVertexArray(gl_quadVAO); glBindBuffer(GL_ARRAY_BUFFER, gl_quadVBO); float gl_quadV[] = { 0.f, 0.f, 0.f }; glBufferData(GL_ARRAY_BUFFER, sizeof(gl_quadV), &gl_quadV, GL_STATIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
그리고 main loop에서
glslShader.use(); glslShader.setMat4("view", view); glslShader.setMat4("projection", projection); glm::mat4 pmodel(1.0); pmodel = glm::translate(pmodel, glm::vec3(1.f, 1.f, 2.f)); pmodel = glm::scale(pmodel, glm::vec3(0.2f)); glslShader.setMat4("model", pmodel); glBindVertexArray(gl_quadVAO); glDrawArrays(GL_POINTS, 0, 1); glslShader.use(); glslShader.setMat4("view", view); glslShader.setMat4("projection", projection); pmodel = glm::mat4(1.0); pmodel = glm::translate(pmodel, glm::vec3(-1.f, 1.f, 2.f)); pmodel = glm::scale(pmodel, glm::vec3(0.4f)); glslShader.setMat4("model", pmodel); glBindVertexArray(gl_quadVAO); glDrawArrays(GL_POINTS, 0, 1);
이런식으로 그릴 수 있다.
vertex shader를 좀 더 가지고 놀았는데, 나중에 particle system에 활용하기 위해서.
#version 330 core layout (location = 0) in vec3 aPos; uniform mat4 model; uniform mat4 view; uniform mat4 projection; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); gl_PointSize = 50 * (1 /gl_Position.z); }
gl_VertexID
gl_Position과 gl_PointSize는 output variables이다. 왜냐하면 그것들의 값은 vertex shader로부터 output으로서 read되기 때문이다; 우리는 그것들에 write를 해서 결과에 영향을 줄 수 있다. vertex shader는 또한 우리에게 흥미로운 input 변수를 준다. 우리가 오직 read만 할 수 있는. gl_VertexID라고 한다.
그 정수 변수 gl_VertexID는 우리가 그리는 정점의 현재 ID를 가지고 있다. indexed rendering을 할 때 (glDrawElements으로) 이 변수는 우리가 그리는 정점의 현재 index를 가지고 있다. 정점 없이 그릴 때 (glDrawArrays로) 이 변수는 render call 시작 이후로 현재 처리되는 정점의 개수를 가지고 있다.
비록 지금 당장은 특별히 유용하지 않더라도, 우리가 이것과 같은 정보에 접근할 수 있다는 것을 아는 것은 좋다.
Fragment shader variables
fragment shader내에서, 우리는 또한 몇 가지 흥미로운 변수들에 접근할 수 있다. GLSL은 우리에게 gl_FragCoord와 gl_FrontFacing이라는 두 가지 흥미로운 input variables를 준다.
gl_FragCoord
우리는 depth testing 을 다루는 동안에, 몇번 gl_FragCoord를 보았다. 왜냐하면 그 gl_FragCoord vector의 z 컴포넌트가 특별한 fragment의 depth value와 동일하기 때문이다. 그러나, 우리는 또한 몇 가지 흥미로운 효과를 위해 그 벡터의 x와 y component를 사용할 수 있다.
gl_FragCoords의 x와 y요소는 fragment의 window-space 좌표들이다. 이것은 윈도우의 왼쪽 하단이 원점이다. 우리는 glViewport로 800x600의 윈도우를 명시했다. 그래서 그 fragment의 window-space 좌표는 0~800사이의 x값과 0~600사이의 y값을 갖는다.
fragment shader를 사용하여, 우리는 fragment의 윈도우 좌표를 기반으로 다른 컬러 값을 계산할 수 있다. gl_FragCoord 변수에 대해 흔한 사용은 다른 fragment calculations의 시각적 output을 비교하기 위함이다. 보통 tech demos에서 볼 수 있듯이. 우리는 예를들어, 한 output을 윈도우의 왼쪽에 렌더링하고, 다른 output을 윈도우의 오른쪽에 렌더링하여 두 개에서 스크린을 분할할 수 있따. fragment's의 윈도우 좌표를 기반으로 다른 color를 만들어내는 예시 fragment shader는 아래에서 주어진다:
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); }
window의 너비가 800이기 때문에, 픽셀의 x좌표가 400보다 작다면, 그것은 윈도우의 왼쪽에 있을 것이고 따라서 우리는 오브젝트에게 다른 색을 준다:
우리는 이제 두 개의 완전히 다른 fragment shader 결과들을 계산할 수 있고, 그것들 각각을 윈도우의 다른 쪽에 보여줄 수 있다. 이것은 예를들어 다른 lighting techniques을 테스트하는데 훌륭하다.
gl_FrontFacing
fragment shader에서 또 다른 훌륭한 input 변수는 gl_FrontFacing 변수이다. face culling 튜토리얼에서, 우리는 OpenGL이 한 면이 정점의 감기눈 순서에 의해 앞면인지 뒷면인지를 알아낼 수 있다고 언급했었다. 만약 우리가 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); }
그래서 만약 그 컨테이너 안을 살짝 본다면, 우리는 이제 다른 텍스쳐가 사용되고 있는 것을 볼 수 있다.
만약 너가 face culling을 활성화 했다면, 너는 container안 어떤 것도 볼 수 없을 거라는 것을 유의해라. gl_FrontFacing을 사용하는 것은 그러면 의미가 없을 것이다.
gl_FragDepth
input 변수 gl_FragCoord는 우리가 window-space 좌표를 읽게 해주고 현재 fragment의 depth value를 얻게 해주는 input variable이다. 그러나 그것은 read-only 변수이다. 우리는 그 fragment의 window-space coordinates에 영향을 미칠 수 없지만, fragment의 depth value를 설정하는 것은 가능하다. GLSL은 우리에게 gl_FragDepth라는 output 변수를 준다. 이것은 우리가 shader 내에서 fragment의 depth value를 설정하기 위해 사용할 수 있는 것이다.
실제로 shader에서 depth value를 설정하기 위해서, 우리는 간단히 0.0 ~ 1.0 사이의 float value를 그 output variable에 쓴다:
gl_FragDepth = 0.0; // this fragment now has a depth value of 0.0
만약 그 쉐이더가 gl_FragDepth에 어떤 값을 쓰지 않는다면, 그 변수는 자동적으로 gl_FragCoord.z로부터 값을 취할 것이다.
그러나, depth value를 우리 스스로 설정하는 것은 매우 큰 불이익이다, 왜냐하면 OpenGL은 fragment shader에서 gl_FragDepth에 우리가 write를 하자마자 모든 early depth testing (depth testing tutorial에서 다루었듯이)을 비활성화하기 때문이다. OpenGL은 우리가 fragment shader를 작동시키기전에 그 fragment가 무슨 depth value값을 가질지 모르기 때문에, 비활성화 된다. fragment가 완전히 이 depth value를 바꿀 것이기 때문이다.
gl_FragDepth에 write해서, 너는 이 퍼포먼스 페널티를 고려해야만 한다. 그러나 OpenGL 4.2부터, 우리는 어느정도 gl_FragDepth 변수를 fragment shader의 맨 위에 depth condition과 함께 재선언하여 두 장단점 사이를 중재할 수 있다:
layout (depth_<condition>) out float gl_FragDepth;
이 condition은 다음의 값을 취한다:
- any : 기본 값, Early depth testing은 비활성화 되고, 너는 대부분의 성능을 잃는다.
- greater : 너는 gl_FragCoord.z와 비교하여 오직 더 큰 depth value만을 만들 수 있다.
- less : 너는 gl_FragCoord.z와 비교하여 오직 더 작은 depth value만을 만들 수 있다.
- unchanged : 만약 너가 gl_FragDepth에 write한다면, 너는 정확히 gl_FragCoord.z를 쓸 것이다.
depth condition을 greater or less로 명시하여, OpenGL은 너는 오직 fragment의 depth value 보다 더 작거나 큰 depth values를 쓸 것이라고 가정할 수 있다. 이 방식으로 OpenGL은 여전히 depth value가 fragment의 depth value보다 더 작은 경우에 early depth test를 여전히 할 수 있다.
우리가 fragment shader에서 depth value를 증가시키지만, 여전히 early depth testing을 유지하기를 원하는 예제는 아래의 fragment shader에서 보여진다:
#version 420 core // note the GLSL version! out vec4 FragColor; layout (depth_greater) out float gl_FragDepth; in vec2 TexCoords; uniform sampler2D frontTexture; uniform sampler2D backTexture; void main() { gl_FragDepth = gl_FragCoord.z + 0.05; FragColor = vec4(1.0); }
이 기능은 오직 OpenGL 4.2 버전 이상에서만 이용가능하다는 것을 주의해라.
Interface blocks
지금까지, vertex shader에서 fragment shader로 데이터를 보내길 원할 때 마다, 우리는 몇 가지 부합하는 input/output variables를 선언했었다. 한 번에 이러한 것들을 하나씩 선언하는 것은 한 쉐이더에서 다른 것으로 데이터를 보내느 가장 쉬운 방법이지만, 프로그램이 커짐에 따라, 너는 아마도 arrays and/or structs를 포함할지도 모르는 몇 가지 이상의 변수를 보내길 원한다.
우리가 이러한 변수들을 구성하는 것을 돕기 위해, GLSL은 우리에게 함꼐 그러한 변수들을 그룹화 하도록 하는 interface blocks이라고 불려지는 어떤 것을 제공한다. 그러한 interface block의 선언은 struct declartion처럼 보인다. 그것이 이제 input 또는 output block이 되는 블럭에 기반으로하여 in or out keyword를 사용하여 선언되는 것을 제외하고.
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aNormal; 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; }
그러고나서 우리는 또한 fragment shader인 다음 쉐이더에서 input interface block을 선언할 필요가 있다. 그 block name (VS_OUT)은 fragment shader에서 같아야 하지만, 그 instance name (vertex shader에서 사용되었듯이 vs_out)은 우리가 좋아하는 무엇이든 될 수 있다. - 실제 input variables를 포함하는 vs_out같은 혼동스러운 이름을 피해라.
#version 330 core out vec4 FragColor; in VS_OUT { vec2 TexCoords; } fs_in; uniform sampler2D frontTexture; uniform sampler2D backTexture; void main() { if(gl_FrontFacing) FragColor = texture(frontTexture, fs_in.TexCoords); else FragColor = texture(backTexture, fs_in.TexCoords); }
두 interface blcok 이름들이 같은 한, 그것들에 상응하는 input과 output은 함게 부합된다. 이것은 너의 코드를 구성하는데 도와주는 또 다른 유용항 특징이고, geometry shader와 같은 어떤 쉐이더 단계뜰 사이를 넘어다닐 때 유용하다.
Uniform buffer objects
우리는 이제 꽤 어느정도 OpenGL을 써왔고, 몇 가지 꽤 멋진 트릭들을 배워왔을 뿐만 아니라, 몇 가지 성가신 것도 그렇다. 예를들어, 1개 이상의 쉐이더를 쓸 때, 우리는 지속적으로 uniform variables를 설정해야만 한다. 그 uniform 변수들에서, 그것들 대부분은 각 쉐이더에 대해 정확히 같다 - 그래서 왜 우리는 그것들을 다시 설정하느라 시달리는가?
OpenGL은 우리가 몇 가지 shader programs에 대해 같게 남아 있는 global uniform 변수들의 한 집합을 선언하도록 허용하는 uniform buffer objects라고 불려지는 도구를 준다. uniform buffer objects를 사용할 때, 우리는 따라서 관련된 uniforms를 오직 한 번만 설정해야만 한다. 우리는 여전히 수동으로 shader마다 고유한 uniforms를 설정해야만 한다. 근데 uniform buffer object를 만들고 설정하는 것은 꽤 작업이 필요하다.
uniform buffer object가 다른 버퍼와 같은 버퍼이기 때문에, 우리는 glGenBuffers를 통해 그것을 만들 수 있고, GL_UNIFORM_BUFFER 버퍼 타겟에 바인드 시키고, 모든 관련된 uniform data를 buffer에 저장할 수 있다. uniform buffer objects의 데이터가 어떻게 저장되어야 하는지에 대한 어떤 규칙이 있는데, 우리는 나중에 그것에 알아 볼 것이다. 처음에, 우리는 간단한 vertex shader를 가지고와서, 우리의 projection과 view matrix를 소위 uniform block에 저장할 것이다:
#version 330 core layout (location = 0) in vec3 aPos; layout (location = 1) in vec2 aNormal; layout (std140) uniform Matrices { mat4 view; mat4 projection; }; uniform mat4 model; out CHAN_VS_OUT { vec2 TexCoords; } vs_out; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); vs_out.TexCoords = aNormal; }
우리의 대부분의 샘플들에서, 우리가 사용하고 있는 각 shader에 대해 render iteration마다 projection과 view matrix를 설정한다. 이것은 uniform buffer objects가 유용한 완벽한 예시이다. 왜냐하면 우리는 이제 오직 이러한 행렬들을 한 번만 저장하면 되기 때문이다.
여기에서 우리는 두 개의 4x4 행렬을 저장하는 Matrices라고 불리는 한 uniform block을 선언했다. uniform block에 있는 변수들은 직접적으로 접두사에 block name없이 접근될 수 있다. 그러고나서 우리는 한 버퍼에 있는 이러한 행렬 값을 OpenGL code 어딘가에 저장한다. 이 uniform block을 선언한 각 쉐이더는 그 행렬에 접근한다.
너는 아마도 layout (std140) 문이 무엇을 의미하는지 궁금할 것이다. 이것이 말하는 것은 현재 정의된 uniform block이 그것의 내용에 대해 특정한 memory layout을 사용한다는 것이다. 이 문장은 uniform block layout을 설정한다.
Uniform block layout
uniform block의 내용은 기본적으로 보유된 메모리 조각일 뿐인 버퍼 오브젝트에 저장된다. 이 메모리 조각은 그것이 무슨 종류의 데이터를 보유하는지에 대한 정보를 가지고 있지 않기 때문에, 우리는 OpenGL에게 그 메모리의 무슨 부분이 쉐이더에 어떤 uniform 변수와 일치하는지를 말할 필요가 있다.
한 쉐이더에 다음의 uniform block을 상상해보아라:
layout (std140) uniform ExampleBlock { float value; vec3 vector; mat4 matrix; float values[3]; bool boolean; int integer; };
우리가 알기 원하는 것은 (바이트) 사이즈이고, 이러한 변수들의 각각의 offset (그 블락의 시작)이다. 그래서 우리는 그것들의 개별적인 순서에서 버퍼에 그것들을 배치할 수 있다. 그 원소들의 각각의 사이즈는 명백히 OpenGL에 명시되어있고, 직접적으로 C++ data types과 일치한다; vectors와 matrices는 floats의 (큰) 배열이다. OpenGL이 깔끔하게 명시하지 않는 것은, 변수들 간의 spacing이다. 이것은 하드웨어가 그것이 맞아보이게 변수들을 배치하게 한다. 예를들어 몇몇 하드웨어는 vec3를 float에 인접하게 배치할 수 있다. 모든 하드웨어가 이것을 다루고 ve3를 4개의 float의 배열에 패딩하는건 아니다. float에 붙이기전에. 훌륭한 기능이지만, 우리에게는 불편하다.
기본적으로 GLSL는 shared layout이라고 불려지는 uniform memory layout을 사용한다. - 하드웨어에 의해 그 offsets이 한 번 정의된다면, 그것들은 다양한 프로그램들 사이에서 끊임없이 공유되기 때문에 공유된다고 하는 것이다. shared layout으로, GLSL는 그 변수들의 순서가 변하지 않은 한 최적화를 위해 uniform 변수들을 재위치 시키는 것이 허용된다. 우리는 각 uniform variable이 어떤 offset에 있을지 모르기 때문에, 우리는 어떻게 우리의 uniform buffer를 정확히 채울지 모른다. 우리는 이 정보를 glGetUniformIndices같은 함수를 통해 query할 수 있지만, 그것은 이 튜토리얼의 범위를 넘는 것이다.
shared layout이 우리에게 몇 가지 공간을 절약하는 최적하를 주지만, 우리는 많은 일을 해주는 각 uniform 변수들에 대해 각 offset을 query할 필요가 있다. 그러나 일반적인 관례는 shared layout을 쓰는 것이 아니라, std140 layout을 사용하는 것이다. 그 std140 layout은 explicitly하게 한 규칙들의 집합에 의해 관리되는 그들의 각각의 offsets을 명시하여 각 변수 타입에 대해 메모리 layout을 명시한다. 이것이 explicitly하게 언급되기 때문에, 우리는 수동으로 각 변수에 대한 offset을 알아낼 수 있다.
각 변수는 한 uniform block내에서 한 변수가 차지하는(padding을 포함해서) 공간과 동일한 base alignment를 가진다. 이 base alignment는 std140 layout rules를 사용하여 계산한다. 그러고나서, 각 변수에 대해, 우리는 그것의 aligned offset을 계산한다. 그 aligned offset은 block의 시작으로부터의 변수의 byte offset이다. 한 변수의 aligned byte offset은 그것의 base alignment의 배수와 동일해야만 한다.
그 정확한 layout rules은 OpenGL의 uniform buffer specification인 여기에서 볼 수 있지만, 우리는 가장 흔한 규칙들을 아래에 열거할 것이다. GLSL에서 int, float, bool과 같은 각 변수 타입은 N으로 나타내어지는 4 바이트의 각 entity로 4바이트 양으로 정의된다.
- Scalar(예, int or bool) : 각 스칼라는 N의 base alignment를 갖는다.
- Vector : 2N 또는 4N. 이것은 vec3가 4N의 base alignment를 갖는 것을 의미한다.
- Array of scalars or vectors : 각 원소는 vec4의 base alignment와 동일하다.
- Matrices : 열 벡터들의 큰 배열로서 저장되는데, 그러한 벡터들 각각은 vec4의 base alignment를 갖는다.
- Struct : 이전의 규칙에 따라 그것의 원소들의 연산된 크기와 동일하다. 그러나 vec4의 사이즈의 배수로 패딩된다.
대부분의 OpenGL의 명세처럼, 한 예시로 이해하는 것이 더 쉽다. 우리는 우리가 이전에 소개한 ExampleBlock이라는 uniform block을 가지고 있다. 우리는 std140 layout을 사용하여 그것의 멤버 각각에 대해 aligned offset을 계산한다:
(이해를 위해 한가지 추가하자면, base alignment가 K라면, aligned offset은 K의 배수가 되도록 해야 한다. 그래서 K의 배수가 아니라면 padding을 통해 aligned offset을 증가시켜 K의 배수를 만들어낸다.
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 };
(이해를 더하기 위해, 명세에 있는 예시도 가져온다. 내가 위에서 괄호 쳐놓은 규칙과 기본 규칙을 조금만 알면 이 memory layout을 알 수 있게 된다.)
layout(std140) uniform Example { // Base types below consume 4 basic machine units // // base base align // rule align off. off. bytes used // ---- ------ ---- ---- ----------------------- float a; // 1 4 0 0 0..3 vec2 b; // 2 8 4 8 8..15 vec3 c; // 3 16 16 16 16..27 struct { // 9 16 28 32 (align begin) int d; // 1 4 32 32 32..35 bvec2 e; // 2 8 36 40 40..47 } f; // 9 16 48 48 (pad end) float g; // 1 4 48 48 48..51 float h[2]; // 4 16 52 64 64..67 (h[0]) // 80 80..83 (h[1]) // 4 16 84 96 (pad end of h) mat2x3 i; // 5/4 16 96 96 96..107 (i, column 0) // 112 112..123 (i, column 1) // 5/4 16 124 128 (pad end of i) struct { // 10 16 128 128 (align begin) uvec3 j; // 3 16 128 128 128..139 (o[0].j) vec2 k; // 2 8 140 144 144..151 (o[0].k) float l[2]; // 4 16 152 160 160..163 (o[0].l[0]) // 176 176..179 (o[0].l[1]) // 4 16 180 192 (pad end of o[0].l) vec2 m; // 2 8 192 192 192..199 (o[0].m) mat3 n[2]; // 6/4 16 200 208 208..219 (o[0].n[0], column 0) // 224 224..235 (o[0].n[0], column 1) // 240 240..251 (o[0].n[0], column 2) // 256 256..267 (o[0].n[1], column 0) // 272 272..283 (o[0].n[1], column 1) // 288 288..299 (o[0].n[1], column 2) // 6/4 16 300 304 (pad end of o[0].n) // 9 16 304 304 (pad end of o[0]) // 3 16 304 304 304..315 (o[1].j) // 2 8 316 320 320..327 (o[1].k) // 4 16 328 336 336..347 (o[1].l[0]) // 352 352..355 (o[1].l[1]) // 4 16 356 368 (pad end of o[1].l) // 2 8 368 368 368..375 (o[1].m) // 6/4 16 376 384 384..395 (o[1].n[0], column 0) // 400 400..411 (o[1].n[0], column 1) // 416 416..427 (o[1].n[0], column 2) // 432 432..443 (o[1].n[1], column 0) // 448 448..459 (o[1].n[1], column 1) // 464 464..475 (o[1].n[1], column 2) // 6/4 16 476 480 (pad end of o[1].n) // 9 16 480 480 (pad end of o[1]) } o[2]; };
연습으로서, 스스로 offset values를 계산하고 그것들을 이 테이블과 비교하려고 해라. std140 layout의 규칙들을 기반으로 계산된 offset values로, 우리는 glBufferSubData같은 함수를 사용하여 버퍼를 변수 데이터로 각 offset에 채울 수 있다. 가장 효율적인건 아니지만, 그 std140 layout은 우리에게 그 memory layout이 이 uniform block을 선언한 각 프로그램 마다 같게 남도록 보장해준다.
uniform block의 정의전에 layout (std140) 문장을 추가하여, 우리는 OpenGL에게 이 uniform block이 std140 layout을 사용한다고 말하는 것이다. 우리에게 버퍼들을 채우기전에 각 offset을 query할 것을 요구하는 선택할 두 가지 다른 layouts가 있다. 우리는 이미 shared layout과 packed되는 다른 나머지 layout을 보았다. packed layout을 사용할 때, layout이 프로그램 사이에서 같게 남아있을 보장이 없다. (not shared) 왜냐하면 그것은 컴파일러가 쉐이더마다 다를지도 모르는 uniform block으로부터 uniform 변수들을 최적화하도록 하기 때문이다.
Using uniform buffers
우리는 쉐이더에서 uniform blocks을 정의하는 것과 그것들의 memory layout을 명시하는 것을 다루었지만, 우리는 아직 그것들을 실제로 어떻게 사용할지를 이야기하지 않았다.
처음에, 우리는 glGenBuffers를 통해 처리되는 uniform buffer object를 만들 필요가 있다. 일단 우리가 buffer object를 가진다면, 우리는 그것을 GL_UNIFORM_BUFFER target에 바인드시키고, glBufferData를 호출하여 충분한 메모리를 할당한다.
unsigned int uboExampleBlock; glGenBuffers(1, &uboExampleBlock); glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); glBufferData(GL_UNIFORM_BUFFER, 128, NULL, GL_STATIC_DRAW); glBindBuffer(GL_UNIFORM_BUFFER, 0);
이제 우리가 데이터를 버퍼에 업데이트 시키거나 넣고 싶을 때 마다, 우리는 uboExampleBlock에 바인드 시키고, glBufferSubData 를 사용하여 그것의 메모리를 업데이트 시킨다. 우리는 오직 이 uniform buffer를 한 번만 업데이트 해야만 한다. 그리고 이 버퍼를 사용하는 모든 쉐이더들은 이제 그것의 업데이트된 데이터를 사용한다. 그러나 어떻게 OpenGL은 어떤 uniform buffers가 어떤 uniform block에 대응되는지 아는가?
OpenGL context에서, 정의된 수많은 binding points들이 있다. 거기에서 우리는 uniform buffer를 연결할 수 있다. 일단 우리가 uniform buffer를 만들기만하면, 우리는 그것을 그러한 binding points 중의 하나에 링크시킨다. 그리고 우리는 또한 쉐이더에 있는 uniform block을 같은 binding point에 링크시킨다. 이것은 그것들을 효과적으로 서로 링크시킨다. 다음의 다이어그램이 이것을 설명한다:
너도 볼 수 있듯이, 우리는 다양한 uniform buffers들을 다른 binding points에 바인드 시킬 수 있다. 쉐이더 A와 쉐이더 B가 둘 다 같은 binding point 0에 연결된 uniform block을 가지고 있기 때문에, 그것들의 uniform blocks들은 uboMatrices에서 발견되는 같은 uniform data를 공유한다; 두 쉐이더들이 같은 Matrices uniform block을 정의했 던 것이 하나의 요구사항이 된다.
uniform block을 특정한 binding point에 설정하기 위해서, 우리는 program object를 그것의 첫 번째 인자로 받고, uniform block index와 링크시킬 binding point를 취하는 glUniformBlockBinding 을 호출한다. uniform block index는 shader에서 정의된 uniform block의 location index이다. 이것은 program object와 uniform block의 이름을 받는 glGetUniformBlockIndex에 대한 호출을 통해서 얻어진다. 우리는 diagram의 Lights uniform block을 다음과같이 binding point 2로 지정할 수 있다:
unsigned int lights_index = glGetUniformBlockIndex(shaderA.ID, "Lights");
glUniformBlockBinding(shaderA.ID, lights_Index, 2);
우리가 각 shader에 대해 이 프로세스를 반복해야만 하는 것에 주의해라.
Green Box
OpenGL 4.2부터, 다른 layout specifier를 추가하여, shader에 explicitly하게 한 uniform block의 binding point를 저장하는 것이 또한 가능하다. 이것은 우리가 glGetUniformBlockIndex와 glUniformBlockBinding를 호출하는 것을 줄여준다. 다음의 코드는 Lights uniform block의 binding point를 explicitly하게 설정한다:
layout(std140, binding = 2) uniform Lights {...};
그러고나서 우리는 또한 uniform buffer object를 같은 binding point에 바인드 시킬 필요가 있다. 이것은 glBindBufferBase 또는 glBindBufferRange 둘 중 하나로 처리될 수 있다.
glBindBufferBase(GL_UNIFORM_BUFFER, 2, uboExampleBlock);
// or
glBindBufferRange(GL_UNIFORM_BUFFER, 2, uboExampleBlock, 0, 152);
glBindBufferBase 함수는 target, binding point index 그리고 uniform buffer object를 그것의 인자로서 기대한다. 이 함수는 uboExampleBlock을 binding point 2에 링크하고, 이 시점으로부터 binding point의 양쪽에 연결되어진다. 너는 또한 추가 offset과 size parameter를 기대하는 glBindBufferRange를 사용할 수 있다. - 이 방식으로 너는 오직 binding point에 대해 uniform buffer의 특정한 범위만을 바인드 할 수 있다. glBindBufferRange를 사용하여, 너는 단일의 uniform buffer object에 연결된 수 많은 다른 uniform blocks을 가질 수 있다.
모든 것이 설정되었기에, 우리는 데이터를 uniform buffer에 추가할 수 있다. 우리는 모든 데이터를 single byte array로서 추가할 수 있고 또는 우리가 원할 때 마다 glBufferSubData를 사용하여 버퍼의 부분들을 업데이트 시킬 수 있다. uniform 변수 boolean을 업데이트 하기위해, 우리는 uniform buffer object를 다음과 같이 업데이트할 수 있다:
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 내의 모든 다른 uniofrm variables에 적용된다. 그러나 다른 범위의 인지와 함께.
A simple example
그래서, uniform buffer objects를 사용해서 진짜 실용적인 예를 해보자. 만약 우리가 이전의 코드 샘플들을 되돌아 본다면, 우리는 지속적으로 3개의 행렬들을 사용해왔다: projection, view and model 행렬들. 모든 그러한 행렬중에, 오직 model matrix만이 자주 바뀐다. 만약 우리가 이 같은 행렬들의 집합을 사용하는 다양한 shaders를 가진다면, 우리는 아마도 uniform buffer objects를 사용하는게 더 나을 것이다.
우리는 projection과 view matrix를 Matrices라고 불리는 uniform block에 저장할 것이다. 우리는 거기에 model matrix를 저장하지 않을 것이다. 왜냐하면 model matrix는 꽤 자주 쉐이더간에 바뀌는 경향이 있기 때문이다. 그래서 우리는 uniform buffer objects로부터 이익을 못 얻을 것이다.
layout (std140) uniform Matrices { mat4 view; // col0 0 ~ 15 // col1 16 ~ 31 // col2 32 ~ 47 // col3 48 ~ 63 mat4 projection; // col0 64 ~ 79 // col1 80 ~ 95 // col2 96 ~ 111 // col3 112 ~ 128 }; uniform mat4 model; out CHAN_VS_OUT { vec2 TexCoords; } vs_out; void main() { gl_Position = projection * view * model * vec4(aPos, 1.0); vs_out.TexCoords = aNormal; }
우리가 std140layout을 가진 uniform block을 사용하는 것 말고는 여기에 특별한 건 없다. 우리가 우리의 sample application에서 하려는 것은 4개의 큐브를 보여주는 것이다. 그리고 각 큐브는 다른 shader program을 사용하여 보여진다. 그 4개의 쉐이더 프로그램들은 각각 같은 vertex shader를 쓰지만, 다른 fragment shader를 가진다. 그 fragment shader는 오직 쉐이더마다 다른 single color를 만들어 낸다.
처음에, 우리는 binding point 0과 동일하게 vertex shader들의 uniform block을 설정한다. 우리가 이것을 각 쉐이더에 해야하는 것을 주목해라: (내꺼 코드로 한다.)
unsigned int glsladvanindex = glGetUniformBlockIndex(glslShader.ID, "Matrices"); unsigned int lighting_sha = glGetUniformBlockIndex(assetManager.getShader(SHADER_BASIC_LIGHT)->ID, "Matrices"); glUniformBlockBinding(glslShader.ID, glsladvanindex, 0); glUniformBlockBinding(assetManager.getShader(SHADER_BASIC_LIGHT)->ID, lighting_sha, 0);
다음으로, 우리는 실제 uniform buffer object를 만들고, 또한 그 buffer를 binding point 0에 바인드 한다:
unsigned int uboExampleBlock; glGenBuffers(1, &uboExampleBlock); glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); glBufferData(GL_UNIFORM_BUFFER, sizeof(glm::mat4) * 2, NULL, GL_STATIC_DRAW); glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboExampleBlock, 0, sizeof(glm::mat4) * 2);
처음에 우리는 우리의 버퍼를 위해 glm::ma54의 크기의 두배인 충분한 메모리를 할당한다. GLM의 행렬 type의 사이즈는 GLSL에서 mat4와 직접적으로 대응된다. 그러고나서 우리는 그 buffer의 특정한 범위를 연결한다. 이 경우에 그 버퍼는 전체 버퍼이고, binding point는 0이다.
이제 해야할 남은 것은 실제로 버퍼를 채우는 것이다. 만약 우리가 projection matrix의 view value의 필드를 유지한다면(그래서 어떠한 카메라 줌도 없다), 우리는 오직 그것을 우리의 프로그램에서 한 번 정의해야만 한다. - 이것은 우리가 오직 이것을 그 버퍼에 한번만 넣는 것을 의미한다. 왜냐하면 우리는 buffer object에 충분한 메모리를 할당했기 때문에, 우리는 우리가 game loop에 들어가기전에 projection matrix를 저장하기 위해 glBufferSubData를 사용할 수 있다.
여기에서 우리는 uniform buffer의 첫 절반을 projection matrix로 저장한다. objects를 그리기전에, render iteration마다, 우리는 view matrix의 버퍼의 두 번째 절반을 업데이트한다:
(나는, camera zoom을 그대로 사용했고, uniform buffer block의 순서는 view, projection순서대로이다.)
view = camera.GetViewMatrix(); projection = glm::perspective(glm::radians(camera.Zoom), (float)SCREEN_WIDTH / SCREEN_HEGIHT, 0.1f, 100.0f); glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(view)); glBindBuffer(GL_UNIFORM_BUFFER, 0); glBindBuffer(GL_UNIFORM_BUFFER, uboExampleBlock); glBufferSubData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), sizeof(glm::mat4), glm::value_ptr(projection)); glBindBuffer(GL_UNIFORM_BUFFER, 0);
그리고 이것이 uniform buffer objects에 대해 다이다. Matrices uniform block을 포함하는 각 vertex shader는 이제 uboMatrices에 저장된 데이터를 포함할 것이다. 그래서 만약 우리가 이제 4개의 다른 쉐이더들을 사용하여 4개의 큐브들을 그리려한다면, 그것들이 projection과 view matrix는 항상 같을 것이다:
test.updateAndrender(view, projection, camera.Position); monster.updateAndrender(view, projection, camera.Position); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, frontTex); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, backTex); glslShader.use(); glm::mat4 pmodel(1.0); pmodel = glm::translate(pmodel, glm::vec3(1.f, 1.f, 2.f)); pmodel = glm::scale(pmodel, glm::vec3(0.5f)); glslShader.setMat4("model", pmodel); glBindVertexArray(cubeVAO); glDrawArrays(GL_TRIANGLES, 0, 36);
우리가 여전히 설정할 필요가 있는 유일한 uniform은 model uniform이다. 이것과 같은 시나리오에서 uniform buffer objects를 사용하는 것은 shader마다 몇 가지 uniform 호출을 줄여준다. 그 결과는 이것과 같다:
(똑같이 렌더링이 잘 된다.)
그 모델 행렬을 변경하여 그 큐브 각각은 윈도우의 한쪽으로 이동된다. 다른 fragment shader 때문에 그것들의 오브젝트 컬러들은 다르다. 이것은 우리가 uniform buffer objects를 사용할 상대적으로 간단한 시나리오이다. 그러나 어떤 큰 렌더링 프로그램은 수백개의 쉐이더 프로그램을 시킬 수 있다; 이것은 uniform buffer objects가 정말 빛나기 시작하는 곳이다.
너는 uniform example application의 완전하 소스코드를 여기에서 볼 수 있다.
Uniform buffer objects는 single uniforms에 비해 몇 가지 이점을 가진다. 첫 째로, 한 번에 많은 uniforms을 설정하는 것은 한 번에 다양한 uniforms을 설정하는 것보다 더 빠르다. 두 번째로, 만약 너가 몇 가지 쉐이더들에 대해 같은 uniform을 바꾸려 한다면, uniform buffer에서 한 uniform을 한 번 바꾸는 것이 더 쉽다. 즉시 명백하지 않은 한 가지 마지막 장점은 너는 uniform buffer objects를 사용하여 쉐이더들에서 좀 더 많은 uniforms을 사용할 수 있다. OpenGL은 GL_MAX_VERTEX_UNIFORM_COMPONENTS로 쿼리되어 그것이 얼마나 많은 uniform data를 다룰 수있는지에 대한 한계를 가지고 있다. uniform buffer objects를 사용할 때, 이 한계는 더 높아진다. 그래서 너가 최대 uniforms에 도달했을 때 (예를들어 skeletal animation을 할 때), 너는 uniform buffer objects를 항상 쓸 수 있다.
==================================================
조금 빡셌다. 내용이 길어서.
Uniform buffer objects는 성능을 위해서 반드시 선택해야 하는 것이다.
지금 당장 이것을 사용하는 구조로서 코드를 확장할 수 있지만,
나중에 만드는 것으로 하자.
미리 그 코드에 대한 구조를 말해보자면,
Uniform buffer objects만을 관리하는 클래스를 만들어서,
여러가지 쉐이더들을 덧붙여서 그 클래스가 한 번에 데이터를 쉐이더에 전송하여
효율을 끌어올리게 해야겠다.
댓글 없음:
댓글 쓰기