Tessellation Shader的底层实现


Tessellation Shader

总结一下在vertex和fragment shader里面如何使用曲面细分着色器,在Surface shader里面这个过程已经被高度封装好了,所以非常简单,直接tessellation :tess调用处理函数就行了,要在vertex-frag shader里面做就需要完全了解它的底层实现。

这个shader和Geometry Shader是顶点着色器之后的可选着色器,是在光栅化之前最后对顶点做操作的机会。为了使用细分着色器,在顶点着色器里面就可以不对顶点做任何的操作,直接把数据传到细分着色器。

细分着色器是由三个阶段实现的:

Hull Shader

由两部分组成:Constant Hull Shader、Control Point Shader

对于Constant Hull Shader主要就是设置细分因子,告诉后面的细分阶段如何去细分。

然后是Control Point Shader,它主要就是把目前模型以及有的顶点作为控制点然后输入,最后输出多个控制点(一般还是输出一个控制点),每输出一个控制点都要调用一次这个阶段。

传入InputPatch数据以及SV_OutputControlPointID,曲面细分可以处理三角形、四边形或者线。patch是一个mesh vertices的集合,对于这个输入参数需要定义它是怎样的structure和每一个patch包含了几个vertices,SV_OutputControlPointID则是需要传入的id,每次向tessellation stage传送的数据集合只能有一个,它的标识是一个unsighed integer,用来确定具体是哪一个。

然后还需要指定如何进行细分:

1
2
3
4
5
6
7
8
9
10
//确定要细分的是什么图元
[UNITY_domain("tri")]
//对于每套的输入数据有几个控制点
[UNITY_outputcontrolpoints(3)]
//顺时针或者逆时针的定义
[UNITY_outputtopology("triangle_cw")]
//细分方式
[UNITY_partitioning("integer")]
//会被如何cut,传入一个细分控制函数,Const Hull Shader
[UNITY_patchconstantfunc("MyPatchConstantFunction")]

hullshader的作用就是pass the required vertex data to the tessellation stage。

Tessellation Stage

不可编程,由硬件完成,根据hull shader的数据(细分因子和控制点)进行细分,但它并不会生成新的顶点,只是对之前的顶点生成了重心坐标,之后Domain Shader才用这些数据去生成新的坐标点。

Domain Shader

把细分过的顶点坐标,投影到Clip Space。在domain shader中,TessellationFactors、细分过的顶点数据、重心坐标(SV_DomainLocation)会作为(u,v,w)输入,使用这个和实际顶点位置一来计算得到实际的顶点坐标。

Okk,来实践

首先是顶点着色器相关的处理流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#pragma vertex vert
struct vertexInput
{
float4 vertex : POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

struct vertexOutput
{
float4 vertex : SV_POSITION;
float3 normal : NORMAL;
float4 tangent : TANGENT;
};

//在顶点着色器中不做任何变换操作
vertexInput vert(vertexInput v)
{
return v;
}

vertexOutput tessVertTransformed(vertexInput v)
{
vertexOutput o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.normal = v.normal;
o.tangent = v.tangent;
return o;
}

进入细分着色器,首先是Hull Shader:

1
2
3
4
5
6
7
8
9
[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("patchConstantFunction")]
vertexInput hull (InputPatch<vertexInput, 3> patch, uint id : SV_OutputControlPointID)
{
return patch[id];//传递给下一阶段的一个数据
}

设置细分因子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

struct TessellationFactors
{
float edge[3] : SV_TessFactor;//边
float inside : SV_InsideTessFactor;
};

float _TessellationUniform;

//hull shader需要的细分控制函数
TessellationFactors patchConstantFunction (InputPatch<vertexInput, 3> patch)
{
TessellationFactors f;
f.edge[0] = _TessellationUniform;//在一条边上进行细分,细分成 _TessellationUniform段
f.edge[1] = _TessellationUniform;
f.edge[2] = _TessellationUniform;
f.inside = _TessellationUniform;
return f;
}

计算顶点,插值操作,确定新顶点位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[UNITY_domain("tri")]
vertexOutput custom_domain(TessellationFactors factors, OutputPatch<vertexInput, 3> patch, float3 barycentricCoordinates : SV_DomainLocation)
{
vertexInput v;

#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) v.fieldName = \
patch[0].fieldName * barycentricCoordinates.x + \
patch[1].fieldName * barycentricCoordinates.y + \
patch[2].fieldName * barycentricCoordinates.z;

MY_DOMAIN_PROGRAM_INTERPOLATE(vertex)
MY_DOMAIN_PROGRAM_INTERPOLATE(normal)
MY_DOMAIN_PROGRAM_INTERPOLATE(tangent)

return tessVertTransformed(v);
}

这样就从最基础的层面实现了曲面细分shader,也就知道了surfaceshader是如何帮我们打包的了。


Author: cdc
Reprint policy: All articles in this blog are used except for special statements CC BY 4.0 reprint polocy. If reproduced, please indicate source cdc !
  TOC