테셀레이션은 우리가 만든 메시의 삼각형들을 더 잘게 쪼개서 새로운 삼각형들을 만드는 과정을 말한다.
이렇게 새로운 삼각형들을 만들어서 원래 메시에선 표현하기 힘들었던 부분을 더 자세하게 표현할 수 있다.
테셀레이션의 장점은 다음과 같다.
이런 장점을 가진 테셀레이션 단계는 사실 덮개 쉐이더(Hull shader), 테셀레이션(Tessellation), 영역 쉐이더 (Domain shader) 이렇게 세 가지의 단계로 구성되어 있다.
시작하기 전에 테셀레이션에서 사용되는 용어을 먼저 보면 정점 = 컨트롤 포인트이고, 패치 = 컨트롤 포인트들로 만들어낸 다각형 전체를 말한다.
이제부터 이 단계들이 어떤 작업을 하는지 알아보도록 하자.
테셀레이션을 사용하기 위해선 몇 가지 달라지는 부분이 있다.
우선 Input Assembler 단계에서 Primitive Topology 타입을 설정했는데 이 부분을 D3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST와 같은 타입으로 바꿔줘야 한다.
이 타입은 1부터 32까지의 컨트롤 포인트를 사용하겠다는 것이고 이 컨트롤 포인트로 하나의 다각형을 만들어 테셀레이션을 수행한다. 또한 파이프라인 설정을 할 때도 PrimitiveTopologyType을 설정하는데 D3D12_PRIMITIVR_TOPOLOGY_TYPE_PATCH로 설정해야 한다.
테셀레이션은 단계 내부적으로 컨트롤 포인트들을 이용해 배지에 곡선 만들고, 각 점들에 대해 선형보간을 통해 베지에 곡선의 한 점을 얻게된다.
그렇게 얻게 된 한 점이 추가된 정점이 되는 것이다.
덮개 쉐이더는 두 종류의 쉐이더로 나눠지는데 하나는 상수 덮개 쉐이더고 다른 하나는 제어점 덮개 쉐이더다.
상수 덮개 쉐이더는 패치마다 실행되는 쉐이더 함수로서 소위 테셀레이션 계수(tessellation factor)를 출력하는데 이 계수가 다음 단계에서 패치를 얼마나 세분할 것인지 결정하는 것이다.
우선 컨트롤 포인트 3개로 삼각형을 만들어서 제출했다고 가정하고 보자.
PatchTess ConstantHS(InputPatch<VS_OUT, 3> input, int patchID : SV_PrimitiveID)
{
PatchTess output = (PatchTess)0.f;
output.edgeTess[0] = 1;
output.edgeTess[1] = 2;
output.edgeTess[2] = 3;
output.insideTess = 1;
return output;
}
edgeTess는 삼각형의 한 변을 몇 개로 나눌것인지 말하고, insideTess는 내부적으로 테셀레이션을 몇 번 수행할 것인지를 말한다.
edgeTess가 1이면 변을 나누지 않겠다는 말이고 insideTess가 1이면 삼각형의 경우 내부 컨트롤 포인트가 하나가 될 것이다.
이 단계에서는 우리가 처음에 입력한 메시 컨트롤 포인트에 대해서 호출이 된다.
원한다면 컨트롤 포인트를 증가시킬 수 있고, 변환도 가능하다. 목적에 따라서 컨트롤 포인트를 조작하기 위한 단계라고 볼 수 있다.
[domain("tri")] //패치의 종류다. 삼각형이니 tri(tri, quad, isoline)
[partitioning("integer")] // 테셀레이션 세분화 모드.(integer, fractional_even, fractional_odd)
[outputtopology("triangle_cw")] // 새로 만들어진 삼각형들의 감기는 순서.(cw : 시계방향, ccw : 반시계, line : 선 테셀레이션)
[outputcontrolpoints(3)] // 입력된 컨트롤 포인트 갯수
[patchconstantfunc("ConstantHS")] // Constant hull shader 함수의 이름
HS_OUT HS_Main(InputPatch<VS_OUT, 3> input, int vertexIdx : SV_OutputControlPointID, int patchID : SV_PrimitiveID)
{
HS_OUT output = (HS_OUT)0.f;
output.pos = input[vertexIdx].pos;
output.uv = input[vertexIdx].uv;
return output;
}
이 함수는 모든 제어점에 대해서 호출되는데 제공되는 값은 패치로 넘겨받는다. 그렇기 때문에 SV_OutputControlPointID를 통해 인덱스 접근으로 현재 호출된 제어점을 알 수 있다.
이 단계는 새로 만들어진 컨트롤 포인트을 포함해서 모든 컨트롤 포인트에 대해서 호출된다. 바로 이 곳에서 모든 정점에 대한 변환을 할 수 있는 것이다.
[domain("tri")]
DS_OUT DS_Main(const OutputPatch<HS_OUT, 3> input, float3 location : SV_DomainLocation, PatchTess patch)
{
DS_OUT output = (DS_OUT)0.f;
float3 localPos = input[0].pos * location[0] + input[1].pos * location[1] + input[2].pos * location[2];
float2 uv = input[0].uv * location[0] + input[1].uv * location[1] + input[2].uv * location[2];
output.pos = mul(float4(localPos, 1.f), g_matWVP);
output.uv = uv;
return output;
}
여기서 주의 할 점은 그 컨트롤 포인트에 바로 접근하는게 아니라 역시나 패치를 인자로 받게 되는데 그 패치의 컨트롤 포인트에 접근하기 위해선 SV_DomainLocation을 이용해야 한다.
SV_DomainLocation은 인덱스 같은게 아니라 그 컨트롤 포인트의 위치를 비율로 알려준다.
다음과 같은 상태일때 cp0은 SV_DomainLocation의 값이 {1, 0, 0}, cp1은 {0, 1, 0}, cp2는 {0, 0, 1}이고 가운데에 새로 생긴 cp는 정 가운데에 있으니 {0.33, 0.33, 0.33} 이런 식으로 기존 컨트롤 포인트를 기준으로 얼마나 가중치를 갖고있냐가 SV_DomainLocation의 값이다.
그래서 Position에 가중치를 곱해서 그 컨트롤 포인트(정점)의 위치를 정확하게 알 수 있다.