Unity URP 曲面细分
我终于变得不像我
文章目录
- Unity URP 曲面细分
- 1 曲面细分与镶嵌
- 1.1 外壳着色器 Hull Shader
- 1.2 镶嵌器阶段 Tessellator
- 1.3 域着色器阶段 Domain Shader
- 2 具体实现
- 2.2 不同的细分策略
- 2.2.1 Flat Tessellation
- 2.2.2 PN Tessellation
- 2.2.3 Phone Tessellation
- 2.3 不同的细分因子
- 2.3.1 基于相机距离
- 2.3.2 基于屏幕占用范围
- 2.3.3 其他
- 3 参考文献
阅读注意:
- 本文的URP版本为10.8.1
- 本文会结合Tessellation.hlsl中的内容进行说明
1 曲面细分与镶嵌
曲面细分或细分曲面(Subdivision surface)是指一种通过递归算法将一个粗糙的几何网格细化的技术。镶嵌(Tessellation)则是实现曲面细分的具体手段,它能将场景中的几何物体顶点集划分为合适的渲染结构,例如三角形。在一些情况下,“镶嵌”也会被代称为“曲面细分”。
通过曲面细分,我们可以在内存中维护一个低模,根据需求再动态的增加三角网格,来节省资源。我们还能在GPU层面实现LOD,并能通过一些因素(例如:相机距离等)来调节曲面细分的程度,让近处呈现高模,远处呈现低模。
在Direct3D 11之前,要想达到这种操作,只能在CPU阶段细化网格,再传入GPU中,比较低效。于是,在Direct3D 11中引入曲面细分阶段(Tessllation Stage),将任务交给了GPU:
曲面细分阶段分为三个阶段:外壳着色器(Hull Shader)、镶嵌器阶段(Tessellator)和域着色器阶段(Domain Shader)。
1.1 外壳着色器 Hull Shader
Hull Shder 实际上是由两个阶段(Phase)组成:常量外壳着色器(Constant Hull Shader)和 控制点外壳着色器(Control point hull shader)。
Constant Hull Shader 会对每一个面片进行处理,其主要任务是输出网格的曲面细分因子(Tessellation Factor),细分因子用于指导面片细分的份数。我们这里以三角面片(triangle patch)为例:
// 三角面片
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = 4;
o.edgeFactor[1] = 4;
o.edgeFactor[2] = 4;
o.insideFactor = 4;
return o;
}
常量外壳着色器会以面片的所有顶点(或控制点)为输入,InputPatch<VertexOut,3>
其中VertexOut
是顶点着色器输出的结构体,后面的数字3代表一共传入三角面片的三个顶点数据(以此类推,如果你传入是四边形面片,那么这里应该是InputPatch<VertexOut,4>
)。
代码中还包含了三个语义:
SV_PrimitiveID
:提供传入面片的ID值。此处传入的参数patchID
可以根据具体需求进行操作。SV_TESSFACTOR
:用于标识对应边缘的细分因子SV_INSIDETESSFACTOR
:用于标识内部的细分因子
由于我们传入的是三角面片,那么自然会输出3个边缘细分因子和1个内部细分因子。如果传入的是四角面片,那么PatchTess的结构如下:
// 四角面片
struct PatchTess {
float edgeFactor[4] : SV_TESSFACTOR; // 分别对应四角面片的四个边
float insideFactor[2] : SV_INSIDETESSFACTOR; // 分别对应内部细分的列数与行数
};
此处,我们把细分因子全部固定为了4,但实际操作中可以根据不同的策略灵活地调整因子的大小,以实现LOD的效果,例如后面会提到基于相机位置来调整因子大小。
Control Point Hull Shader 可以用来改变每个输出顶点的位置等信息,例如将一个三角形变为一个3次的贝塞尔三角面片。
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut ControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionOS = patch[id].positionOS;
o.texcoord = patch[id].texcoord;
return o;
}
不过了为了方便说明,这里函数并没做过多的事情,只是将值传递了一下。我们一步一步来看一下这个复杂的写法:
-
domain
:面片类型。参数有三角面皮tri、四角面片quad、等值线isoline -
partitioning
:曲面细分模式。参数有integer、fractional_even、fractional_odd。integer
指新顶点的添加只取决于细分的整数部分,在Opengl中对应的模式是
equal_spacing
。
这个模式由于只取整数,所以当细分级别改变时,图形会发生明显的突变(pop)。例如:当你靠经时,从正方体突然变为球体等。
fractional_even
向上取最近的偶数n,将整段切割为n-2个相等长度的部分,和两端较短的部分。
fractional_odd
向上取最近的奇数n,将整段切割为n-2个相等长度的部分,和两端较短的部分。
-
outputtopology
:细分创建的三角面片绕序。参数有顺时针triangle_cw、逆时针triangle_ccw -
patchconstantfunc
:指定常量外壳着色器的函数名。 -
outputcontrolpoints
:外壳着色器的执行次数,每执行一次就会生成一个控制点。这个数量不一定要和输入的控制点数保持一致,例如输入4个控制点可以输出16个控制点。 -
maxtessfactor
:程序会使用到的最大细分因子。Direct3D 11 支持的最大细分因子为64。 -
SV_OutputControlPointID
:该语义标识出当前正在操作的控制点索引ID。
更多的细节请查阅Tessellation Stages。
常量外壳着色器和控制点外壳着色器两个阶段由硬件并行运行。Constant Hull Shader 对每一个面片运行一次,输出边缘细分因子等信息。Control Point Hull Shader 对每一个控制点运行一次,并输出对应的或衍生的控制点。
1.2 镶嵌器阶段 Tessellator
这一阶段我们无法对其做出任何控制,全程由硬件控制。在这一阶段,硬件根据之前曲面细分因子对面片走出细分操作。
我们以三角面片为例,选择细分模式为integer
,增大边缘细分因子,可以看到三角形的每条边都分割为了对应的数量。
增大内部细分因子,你可以看到内部镶嵌的规则有点不直观。因为内部因子的数量并不直接对应内部三角形的数量。
内三角环的数目几乎是细分数量的一半,当细分是偶数的话,最内层将会是一个顶点。
关于细分的具体规则可以查阅Opengl官网的解释Tessellation 。
1.3 域着色器阶段 Domain Shader
域着色器相当于是每一个控制的“顶点着色器”。
就和普通的顶点着色器要做的差不多,我们需要计算每一个控制点的顶点位置等信息。
struct DomainOut
{
float4 positionCS : SV_POSITION;
float3 color : TEXCOORD0;
};
[domain("tri")]
DomainOut DomainShader (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionOS = patch[0].positionOS * bary.x + patch[1].positionOS * bary.y + patch[2].positionOS * bary.z;
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.texcoord = texcoord;
return output;
}
这里值得注意的是,我们是怎么获取细分后的顶点位置。以三角面片为例,我们传入基础的三个控制点patch
信息,然后用语义SV_DOMAINLOCATION
细分后的顶点参数坐标,此处一个质心坐标(u, v, w)。(如果是四角面片,这个值会是个二维坐标(u,v))
有了顶点的质心坐标,我们就可以插值获得对应的顶点信息。
经过域着色器的处理,数据会按照流水线传递给几何着色器和片元着色器。
2 具体实现
上面简单介绍了一下,曲面细分阶段的各个部分的负责的内容。接下来,我们将他们实际应用起来!
2.2 不同的细分策略
2.2.1 Flat Tessellation
我们只需要把上面的代码组装一下,就可以获得一个最简单的平面镶嵌形式。
Shader "Tessellation/Flat Tessellation"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 0
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
_EdgeFactor ("EdgeFactor", Range(1,8)) = 4
_InsideFactor ("InsideFactor", Range(1,8)) = 4
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex FlatTessVert
#pragma fragment FlatTessFrag
#pragma hull FlatTessControlPoint
#pragma domain FlatTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _InsideFactor;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionOS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionOS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VertexOut FlatTessVert(Attributes input){
VertexOut o;
o.positionOS = input.positionOS;
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = _EdgeFactor;
o.edgeFactor[1] = _EdgeFactor;
o.edgeFactor[2] = _EdgeFactor;
o.insideFactor = _InsideFactor;
return o;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut FlatTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionOS = patch[id].positionOS;
o.texcoord = patch[id].texcoord;
return o;
}
[domain("tri")]
DomainOut FlatTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionOS = patch[0].positionOS * bary.x + patch[1].positionOS * bary.y + patch[2].positionOS * bary.z;
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.texcoord = texcoord;
return output;
}
half4 FlatTessFrag(DomainOut input) : SV_Target{
half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
return half4(color, 1.0);
}
ENDHLSL
}
}
}
代码通过#pragma hull
和#pragma domain
来指定外壳着色器 和 域着色器。并定义了枚举的宏定义,方便我们在这几个模式之前切换。
平面镶嵌只是线性插值位置信息,细分后的图案只比之前多了一些三角面片,单独使用并不能平滑模型。它通常和置换贴图(Displacement Map)配合使用,来创建一个凹凸不平的平面。
2.2.2 PN Tessellation
在之前的尝试中,我们没有过多地设计外壳着色器的Control Point阶段。这里我们可以尝试一下不同的控制点策略。
在外壳着色器阶段,把一个三角面片(3个控制点)转换为一个3次贝塞尔三角面片(Cubic Bezier Triangle Patch,一种具有10个控制点的面片),这种策略称为 Curved Point-Normal Triangles(PN triangles)。 它不同于Flat Tessellation,即使没有置换贴图,也能实现改变模型形状,平滑轮廓的作用。
由于控制点的增多,在Hull Shader输出时每个顶点需要多携带两个顶点信息(中心控制点b111可以直接推算出来),例如:b030 可能需要携带b021和b012的顶点信息。
按照这个策略,我们重新设计一下代码。
struct HullOut{
float3 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
float3 positionOS1 : TEXCOORD1; // 三角片元每个顶点多携带两个顶点信息
float3 positionOS2 : TEXCOORD2;
};
float3 ComputeCP(float3 pA, float3 pB, float3 nA){
return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;
}
[domain("tri")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut PNTessControlPoint(InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut output;
const uint nextCPID = id < 2 ? id + 1 : 0;
output.positionOS = patch[id].positionOS;
output.normalOS = patch[id].normalOS;
output.texcoord = patch[id].texcoord;
output.positionOS1 = ComputeCP(patch[id].positionOS, patch[nextCPID].positionOS, patch[id].normalOS);
output.positionOS2 = ComputeCP(patch[nextCPID].positionOS, patch[id].positionOS, patch[nextCPID].normalOS);
return output;
}
输出结构体HullOut
中用positionOS1 和 positionOS2 两个位置来存放多出来的控制点信息。通过一个简单的推算得到相邻顶点的ID——nextCPID
,有了当前顶线和相邻顶点,就能推算出两点之间的多出来的控制点。
ComputeCP
函数的原理其实就是一个简单的几何关系。以上图为例(图片来自:CurvedPNTriangles),那么各个点之前存在以下关系:
再来看看域着色器阶段:
[domain("tri")]
DomainOut PNTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float u = bary.x;
float v = bary.y;
float w = bary.z;
float uu = u * u;
float vv = v * v;
float ww = w * w;
float uu3 = 3 * uu;
float vv3 = 3 * vv;
float ww3 = 3 * ww;
float3 b300 = patch[0].positionOS;
float3 b210 = patch[0].positionOS1;
float3 b120 = patch[0].positionOS2;
float3 b030 = patch[1].positionOS;
float3 b021 = patch[1].positionOS1;
float3 b012 = patch[1].positionOS2;
float3 b003 = patch[2].positionOS;
float3 b102 = patch[2].positionOS1;
float3 b201 = patch[2].positionOS2;
float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;
float3 V = (b003 + b030 + b300) / 3.0;
float3 b111 = E + (E - V) / 2.0f;
// 插值获得细分后的顶点位置
float3 positionOS = b300 * uu * u + b030 * vv * v + b003 * ww * w
+ b210 * uu3 * v
+ b120 * vv3 * u
+ b021 * vv3 * w
+ b012 * ww3 * v
+ b102 * ww3 * u
+ b201 * uu3 * w
+ b111 * 6.0 * w * u * v;
// 此处简化了法线的计算
float3 normalOS = patch[0].normalOS * u
+ patch[1].normalOS * v
+ patch[2].normalOS * w;
normalOS = normalize(normalOS);
float2 texcoord = patch[0].texcoord * u
+ patch[1].texcoord * v
+ patch[2].texcoord * w;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.normalWS = TransformObjectToWorldNormal(normalOS);
output.uv = texcoord;
return output;
}
控制点增加到10个后,利用质心坐标插值的运算复杂度也自然就上去了。所以,在处理法线时,这里采用了最简单的三个控制点插值获取的方式。
这里贴一下整段代码。
Shader "Tessellation/PN Tri Tessellation + Cubic Bezier Triangle Patch"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd )]_Partitioning ("Partitioning Mode", Float) = 2
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
_EdgeFactor ("EdgeFactor", Range(1, 8)) = 4
_InsideFactor ("InsideFactor", Range(1, 8)) = 4
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex PNTessVert
#pragma fragment PNTessFrag
#pragma hull PNTessControlPoint
#pragma domain PNTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Lighting.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _InsideFactor;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionOS : INTERNALTESSPOS;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
float3 positionOS1 : TEXCOORD1;
float3 positionOS2 : TEXCOORD2;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float3 normalWS : TEXCOORD0;
float2 uv : TEXCOORD1;
};
VertexOut PNTessVert(Attributes input){
VertexOut o = (VertexOut)0;
o.positionOS = input.positionOS;
o.normalOS = input.normalOS;
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = _EdgeFactor;
o.edgeFactor[1] = _EdgeFactor;
o.edgeFactor[2] = _EdgeFactor;
o.insideFactor = _InsideFactor;
return o;
}
float3 ComputeCP(float3 pA, float3 pB, float3 nA){
return (2 * pA + pB - dot((pB - pA), nA) * nA) / 3.0f;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut PNTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut output;
const uint nextCPID = id < 2 ? id + 1 : 0;
output.positionOS = patch[id].positionOS;
output.normalOS = patch[id].normalOS;
output.texcoord = patch[id].texcoord;
output.positionOS1 = ComputeCP(patch[id].positionOS, patch[nextCPID].positionOS, patch[id].normalOS);
output.positionOS2 = ComputeCP(patch[nextCPID].positionOS, patch[id].positionOS, patch[nextCPID].normalOS);
return output;
}
[domain("tri")]
DomainOut PNTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float u = bary.x;
float v = bary.y;
float w = bary.z;
float uu = u * u;
float vv = v * v;
float ww = w * w;
float uu3 = 3 * uu;
float vv3 = 3 * vv;
float ww3 = 3 * ww;
float3 b300 = patch[0].positionOS;
float3 b210 = patch[0].positionOS1;
float3 b120 = patch[0].positionOS2;
float3 b030 = patch[1].positionOS;
float3 b021 = patch[1].positionOS1;
float3 b012 = patch[1].positionOS2;
float3 b003 = patch[2].positionOS;
float3 b102 = patch[2].positionOS1;
float3 b201 = patch[2].positionOS2;
float3 E = (b210 + b120 + b021 + b012 + b102 + b201) / 6.0;
float3 V = (b003 + b030 + b300) / 3.0;
float3 b111 = E + (E - V) / 2.0f;
float3 positionOS = b300 * uu * u + b030 * vv * v + b003 * ww * w
+ b210 * uu3 * v
+ b120 * vv3 * u
+ b021 * vv3 * w
+ b012 * ww3 * v
+ b102 * ww3 * u
+ b201 * uu3 * w
+ b111 * 6.0 * w * u * v;
float3 normalOS = patch[0].normalOS * u
+ patch[1].normalOS * v
+ patch[2].normalOS * w;
normalOS = normalize(normalOS);
float2 texcoord = patch[0].texcoord * u
+ patch[1].texcoord * v
+ patch[2].texcoord * w;
DomainOut output;
output.positionCS = TransformObjectToHClip(positionOS);
output.normalWS = TransformObjectToWorldNormal(normalOS);
output.uv = texcoord;
return output;
}
half4 PNTessFrag(DomainOut input) : SV_Target{
Light mainLight = GetMainLight();
half3 baseColor = SAMPLE_TEXTURE2D(_BaseMap,sampler_BaseMap,input.uv).xyz;
half NdotL = saturate(dot(input.normalWS, mainLight.direction) * 0.5 + 0.5);
half3 diffuseColor = mainLight.color * NdotL;
return half4(diffuseColor * baseColor ,1.0);
}
ENDHLSL
}
}
}
我这里随便掰了一下三角面片的法线,Shader运行效果如下:
然而,目前的做法是有缺陷的,在面对一些相同位置有不同法线的模型时,细分后会造成模型边缘的不连续,形成裂缝(Crack)。
为了解决这一问题,NVIDIA它们采用了一种改进的策略 PN-AEN (Point-Normal Triangles Using Adjacent Edge Normals)。这里摆出相关链接:
- My Tessellation Has Cracks
- PN-AEN-Triangles-Whitepaper
这种策略会在数据预处理阶段生成一个携带有邻接信息的索引缓冲区。在细分时,就可以通过邻接顶点信息,来消除裂缝。
但Unity中似乎不方便生成邻接信息的索引缓冲区,要生成这个数据可能得手动操作一下。我看到Asset Store中有人实现过PN-AEN Crack-Free Tessellation Displacement,有兴趣的可以看看。(或者直接让美术硬凹!)
2.2.3 Phone Tessellation
Phone Tessellation 和 PN Tessellation 类似,也是为了到达平滑模型轮廓的目的。只不过Phone镶嵌可以用更少的计算达到类似的效果。更多内容推荐阅读Phone Tessellation。
其核心思想是,将生成的顶点
P
P
P投影到三个顶点的切平面上,然后再用质心坐标插值这些投影点,最后得到顶点
P
∗
P^*
P∗。
在代码上,我们需要修改域着色器,下面贴出整段代码:
Shader "Tessellation/Flat Tri Tessellation + Phone Tess"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 2
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
_EdgeFactor ("EdgeFactor", Range(1,16)) = 4
_InsideFactor ("InsideFactor", Range(1,16)) = 4
_PhoneShape ("PhoneShape", Range(0, 1)) = 0.5
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex PhoneTriTessVert
#pragma fragment PhoneTriTessFrag
#pragma hull PhoneTriTessControlPoint
#pragma domain PhoneTriTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _InsideFactor;
float _PhoneShape;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float3 normalOS : NORMAL;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
float3 normalWS : TEXCOORD1;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
float3 normalWS : TEXCOORD1;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VertexOut PhoneTriTessVert(Attributes input){
VertexOut o;
o.positionWS = TransformObjectToWorld(input.positionOS);
o.normalWS = TransformObjectToWorldNormal(input.normalOS);
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
o.edgeFactor[0] = _EdgeFactor;
o.edgeFactor[1] = _EdgeFactor;
o.edgeFactor[2] = _EdgeFactor;
o.insideFactor = _InsideFactor;
return o;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut PhoneTriTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionWS = patch[id].positionWS;
o.texcoord = patch[id].texcoord;
o.normalWS = patch[id].normalWS;
return o;
}
[domain("tri")]
DomainOut PhoneTriTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionWS = patch[0].positionWS * bary.x + patch[1].positionWS * bary.y + patch[2].positionWS * bary.z;
positionWS = PhongTessellation(positionWS, patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, patch[0].normalWS, patch[1].normalWS, patch[2].normalWS, bary, _PhoneShape);
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformWorldToHClip(positionWS);
output.texcoord = texcoord;
return output;
}
half4 PhoneTriTessFrag(DomainOut input) : SV_Target{
half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
return half4(color, 1.0);
}
ENDHLSL
}
}
}
PhongTessellation
函数的内容如下:
// ===================== GeometricTools.hlsl ======================
float3 ProjectPointOnPlane(float3 position, float3 planePosition, float3 planeNormal)
{
return position - (dot(position - planePosition, planeNormal) * planeNormal);
}
// ===================== Tessellation.hlsl =========================
// p0, p1, p2 triangle world position
// p0, p1, p2 triangle world vertex normal
real3 PhongTessellation(real3 positionWS, real3 p0, real3 p1, real3 p2, real3 n0, real3 n1, real3 n2, real3 baryCoords, real shape)
{
// 分别计算三个切平面的投影点
real3 c0 = ProjectPointOnPlane(positionWS, p0, n0);
real3 c1 = ProjectPointOnPlane(positionWS, p1, n1);
real3 c2 = ProjectPointOnPlane(positionWS, p2, n2);
// 利用质心坐标插值得到最终顶点位置
real3 phongPositionWS = baryCoords.x * c0 + baryCoords.y * c1 + baryCoords.z * c2;
// 通过shape 控制平滑程度
return lerp(positionWS, phongPositionWS, shape);
}
Shader的运行效果如下:
同样,Phone Tessellation 和 PN Tessellation 一样都是以来模型的法线来平滑表面,如果一个顶点位置有多个法线方向,那也是会出现裂缝情况的。
2.3 不同的细分因子
到此为止,我们的细分因子都是受Shader面板统一控制。接下来,我们会运用算法灵活调整细分因子。
2.3.1 基于相机距离
为了让距离相机近的位置细分程度高一点,现将常量外壳着色器的代码调整如下:
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
float3 cameraPosWS = GetCameraPositionWS();
real3 triVectexFactors = GetDistanceBasedTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, cameraPosWS, _TessMinDist, _TessMinDist + _FadeDist);
float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
o.edgeFactor[0] = max(1.0, tessFactors.x);
o.edgeFactor[1] = max(1.0, tessFactors.y);
o.edgeFactor[2] = max(1.0, tessFactors.z);
o.insideFactor = max(1.0, tessFactors.w);
return o;
}
这里获取了相机的位置,并将三个顶点的世界空间坐标传入GetDistanceBasedTessFactor
函数中:
// Tessellation.hlsl
real3 GetDistanceBasedTessFactor(real3 p0, real3 p1, real3 p2, real3 cameraPosWS, real tessMinDist, real tessMaxDist)
{
real3 edgePosition0 = 0.5 * (p1 + p2);
real3 edgePosition1 = 0.5 * (p0 + p2);
real3 edgePosition2 = 0.5 * (p0 + p1);
// In case camera-relative rendering is enabled, 'cameraPosWS' is statically known to be 0,
// so the compiler will be able to optimize distance() to length().
real dist0 = distance(edgePosition0, cameraPosWS);
real dist1 = distance(edgePosition1, cameraPosWS);
real dist2 = distance(edgePosition2, cameraPosWS);
// The saturate will handle the produced NaN in case min == max
real fadeDist = tessMaxDist - tessMinDist;
real3 tessFactor;
tessFactor.x = saturate(1.0 - (dist0 - tessMinDist) / fadeDist);
tessFactor.y = saturate(1.0 - (dist1 - tessMinDist) / fadeDist);
tessFactor.z = saturate(1.0 - (dist2 - tessMinDist) / fadeDist);
return tessFactor;
}
函数取每一边中点与相机的距离,从最小细分距离tessMinDist
开始,到最远细分距离tessMaxDist
,细分因子逐渐衰减到0。
取得边缘细分因子后,内部细分因子就简单地处理为三条边的平均就行了。
// Tessellation.hlsl
real4 CalcTriTessFactorsFromEdgeTessFactors(real3 triVertexFactors)
{
real4 tess;
tess.x = triVertexFactors.x;
tess.y = triVertexFactors.y;
tess.z = triVertexFactors.z;
tess.w = (triVertexFactors.x + triVertexFactors.y + triVertexFactors.z) / 3.0;
return tess;
}
下面贴出完整代码:
Shader "Tessellation/Flat Tri Tessellation + Distance-Based"
{
Properties
{
[NoScaleOffset]_BaseMap ("Base Map", 2D) = "white" {}
[Header(Tess)][Space]
[KeywordEnum(integer, fractional_even, fractional_odd)]_Partitioning ("Partitioning Mode", Float) = 2
[KeywordEnum(triangle_cw, triangle_ccw)]_Outputtopology ("Outputtopology Mode", Float) = 0
[IntRange]_EdgeFactor ("EdgeFactor", Range(1,8)) = 4
_TessMinDist ("TessMinDist", Range(0,10)) = 10.0
_FadeDist ("FadeDist", Range(1,20)) = 15.0
}
SubShader
{
Tags { "RenderType"="Opaque" }
Pass
{
HLSLPROGRAM
#pragma target 4.6
#pragma vertex DistanceBasedTessVert
#pragma fragment DistanceBasedTessFrag
#pragma hull DistanceBasedTessControlPoint
#pragma domain DistanceBasedTessDomain
#pragma multi_compile _PARTITIONING_INTEGER _PARTITIONING_FRACTIONAL_EVEN _PARTITIONING_FRACTIONAL_ODD
#pragma multi_compile _OUTPUTTOPOLOGY_TRIANGLE_CW _OUTPUTTOPOLOGY_TRIANGLE_CCW
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/GeometricTools.hlsl"
#include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Tessellation.hlsl"
CBUFFER_START(UnityPerMaterial)
float _EdgeFactor;
float _TessMinDist;
float _FadeDist;
CBUFFER_END
TEXTURE2D(_BaseMap); SAMPLER(sampler_BaseMap);
struct Attributes
{
float3 positionOS : POSITION;
float2 texcoord : TEXCOORD0;
};
struct VertexOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct PatchTess {
float edgeFactor[3] : SV_TESSFACTOR;
float insideFactor : SV_INSIDETESSFACTOR;
};
struct HullOut{
float3 positionWS : INTERNALTESSPOS;
float2 texcoord : TEXCOORD0;
};
struct DomainOut
{
float4 positionCS : SV_POSITION;
float2 texcoord : TEXCOORD0;
};
VertexOut DistanceBasedTessVert(Attributes input){
VertexOut o;
o.positionWS = TransformObjectToWorld(input.positionOS);
o.texcoord = input.texcoord;
return o;
}
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
float3 cameraPosWS = GetCameraPositionWS();
real3 triVectexFactors = GetDistanceBasedTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, cameraPosWS, _TessMinDist, _TessMinDist + _FadeDist);
float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
o.edgeFactor[0] = max(1.0, tessFactors.x);
o.edgeFactor[1] = max(1.0, tessFactors.y);
o.edgeFactor[2] = max(1.0, tessFactors.z);
o.insideFactor = max(1.0, tessFactors.w);
return o;
}
[domain("tri")]
#if _PARTITIONING_INTEGER
[partitioning("integer")]
#elif _PARTITIONING_FRACTIONAL_EVEN
[partitioning("fractional_even")]
#elif _PARTITIONING_FRACTIONAL_ODD
[partitioning("fractional_odd")]
#endif
#if _OUTPUTTOPOLOGY_TRIANGLE_CW
[outputtopology("triangle_cw")]
#elif _OUTPUTTOPOLOGY_TRIANGLE_CCW
[outputtopology("triangle_ccw")]
#endif
[patchconstantfunc("PatchConstant")]
[outputcontrolpoints(3)]
[maxtessfactor(64.0f)]
HullOut DistanceBasedTessControlPoint (InputPatch<VertexOut,3> patch,uint id : SV_OutputControlPointID){
HullOut o;
o.positionWS = patch[id].positionWS;
o.texcoord = patch[id].texcoord;
return o;
}
[domain("tri")]
DomainOut DistanceBasedTessDomain (PatchTess tessFactors, const OutputPatch<HullOut,3> patch, float3 bary : SV_DOMAINLOCATION)
{
float3 positionWS = patch[0].positionWS * bary.x + patch[1].positionWS * bary.y + patch[2].positionWS * bary.z;
float2 texcoord = patch[0].texcoord * bary.x + patch[1].texcoord * bary.y + patch[2].texcoord * bary.z;
DomainOut output;
output.positionCS = TransformWorldToHClip(positionWS);
output.texcoord = texcoord;
return output;
}
half4 DistanceBasedTessFrag(DomainOut input) : SV_Target{
half3 color = SAMPLE_TEXTURE2D(_BaseMap, sampler_BaseMap, input.texcoord).rgb;
return half4(color, 1.0);
}
ENDHLSL
}
}
}
Shader应用效果如下:
2.3.2 基于屏幕占用范围
将常量外壳着色器修改如下:
PatchTess PatchConstant (InputPatch<VertexOut,3> patch, uint patchID : SV_PrimitiveID){
PatchTess o;
real3 triVectexFactors = GetScreenSpaceTessFactor(patch[0].positionWS, patch[1].positionWS, patch[2].positionWS, GetWorldToHClipMatrix() , _ScreenParams, _TriangleSize);
float4 tessFactors = _EdgeFactor * CalcTriTessFactorsFromEdgeTessFactors(triVectexFactors);
o.edgeFactor[0] = tessFactors.x;
o.edgeFactor[1] = tessFactors.y;
o.edgeFactor[2] = tessFactors.z;
o.insideFactor = tessFactors.w;
return o;
}
GetScreenSpaceTessFactor
函数如下:
// Tessellation.hlsl
// Reference: http://twvideo01.ubm-us.net/o1/vault/gdc10/slides/Bilodeau_Bill_Direct3D11TutorialTessellation.pdf
// Compute both screen and distance based adaptation - return factor between 0 and 1
real3 GetScreenSpaceTessFactor(real3 p0, real3 p1, real3 p2, real4x4 viewProjectionMatrix, real4 screenSize, real triangleSize)
{
// Get screen space adaptive scale factor
real2 edgeScreenPosition0 = ComputeNormalizedDeviceCoordinates(p0, viewProjectionMatrix) * screenSize.xy;
real2 edgeScreenPosition1 = ComputeNormalizedDeviceCoordinates(p1, viewProjectionMatrix) * screenSize.xy;
real2 edgeScreenPosition2 = ComputeNormalizedDeviceCoordinates(p2, viewProjectionMatrix) * screenSize.xy;
real EdgeScale = 1.0 / triangleSize; // Edge size in reality, but name is simpler
real3 tessFactor;
tessFactor.x = saturate(distance(edgeScreenPosition1, edgeScreenPosition2) * EdgeScale);
tessFactor.y = saturate(distance(edgeScreenPosition0, edgeScreenPosition2) * EdgeScale);
tessFactor.z = saturate(distance(edgeScreenPosition0, edgeScreenPosition1) * EdgeScale);
return tessFactor;
}
大致思路是,通过ComputeNormalizedDeviceCoordinates
函数计算得到屏幕坐标,然后乘以屏幕尺寸screenSize
,获得顶点的屏幕位置。如果屏幕上边的长度小于triangleSize
,那么其细分因子就会衰减。
2.3.3 其他
在《DirectX12 3D 游戏开发实战》中还提到了两种计算细分因子的衡量标准:
-
根据三角形的朝向:比如位于轮廓线(Sihouette edge)周围的三角形应该比其他位置拥有更多的细节。我们可以通过面片法线和观察方向点乘来判断是否位于轮廓线附近。
更多内容可以阅读Real-time linear silhouette enhancement。
-
根据粗糙程度:粗糙不平的表面比光滑的表面更需要细致的曲面细分处理。可以通过纹理获取粗糙度数据,来决定镶嵌次数。
3 参考文献
- Tessellation
- Direct3d-11-advanced-stages-tessellation
- CurvedPNTriangles
- My Tessellation Has Cracks
- PN-AEN-Triangles-Whitepaper
- Phone Tessellation
- Real-time linear silhouette enhancement
- Direct3D 11 Tutorial: Tessellation
水平有限,如有错误,请多包涵 (〃‘▽’〃)