在实时渲染的时候,通常我们会因为带宽,效率等原因,不会采样面数非常多的高精模型建模。但是又想渲染出细节非常多的场景来怎么办?各种办法,动态纹理,法向量纹理,曲面细分等技术是比较常用的技术。这里主要说曲面细分,在DX11渲染管道用了三个stage(阶段)来实现,曲面细分就是在GPU内部使用硬件加速的方式,动态生成顶点。其中:
Hull shader :相当细分前的预处理工作,定制一些细分规则和生成规则。
Tessellator :执行细分操作,根据Hull shader预置的细分规则。
Domain shader :在Tessellator 会生成对应的顶点和线,但是这些个顶点它是相对于控制点的,使用的是(U,V,W)坐标,这一步就是对顶点做一些处理来的。
(或许是涉及到硬件的非通用计算的原因,致使这样一个功能需要整出三个阶段,2个shader,如果只有一个API调用多好)
如果有一天,你在tess的时候失败了,请第一时间思考,是否有别的地方没有使用tess,这是首先需要排查的。
这个图很清晰的描述了整个曲面细分的过程。
1.HS input :第一步不是HS,而是HS之前的input操作,这里有个patch control points(补丁控制点)它是在原语里面设定的,IASetPrimitiveTopology方法D3D11_PRIMITIVE_TOPOLOGY枚举,里面有很多PATCHLIST为1-32。这说明在我们需要进行曲面细分的时候,我们会把原语定义为PATCHLIST系列类型,这个时候传入到vs里面的已经不是普通的顶点,而是补丁控制点,1-32对应的是控制点的个数,一般我们网格是三角形,三个顶点,所以会选择3。
2.HS(Hull Shader) 阶段 :这个阶段会并且的做两件事:1.根据输入的补丁控制点生成新的控制点,然后输出,输出时调用的是DS(Domain Shader),每个点调用一次。2.设置参数,计算的次数方式生成的结果等,参数又如下:
SV_TessFactor :这是个系统语义,定义Tessellation(曲面细分)的每个边缘上的一个补丁,它确定的是边。它的类型有三种:
(1) float[2] :生成的拓扑图为等值线。
float edges[2] : SV_TessFactor; output.edges[0] = tessAmount; // 第一个值在SV_TessFactor的线密度镶嵌的因素 output.edges[1] = tessAmount; // 第二个值是行了详细的Tessellation(曲面细分)系数
(2)float[3] :拓扑图为三角形。
float edges[3] : SV_TessFactor; output.edges[0] = tessAmount; // 第一个组件提供为U = 0边镶嵌因素的补丁 output.edges[1] = tessAmount; // 第二部分提供了镶嵌系数为v = 0边的补丁 output.edges[2] = tessAmount; // 第三部分提供了镶嵌系数为w = 0边的补丁
(3)float[4] :拓扑图为四边形。
float edges[4] : SV_TessFactor; // 为顺时针方向的边缘的排序,从u == 0的边缘,这是的补丁的左侧开始,并从v == 0边缘,这是顶部的补丁。(意思是(0,0)在左上方,u为纵轴,V为横轴) output.edges[0] = tessAmount; // 第一个组件提供为U = 0边镶嵌因素的补丁 output.edges[1] = tessAmount; // 第二部分提供了镶嵌系数为v = 0边的补丁 output.edges[2] = tessAmount; //第三个组成部分为U = 1的边缘镶嵌因素的补丁 output.edges[3] = tessAmount; // 第四部分提供了镶嵌系数为v == 1边缘的补丁
注意:这里必须使用数组float edges[4] ,不能使用float4 edges,上面的一样。另外就是三角形时,用到了W轴,四边形的时候只用到了U,V轴。等值线这个暂时不懂。tessAmount值的区间为[1,8]
SV_InsideTessFactor :这个也是系统语义,定义的是内部的细分规则,等值线不需要设置这个值。
float[2] :拓扑图为四边形。
float :拓扑图为三角形。
[domain("tri")] :这个指定细分的类型。值:quad,tri,isoline
[partitioning("integer")] :// 指定TS阶段细分是按照什么规则细分,一个有四种类型:Fractional_odd(1,3..63),Fractional_even(2,4...64),integer(1,2,3...64),Pow2。有这四种类型的主要原因是奇数生成的图形一致,偶数的一致,但是奇数和偶数的规则不一致。
[outputtopology("triangle_cw")] :指定拓扑类型,这些拓扑信息会被传输到PA block中去。值:line, triangle_cw(逆时针方向三角形), triangle_ccw(???)。
[outputcontrolpoints(3)] // 输出的控制点的数目。
[patchconstantfunc("ColorPatchConstantFunction")] // 当前HS中使用的常量函数,这个函数是自己定义的,引号里面是名字。
//////////////////////////////////////////////////////////////////////////////// // Patch 常量函数,决定tessellation因子,每个patch执行一次,所以是per patch的,不是per 控制点的 //////////////////////////////////////////////////////////////////////////////// ConstantOutputType ColorPatchConstantFunction(InputPatch<HullInputType, 3> inputPatch, uint patchId : SV_PrimitiveID) { ConstantOutputType output; // Set the tessellation factors for the three edges of the triangle. output.edges[0] = tessellationAmount; output.edges[1] = tessellationAmount; output.edges[2] = tessellationAmount; // Set the tessellation factor for tessallating inside the triangle. output.inside = tessellationAmount; return output; }
3.Tessellator :这个阶段就是生成点的阶段,上面已经描述清楚了,照着做就行了,这个阶段不需要做任何事情。
4.DS(Domain Shader):H和T阶段的每个补丁点都会调用一次DS,在DS里面输出最终的点。由于曲面细分会生成很多新的点,所以如果要使用这些shader,最好在DS里面对顶点的坐标进行转换,就是把VS里面的事情拿到DS里面来做。
代码:
//////////////////////////////////////////////////////////////////////////////// // Filename: color.hs //////////////////////////////////////////////////////////////////////////////// ///////////// // GLOBALS // ///////////// cbuffer TessellationBuffer { float tessAmount; float3 padding; }; ////////////// // TYPEDEFS // ////////////// struct HullInputType { float3 hiPosition : POSITION; float3 hiNormal: NORMAL; float3 hiTangent : TANGENT; float3 hiBinormal : BINORMAL; float2 hiTexture : TEXCOORD0; //纹理坐标 }; struct ConstantOutputType { float edges[3] : SV_TessFactor; float inside : SV_InsideTessFactor; }; struct DomainInputType { float3 diPosition : POSITION; float3 diNormal: NORMAL; float3 diTangent : TANGENT; float3 diBinormal : BINORMAL; float2 diTexture : TEXCOORD0; //纹理坐标 }; //////////////////////////////////////////////////////////////////////////////// // Patch 常量函数,决定tessellation因子,每个patch执行一次,所以是per patch的,不是per 控制点的 //////////////////////////////////////////////////////////////////////////////// ConstantOutputType ColorPatchConstantFunction(InputPatch<HullInputType, 3> inputPatch, uint patchId : SV_PrimitiveID) { ConstantOutputType output; // Set the tessellation factors for the three edges of the triangle. output.edges[0] = tessAmount; output.edges[1] = tessAmount; output.edges[2] = tessAmount; // Set the tessellation factor for tessallating inside the triangle. output.inside = tessAmount; return output; } //////////////////////////////////////////////////////////////////////////////// // Hull Shader //////////////////////////////////////////////////////////////////////////////// [domain("tri")] // 这个指定细分的类型,三角形,四边形神马的 [partitioning("integer")] // 指定TS阶段细分是按照什么规则细分,一个有四种类型:Fractional_odd(1,3..63),Fractional_even(2,4...64),integer(1,2,3...64),Pow2 [outputtopology("triangle_cw")] // 细分后的输出语义是逆时针方向三角形,这些拓扑信息会被传输到PA block中去 [outputcontrolpoints(3)] // 输出的控制点的数目,三角形是3个,也是hull shader被调用的次数 [patchconstantfunc("ColorPatchConstantFunction")] // 当前HS中使用的常量函数 上面的那个函数 DomainInputType PortHullShader(InputPatch<HullInputType, 3> patch, uint pointId : SV_OutputControlPointID, uint patchId : SV_PrimitiveID) { DomainInputType output; output.diPosition = patch[pointId].hiPosition; output.diNormal = patch[pointId].hiNormal; output.diTangent = patch[pointId].hiTangent; output.diBinormal = patch[pointId].hiBinormal; output.diTexture = patch[pointId].hiTexture; return output; }
//////////////////////////////////////////////////////////////////////////////// // Filename: color.ds //////////////////////////////////////////////////////////////////////////////// ///////////// // GLOBALS // ///////////// cbuffer MatrixBuffer { matrix mbWorld; matrix mbTransform; }; ////////////// // TYPEDEFS // ////////////// struct ConstantOutputType { float edges[3] : SV_TessFactor; float inside : SV_InsideTessFactor; }; struct DomainInputType { float3 diPosition : POSITION; float3 diNormal: NORMAL; float3 diTangent : TANGENT; float3 diBinormal : BINORMAL; float2 diTexture : TEXCOORD0; //纹理坐标 }; struct PixelInputType { float4 piPosition : SV_POSITION; //float4 color : COLOR; }; //////////////////////////////////////////////////////////////////////////////// // Domain Shader //////////////////////////////////////////////////////////////////////////////// //对三角形u, v, w表示细分点相对于三个控制点的u,v, w坐标,或者说是重心坐标, //我们可以根据这三个值计算出细分点的位置,然后转化为世界坐标系中点, //输出颜色也是用三个控制点的颜色根据u, v, w差值得到。(这个u和v是不是纹理坐标呢?) [domain("tri")] PixelInputType PortDomainShader(ConstantOutputType input, float3 uvwCoord : SV_DomainLocation, const OutputPatch<DomainInputType, 3> patch) { float3 vertexPosition; PixelInputType output; // Determine the position of the new vertex. //Baricentric Interpolation to find each position the generated vertices //基于重心坐标的顶点生成 vertexPosition = uvwCoord.x * patch[0].diPosition + uvwCoord.y * patch[1].diPosition + uvwCoord.z * patch[2].diPosition; // Calculate the position of the new vertex against the world, view, and projection matrices. output.piPosition = mul(float4(vertexPosition, 1.0f), mbTransform); //新生成顶点颜色也为各个控制点颜色组合 //output.color = uvwCoord.x * patch[0].color + uvwCoord.y * patch[1].color + uvwCoord.z * patch[2].color; return output; }