曲面细分:将几何体细分为更小的三角形并偏移产生曲面,达到细节丰富的效果。工作都在shader中,进行格式的配置即可。
一、CPU侧
启用了曲面细分,就要把点作为控制点来传输:
1.图元装配阶段
cmdList->IASetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST);
2.PSO定义阶段
opaquePsoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH;
opaquePsoDesc.HS =
{
reinterpret_cast<BYTE*>(mShaders["tessHS"]->GetBufferPointer()),
mShaders["tessHS"]->GetBufferSize()
};
opaquePsoDesc.DS =
{
reinterpret_cast<BYTE*>(mShaders["tessDS"]->GetBufferPointer()),
mShaders["tessDS"]->GetBufferSize()
};
以上就是在CPU端从设置PSO和绑定渲染流水线的控制点图元设置全过程。其中输入装配可以根据控制点个数具有不同的类型(修改格式而已,从1-32)
二、GPU侧
应用曲面细分之后,顶点着色器处理对象就是单个控制点,可用于对控制点进行调整:
1.顶点着色器
VertexOut VS(VertexIn vin)
{
VertexOut vout;
vout.PosL = vin.PosL;//不调整数据,只做传递用
return vout;
}
出了顶点着色器,就进入外壳着色器。
2.外壳着色器
所谓外壳着色器就是一种规则的制定者,共分为两种:常量外壳着色器与控制点外壳着色器。
1).常量外壳着色器HS
如上,指定4个控制点,实际意义就是将4个控制点组成一个单个面片。常量HS的作用就是处理单个面片(处理一个面片就调用一次),输出该面片细分的因子(边缘与内部细分方式):
struct PatchTess
{
float EdgeTess[4] : SV_TessFactor;//四条边细分因子,几边形就是几条边
float InsideTess[2] : SV_InsideTessFactor;//内部细分因子,三角形一个,四边形两个(横竖)
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
{
PatchTess pt;
//各边均分为3等份,
pt.EdgeTess[0] = 3;
pt.EdgeTess[1] = 3;
pt.EdgeTess[2] = 3;
pt.EdgeTess[3] = 3;
//四边形内部行列数
pt.InsideTess[0] = 3;
pt.InsideTess[1] = 3;
return pt;
}
注意:
- InputPatch<VertexOut, 4> patch表示是由顶点着色器输出的4个控制点构成的一个面片;
- 每个面片具有独立的图元索引:uint patchID : SV_PrimitiveID;
- 返回值 return pt;将细分因子返回至流水线以供调用。
2).控制点外壳着色器HS
以大量的控制点作为输入与输出,每输出一个控制点着色器被调用一次。
龙书对这个部分有大量的叙述,什么PN三角形法,N-patches方法啥的。个人感觉没太大感触,这个控制点外壳着色器,就是细分规则的制定者。
struct HullOut
{
float3 PosL : POSITION;
};
[domain("quad")]//面片类型(quad / tri / isoline)
[partitioning("integer")]//剔除小数部分,使用细分因子整数部分,如果考虑小数(fractional_even / fractional_odd)
[outputtopology("triangle_cw")]//细分生成的三角形绕序(自动组织定义正反面),对线段细分则:line
[outputcontrolpoints(4)]//控制点个数,一个控制点HS执行一次
[patchconstantfunc("ConstantHS")]//指定常量外壳着色器以获得细分因子
[maxtessfactor(64.0f)]//钳制最大细分因子
HullOut HS(InputPatch<VertexOut, 4> p, //同常量HS,从顶点着色器过来
uint i : SV_OutputControlPointID,//控制点ID,单次执行用于索引点
uint patchId : SV_PrimitiveID)
{
HullOut hout;
//更复杂的逻辑,可以调整输出控制点点位置
hout.PosL = p[i].PosL;
return hout;
}
3.镶嵌化
通过两个HS,我们似乎只是制定了曲面划分的规则,但是控制点还是那些控制点,没有任何变化。那我们的规则到底谁来执行呢?答案就是镶嵌化。
镶嵌化会根据我们指定的规则:
1.ConstantHS部分细分因子以SV_TessFactor,SV_InsideTessFactor标记的内容被系统识别;
2.HS部分通过[]指定了一堆规则
通过硬件在两个控制点之间进行插值,并给出在控制点构成的面片空间下的uv(w)坐标,表示插值生成的新点位置。提供给域着色器使用。
4.域着色器
struct DomainOut
{
float4 PosH : SV_POSITION;//此时才产生真正用于场景顶点的坐标,代替原顶点着色器部分
};
[domain("quad")]//面片类型,4个控制点的,注意全过程的对应
DomainOut DS(PatchTess patchTess, //虽然这里有曲面细分但是似乎没使用过
float2 uv : SV_DomainLocation, //由镶嵌化对控制点间使用细分因子插值得到的uv(w)
const OutputPatch<HullOut, 4> quad)//从HS穿过来的控制点
{
DomainOut dout;
// 使用uv双线性插值.对于三角形就是uvw的中心坐标插值
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);
// 偏移使之成为曲面,丰富细节
p.y = 0.3f*( p.z*sin(p.x) + p.x*cos(p.z) );
float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
}
之后交给PS,就好像VS交给PS数据一样处理。
三、基于视点距离的曲面细分例子
这里只分享shader部分代码,因为CPU段顶点配置是非常easy的任务。
struct VertexIn
{
float3 PosL : POSITION;
};
struct VertexOut
{
float3 PosL : POSITION;
};
VertexOut VS(VertexIn vin)
{
VertexOut vout;
vout.PosL = vin.PosL;
return vout;
}
struct PatchTess
{
float EdgeTess[4] : SV_TessFactor;
float InsideTess[2] : SV_InsideTessFactor;
};
PatchTess ConstantHS(InputPatch<VertexOut, 4> patch, uint patchID : SV_PrimitiveID)
{
PatchTess pt;
float3 centerL = 0.25f*(patch[0].PosL + patch[1].PosL + patch[2].PosL + patch[3].PosL);
float3 centerW = mul(float4(centerL, 1.0f), gWorld).xyz;
float d = distance(centerW, gEyePosW);
// Tessellate the patch based on distance from the eye such that
// the tessellation is 0 if d >= d1 and 64 if d <= d0. The interval
// [d0, d1] defines the range we tessellate in.
const float d0 = 20.0f;
const float d1 = 100.0f;
//注意等于1表示不细分,0表示剔除
float tess = max(64.0f*saturate( (d1-d)/(d1-d0) ),1.0);
// Uniformly tessellate the patch.
pt.EdgeTess[0] = tess;
pt.EdgeTess[1] = tess;
pt.EdgeTess[2] = tess;
pt.EdgeTess[3] = tess;
pt.InsideTess[0] = tess;
pt.InsideTess[1] = tess;
return pt;
}
struct HullOut
{
float3 PosL : POSITION;
};
[domain("quad")]
[partitioning("integer")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(4)]
[patchconstantfunc("ConstantHS")]
[maxtessfactor(64.0f)]
HullOut HS(InputPatch<VertexOut, 4> p,
uint i : SV_OutputControlPointID,
uint patchId : SV_PrimitiveID)
{
HullOut hout;
hout.PosL = p[i].PosL;
return hout;
}
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,
float2 uv : SV_DomainLocation,
const OutputPatch<HullOut, 4> quad)
{
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);
// Displacement mapping
p.y = 0.3f*( p.z*sin(p.x) + p.x*cos(p.z) );
float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
}
float4 PS(DomainOut pin) : SV_Target
{
return float4(1.0f, 1.0f, 1.0f, 1.0f);
}