jquave 发表于 2022-7-20 16:28

在Unity中使用Tessellation

Tessellation是现代GPU可编程管线中的一个可选部分。它提供Hull shader和Domain shader用于定制。



一个完整的hull shader大概长这样:





TessellationControlPoint MyHullProgram (
    InputPatch<TessellationControlPoint, 3> patch,
    uint id : SV_OutputControlPointID
) {
    return patch;
}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 : SV_TessFactor;
    float inside : SV_InsideTessFactor;
};

TessellationFactors MyPatchConstantFunction (InputPatch<VertexData, 3> patch) {
    TessellationFactors f;
    f.edge = 1;
    f.edge = 1;
    f.edge = 1;
    f.inside = 1;
    return f;
}接着,我们来看一下domain shader,它的用处是为细分出的顶点设置相应的顶点属性,就仿佛像是一个普通的顶点一样,完成之前vertex shader原本需要做的事情。

InterpolatorsVertex MyDomainProgram (
    TessellationFactors factors,
    OutputPatch<TessellationControlPoint, 3> patch,
    float3 barycentricCoordinates : SV_DomainLocation
) {
    VertexData data;

    #define MY_DOMAIN_PROGRAM_INTERPOLATE(fieldName) data.fieldName = \
      patch.fieldName * barycentricCoordinates.x + \
      patch.fieldName * barycentricCoordinates.y + \
      patch.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的值),看看从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的过程中,我们发现变化是完全不连续的。假如我们想实现细分的平滑过渡,要怎么办呢?
这时我们可以修改细分的方式,这可以通过设置实现,效果如下:



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

Tessellation Subdividing Triangles
页: [1]
查看完整版本: 在Unity中使用Tessellation