DirectX 12 曲面细分着色器笔记

1. 背景

曲面细分建模(Tessellation)是一种通过多边形分解来提升几何逼真度的方法。曲面细分会导致图元和顶点的增加,其主要实现依据是对顶点法向量进行插值,达到几何面中用更细腻的多图元模拟表达曲面图元的效果。曲面细分的好处是:节省原始模型的数据空间,提高模型表达的质量、性能和可靠性
注意,对面片(patch)顶点法向量均一致的平面图元而言,曲面细分并不能提高逼真度,只有针对相邻面片有不同法向量的三角形来描述的模型,曲面细分才能提高逼真度。

曲面细分是渲染管线的一个可选项,DirectX 12中采用了三个部件来协同从流水线(rendering pipeline)上接管流数据进行曲面细分。它们分别是:Hull Shader 外壳着色器, Tessellator 曲面细分器, Domain Shader 域着色器。其中,Hull Shader和Domain Shader可编程,Tessellator是固定阶段不可编程。
曲面细分在渲染流水线中的位置
Fig1. Pipeline with hardware tessellation

2. 曲面细分工作机制

曲面细分三个部分的作用分别为:
1)Hull Shader: 生成细分输出面片的顶点,更新所有逐顶点或逐面片的属性值; 设置细分层次因数,以控制生成图元的属性值。
2)Tessellator: 对整个图元几何区域创建采样模式,并根据采样模式生成细分的面片图元。
3)Domain Shader: 对每个域采样计算生成的顶点数据,从而使得细分图元能够接入到流水线的下一步。
曲面细分机制

2.1 Hull Shader(外壳着色器)

以下图为例说明曲面细分中的Hull Shader技术手段。图中黑色粗线表示的是原始模型的面片图元(patch)及其邻接的拓扑顶点,共有5个模型图元。Hull Shader会分别接收每一个图元及其拓扑结构,内部根据法向量等因素生成Bezier曲面的控制点(control points)与曲面细分参数(tess factor, TF)。
曲面细分建模
Hull Shader完成的工作包括两部分:

  • 对每个输入的图元进行运算,转换初始图元的顶点,生成Bezier拟合曲面的控制点;
  • 为Tessellator和Domain Shader准备需要用到的转换参数,即图元的细分因子参数,相当于图元的预处理过程。

Hull Shader实际上是通过两个并行的过程来实现计算任务的:控制点生成过程图元细分因子参数生成过程

  • Hull Shader依据传入的图元顶点生成新的控制点,并通过产生的系统值SV_OutputControlPointID给生成的顶点赋索引值,这个就是控制点生成过程。
  • 图元细分因子参数生成过程也是对每个图元运行一次。细分因子包括:edgeTessFactor[4] (分别指代left edge, right edge, top edge, bottom edge细分程度)、insideTessFactor[2] (分别指代内部u,v坐标轴细分程度)。

其中,生成的控制点数据和TF将直接传给Domain shader,同时TF也传给tessellator。即TF决定了图元的细分过程,只是这个过程在Tessellator中完成,如果TF值无效,则该图元将被忽略,不进行细分优化建模。

生成控制点数据涉及到的参数有:(也称为control points hull shader参数)

参数名称含义可能的取值
domain细分发生的区域isoline(等值线)、 tri(三角形)、quad(四边形) 注:不同的domain类型,细分的方式有区别
partitioning细分模式integer, fractional_even, fractional_odd, pow2
outputtopologytessellator输出图元的拓扑结构triangle_cw(顺时针环绕),triangle_ccw(逆时针环绕)
outputcontrolpointshull shader每个线程生成控制点的数目不一定与输入数量相同,可以新增控制点
max_tessfactor最大细分度告知驱动程序shader用到的细分度,硬件可能会针对这个做出优化。DirectX 11和OpenGL core都支持64
patchconstantfunc指定生成图元细分因子的函数如下文中的SetHullConstantsHS函数

图元细分参数包括:(也称为const hull shader参数)

参数名称含义
edgeTess[0]lefe edge细分程度
edge Tess[1]top edge细分程度
edge Tess[2]right edge细分程度
edge Tess[3]bottom edge细分程度
inside Tess[0]内部细分程度,u-axis
insde Tess[1]内部细分程度,v-axis

外侧细分因子负责控制细分区域的周长,内侧细分因子控制细分区域的内部划分方式(区域内水平和垂直方向上各有多少区域存在)。

值得注意的是,
当面片为quad时,edgeTess的长度为4,insideTess的长度为2,分别指定四条边各被分成多少段、内部在横向和纵向各被分为多少段;
当面片为tri时,edgeTess的长度为3,insideTess的长度为2,分别指定三条边各被分成多少段、内部有多少个点;
当面片为isoline时,edgeTess的长度为2,第0个元素指定线段的个数,第1个元素指定线段被分为多少段,insideTess会被忽略。

2.2 Tessellator(曲面细分器)

Tessellator的主要功能是将图元区域分成若干小的图元对象,对每个图元仅运行一次。它首先将输入图元归化为标量化的规范域([0,1]空间),然后根据hull shader提供的细分因子进行曲面细分。它输出uvw标量坐标和新面片的几何拓扑类型到domain shader中。
输入:patch, TF
输出:uvw或uv标量坐标,patch topology

quad: 顶点以uv坐标的形式传给Domain Shader.
quad uv坐标在这里插入图片描述
tri: 顶点以uvw重心坐标的形式传给Domain Shader.
tri uvw坐标在这里插入图片描述
isoline:顶点以uv坐标的形式传给Domain Shader.
isoline uv坐标在这里插入图片描述三角形的内侧细分因子t,如果t是一个偶数,那么三角形域的中心(重心坐标)将定位于(1/2, 1/2, 1/2),然后再中心点和周长之间生成(t/2)-1个同心三角形。反之,如果t是一个奇数,那么到周长为止将生成(t/2)-1个同心三角形,但中心点
(重心坐标)不再是一个细分坐标。

曲面细分器内部存在两个阶段:
1.对细分因子进行优化处理,如对细分因子进行四舍五入,过滤非常小的因子,减少或合并因子,使用32bit浮点运行等。
2. 根据细分因子进行曲面细分,它使用16bit定点小数进行运算,这样即可采用硬件加速,也可保障运算精度。

2.3 Domain Shader(域细分器)

域着色器用于计算细分图元的顶点位置,它对Tessellator的每个输出点运行一次,能够只读访问其输出的uvw标量坐标,同时使用hull shader的两项输出数据。所有的顶点信息都会在这里计算,会涉及到大量运算。
需要注意的是,经过domain shader处理后的数据流会丢失图元的邻接拓扑关系,geometry shader将无法从这种经过曲面细分后的数据流中正确提取图元的拓扑关系。因此,在geometry shader需要使用图元拓扑结构的情况下,geometry shader与曲面细分两个过程不能同时使用。
域着手器的输入输出
DS输出的数据,可能会先传给GS(Geometry Shader)进行进一步的计算(增加顶点、修改顶点位置,计算顶点属性)。也可以直接(当然首先要进行裁剪和光栅化)传给FS(Fragment Shader)进行片元着色。最后进入输出合并阶段,完成整个渲染管线。

3. 编程开发

3.1 hull shader编程开发

1. 设计输入/输出控制点格式

初始的输入控制点来源于数据流的模型顶点,通常根据细分面片的需要,除了顶点的坐标位置,描述面片方向的顶点法向量、切向量和同样需要关联细分的纹理坐标都需要作为顶点输入数据传给外壳着色器。相关代码如下:

struct HS_POINT
{
  float3 vPosition:WORLDPOS; //顶点坐标
  float2 vUV: TEXCOORD0; //纹理坐标
  float3 vNormal: NORMAL; //法向量
}

经过hull shader运算处理后,输出的数据依然是控制点,数据流的性质没有发生改变。

hull shader还需要面片图元的细分因子参数格式,可采用下面的数据结构:

struct HS_CONSTANT_FACTOR
{
  float Edges[4]: SV_TessFactor; //边缘细分因子
  float Inside[2]: SV_InsideTessFactor; //内部细分因子
}

2. 设计生成面片常数参量数据的函数

在对每个面片进行细分之前,需要先对每个面片生成面片描述常量数据。实现这一功能的函数定义如下:

HS_CONSTANT_FACTOR SetHullConstantsHS(InputPatch<HS_POINTS,3> ip, unit PatchID: SV_PrimitiveID)
{
  HS_CONSTANT_FACTOR output;
  output.edges[0] = 4;
  output.edges[1] = 4;
  output.edges[2] = 4;
  output.inside = 4;
  return output;
}

常量生成函数的参数包括以下几个:
1)一个通过SV_PrimitiveID定义的区别标识面片的唯一ID变量PatchID;
2) 输入控制点ip,同上面定义的输入控制点结构一样,如通过ip[0]访问图元的第1个控制点;
3) 最基本的函数实现必须返回后一步Tessellator计算每个面片的TF,如HS_CONSTANT_FACTOR结构。本例中, 将所有的因子都赋值为常量4是最简单的情形,也只可以通过控制点来动态计算这些值。

3. 设计生成控制点的主函数

hull shader的主入口函数除了负责生成控制点外,还负责对整个曲面细分过程进行参数化配置,这主要是通过HLSL修饰关键词来描述。

[domain("tri")] [partitioning("integer")] [outputtopology("triangle_cw")]
[outputcontrolpoints(3)] [patchconstantfunc("SetHullConstantsHS")]
HS_POINT HullShaderMain(InputPatch<HS_POINT, 3> ip, unit i:SV_OutputControlPointID, uint PatchID: SV_PrimitiveID)
{
  HS_POINT output;
  output.vPosition = ip[PatchID].vPosition;
  output.vNormal = ip[PatchID].vNormal;
  output.vUV = ip[PatchID].vUV;
  return output;
}

3.2 domain shader编程开发

domain shader的任务是结合原始图元的顶点坐标xyz,将tessellator内部标题化的细分图元顶点坐标uvw反算到世界坐标空间中,保障细分图元的空间坐标与原始图元的一致,与周边其他图元的边进行无缝对接。
domain shader的输入参数包括hull shader输出的控制点数据、TF和区域位置数据。控制点与常量数据的结构定义均应在hull shader中指定并保持一致,如上段代码中的HS_POINT结构定义。
domain shader的输出为细分后顶点,由于domain shader后数据将回归到rendering pipeline的正常数据流上,所以可以根据需要另行指定输出顶点格式,或直接采用顶点着色器的输出顶点格式。

区域位置相当于记载图元在模型上定义的原点位置,以便反算出细分图元的模型坐标。对quad, isoline图元而言,采用float2类型的变量声明就可以,对于tri类型的图元,则要通过float3类型的变量来表示其重心坐标。定位原点可以是图元的顶点或其他特征点,如重心、中点、垂心,但由于在hull shader内部有约束,只需要按其指定的规则声明,声明的系统值变量应以SV_DomainLocation语义关键词来描述,代码如下:
float2 UV: SV_DomainLocation
在主函数前一定要加上对图元类型的域描述,其定义与hull shader一致。如有下面一个主函数:

[domain("tri")]
HS_POINT Domain_Shader(HS_CONSTANT_FACTOR input, 
                       float3 uvw: SV_DomainLocation, 
                       const outputPatch<HS_POINT, 3> patch)
{
  HS_POINT output;
  float3 pos;
  //基于重心坐标的控制顶点生成
  pos = uvw.x * patch[0].vPosition + uvw.y * patch[1].vPosition +
        uvw.z * patch[2].vPosition;
  //反算新产生的细分顶点在投影空间中的位置
  output.vPosition = mul(float4(pos, 1.0f), worldViewProjectMatrix);   
  //统计图元三个顶点实现的纹理坐标填充
  output.vUV = uvw.x * patch[0].vUV + uvw.y * patch[1].vUV + 
               uvw.z * patch[2].vUV;
  return output;
}

或者quad类型示例:

struct DomainOut{float4 PosH : SV_POSITION;};
// The domain shader is called for every vertex created by the tessellator.
// It is like the vertex shader after tessellation.
[domain(“quad”)]
DomainOut DS(PatchTess patchTess, //patchTess:细分参数
             float2 uv : SV_DomainLocation, //uv:曲面细分阶段传入的顶点位置信息
             const OutputPatch<HullOut, 4> quad) //quad:HS传入的patch数据,尖括号的第二个参数与HS的outputcontrolpoints对应
{  
  DomainOut dout;
  // Bilinear interpolation. 先求顶点坐标,再转换到投影空间
  float3 v1 = lerp(quad[0].PosL, quad[1].PosL, uv.x);
  float3 v2 = lerp(quad[2].PosL, quad[3].PosL, uv.x);
  float3 p = lerp(v1, v2, uv.y);
  float4 posW = mul(float4(p, 1.0f), gWorld);
  dout.PosH = mul(posW, gViewProj);
  return dout;
}
  • 4
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值