在Unity中使用Tessellation

在Unity中使用Tessellation

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("tri")属性表示该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("tri")]
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("fractional_odd")]实现,效果如下:

在这里插入图片描述

最后,让我们考虑一下如何选择合适的细分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

猜你喜欢

转载自blog.csdn.net/weixin_45776473/article/details/125824226