
쉐이더는 파이프라인화 되어있음
따라서 그 방식에 맞춰 uniform, vertex attributes, sampler등을 이용해 쉐이더 외부의 값을 이용해 쉐이더 파이프라인 내부에서 사용하도록 하는법을 공부했었음
GLSL내부에서 데이터를 입출력할 수 있는 몇가지 메서드가 있음
gl_로 시작하는 메서드인데 몇가지를 알아보도록 하자
glDrawArrays(GL_TRIANGLES, 0, 36);
이렇게 렌더링 옵션을 설정해줄 수 있었음
여기서 사용할 수 있는게
GL_POINTS임
그리고 gl_PointSize = gl_position.z
이런식으로 사용해서 pointSize를 결정지어주면 됨
cpu, opengl에서 glPointSize를 해도 되지만
gpu에서 gl_PointSize하는게 더 낫겠지?
전체 사용은 다음과 같음
//...
glEnable(GL_GL_PROGRAM_POINT_SIZE);
//rendering
//...
glDrawArrays(GL_POINTS, 0, 36);
//...
//---------------
//vertex shader
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;
out vec2 TexCoords;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main()
{
TexCoords = aTexCoords;
gl_Position = projection * view * model * vec4(aPos, 1.0);
gl_PointSize = gl_Position.z;
}

이렇게 멀어질수록 커지고, 가까워질수록 작아지는 큐브형태 점 완성~~
cpu에서 VBO를 통해 gpu로 데이터를 보내지 않고
gpu에서 바로 vertex id지정해서 사용할 수 있음
glDrawArrays(GL_TRIANGLES, 0, 6);
여기서 6개의 정점을 사용한다고 했음
그럼 gpu에서는 vertexId가 0~5까지 증가하는 id를 제공함
만약 glDrawElements를 사용한다면 index에 해당되는 번호를 제공함
따라서 vertex shader에서
#version 330 core
layout (location = 1) in vec2 aTexCoords;
out vec2 TexCoords;
void main()
{
vec2 pos;
if (gl_VertexID == 0) pos = vec2(-0.7, 1.0);
else if (gl_VertexID == 1) pos = vec2(-1.0, -1.0);
else if (gl_VertexID == 2) pos = vec2(1.0, 1.0);
else if (gl_VertexID == 3) pos = vec2(-1.0, -1.0);
else if (gl_VertexID == 4) pos = vec2(1.0, -1.0);
else pos = vec2(1.0, 1.0);
gl_Position = vec4(pos, 0.0, 1.0);
TexCoords = pos * 0.5 + 0.5;
}
이렇게 vertexID를 이용해 삼각형을 그릴 수 있음

굳~~
gl_FragCoord는 읽기 전용 변수임
fragment픽셀의 화면공간좌표 값임
vec4자료형으로 x,y,z,값을 가짐
x,y는 화면공간의 x,y를 의미함
그냥 화면 가로/세로에서의 fragment좌표 인거임
z는 해당 fragment의 깊이를 의미함
는 원근 투영시에 보간되는 값을 의미함
LearnOpenGL(6) - 좌표 시스템, 클립 공간의 원근 분할쪽을 보면 됨
의 이유
w값은 원근투영시에 원근 분할시에
x,y,z와 각각 나누어 원근감을 주게 됨하지만 나눗셈 연산은 연산비용이 좀 있음
따라서 각 fragment에 대해 w로 나누기보다
w의 역수인 를 저장해두고 이를 곱하는 방식으로 연산비용을 줄일 수 있음즉, 2D 화면공간에서 텍스처나 색상 데이터가 원근감에 맞게 왜곡 없이
보간되기 위해가 쓰이는 거임
그럼 이걸 어떻게 사용하냐?
fragment shader를 수정하면 됨
#version 330 core
out vec4 FragColor;
void main()
{
if (gl_FragCoord.x < 640 && gl_FragCoord.y < 360)
{
FragColor = vec4(0.0, 0.0, 1.0, 1.0);
}
else if (gl_FragCoord.x < 640 && gl_FragCoord.y > 360)
{
FragColor = vec4(0.0, 1.0, 0.0, 1.0);
}
else if (gl_FragCoord.x > 640 && gl_FragCoord.y < 360)
{
FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
else
{
FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
}
간단하게 fragment를 4사분면으로 나눠서
각각 다른 색상을 지정해준거임

굳~~~
그냥 fragment shader에서 읽기 전용인 변수임
해당 fragment가 앞면이면 true, 아니면 false를 반환함
따라서 보이는 면의 texture와 안보이는 면의 texture를 다르게 설정해줄 수 있음
하지만 Advanced OpenGL(4) - face culling
여기서 살펴본 face culling을 활성화한다면 gl_FrontFacing는 무용지물이 됨
이번엔 읽기변수가 아닌 입력변수임
값을 수정할 수 있다는거
gl_FragDepth는 fragment의 깊이를 수정할 수 있는 변수임
이 변수를 사용하지 않을때는 자동으로 gl_FragCoord.z를 사용함
하지만 직접 사용하지 않는 편이 최적화 관점에서는 더 좋음
Advanced OpenGL(1) - Depth test, early depth test에서 알 수 있듯
조기 깊이 테스트를 할 수 없음
!!!!! 하지만 GL4.2버전 이상에서부턴 shader 위에 layout으로 설정을 통해 가능하게 할 수 있음
#version 420 core // 4.2 OPENGL
out vec4 FragColor;
layout (depth_<condition>) out float gl_FragDepth;
void main()
{
FragColor = vec4(1.0);
gl_FragDepth = gl_FragCoord.z + 0.1;
}
여기 layout의 depth_<condition>를 이용해 조기 깊이테스트를 가능하게 할 수 있음
condition은 다음과 같음
| condition | 설명 |
|---|---|
| any | 기본값, 조기 깊이 테스트가 비활성화 |
| greater | gl_FragCoord.z에 비해 깊이 값을 더 크게만 만들 수 있음 |
| less | gl_FragCoord.z에 비해 깊이 값을 더 작게만 만들 수 있음 |
| unchanged | gl_FragCoord.z에 사용중인 값이 그대로 사용됨 |
unchanged는 z값에 변경이 없다는걸 의미함
any를 제외한 모든 condition은 early depth testing이 활성화된다는거 잊지말고~~
앞의 모든 챕터에서 shader를 다룰때,
in, out이라는걸 이용해 각 shader사이에서 데이터를 사용하도록 했음
하지만 어플리케이션의 규모가 커지면
당연히 in, out해야할 변수도 많아지고
이걸 하나로 관리하고 싶어질때가 올거임
이때 사용하는게 구조체랑 비슷한 인터페이스 블록임
코드로 보는게 더 이해가 빠를거임
아래는 vertex shader코드
#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;
}
아래는 fragment shader코드
#version 330 core
out vec4 FragColor;
in VS_OUT //인터페이스 이름 통일
{
vec2 TexCoords;
} fs_in; //원하는 이름으로 사용 가능
uniform sampler2D texture;
void main()
{
FragColor = texture(texture, fs_in.TexCoords);
}
이렇게 볼 수있듯이
인터페이스 이름과 내부 변수의 이름만 통일하고(필수)
사용할 인터페이스 변수명은 다르게 가져갈 수 있음
지금까지 모든 챕터에서 glsl의 uniform을 설정하려고 할때
각각 개별의 uniform으로 설정했음
예를들어 오브젝트를 화면 공간에 넣어서 출력하기위해 계산하는 여러 행렬이 있었음
model, view, projection행렬이 그것이었음 ㅇㅇ
이 각각의 행렬을 uniform을 통해
cubeShader.setMat4("model", model);
cubeShader.setMat4("view", view);
cubeShader.setMat4("projection", projection);
이렇게 각각 데이터를 넣어줬음
근데 ubo(Uniform buffer object)를 이용하면 이렇게 각각 넣어줄 필요 없이
uniform을 공통되는 역할로 묶어 하나의 uniform으로 관리할 수 있음
#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);
}
사용예시로 Matrices라는 uniform블럭 안에 projection, view를 냅두고 model은 따뤄 함 냅둬봄
그럼에도 불구하고 제대로 동작한다는거임
자, 이제 여기서 1가지 모르겠는 부분이 코드에서 보일거임
천천히 살펴보자
유니폼 블럭 레이아웃이라 부르는게 있음
layout (std140)ㅇㅇ
해당 uniform block이 어떻게 gpu에 저장될지를 결정짓는 요소임
예를들어
layout (std140) uniform ExampleBlock
{
float value;
vec3 vector;
mat4 matrix;
float values[3];
bool boolean;
int integer;
};
이런 uniform block이 있다고 해보자
기본 layout은
layout (shared)임
shared layout은 gpu가 자체적으로 최적화를 진행해서
cpu에서 우리가 구조체를 만들어 넘겨줘도 메모리 위치를 확실하게 모르기때문에
glGetUniformBlockIndex를 이용해 shader프로그램의 uniform을 읽음glGetUniformIndices를 이용해 해당 uniform의 블럭 변수들을 다시 읽음glGetActiveUniformsiv를 이용해 각 블럭 변수들의 offset을 가져옴glBufferSubData등을 이용해 값 넣기
layout(packed)도 존재함이는 offset을 최대한으로 압축해서 사용하는 layout임
컴파일러마다 내부 구조가 달라져서 사실상 공유되는 uniform일수 없는거임
float은 n, vec3는 4n, vec2는 2n, mat4는 4*4n, 배열은 4\xn, bool은 n, int는 n 이런식으로 offset이 결정되게 됨
layout은 우리가 원하는 offset으로 지정할 수 있지만,
혹은 하드웨어에서 직접 원하는 방식대로 offset을 지정하고 최적화 하도록 할 수도 있음
이 offset을 올바르게 지정하지 못하면 무슨 에러가 생길지 모름
따라서 되도록이면 우리가 직접 지정하고 사용하는게 좋음
그렇게 사용자가 직접 지정하는 엄격한 offset을 layout(std140)이라고 하는거임
OpenGL과 cpp의 primitive 타입은 결국 상호호환되니까 ㅇㅇ
그리고 업계 표준이 layout(std140)이고 ㅇㅇ
위 예시 코드의 offset을 계산하면 아래와 같음
layout (std140) uniform ExampleBlock
{
// 기본 정렬 // 정렬된 오프셋
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
};

이 사진을 봐보자
여러 shader에서 binding points라는걸 통해 값을 받아올 수 있음
이때 binding points에 데이터는 ubo를 통해 넣어주게 됨
그럼 어떤 binding points에 어떤 데이터를 넣어줘야하는지
어떻게 결정할까?
정답은 우리가 직접 결정해주면됨
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;
out vec2 TexCoords;
layout(std140) uniform UniformBlockView
{
mat4 view;
};
layout(std140) uniform UniformBlockProjection
{
mat4 projection;
};
uniform mat4 model;
void main()
{
TexCoords = aTexCoords;
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
이렇게 vertex shader를 수정해줌
일부러 여러개의 binding points를 설정하는걸 보여주기위해 예제를 짬
여러개의 shader에서 동일한 2개의 uniform block을 사용할때 사용한다는 가정하에 코드를 짜는거지비
uint32_t redUBView = glGetUniformBlockIndex(redCubeShader.ID, "UniformBlockView");
uint32_t redUBProjection = glGetUniformBlockIndex(redCubeShader.ID, "UniformBlockProjection");
uint32_t greenUBView = glGetUniformBlockIndex(greenCubeShader.ID, "UniformBlockView");
uint32_t greenUBProjection = glGetUniformBlockIndex(greenCubeShader.ID, "UniformBlockProjection");
glUniformBlockBinding(redCubeShader.ID, redUBView, 0);
glUniformBlockBinding(redCubeShader.ID, redUBProjection, 1);
glUniformBlockBinding(greenCubeShader.ID, greenUBView, 0);
glUniformBlockBinding(greenCubeShader.ID, greenUBProjection, 1);
uint32_t uboView, uboProjection;
glGenBuffers(1, &uboView);
glGenBuffers(1, &uboProjection);
glBindBuffer(GL_UNIFORM_BUFFER, uboView);
glBufferData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), nullptr, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// binding point에 들어갈 데이터의 크기 정의
//glBindBufferBase(GL_UNIFORM_BUFFER, 0, uboView);
glBindBufferRange(GL_UNIFORM_BUFFER, 0, uboView, 0, sizeof(glm::mat4));
glBindBuffer(GL_UNIFORM_BUFFER, uboProjection);
glBufferData(GL_UNIFORM_BUFFER, sizeof(glm::mat4), nullptr, GL_STATIC_DRAW);
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// binding point에 들어갈 데이터의 크기 정의
//glBindBufferBase(GL_UNIFORM_BUFFER, 1, uboProjection);
glBindBufferRange(GL_UNIFORM_BUFFER, 1, uboProjection, 0, sizeof(glm::mat4));
이렇게 binding points를 공유하도록 하면 됨
원하는 형식, 크기, 값을 넘겨주면 됨
glGetUniformBlockIndex를 이용해 uniform block의 index를 찾고,glUniformBlockBinding를 이용해 해당 uniform block index에 binding points 번호를 부여glBufferData로 사용할 버퍼의 크기 부여 glBindBufferBase혹은glBindBufferRange로 binding points의 번호에 데이터 넣기이제 렌더링에서 사용하기만 하면 되는데 엄청 간단함
while(...)
{
//...
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = camera.GetViewMatrix();
glBindBuffer(GL_UNIFORM_BUFFER, uboView);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(view));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
glm::mat4 projection = glm::perspective(glm::radians(camera.Zoom), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
glBindBuffer(GL_UNIFORM_BUFFER, uboProjection);
glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(glm::mat4), glm::value_ptr(projection));
glBindBuffer(GL_UNIFORM_BUFFER, 0);
// cubes
glBindVertexArray(cubeVAO);
redCubeShader.use();
model = glm::mat4(1.0);
model = glm::translate(model, glm::vec3(-0.7f, 0.0f, 0.0f));
redCubeShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
greenCubeShader.use();
model = glm::mat4(1.0);
model = glm::translate(model, glm::vec3(0.7f, 0.0f, 0.0f));
greenCubeShader.setMat4("model", model);
glDrawArrays(GL_TRIANGLES, 0, 36);
}

그럼 이렇게 되어버리는거지비
3번이 무슨소리인가 하면...
skeletal animation이라는게 있음
단순하게 설명하면 3D모델을 리깅을 통해 애니메이팅한거임
리깅을 하려면 bone이라는 개념이 필요함
bone은 말그대로 뼈라는 뜻으로 관절을 의미하는데,
요구사항에 따라 다르겠지만 고퀄 3D모델의 애니메이션에는 많은 관절이 필요함
일반 uniform으로 해당 bone들의 shader program을 짜야하면 uniform의 크기는 제한적이기 때문에 쉐이더가 터질 가능성이 있음
이때 사용하는게 uniform block으로 일반 uniform보다 더 많은 메모리를 할당할 수 있기때문에, 이를 사용하는편이 정신건강에 더 좋은거임