2021년 2월 9일 화요일

DirectX - Tessellation, Geometry

참고할 사이트

https://www.informit.com/articles/article.aspx?p=2120983

http://reedbeta.com/blog/tess-quick-ref/

https://vsts2010.tistory.com/search/JumpToDX11-20


파이프라인

Input Assembler => Vertex Shader

=> ( Hull Shader => Tessellator => Domain Shader )

=> Geometry Shader (=> Stream output) 

=> Rasterizer => Pixel Shader => Output Merget


개념

Patch

 Vertex 의 묶음임.

 한번에 32개까지 보낼 수 있으며 이 경우 각 Vertex 는 Control Point 라고 부름. 

 각 Control Point 마다 HS 쉐이더가 호출되며 같은 Patch 의 Control Point 끼리는 HS, DS 에서 참조가 가능함.

 IASetPrimitiveTopology 에서 PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST 같은걸로 원하는 Patch 의 수로 지정해 넣어줘야함. 이때 말하는 Topology 는 HS, DS 에서 말하는 Topology 와는 별개임을 주의.

 Control Points 의 정보는 DS 에서 사용되지 Tessellation 에서 사용되지 않음에 주의. 즉 Primitive 와는 관계가 없음. Tessellation 은 Tessellation Factor 만 가지고 Primitive를 쪼개고 상대적 위치인 uv/uvw 를 내놓음. 우리는 uv와 Control Points 를 가지고 DS 에서 쪼개진 각각의 위치값을 처리하는 프로그래밍을 하는 것임.


Tessellation Factor

Primitive 를 얼마나 쪼갤건가에 대한 값.

TessFactor, InsideTessFactor 가 있으며 Primitive 마다 구성이 다름. 아래 Primitive 참고.

각 Patch 마다 호출되는 PatchConstantFunc 에서 Factor 를 설정해 리턴해주게 됨.

hull-shader에서 edge tessellation factor가 0이하거나 NaN으로 설정되면 patch는 컬링됨. 



Hull Shader 

openGL 에서는 TCS(Tessellation Control Shader) 라고 부름.

VS 의 리턴값인 Control Point 마다 호출이 됨.


하는 일

Inner/Outer Tessellation Factor 를 처리

Output Control Point 마다 Position, Normal 등 정보를 전달

쉐이더니까 유저가 Control Point 관련 조작 가능


Input Layout Qualifier

함수를 보면 위에 덕지덕지 붙어있는걸 말함.

여기선 중요한거 몇개만 설명.


Tessellation Primitive Mode

quad, triangle, isoline 이 가능하며 Tessellation 에서 Primitive 를 해석하는데 쓰임.

Tessellation Factor 는 이거에 따라서 달라짐. quad 의 경우 inner 2개 outer 4개, tri 의 경우 inner 1개 outer 3개, isoline 의 경우 outer 2 개가 됨.

Domain Shader 의 input 으로 오는 상대좌표의 해석도 달라짐. 


Tessellation Subdivision Mode

Primitive 를 주어진 Tessellation Factor 에 대해 어떻게 쪼갤지 결정함.

Integer, Factional_Even/Odd 등이 있으며 후자의 경우 Realtime 에서 Tessellation Factor 가 Animate 될 때 보기 좋아짐. 

이는 그 알고리즘이 소숫점 단위에서도 적용되기 때문인데 5.3 이 factor 인 경우 다음과 같음.

바로 다음의 Even/Odd 의 수의 값인 것처럼 나눔(5.3 이면 6, 7). 위에서 올린 값 - 원래 값 (6 - 5.3, 7 - 5.3) 을 반으로 나눈 값이 양 끝의 segment 의 길이이고 나머지는 같은 간격임. 그럼 fraction 값이 커질수록 양끝이 나머지와 같은 간격이 되고 반대의 경우 짧아질 것임.

이를 설명하는 동적인 이미지는 이거 보셈. 이거


Output Topology

선, 점도 있지만 대개 삼각형이며 이 경우 triangle_cw, triangle_ccw 로 Winding Mode 도 설정가능


Patch Constant Func

Tessellation Factor 를 지정하는 함수를 만들고 그 이름을 적음. 

각 Patch 마다 호출되며 함수 디자인 시에 Hull Shader 의 InputPatch 와 PatchID 를 선택적으로 인자로 지정할 수 있음.


Input

Input 은 VS 가 각 vertex 마다 호출되듯이 HS 도 각 control point ( vs 의 output ) 마다 호출이 됨. 하지만 Patch 에 묶인 control point 들은 서로 접근이 가능하고 그래서 array 로 들어오게 됨. 그래서 ControlPoint 의 값들인 array 와 PatchID 그리고 PointID 를 인자로 받을 수 있는 것임. 

Patch Constant Function 처럼 필요없는 인자는 함수 디자인 때 빼도 상관없음.


Tessellator

Primitive 인 isoline, triangle, quad 를 Factor 에 맞게 쪼개고 uv/uvw 로 상대위치를 내놓음.

Patch 는 전혀 보지 않음에 주의.

자세한건 맨 위의 참고할 사이트들을 참고.

다음 단계인 DS 에서 Primitive 의 상대 위치값을 사용하게 됨.


Domain Shader

openGL 에서는 TES(Tessellation Evaluation Shader) 라고 부름

Tessellator 가 쪼갠 각 정점마다 호출이 됨.


Input

Patch Constant Function 의 결과,

Patch 에서의 상대좌표를 말하는 uv/uvw,

HS 의 patch 단위 control point array 를 받을 수 있음.


Position 구하기

Primitive 마다 다르며 Patch 에서의 상대좌표로 원하는 위치를 구함.

이 경우 Beizer 등 여러가지 기법을 동원해서 다양한 위치를 얻을 수도 있음.

여기선 원래 위치만을 설명함. ( 쪼갠 결과도 평면이 되므로 실제론 사용하지 않음)

삼각형에선 위치가 uvw 로 옴. 이 값은 weight 가 들어간 무게중심으로 여기서 잘 설명되어 있음. 각 0, 1, 2 꼭짓점에 u,v,w 를 각각 곱하면 원래의 위치를 얻을 수 있음. 

사각형에선 위치가 uv 로 옴. 사각형이므로 평범한 uv 를 생각하면 됨. 왼위가 (0,0) 오른 아래가 (1,1) 을 의미함.(openGL 은 y 값 반전임). view space 에서는 반대로 아래가 0 이므로 Patch 의 위치를 주의깊게 고려해야함. 쫄리면 Cull None 해야함.



예제. Cubic Beizer Patch

for (int i = 0; i < 4; i++)
{
	auto offset = D3DXVECTOR3(2, 0, 0);
	switch (i) {
	case 0: offset = D3DXVECTOR3(0, 0, 0); break;
	case 1: offset = D3DXVECTOR3(1, 0, 0); break;
	case 2: offset = D3DXVECTOR3(-1, 0, 0); break;
	case 3: offset = D3DXVECTOR3(3, 0, 0); break;
	}
	vertices[4*i + 0].position = D3DXVECTOR3(0.0f, 0.0f, 0.0f) + offset; 
	vertices[4*i + 1].position = D3DXVECTOR3(0.0f, 1.0f, 0.0f) + offset; 
	vertices[4*i + 2].position = D3DXVECTOR3(0.0f,  2.0f, 0.0f) + offset; 
	vertices[4*i + 3].position = D3DXVECTOR3(0.0f,  3.0f, 0.0f) + offset;  
}

우선 Patch 에 넣을 Vertex 를 설정함. 

삼각형 렌더링할 때와 달리 여기서는, 4개씩 묶여서 y값이 연속적으로 변하고 묶음 끼리는 x 값이 변함. 꼭 이렇게 할 필요는 없는데 bezier 적용하려면 이러는게 나음.



deviceContext->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_16_CONTROL_POINT_PATCHLIST);

사용할 패치는 16 임. 위의 vertices 를 보면 알겠지만 4x4 로 구성함. 

그러니 위처럼 넣는걸 꼭하자.


////////////////////////////////////////////////////////////////////////////////
// Filename: color.hs
////////////////////////////////////////////////////////////////////////////////


/////////////
// GLOBALS //
/////////////
cbuffer TessellationBuffer
{
    float tessellationAmount;
    float3 padding;
};


//////////////
// TYPEDEFS //
//////////////
struct HullInputType
{
    float3 position : POSITION;
    float4 color : COLOR;
};

struct ConstantOutputType
{
    float edges[4] : SV_TessFactor;
    float inside[2] : SV_InsideTessFactor;
};

struct HullOutputType
{
    float3 position : POSITION;
    float4 color : COLOR;
};


////////////////////////////////////////////////////////////////////////////////
// Patch Constant Function
////////////////////////////////////////////////////////////////////////////////
ConstantOutputType ColorPatchConstantFunction(InputPatch<HullInputType, 16> inputPatch, uint patchId : SV_PrimitiveID)
{    
    ConstantOutputType output;

    // Set the tessellation factors for the three edges of the triangle.
    output.edges[0] = 16;//1;// * tessellationAmount;
    output.edges[1] = 16;//1 * tessellationAmount;
    output.edges[2] = 16;//1;// * tessellationAmount;
    output.edges[3] = 16;//1;// * tessellationAmount;

    // Set the tessellation factor for tessallating inside the triangle.
    output.inside[0] = 16;// * tessellationAmount;
    output.inside[1] = 16;// * tessellationAmount;

    return output;
}


////////////////////////////////////////////////////////////////////////////////
// Hull Shader
////////////////////////////////////////////////////////////////////////////////
[domain("quad")]  // patch 의 종류
[partitioning("integer")]   // 데셀레이션 분할 종류
[outputtopology("triangle_cw")]   // 데셀레이터가 출력할 도형 종류
[outputcontrolpoints(16)]   //  출력할 제어점들의 수
[patchconstantfunc("ColorPatchConstantFunction")]   // 패치상수함수

HullOutputType ColorHullShader(InputPatch<HullInputType, 16> patch, uint pointId : SV_OutputControlPointID, uint patchId : SV_PrimitiveID)
{
    HullOutputType output;


	// Set the position for this control point as the output position.
    output.position = patch[pointId].position;

	// Set the input color as the output color.
    output.color = patch[pointId].color;

    return output;
}

ConstantFunction 과 HullShaderFunction 이 들어가 있는 Hull Shader 임.

ConstantFunction 과 그 리턴값의 형식을 주의깊게 보면 됨. 

HullShaderFunction 은 그냥 원래 값 넘기는 것임.


TessFactor 를 상수로 넣었는데, 맨 위에 버퍼에 넣어서 그 값을 넣어도 됨.

앞에서도 말했듯이 이 값은 동적으로 적당히 변하는게 이쁨.


struct PixelInputType
{
    float4 position : SV_POSITION;
    float4 color : COLOR;
};


float4 cubic_bezier(float4 a, float4 b, float4 c, float4 d, float t)
{
    float4 E = lerp(a, b, t);
    float4 F = lerp(b, c, t);
    float4 G = lerp(c, d, t);
   
    return lerp(lerp(E, F, t), lerp(F, G, t), t);
}

float4 evaluate_patch(const OutputPatch<HullOutputType, 16> patch, float2 uv)
{
    float4 p[4];
    
    for(int i = 0 ; i < 4; i++)
    {
        p[i] = cubic_bezier(float4(patch[i+12].position, 0),
                            float4(patch[i+8].position, 0),
                            float4(patch[i+4].position, 0), 
                            float4(patch[i+0].position, 0), uv.x);
    }
    
    return cubic_bezier(p[0], p[1], p[2], p[3], uv.y);
}

////////////////////////////////////////////////////////////////////////////////
// Domain Shader
////////////////////////////////////////////////////////////////////////////////
[domain("quad")]
PixelInputType ColorDomainShader(ConstantOutputType input, float2 uvCoord : SV_DomainLocation, const OutputPatch<HullOutputType, 16> patch)
{
	float3 vertexPosition;
	PixelInputType output;
 
    // Determine the position of the new vertex.
    //vertexPosition = uvwCoord.x * patch[0].position + uvwCoord.y * patch[1].position + uvwCoord.z * patch[2].position;
    //vertexPosition = lerp(lerp(patch[0].position, patch[1].position, uvCoord.x ),
    //                      lerp(patch[2].position, patch[3].position, uvCoord.x ), 
    //                      uvwCoord.y);
    vertexPosition = evaluate_patch(patch, uvCoord);

	// Calculate the position of the new vertex against the world, view, and projection matrices.    
    output.position = mul(float4(vertexPosition, 1.0f), worldMatrix);
    output.position = mul(output.position, viewMatrix);
    output.position = mul(output.position, projectionMatrix);

	// Send the input color into the pixel shader.
    if(uvwCoord.x < 0.5 && uvwCoord.y < 0.5)
    	output.color = float4(1,0,0,1); // patch[0].color;
    else if(uvwCoord.x >= 0.5 && uvwCoord.y < 0.5)
        output.color = float4(0,1,0,1); // patch[1].color;
    else if(uvwCoord.x < 0.5 && uvwCoord.y >= 0.5)
        output.color = float4(0,0,1,1); //patch[2].color;
    else output.color = float4(1,1,0,1); //patch[3].color;

    return output;
}

Domain Shader 로 cubic bezier function 이 들어있음.

 주의깊게 봐야할 곳은 PixelInputType 에 Direct9 이전 VertexShader 에서 꼭 리턴해야할 SV_POSITION 이 있단 것과 Bezier 에서 uv 가 어떻게 사용되는지임.

 Bezier 를 Patch 에 대해서 계산해서 Position 을 내놓는데 여기서 정점 정보가 사용되며 Tessellation 에서 쪼갠 정점의 상대 위치인 uv 는 이 정보를 바탕으로 공간이동을 한다고 보면 됨. 즉 우리가 원래 준 Vertex(Control Point)는 여기서 역할을 마치며 Tessellation 에서 쪼갠 정점들이 공간변환 후 다음 단계로 넘어감.

 또한 위에는 안적혀있지만 VertexShader 에서 World 변환만 했기 때문에 ( 경우에 따라 다른 공간에서 쓸 수 도 있음 ) 여기서 View, Proj 을 곱해줬음.

 덤으로 색깔도 uv 에 따라서 다르게 지정해봤음.



결과값. 

위에는 안적었는데 마지막 Vertex 위치를 조금 늘렸음.

이거 적용안하면 직사각형에 중간에 버텍스가 몰빵된걸 볼 수 있음.

X 값이 0 -> 1 -> -1 -> 3 으로 갔음을 생각하면서 위 그림을 보면 대충 뭔느낌인지 감이 올 것임.




Tessellator 가 만든 새로운 정점들이 이후에 어떻게 처리될지 간략히 설명.


Geometry Shader 단계

 Primitive 마다 호출이 되며 ( 즉 테셀레이션인 경우 패치 당 ) 인접한 Primitive 의 값들을 사용할 수 있음.

 리턴값은 PointStream, LineStream, TriangleStream 으로 정해져있으며, 이 스트림으로 출력값을 증폭, 감소가 가능한 것이 가장 큰 특징. 

 이 Stream 에는 Append, RestartStrip 이라는 메소드가 달려있는데 전자로 Vertex를 스트림에 추가하고 후자로 Strip 를 끊어서 넣을 수 있음.

 또한 이 Stream 의 종류를 바꾸는 것만으로 쉐이더 내에서 Primitive 를 바꿀 수 있음

 RenderTarget 이 array 로 있는경우 SV_RenderTargetArrayindex 를 사용해 어떤 index 에 그릴지 지정가능  ( 예를들어 큐브맵은 6개의 Texture2D 로 구성된 array 로 위 시맨틱을 이용해 어떤 면에 그릴지 지정함, 사실 이게 Geometry Shader 의 자주 쓰이는 용도임)


[maxvertexcount(18)]
void ColorGeometryShader(triangle GeometryInputType InPos[3], inout LineStream<PixelInputType> OutStream)
{
	for(int iFace = 0; iFace < 6; iFace++ )
	{
		PixelInputType output;

		output.RTIndex = iFace;

		for(int v = 0; v < 2; v++ )
		{
			output.position = InPos[v].position;
			output.position.xy += - 8 + iFace * 3;
			output.normal = InPos[v].normal;
			output.color = InPos[v].color;
			//output.position = mul(InPos[v], CubeViewProj[iFace]);
			OutStream.Append(output);
		}
		OutStream.RestartStrip();
	}
}

 위는 원래는 큐브맵에 그릴 용도였으나 편의상 삼각형을 6개 그리는 걸로 바꿈.

 위에 [maxvertexcount(n)] 을 지정해야하는 것에 주의.

 


위는 테셀레이션과 지오메트리 쉐이더를 적용한 결과.


Clipping

Geometry 후,Rasterizer 전에 수행됨. 아래는 사용되는 알고리즘.

https://en.wikipedia.org/wiki/Sutherland%E2%80%93Hodgman_algorithm

간단히 요약하면 Clipping Vertex 랑 Input Vertex 가 있음.

Clipping Vertex 로 선들을 만들어서 선 각각마다 Input Vertex 각각에 대해서 다음을 수행.

Input Vertex 의 Prev_Vertex 와 Cur_Vertex 를 이은 선이 Clipping Vertex 로 만든 선 안에 있는지 밖에 있는지 확인. 교차하면 바깥에 있는 Vertex 를 제거하고 교차점을 넣음.

Concave(뒷면) 이 가운데 걸려있는 경우 경계선이 중복되는 문제가 있다고 함.

이는 내 생각인데 아마 위와 같은 경우 ( 빨간색이 경계, 검은색이 Input) 선에 잘리는 부분이 아래부분 위부분 두개가 있다는 말인거 같음.

Rasterizer 단계

ViewPort 변환

w 값을 나눔 (proj 행렬에서 z 값을 w 에 넣었으므로 실질적으론 z 로 나눔)

Culling

삼각형인경우 0-1, 0-2 내적해서 양수 음수로 vertex 제거함.

Interpolation

보간x, 선형보간, 그거보다 비싼 보간 등 옵션 설정


Pixel Shader 단계

ㅈㄱㄴ




댓글 없음:

댓글 쓰기

List