드디어 어려운 파트를 넘어갔다..
이제는 좀 가벼운 마음으로 포스팅을 하겠다...
Tessellation Shader란?
이름부터 읽기 어려운 이 쉐이더는 초기 포스팅에도 나온 내용이다.
https://lucid-boundary.tistory.com/115
01. Shader 란? - 다니엘 릿 쉐이더 프로젝트
Shader를 알기 위해서는.. 기초적인 그래픽스 지식과 수학이 필요하다.우선, 필요한 지식들을 먼저 이해해보자. RendererRenderering은 일종의 데이터 처리를 아래 처럼, 순차적으로 하는 단계라고 한다
lucid-boundary.tistory.com
우리는 지금까지 Vertex, Fragment 를 집중적으로 다루었지만
다른 타입의 쉐이더들도 존재한다.
Tessellation Shader, Geometry Shader, Compute Shader 등
다양한 쉐이더들도 알아둬야 할 것이다.
다시 리마인드해서, Shader Pipeline Stages에 대해서 정리하자.
1. Raw Vertex Data
2. Vertex Shader
3. Tessellation Shader
4. Geometry Shader
5. Rasterization
6. Fragment Shader
7. Per-Fragment Operations
우리가 다룰 3번의 쉐이더를 보면 짐작 하듯이
Vertex Shader가 이루어진 후의 처리 부분이다.
Tessellation Shader 의 특징
- 새로운 vertices를 가장자리나 메쉬의 각 삼각형의 표면에 생성 할 수 있게 해준다.
- 존재하는 메쉬의 표면을 Subdividing 할 때, 생성 할 수 있다.
- 실제로는 프로그래밍 가능한 2개의 스테이지와 고정된 함수 스테이지로 구분된다. Tessellation Control Shader (TCS) 혹은 Hull Shader라고 불리며 각 면에 적용할 수 있는 정도를 정의한다.
- patch 라고 불리는 vertics를 받는다. 이 vertics는 Tessellelation의 양을 컨트롤한다. Vertex Shader와 달리 동시에 여러 Vertics에 접근 할 수 있다.
- 고정된 함수 스테이지는 tessellelation primitive generation 혹은 tessellator라고 불리며, 다른 두 스테이지를 다룬다.
- The tessellation Evaluation Shader (TES) 혹은 domain shader라고 불리는 쉐이더는 Tessellator에 의해 vertics 결과물을 배치한다.
- 일반적은 domain shader는 vertics를 보간하는데 쓰이지만, 위치를 우리가 원하는대로 변경 할 수 있다.

특징만 읽고는 좀 감이 안잡힌다.. 역시 예제를 통해 이해하자.
Water Wave Effect With Tessellation

Shader "LucidBoundary/Tessellation_WaterWaveEffect"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BaseColor("Base Color", Color) = (1, 1, 1, 1)
_WaveStrength("Wave Strength", Range(0, 2)) = 0.1
_WaveSpeed("Wave Speed", Range(0, 10)) = 1
[Enum(UnityEngine.Rendering.BlendMode)]
_SrcBlend("Source Blend Factor", Int) = 0
[Enum(UnityEngine.Rendering.BlendMode)]
_DstBlend("Destination Blend Factor", Int) = 1
_TessAmount("Tessellation Amount", Range(1, 64)) = 2
}
SubShader
{
Tags
{
"RenderType"="Transparent"
"Queue"="Transparent"
"RenderPipeline" = "UniversalPipeline"
}
Pass
{
Blend [_SrcBlend] [_DstBlend]
Tags
{
"LightMode" = "UniversalForward"
}
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma hull tessHull
#pragma domain tessDomain
#pragma target 4.6
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct appdata
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct tessControlPoint
{
float4 positionOS : INTERNALTESSPOS;
float2 uv : TEXCOORD0;
};
struct tessFactors
{
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
};
texture2D _MainTex;
SamplerState sampler_MainTex;
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _BaseColor;
float _TessAmount;
float _WaveStrength;
float _WaveSpeed;
CBUFFER_END
tessControlPoint vert(appdata v)
{
tessControlPoint o;
o.positionOS = v.positionOS;
o.uv = v.uv;
return o;
}
v2f tessVert(appdata v)
{
v2f o;
float4 positionWS = mul(unity_ObjectToWorld, v.positionOS);
float height = sin(_Time.y * _WaveSpeed + positionWS.x + positionWS.z);
positionWS.y += height* _WaveStrength;
o.positionCS = mul(UNITY_MATRIX_VP, positionWS);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
tessFactors patchConstantFunc(InputPatch<tessControlPoint, 3> patch)
{
tessFactors f;
f.edge[0] = f.edge[1] = f.edge[2] = _TessAmount;
f.inside = _TessAmount;
return f;
}
[domain("tri")]
[outputcontrolpoints(3)]
[outputtopology("triangle_cw")]
[partitioning("fractional_even")]
[patchconstantfunc("patchConstantFunc")]
tessControlPoint tessHull(InputPatch<tessControlPoint, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];
}
[domain("tri")]
v2f tessDomain(tessFactors factors, OutputPatch<tessControlPoint, 3> patch, float3 bcCoords : SV_DomainLocation)
{
appdata i;
i.positionOS = patch[0].positionOS * bcCoords.x + patch[1].positionOS * bcCoords.y + patch[2].positionOS * bcCoords.z;
i.uv = patch[0].uv * bcCoords.x + patch[1].uv * bcCoords.y + patch[2].uv * bcCoords.z;
return tessVert(i);
}
float4 frag (v2f i) : SV_Target
{
float4 textureSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
return textureSample * _BaseColor;
}
ENDHLSL
}
}
}
이번 코드도 복잡하다. 잘 따라와보자.
Properties
Properties
{
_MainTex ("Texture", 2D) = "white" {}
_BaseColor("Base Color", Color) = (1, 1, 1, 1)
_WaveStrength("Wave Strength", Range(0, 2)) = 0.1
_WaveSpeed("Wave Speed", Range(0, 10)) = 1
[Enum(UnityEngine.Rendering.BlendMode)]
_SrcBlend("Source Blend Factor", Int) = 0
[Enum(UnityEngine.Rendering.BlendMode)]
_DstBlend("Destination Blend Factor", Int) = 1
_TessAmount("Tessellation Amount", Range(1, 64)) = 2
}
기본적인 _MainTex, _BaseColor 외에,
물결의 강도에 해당하는 _WaveStrength
물결의 속도에 해당하는 _WaveSpeed 가 포함되어 있고
타일화, 혹은 면에 버텍스를 추가하는 Tessellation의 정도. _TessAmount가 있다.
_SrcBlend, _DstBlend 는 각각 Transparency 블렌딩을 어떻게 할 지, 커스터마이징 가능하다.
Pre-Compiler
#pragma vertex vert
#pragma fragment frag
#pragma hull tessHull
#pragma domain tessDomain
#pragma target 4.6
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
이번에는 hull, domain, target이 포함되었다.
- target
target은 어떤 쉐이더 모델로 컴파일 할 지 정한다. 여기서 몇 버전을 써야할지 어떻게 구분할 지 감이 안잡히지만, Tessellation 관련 쉐이더는 4 버전 이상이여야 동작한다.
- hull, domain
Tessellation 관련 쉐이더들이다.
Hull Shader, Domain Shader를 각각 컴파일한다.
Struct
struct appdata
{
float4 positionOS : POSITION;
float2 uv : TEXCOORD0;
};
struct tessControlPoint
{
float4 positionOS : INTERNALTESSPOS;
float2 uv : TEXCOORD0;
};
struct tessFactors
{
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 positionCS : SV_POSITION;
};
각 구조체를 살펴보자.
appdata, v2f 는 넘어가겠다.
- tessControlPoint
appdata와 사실상 동일한 구조체인데 Tess 작업을 하기 위해 변경점이 있다.
INTERNALTESSPOS 는 Hull, Domain 쉐이더에서 사용하는 내부 시멘틱으로, 기존의 Vertex 위치인 Position을 대체하여 사용한다.
- tessFactors
patch 생성을 위해 도와주는 구조체이다.
Edge는 3개의 float 배열로 되어 있으며 SV_TessFactor로 사용되고
inside는 SV_InsideTessFactor로 사용된다.
vert 함수
tessControlPoint vert(appdata v)
{
tessControlPoint o;
o.positionOS = v.positionOS;
o.uv = v.uv;
return o;
}
vert 함수는 자가 생산에 가깝다.
appdata 형식의 Input을 Tessellation에서 사용 가능한 tessControlPoint 로 변환한다.
tessVert 함수
v2f tessVert(appdata v)
{
v2f o;
float4 positionWS = mul(unity_ObjectToWorld, v.positionOS);
float height = sin(_Time.y * _WaveSpeed + positionWS.x + positionWS.z);
positionWS.y += height* _WaveStrength;
o.positionCS = mul(UNITY_MATRIX_VP, positionWS);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
실질적으로 Vertex를 건드리는 부분이다.
각 vertex들은 월드 스페이스의 포지션 값에서 Sine 함수에 의해 Wave 효과로 위 아래로 출렁거리는 효과를 얻는다.
UNITY_MATRIX_VP 는 Current view * projection matrix. 이다.
클립 스페이스 상의 포지션을 구해준다.
Wave Tessellation Control Shader (Hull Shader)
[domain("tri")]
[outputcontrolpoints(3)]
[outputtopology("triangle_cw")]
[partitioning("fractional_even")]
[patchconstantfunc("patchConstantFunc")]
tessControlPoint tessHull(InputPatch<tessControlPoint, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];
}
Attribute로 떡칠되어 있는 tessHull 함수를 보자.
(1) domain Attribute는 quad, isoline, tri 의 값을 받게 해준다. 값들은 메쉬의 타입에 달려있다.
(2) outputcontrolpoints Attribute는 패치당 생성되는 컨트롤 포인트들을 얼마큼 정의할 것인가에 대한 설정이다.
(3) outputtoplogy Attribute는 어떤 primitive type이 tessellator에 의해 허용 되어야 할 지 정의한다.
triangle_cw (clockwise) 는 이름처럼 시계방향으로 삼각형들을 사용할 것이다란 뜻이다.
(반대는 trianlge_ccw, 그 외에는 point, line이 있다.)
(4) partitioning Attribute는 tessellator가 tessellation factor들을 어떻게 다룰 것인지 정의한다.
- interger
- fractional_even
-fractional_odd
-pow2
(5) 입력 파라미터
Hull Shader에서는 입력 파라미터로 2개를 받는다.
첫번째 파라미터는 tessControlPoint 타입의 Patch를 받는데. 3은 Triangle을 뜻한다.
두번째 파라미터 ID는 입력으로 들어올 Patch의 ID이다. 한번 정해지면 출력때도 똑같은 vertex가 선택된다.
(6) patchconstantfunc Attribute는 patch constant function을 특정화 하는데 사용한다. 함수 이름을 적어서 사용 할 수 있다.
tessFactors patchConstantFunc(InputPatch<tessControlPoint, 3> patch)
{
tessFactors f;
f.edge[0] = f.edge[1] = f.edge[2] = _TessAmount;
f.inside = _TessAmount;
return f;
}
patchConstantFunc 는 patch를 입력으로 받는 함수로
Triangle 각 면에 _TessAmount의 양만큼 새로운 vertex를 생성하고, 각 vertex끼리 연결하는 Line을 이어줄 파라미터들을 저장한다. (실제로 만들어주는게 아니라, 만들 예정인 값이다.)

Wave Tessellation Evaluation Shader (Domain Shader)
[domain("tri")]
v2f tessDomain(tessFactors factors, OutputPatch<tessControlPoint, 3> patch, float3 bcCoords : SV_DomainLocation)
{
appdata i;
i.positionOS = patch[0].positionOS * bcCoords.x + patch[1].positionOS * bcCoords.y + patch[2].positionOS * bcCoords.z;
i.uv = patch[0].uv * bcCoords.x + patch[1].uv * bcCoords.y + patch[2].uv * bcCoords.z;
return tessVert(i);
}
Tessellator는 Hull Shader에 의해 생성된 Contorl Points 출력 결과와 Tessellation Factor들을 가져와 새로운 포인트들을 새성하며 새로운 좌표 값의 모음 ( set of coordinates )를 한번 생성한다.
- barycentric coordianates.
이 새로운 좌표값들은 barycentric coordinates라고 부르는데, 새로운 vertex가 삼각형 위의 오리지널 포인트 (3개)로부터 얼만큼 떨어져있는 지에 대한 모음이다. 타겟 시맨틱은 SV_DomainLocation을 사용한다.
Domain Shader는 새로운 vertex들은 오리지널 vertex로부터 보간하며 v2f struct 로 리턴한다.
새로운 vertex들은 appdata를 정의하여 tessVert 함수로 보내 v2f 화 시켜서 Fragment로 보내는 것이다.
위에서 언급한 tessVert에는 기존 오리지널 vertex뿐만 아니라 새로이 생성된 vertex도 들어가게 되는 것이다.
Fragment Shader
float4 frag (v2f i) : SV_Target
{
float4 textureSample = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, i.uv);
return textureSample * _BaseColor;
}
Fragment 는 평범하게 텍스처링 하고 색상을 입히는 쉐이더 함수이다.
대신에 입력 값 v2f 는 기존 오리지널 픽셀뿐 아니라 tessellator에 의해 생성된 픽셀도 포함된다.
마치며
내용 자체는 어렵지 않았지만, 생소한 기능들이 들어가있어 진입장벽이 좀 있을지도..
그리고 단어가 너무 오타가 심하게 나기 좋다..
포스팅하면서 몇번을 틀려먹은건지
tessellation tessellation tessellation
'Game Dev > Unity Shader' 카테고리의 다른 글
| 22. Compute Shader - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.27 |
|---|---|
| 21. Geometry Shader - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.27 |
| 19. Image Effects, Post-Processing Effects - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.24 |
| 18. Shadow Casting - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.21 |
| 17. Physically Based Rendering (PBR) - 다니엘 릿 쉐이더 프로젝트 (0) | 2025.10.21 |