참조 :
https://learnopengl.com/Guest-Articles/2021/Tessellation/Tessellation
https://www.khronos.org/opengl/wiki/Tessellation
Tessellation Shader는 OpenGL 4.0 버전부터 지원하는 기능이다.
용어 정리부터 해보자면,
Primitive란 폴리곤의 타입(triangle, quad, ...)이며,
Patch란 임의의 n개의 버텍스를 가지는 abstract primitive를 의미한다.
CPU 단에서 patch가 몇 개의 버텍스를 가질지 정해줄 수 있는데,
glPatchParameteri(GL_PATCH_VERTICES, 4);
위 코드처럼 4를 넣었다는 것은 patch가 4개의 버텍스로 이루어질거라는 것, 즉 quad와 같다는 것을 의미한다.
CPU에서 primitive를 patch로 정하여 드로우콜을 해줬다면, 즉 이런 코드를 호출했다면
glBindVertexArray(VAO);
glDrawArrays(GL_PATCHES, 0, patchCount);
GPU 안에서는 내가 받은 patch들을 triangle 등으로 더 잘게 쪼갤 수 있다.
이렇게 GPU 내부에서 patch를 더 작은 primitive로 쪼개는 것을 Tessellation이라고 한다.
OpenGL의 렌더링 파이프라인을 다시 한번 살펴보면 다음과 같이 구성되어있다.
보시다시피 Tessellation은 Vertex Shader와 Geometry Shader 중간에 위치하고 있다.
그런데 Tessellation이라는 저 단계는 하나의 Shader가 해결하는게 아니고, 그 안에서 또 3개의 단계로 나누어진다.
그 3단계는 다음과 같다.
- Tessellation Control Shader : 테셀레이션을 얼마나 할지, 즉 patch를 얼마나 잘게 쪼갤 것인지 정한다.
- Tessellation Primitive Generator : 테셀레이션을 수행한다. 정확하게는 patch 내부에 분할점들을 형성한다.
- Tessellation Evaluation Shader : 형성된 분할점들을 새로운 버텍스로 만들어준다.
Tessellation Control Shader에서는 patch가 몇 개의 버텍스로 구성되어있는지 명시해주고, 수행할 테셀레이션의 레벨을 정해주어야 한다.
중요한 것은, Tessellation Control Shader는 patch 내부의 각 버텍스에 대해서 돌아간다는 것이다.
Patch 내 버텍스의 개수를 4로 정했다면, 쉐이더의 인풋은 4개의 버텍스가 된다. 즉, 버텍스 배열을 받는다.
아웃풋도 마찬가지로 크기가 4인 버텍스의 배열이다.
각각의 버텍스는 다음과 같은 형태의 구조체로써 전달된다.
in gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
}
배열의 index로는 built-in변수인 gl_InvocationID를 사용한다.
gl_InvocationID는 patch 내 어떤 버텍스인지를 의미하는 ID이다.
그리고 다음 단계의 쉐이더로 버텍스 데이터를 넘겨주기 위해서는 gl_in, gl_out 키워드를 사용한다.
gl_Position을 그대로 뒤로 넘겨주는 간단한 코드는 다음과 같다.
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
또 한가지 역할인 테셀레이션의 레벨을 정하는 것은, 우선 아래의 그림을 보자.
IL은 Inner Level, OL은 Outer Level을 나타낸다.
쉐이더에서는 gl_TessLevelInner, gl_TessLevelOuter 키워드를 사용한다.
예시:
gl_TessLevelOuter[0] = 16;
gl_TessLevelOuter[1] = 16;
gl_TessLevelOuter[2] = 16;
gl_TessLevelOuter[3] = 16;
gl_TessLevelInner[0] = 16;
gl_TessLevelInner[1] = 16;
이렇게 테셀레이션의 레벨까지 정해주면 Tessellation Control Shader의 역할이 끝난다.
간단한 예시:
#version 460 core
layout (vertices = 4) out;
void main()
{
gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
// invocation 0이 전체 patch의 테셀레이션 레벨을 지정
if (gl_InvocationID == 0)
{
gl_TessLevelOuter[0] = 16;
gl_TessLevelOuter[1] = 16;
gl_TessLevelOuter[2] = 16;
gl_TessLevelOuter[3] = 16;
gl_TessLevelInner[0] = 16;
gl_TessLevelInner[1] = 16;
}
}
이제 Tessellation Primitive Generator에서는 정해진 레벨에 따라 patch를 쪼갠다.
정확히 어떤식으로 쪼개냐하면, Inner Level을 다음과 같이 정했다고 하자.
gl_TessLevelInner[0] = 6;
gl_TessLevelInner[1] = 4;
그 경우 patch는 다음과 같이 쪼개진다.
이렇게, IL-0은 가로를 n등분, IL-1은 세로를 n등분한다.
이후 patch의 각 모서리는 Outer Level에 의해 따로 분할된다.
예를 들어 Level을 다음과 같이 정했다고 가정하자.
gl_TessLevelOuter[0] = 4;
gl_TessLevelOuter[1] = 2;
gl_TessLevelOuter[2] = 9;
gl_TessLevelOuter[3] = 3;
gl_TessLevelInner[0] = 6;
gl_TessLevelInner[1] = 7;
그럼 patch는 다음과 같이 분할된다.
이렇게 patch 내부에 분할점들이 생긴 뒤, 이런식으로 쪼개진다.
그리고 분할 spacing에는 3가지 방식이 있다.
Equal Subdivision:
Fractional Odd Subdivision:
Fractional Even Subdivision:
위 예시는 Equal Subdivision을 사용한 걸 볼 수 있다.
이제 마지막 단계인 Tessellation Evaluation Shader에서는 생성된 분할점 각각에 대해 돌아간다.
분할점의 좌표를 전달받아, 해당 좌표와 patch의 버텍스 4개의 데이터를 이용해 Bilinear Interpolation을 수행하여 분할점들에 데이터를 넣어준다.
Bilinear Interpolation에 대한 글은 https://ciel45.tistory.com/90
이 때의 좌표들은 Patch Space로, 텍스쳐 좌표계와 비슷하다.
각 분할점의 좌표는 [0, 1] 범위의 값을 가지며, gl_TessCoord라는 키워드로 전달된다.
이제 해당 키워드를 이용해 분할점에 데이터를 채워주면, 그 데이터는 Geometry Shader(구현했다면) / Fragment Shader로 넘어가 Rasterization을 거쳐 per_fragment 데이터가 된다.
주의할 점은, Tessellation Evaluation Shader의 경우 인풋은 Tessellation Control Shader처럼 patch 내 버텍스의 배열로 받지만, 아웃풋은 단일 버텍스(분할점) 데이터라는 것이다. 즉 Vertex Shader랑 비슷하게 gl_Position = ~~ 이런식으로 해주면 된다.
간단한 예시:
#version 460 core
layout (quads, equal_spacing, ccw) in;
uniform mat4 PVM;
void main()
{
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
vec4 p00 = gl_in[0].gl_Position;
vec4 p01 = gl_in[1].gl_Position;
vec4 p10 = gl_in[2].gl_Position;
vec4 p11 = gl_in[3].gl_Position;
vec4 p0 = mix(p00, p01, u);
vec4 p1 = mix(p10, p11, u);
vec4 p = mix(p0, p1, v);
gl_Position = PVM * p;
}
쉐이더 코드 내용이 Bilinear Interpolation 그 자체인 것을 볼 수 있다.
이러한 과정을 통해 CPU에서는 적은 양의 버텍스만 보내더라도 GPU 내에서 폴리곤을 잘게 쪼개 쓸 수 있다.
CPU의 부하를 GPU로 덜어주는 기술이라고 할 수 있다.
'OpenGL > 공부' 카테고리의 다른 글
[OpenGL] Skybox (0) | 2024.07.18 |
---|---|
[OpenGL] FrameBuffer 생성, 사용 (0) | 2024.07.16 |
[OpenGL] ImGuizmo 설치, 사용법 (0) | 2024.07.16 |
[OpenGL] Model Loading (Assimp) (1) | 2024.07.13 |
[OpenGL] Phong Reflection Model - 구현 (0) | 2024.06.19 |