细分着色器的构成
- 细分着色器是为了将一大块的区域继续划分,划分成很多的小块
- 大体上由三部分构成,但也会涉及一些其它阶段的内容。
- 这三个阶段分别为 Hull Shader Stage, Tesslator Stage, Domain Shader Stage,他们需要一起工作,从名字就可以看出来是两个可编程着色器阶段中间夹着一个可配置阶段,具体顺序如图
第一部分:Hull Shader
首先先来看一段 Hull Shader 的代码
//--------------------------------------------------------------------------------
struct HS_CONTROL_POINT_INPUT
{
float3 WorldPosition : POSITION;
};
//--------------------------------------------------------------------------------
struct HS_CONTROL_POINT_OUTPUT
{
float3 WorldPosition : POSITION;
};
//--------------------------------------------------------------------------------
struct HS_CONSTANT_DATA_OUTPUT
{
float Edges[3] : SV_TessFactor;
float Inside : SV_InsideTessFactor;
};
//--------------------------------------------------------------------------------
//--------------------------------------------------------------------------------
HS_CONSTANT_DATA_OUTPUT PassThroughConstantHS(
InputPatch<HS_CONTROL_POINT_INPUT, 3> ip,
uint PatchID : SV_PrimitiveID )
{
HS_CONSTANT_DATA_OUTPUT output;
output.Edges[0] = 2.0f;
output.Edges[1] = 2.0f;
output.Edges[2] = 2.0f;
output.Inside = 2.0f;
return output;
}
//--------------------------------------------------------------------------------
[domain("tri")]
[partitioning("fractional_even")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("PassThroughConstantHS")]
HS_CONTROL_POINT_OUTPUT HSMAIN(
InputPatch<HS_CONTROL_POINT_INPUT, 3> ip,
uint i : SV_OutputControlPointID,
uint PatchID : SV_PrimitiveID )
{
HS_CONTROL_POINT_OUTPUT output;
output.WorldPosition = ip[i].WorldPosition;
return output;
}
//--------------------------------------------------------------------------------
从这个代码片段来看,实现由两部分组成
- 一部分是 shader program 本体,主要是用来传递 control point (与顶点基本类似,但有小小的差别) 和其他参数
- 另一部分是在 [patchconstantfunction] 属性中定义的,用于得到细分常量参数的方法,后面的阶段会根据这里提供的参数进行细分,代码中是 PassThroughConstantHS 方法
这里面有几点要知道的知识点
- 方法的调用顺序:shader program 先于 constant function 执行
- control points 与 vertex 的内容没有什么差别,唯一区别在于如果管线配置要使用细分着色器的话,必须定义control point patch list中的其中一种图形类型,即设置D3D11_PRIMITIVE_TOPOLOGY的值。(typedef D3D_PRIMITIVE_TOPOLOGY D3D11_PRIMITIVE_TOPOLOGY;)。所以 control points 除了顶点有的信息之外,它们是被包括在 patch 中的比顶点多了这份连接信息的顶点
- Input and Output Patches:他们都是用来获取一组 control points 的数据的。Input Patch 作为 hull shader, patch constant function, 和 geometry shader 的输入。Output Patch 作为 domain shader 的输入。
- shader program 的 [attribute]
- [domain(tri)]:确定输出至 domain shader 的 patch 类型,常用 "tri" 和 "quad"
- [partitioning("fractional_odd")]:影响下一个配置阶段对参数进行插值处理的方式,共有四种类型:integer,fractional_even,fraction_odd,pow2
- [outputtopology(triangle_cw)]:用于指导 tessellator 重新将单独的点组装成图元,可选项还有 triangle_ccw 和 line
- [outputcontrolpoints(3)]:输出的 control point 数量
- [patchconstantfunc("PassThroughConstantHS")]:上文说过,声明方法
- [maxtessfactor(5)]:定义 patch constant function 产生的最大的 tessellation facto
然后我们开始分析一下当前方法做了些什么:
- 拆分原有传递进来的 patch,根据它的信息生成用于构成新的 patch 的 points 和 产生一些细分参数
- main program:主要是根据 patch 中的 control points 的信息产生[outputcontrolpoints(n)] 中声明的数量的输出数据,作为 OutputPatch 中的 control point 传递到 domain shader
- 输入:
- SV_OutputControlPointID:uint i : SV_OutputControlPointID 这个是当前调用的次数,也就相当于一个 for 循环的计数器
- SV_PrimitiveID:唯一 id
- InputPatch 上面提过了
- 输出:
- 新产生的 points
- 输入:
- constant function:
- 决定细分参数
- 两种细分参数输出写入至SV_InsideTessFactor和SV_TessFactor,这两个参数会指导细分阶段划分patch
- 输出:
- SV_InsideTessFactor 和 SV_TessFactor
第二部分:Tessellator
- 这是一个需要配置的阶段,而非可编程阶段
- 根据 hull shader 传递进来的点去生成图元信息给 domain shader
- 这个阶段同样由两部分组成
- 根据 SV_TessFactor 和 SV_InsideTessFactor (constant function 存储参数的系统类型) 的值产生一些坐标点
- 用这些坐标点来生成图元传递至 domain shader
第三部分:Domain shader
- 这是细分的最后一个阶段,它接收从前面 tessellator 传过来的坐标点,然后产生最终的输出顶点,而产生输出顶点最重要的部分也就是计算顶点的位置。之后我们计算贝塞尔曲面的位置也是在这里,所以这是整个细分最核心的部分
- 首先还是来看一段这部分的代码,大致的了解一下
-
struct DS_OUTPUT { float4 Position : SV_Position; }; [domain("tri")] DS_OUTPUT DSMAIN( const OutputPatch<HS_CONTROL_POINT_OUTPUT, 3> TrianglePatch, float3 BarycentricCoordinates : SV_DomainLocation, HS_CONSTANT_DATA_OUTPUT input ) { DS_OUTPUT output; // Interpolate world space position with barycentric coordinates float3 vWorldPos = BarycentricCoordinates.x * TrianglePatch[0].WorldPosition + BarycentricCoordinates.y * TrianglePatch[1].WorldPosition + BarycentricCoordinates.z * TrianglePatch[2].WorldPosition; // Transform world position with viewprojection matrix output.Position = mul( float4(vWorldPos.xyz, 1.0), ViewProjMatrix ); return output; }
-
tesselator 阶段每产生一个坐标点就会调用一次,根据输入参数 OutputPatch<type,num>,数量范围是 [1 - 32]
-
输入:
-
OutputPatch<type, n>:整个 patch 的所有信息
-
SV_DomainLocation:在 domain 中的位置,可以理解成类似于 texture sampler 中的 uv
-
type:传递进来的 patch 中的control points 的结构
-
-
输出:
扫描二维码关注公众号,回复: 3267854 查看本文章-
SV_Position 和其他信息
-
第四部分:生成曲面和输出顶点计算
Curved PN Triangles 大部分内容都是从这篇论文中来的
- 结果是为了让模型生成更加平滑的曲面,造成模型有棱角的原因无非是顶点少,过多的话会影响渲染性能,所以才会用到细分来产生更加平滑的曲面
- (小贴士)要产生曲面,那么生成的点必定不在原本的平面上
- 还是先放代码,然后解释
-
cbuffer Transforms { matrix WorldMatrix; matrix ViewProjMatrix; }; cbuffer RenderingParameters { float3 cameraPosition; float3 cameraLookAt; }; cbuffer TessellationParameters { float4 EdgeFactors; }; //-------------------------------------------------------------------------------- // Inter-stage structures //-------------------------------------------------------------------------------- struct VS_INPUT { float3 position : POSITION; float3 normal : NORMAL; //float2 tex : TEXCOORDS0; }; struct VS_OUTPUT { float3 position : POSITION; float3 normal : NORMAL; }; //-------------------------------------------------------------------------------- struct HS_OUTPUT { float3 position : POSITION; float3 normal : NORMAL; }; //-------------------------------------------------------------------------------- struct HS_CONSTANT_DATA_OUTPUT { float Edges[3] : SV_TessFactor; float Inside : SV_InsideTessFactor; }; //-------------------------------------------------------------------------------- struct DS_OUTPUT { float4 Position : SV_Position; float3 Colour : COLOUR; }; float ComputeWeight(InputPatch<VS_OUTPUT, 3> inPatch, int i, int j) { return dot(inPatch[j].position - inPatch[i].position, inPatch[i].normal); } float3 ComputeEdgePosition(InputPatch<VS_OUTPUT, 3> inPatch, int i, int j) { return ( (2.0f * inPatch[i].position) + inPatch[j].position - (ComputeWeight(inPatch, i, j) * inPatch[i].normal) ) / 3.0f; } float3 ComputeEdgeNormal(InputPatch<VS_OUTPUT, 3> inPatch, int i, int j) { float t = dot ( inPatch[j].position - inPatch[i].position , inPatch[i].normal + inPatch[j].normal ); float b = dot ( inPatch[j].position - inPatch[i].position , inPatch[j].position - inPatch[i].position ); float v = 2.0f * (t / b); return normalize ( inPatch[i].normal + inPatch[j].normal - v * (inPatch[j].position - inPatch[i].position) ); } VS_OUTPUT VSMAIN(in VS_INPUT input) { VS_OUTPUT output; output.position = mul(float4(input.position, 1.0f), WorldMatrix).xyz; output.normal = normalize(mul(input.normal,(float3x3)WorldMatrix)); return output; } //-------------------------------------------------------------------------------- HS_CONSTANT_DATA_OUTPUT PassThroughConstantHS( InputPatch<VS_OUTPUT, 3> ip, uint PatchID : SV_PrimitiveID) { HS_CONSTANT_DATA_OUTPUT output; output.Edges[0] = 64.0f; output.Edges[1] = 64.0f; output.Edges[2] = 64.0f; output.Inside = 64.0f; return output; } //-------------------------------------------------------------------------------- [domain("tri")] [partitioning("fractional_even")] [outputtopology("triangle_cw")] [outputcontrolpoints(13)] [patchconstantfunc("PassThroughConstantHS")] HS_OUTPUT HSMAIN( InputPatch<VS_OUTPUT, 3> ip, uint i : SV_OutputControlPointID, uint PatchID : SV_PrimitiveID) { HS_OUTPUT output; // Must provide a default definition just in // case we don't match any branch below output.position = float3(0.0f, 0.0f, 0.0f); output.normal = float3(0.0f, 0.0f, 0.0f); switch(i) { // Three actual vertices: // b(300) case 0: // b(030) case 1: // b(003) case 2: output.position = ip[i].position; output.normal = ip[i].normal; break; // Edge between v0 and v1 // b(210) case 3: output.position = ComputeEdgePosition(ip, 0, 1); break; // b(120) case 4: output.position = ComputeEdgePosition(ip, 1, 0); break; // Edge between v1 and v2 // b(021) case 5: output.position = ComputeEdgePosition(ip, 1, 2); break; // b(012) case 6: output.position = ComputeEdgePosition(ip, 2, 1); break; // Edge between v2 and v0 // b(102) case 7: output.position = ComputeEdgePosition(ip, 2, 0); break; // b(201) case 8: output.position = ComputeEdgePosition(ip, 0, 2); break; // Middle of triangle // b(111) case 9: float3 E = ( ComputeEdgePosition(ip, 0, 1) + ComputeEdgePosition(ip, 1, 0) + ComputeEdgePosition(ip, 1, 2) + ComputeEdgePosition(ip, 2, 1) + ComputeEdgePosition(ip, 2, 0) + ComputeEdgePosition(ip, 0, 2) ) / 6.0f; float3 V = (ip[0].position + ip[1].position + ip[2].position) / 3.0f; output.position = E + ( (E - V) / 2.0f ); break; // Normals // n(110) - between v0 and v1 case 10: output.normal = ComputeEdgeNormal(ip, 0, 1); break; // n(011) - between v1 and v2 case 11: output.normal = ComputeEdgeNormal(ip, 1, 2); break; // n(101) - between v2 and v0 case 12: output.normal = ComputeEdgeNormal(ip, 2, 0); break; } return output; } //-------------------------------------------------------------------------------- [domain("tri")] DS_OUTPUT DSMAIN(const OutputPatch<HS_OUTPUT, 13> TrianglePatch, float3 BarycentricCoordinates : SV_DomainLocation, HS_CONSTANT_DATA_OUTPUT input) { DS_OUTPUT output; float u = BarycentricCoordinates.x; float v = BarycentricCoordinates.y; float w = BarycentricCoordinates.z; // Original Vertices float3 p300 = TrianglePatch[0].position; float3 p030 = TrianglePatch[1].position; float3 p003 = TrianglePatch[2].position; // Edge between v0 and v1 float3 p210 = TrianglePatch[3].position; float3 p120 = TrianglePatch[4].position; // Edge between v1 and v2 float3 p021 = TrianglePatch[5].position; float3 p012 = TrianglePatch[6].position; // Edge between v2 and v0 float3 p102 = TrianglePatch[7].position; float3 p201 = TrianglePatch[8].position; // Middle of triangle float3 p111 = TrianglePatch[9].position; // Calculate this sample point float3 p = (p300 * pow(w,3)) + (p030 * pow(u,3)) + (p003 * pow(v,3)) + (p210 * 3.0f * pow(w,2) * u) + (p120 * 3.0f * w * pow(u,2)) + (p201 * 3.0f * pow(w,2) * v) + (p021 * 3.0f * pow(u,2) * v) + (p102 * 3.0f * w * pow(v,2)) + (p012 * 3.0f * u * pow(v,2)) + (p111 * 6.0f * w * u * v); //p = w*TrianglePatch[0].position + u*TrianglePatch[1].position + v*TrianglePatch[2].position; // Transform world position with viewprojection matrix output.Position = mul( float4(p, 1.0), ViewProjMatrix ); // Compute the normal - LINEAR float3 vWorldNorm = w*TrianglePatch[0].normal + u*TrianglePatch[1].normal + v*TrianglePatch[2].normal; // Compute the normal - QUADRATIC float3 n200 = TrianglePatch[0].normal; float3 n020 = TrianglePatch[1].normal; float3 n002 = TrianglePatch[2].normal; float3 n110 = TrianglePatch[10].normal; float3 n011 = TrianglePatch[11].normal; float3 n101 = TrianglePatch[12].normal; vWorldNorm = (pow(w,2) * n200) + (pow(u,2) * n020) + (pow(v,2) * n002) + (w * u * n110) + (u * v * n011) + (w * v * n101); vWorldNorm = normalize( vWorldNorm ); output.Colour = w*TrianglePatch[0].normal+u*TrianglePatch[1].normal+v*TrianglePatch[2].normal; return output; } [maxvertexcount(3)] void GSMAIN( triangle DS_OUTPUT input[3], inout TriangleStream<DS_OUTPUT> TriangleOutputStream ) { TriangleOutputStream.Append( input[0] ); TriangleOutputStream.Append( input[1] ); TriangleOutputStream.Append( input[2] ); TriangleOutputStream.RestartStrip(); } float4 PSMAIN(in DS_OUTPUT input) : SV_Target { float4 color = float4(input.Colour, 1.0f); return(color); }
- 先提一下贝塞尔曲线和曲面。
- 一阶贝塞尔曲线(线段):
意义:由 P0 至 P1 的连续点, 描述的一条线段
-
二阶贝塞尔曲线(抛物线):
原理:由 P0 至 P1 的连续点 Q0,描述一条线段。
由 P1 至 P2 的连续点 Q1,描述一条线段。
由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。经验:P1-P0为曲线在P0处的切线。
-
三阶贝塞尔曲线:
通用公式:
- 贝塞尔曲面:
- 一阶贝塞尔曲线(线段):
以上内容出自 贝塞尔曲线
-
说明:看一阶就能明白这东西其实就是个插值,不过复杂以后找插值的位置变化了而已
-
顶点部分:
-
上面的公式看一下大概了解了之后,我们要确定的就是 u,v,w 和 带下标的 b 系列,uvw 就是处于 domain 中的位置,也就是上面提到的 SV_DomainLocation,这个就不用关心了。将模型变平滑,也就是将组成模型的三角形变平滑,这些 b 参数就是我们要将一个三角形变平滑需要改变的地方,将一个三角形变成不在同一个平面的10个顶点,如图:
-
计算十个点:
-
-
从代码中可以看出来,Hull shader 中计算了这十个点然后传递至 Domain Shader 中然后通过贝塞尔曲面公式即可得到对应的点,ComputeWeight,ComputeEdgePosition 则是计算公式中的计算过程对应的两个方法
-
公式中还有一部分是关于法线的,这是一个比较重要的地方,前面提到了新增加进来的点是不能在一个平面的,得到这个结果的方法就是顶点的法线不能共面,所以这个法线并不是真正的法线,而是 bad normal,如图顶点 P1 的法线并不是这个平面的法线,如果相同,则计算出来的点依旧共面而不可能达到平滑的效果 ComputeEdgeNormal 即是计算方法
-
以上是对比图,可以看到还是平滑了许多的,由于没有做抗锯齿,可能边缘有些锯齿,先行忽略吧。
-
然后就到这里!