DirectX11 Tessellation曲面细分实现动态增加模型细节

时间:2021-10-11 03:56:33

DirectX11新增了一个非常吸引人的新技术,就是曲面细分。曲面细分技术可以通过产生新的顶点模拟出更平滑的曲面。

下图是《古墓丽影9中》没有开启曲面的效果,可以看到人物模型轮廓比较僵硬,这是因为原模型的三角形面数较低造成的:
DirectX11 Tessellation曲面细分实现动态增加模型细节

开启了曲面细分后,看到人物模型轮廓已经相当平滑了,使得游戏画面更加真实:
DirectX11 Tessellation曲面细分实现动态增加模型细节

那为什么不直接在原模型中增加模型面数呢?
原因有如下三点:

  1. 动态LOD(levels of detail,细节层次),我们可以基于摄像机的远近和其它因素决定要产生的细节丰富度。如果我们离物体比较远,就没有必要产生那么多模型面数。
  2. 使物理和动画模拟更加高效。低面数模型来计算物理和动画,可以使得计算效率更加高,接着再曲面细分,达到更逼真的显示效果。
  3. 节约内存。我们可以节约硬盘容量、CPU内存、GPU显存,在计算的时候再凭空新增细节顶点。

DirectX11中曲面细分的相关的三个阶段位于顶点着色器和几何着色器之间,Hull Shader Stage 和 Domain Shader Stage 我们可以编写Hull Shader(外壳着色器)和Domain Shader(域着色器)来控制曲面细分,而Tessellator Stage是GPU自动完成的。

DirectX11 Tessellation曲面细分实现动态增加模型细节

一、曲面细分设置
1.Tessellation基本图元
当我们需要曲面细分的时候,就不再是输入三角图元到IA输入装配阶段了。而是需要指定控制点的个数:

D3D11_PRIMITIVE_1_CONTROL_POINT_PATCH = 8,
D3D11_PRIMITIVE_2_CONTROL_POINT_PATCH = 9,
D3D11_PRIMITIVE_3_CONTROL_POINT_PATCH = 10,
D3D11_PRIMITIVE_4_CONTROL_POINT_PATCH = 11,

D3D11_PRIMITIVE_31_CONTROL_POINT_PATCH = 38,
D3D11_PRIMITIVE_32_CONTROL_POINT_PATCH = 39

二、曲面细分相关的着色器
1.Hull Shader
Hull Shader由两部分组成:

(1) Constant Hull Shader
(2) Control Point Hull Shader

(1) Constant Hull Shader
该着色器是用于描述每个Patch的tessellation因子,如SV_TessFactor边细分度、SV_InsideTessFactor内部细分度。
DirectX11 Tessellation曲面细分实现动态增加模型细节

下面是一个根据摄像机位置距离顶点远近来决定细分程度的Constant Hull Shader:

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 60 if d <= d0. The interval
// [d0, d1] defines the range we tessellate in.

const float d0 = 20.0f;
const float d1 = 100.0f;
float tess = 64.0f*saturate( (d1-d)/(d1-d0) );

// 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;
}

(2) Control Point Hull Shader
Control Point Hull Shader输入一系列的控制点,并输出一系列的控制点。该着色器将会在每个控制点输出时被调用。输出的控制点数量可以比输入的控制点个数多。你可以在这里对控制点添加额外的信息。一个对于Hull Shader的用途是输入一个普通的三角(3个控制点),利用该3个控制点产生贝塞尔三角面(10个控制点),该方法称为N-patches(或PN triangles)。

DirectX11 Tessellation曲面细分实现动态增加模型细节
(图为贝塞尔三角面)

下面是一个pass-through外壳着色器,仅仅是按原来的信息通过该着色器。在大多数基于顶点来改变的曲面细分很常见:

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;
}

2.domain shader
域着色器给你提供细分后修改顶点的能力,类似于曲面细分后的顶点着色器。域着色器没有给你具体的顶点,而是给你(u, v)坐标,由你自己来返回细分的顶点信息。

DirectX11 Tessellation曲面细分实现动态增加模型细节

下面是一个返回修改顶点高度的DS:

// 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) );

dout.PosH = mul(float4(p, 1.0f), gWorldViewProj);

return dout;
}

另外,需要注意的是从Domain Shader出来的顶点必须是处于齐次裁剪空间。因此上面要通过WVP矩阵变换。

上述程序运行效果:
DirectX11 Tessellation曲面细分实现动态增加模型细节

三、贝塞尔曲面
上面没有用贝塞尔曲面,只是用Hull Shader和Domain Shader实现动态LOD。一般都我们可以通过贝塞尔曲面对已有的模型进行平滑处理。

贝塞尔曲线是一种通过控制点来生成平滑曲线的技术,相信用过图形处理工具的人都使用过贝塞尔曲线:
DirectX11 Tessellation曲面细分实现动态增加模型细节

贝塞尔曲线的原理很简单,如下图所示:

DirectX11 Tessellation曲面细分实现动态增加模型细节

三个控制点的贝塞尔曲线数学公式来表示的话就是:
DirectX11 Tessellation曲面细分实现动态增加模型细节

推广至四个控制点:

DirectX11 Tessellation曲面细分实现动态增加模型细节

DirectX11 Tessellation曲面细分实现动态增加模型细节

DirectX11 Tessellation曲面细分实现动态增加模型细节

那么,推广至n个顶点的贝塞尔曲线:
DirectX11 Tessellation曲面细分实现动态增加模型细节

贝塞尔曲面也只是两个单维度的贝塞尔曲线组合:
DirectX11 Tessellation曲面细分实现动态增加模型细节

贝塞尔曲面公式,k是第i,j个控制点:

DirectX11 Tessellation曲面细分实现动态增加模型细节

这样就可以在DS中生成平滑的顶点了。

float3 CubicBezierSum(const OutputPatch<HullOut, 16> bezpatch, float4 basisU, float4 basisV)
{
float3 sum = float3(0.0f, 0.0f, 0.0f);
sum = basisV.x * (basisU.x*bezpatch[0].PosL + basisU.y*bezpatch[1].PosL + basisU.z*bezpatch[2].PosL + basisU.w*bezpatch[3].PosL );
sum += basisV.y * (basisU.x*bezpatch[4].PosL + basisU.y*bezpatch[5].PosL + basisU.z*bezpatch[6].PosL + basisU.w*bezpatch[7].PosL );
sum += basisV.z * (basisU.x*bezpatch[8].PosL + basisU.y*bezpatch[9].PosL + basisU.z*bezpatch[10].PosL + basisU.w*bezpatch[11].PosL);
sum += basisV.w * (basisU.x*bezpatch[12].PosL + basisU.y*bezpatch[13].PosL + basisU.z*bezpatch[14].PosL + basisU.w*bezpatch[15].PosL);

return sum;
}

// 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, 16> bezPatch)
{
DomainOut dout;

float4 basisU = BernsteinBasis(uv.x);
float4 basisV = BernsteinBasis(uv.y);

float3 p = CubicBezierSum(bezPatch, basisU, basisV);

dout.PosH = mul(float4(p, 1.0f), gWorldViewProj);

return dout;
}

程序运行结果:
DirectX11 Tessellation曲面细分实现动态增加模型细节

当然对于复杂的模型使用曲面细分,仅仅依靠贝塞尔曲线还是不够生成准确的模型,否则就会细分成一个平滑的大胖子!因此还需要其它信息,在DirectX SDK中自带了一个人物模型的Tessellation示例,比较复杂,我就不深入讲下去了。我也上传到我的GitHub仓库里面,可以自行去下载。

人物曲面细分效果图:
DirectX11 Tessellation曲面细分实现动态增加模型细节

DirectX11 Tessellation曲面细分实现动态增加模型细节

项目源代码:
https://github.com/ljcduo/Introduction-to-3D-Game-Programming-With-DirectX11/tree/master/Chapter%2013%20The%20Tessellation%20Stages