|
Tessellation是现代GPU可编程管线中的一个可选部分。它提供Hull shader和Domain shader用于定制。
一个完整的hull shader大概长这样:
[UNITY_domain("tri")]
[UNITY_outputcontrolpoints(3)]
[UNITY_outputtopology("triangle_cw")]
[UNITY_partitioning("integer")]
[UNITY_patchconstantfunc("MyPatchConstantFunction")]
TessellationControlPoint MyHullProgram (
InputPatch<TessellationControlPoint, 3> patch,
uint id : SV_OutputControlPointID
) {
return patch[id];
}UNITY_domain(&#34;tri&#34;)属性表示该shader是对三角形进行处理。UNITY_outputcontrolpoints属性表示输出的控制点数量,这里一个三角形patch输出的顶点数量是3个。UNITY_outputtopology属性表示输出三角形的绕序。UNITY_partitioning属性用来告诉GPU细分三角形的方式,有integer ,fractional_odd等若干种。UNITY_patchconstantfunc属性用来告诉GPU,要使用哪个函数来细分每个patch。不同patch细分的结果可能不同,这意味着该函数不是在每个控制点上都执行一次,而是在每个patch上执行一次。
那么接下来看一下patch constant function的模样,这个函数接收包含3个点的三角形patch,输出一个名为TessellationFactors的数据结构,该数据结构定义了三角形三条边的细分系数,和三角形内部的细分系数:
struct TessellationFactors {
float edge[3] : SV_TessFactor;
float inside : SV_InsideTessFactor;
};
TessellationFactors MyPatchConstantFunction (InputPatch<VertexData, 3> patch) {
TessellationFactors f;
f.edge[0] = 1;
f.edge[1] = 1;
f.edge[2] = 1;
f.inside = 1;
return f;
}接着,我们来看一下domain shader,它的用处是为细分出的顶点设置相应的顶点属性,就仿佛像是一个普通的顶点一样,完成之前vertex shader原本需要做的事情。
[UNITY_domain(&#34;tri&#34;)]
InterpolatorsVertex MyDomainProgram (
TessellationFactors factors,
OutputPatch<TessellationControlPoint, 3> patch,
float3 barycentricCoordinates : SV_DomainLocation
) {
VertexData data;
#define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.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)
MY_DOMAIN_PROGRAM_INTERPOLATE(uv)
return MyVertexProgram(data);
}这里引入了重心坐标,方便我们对属性进行插值。这里还定义了宏来减少冗余的代码。
这样我们就拥有了一个完整的tessellation流程,但是此时的效果是仿佛无事发生:
原因很简单,就是patch const function返回的细分参数都是1,我们只对一条边的factor进行调节(即修改f.edge[0]的值),看看从2~3的效果:
可以看出,有一条边上多出了控制点,并且三角形的内部也有一个控制点,这些控制点和三角形原本的三个顶点相连,形成了细分后的三角形。再看看调节内部factor的效果(即修改f.inside的值),同样也是从2变化到3:
可以看到,2的时候内部出现了一个控制点,3的时候内部出现了3个控制点,这3个控制点形成了一个三角形。此时规律仍不明显,让我们再看一下4和5的情况:
这时规律就比较明显了,4的时候就是内部会产生4个控制点,3个形成一个三角形,还有1个会出现在三角形的内部;5的时候内部会产生6个控制点,三角形的内部又会嵌套形成一个三角形。以此类推,当factor=2k+1时,内部会出现3k个控制点;当factor=2k时,内部会出现3(k-1) + 1个控制点。
不过在实践调节细分factor的过程中,我们发现变化是完全不连续的。假如我们想实现细分的平滑过渡,要怎么办呢?
这时我们可以修改细分的方式,这可以通过设置[UNITY_partitioning(&#34;fractional_odd&#34;)]实现,效果如下:
最后,让我们考虑一下如何选择合适的细分factor。显然,我们希望离我们越近的物体,显示的细节越丰富,离我们越远的物体,显示的细节越少。我们可以利用屏幕空间的信息来判断。将物体变换到屏幕空间中,计算顶点之间的距离,距离越大,说明离我们越近,需要更多的细分factor:
float4 p0 = UnityObjectToClipPos(cp0.vertex);
float4 p1 = UnityObjectToClipPos(cp1.vertex);
float edgeLength = distance(p0.xy / p0.w, p1.xy / p1.w);
return edgeLength * _ScreenParams.y / _TessellationEdgeLength;还有一种思路,就是取三角形每条边的中点,计算它距离摄像机的距离,根据距离远近来调整细分factor:
float3 p0 = mul(unity_ObjectToWorld, float4(cp0.vertex.xyz, 1)).xyz;
float3 p1 = mul(unity_ObjectToWorld, float4(cp1.vertex.xyz, 1)).xyz;
float edgeLength = distance(p0, p1);
float3 edgeCenter = (p0 + p1) * 0.5;
float viewDistance = distance(edgeCenter, _WorldSpaceCameraPos);
return edgeLength / (_TessellationEdgeLength * viewDistance);最后的效果如下:
如果你觉得我的文章有帮助,欢迎关注我的微信公众号:Game_Develop_Forever
Reference
[1] Tessellation Subdividing Triangles |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|