原文:Introduction to 3D Game Programming with DirectX 12 学习笔记之 --- 第十四章:曲面细分阶段

代码工程地址:

https://github.com/jiabaodan/Direct12BookReadingNotes



曲面细分阶段包含渲染管线中的三个阶段,用以细分几何物体,它在顶点着色器和几何着色器之间。使用曲面细分的主要原因:

  1. 基于GPU的LOD;
  2. 物理和动画的优化,可以在低面模型上计算物理效果和动画,然后细分为高面模型用以渲染;
  3. 节省内存(硬盘,RAM,VRAM)。


学习目标

  1. 学习曲面细分使用的patch基元类型;
  2. 学习曲面细分每个阶段的作用,以及他们的输入输出;
  3. 学习通过编写hull和domain着色器来细分几何体;
  4. 学习曲面细分的不同策略,以及曲面细分的优化;
  5. 学习贝塞尔曲线和平面的数学公式,以及如何用曲面细分来实现它。


1 曲面细分基元类型

当我们使用曲面细分渲染,我们不想IA阶段提交三角形列表,我们提交具有许多控制点的patches。D3D支持patches拥有1~32个控制点,并且由下面的基元类型定义:

D3D_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST = 33,
D3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST = 34,
D3D_PRIMITIVE_TOPOLOGY_3_CONTROL_POINT_PATCHLIST = 35,
D3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST = 36,
.
.
.
D3D_PRIMITIVE_TOPOLOGY_31_CONTROL_POINT_PATCHLIST = 63,
D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64,

一个三角形可以被理解为一个具有3个控制点的三角patch((D3D_PRIMITIVE_3_CONTROL_POINT_PATCH),所以你依然可以提交你的三角形网格。四边形可以被提交为(D3D_PRIMITIVE_4_CONTROL_POINT_PATCH)。这些patch最终会被曲面细分阶段细分为三角形。

当传递控制点基元类型到ID3D12GraphicsCommandList::IASetPrimitiveTopology时,设置D3D12_GRAPHICS_PIPELINE_STATE_DESC::PrimitiveTopologyType为D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH:

opaquePsoDesc.PrimitiveTopologyType = D3D12_PRIMITIVE_TOPOLOGY_TYPE_PATCH;

1.1 曲面细分和顶点着色器

因为我们提交的是patch的控制点,所以顶点着色器可以正常处理这些控制点(和顶点一样)。



2 HULL着色器

HULL着色器实际上包含2个着色器:常量Hull着色器(Constant Hull Shader)和控制点Hull着色器(Control Point Hull Shader)。


2.1 常量Hull着色器

常量Hull着色器针对每个patch执行,并且负责输出网格的曲面细分因子(tessellation factors);曲面细分因子命令曲面细分阶段对patch细分多少。下面是一个将拥有4个控制点的方块patch均匀细分3次的例子:

struct PatchTess
{
float EdgeTess[4] : SV_TessFactor;
float InsideTess[2] : SV_InsideTessFactor; // Additional info you want associated per patch.
}; PatchTess ConstantHS(InputPatch<VertexOut, 4> patch,
uint patchID : SV_PrimitiveID)
{
PatchTess pt; // Uniformly tessellate the patch 3 times.
pt.EdgeTess[0] = 3; // Left edge
pt.EdgeTess[1] = 3; // Top edge
pt.EdgeTess[2] = 3; // Right edge
pt.EdgeTess[3] = 3; // Bottom edge
pt.InsideTess[0] = 3; // u-axis (columns)
pt.InsideTess[1] = 3; // v-axis (rows) return pt;
}

常量Hull着色器必须输出细分因子,细分因子取决于patch的拓扑结构。

除了细分因子(SV_TessFactor和SV_InsideTessFactor),你还可以输出其他patch的信息,让domain着色器接收并使用。

细分一个方块patch包含两部分:

  1. 四条边的细分因子角色四条边怎么细分;
  2. 2个内部细分因子决定内部如何细分。

细分一个三角形patch同样包含两部分:

  1. 3条边的细分因子;
  2. 1个内部细分因子;

D3D11硬件支持的最大细分因子是64。如果所有细分因子都是0,那么当前patch就拒绝进入后面的阶段。它可以帮助我们基于patch在背面消除和视锥体裁切上实现优化。

  1. 如果这个patch不在视锥体内,可以让他拒绝进入后面的阶段;
  2. 如果这个patch是背面,可以让它拒绝进入后面的阶段;

具体裁切多少主要基于需求,不要做不需要的裁切来浪费性能。下面是一些常用的度量单位来决定裁切多少:

  1. 于相机的距离;
  2. 屏幕的覆盖率;
  3. 三角形的方向和定位;
  4. 粗糙度。

[Story10]给出了下面的优化建议:

  1. 如果细分因子是1(也就是不细分),走一遍细分阶段流程是浪费GPU开销;
  2. 因为是基于GPU实现的,不要细分一个覆盖小于8个像素的这种太小的三角形;
  3. 批量调用具有细分的绘制调用(频繁打开和关闭曲面细分非常浪费性能)。

2.1 控制点Hull着色器

控制点Hull着色器输入一系列控制点,输出一系列控制点。它每次控制点输出的时候调用一次。一个Hull着色器是改变平面的表现,比如一个将普通的三角形(拥有3个控制点)修改为立方贝塞尔三角形patch(拥有10个控制点)。这种策略称之为N-patches方案或者PN三角形方案([Vlachos01])。对于我们的第一个Demo,我们只是简单的pass-through着色器,只传递控制点,不修改(驱动可以检测和优化pass-through着色器([Bilodeau10b])):

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

Hull着色器通过InputPatch输入参数传进所有控制点。系统值SV_OutputControlPointID给出控制点的索引。输入控制点的数量不需要匹配输出控制点的数量。

控制点Hull着色器介绍了一些属性:

  1. domain:patch类型:tri,quad或者isoline;
  2. partitioning:指定细分的模式:

    a、integer:新顶点添加/删除值根据整形细分因子,小数部分会无视;

    b、Fractional((fractional_even/fractional_odd)):新顶点添加/删除值根据整形细分因子,但是通过小数部分滑动。
  3. outputtopology:细分后的三角形的缠绕顺序,triangle_cw(顺时针)、triangle_ccw(逆时针)、line(针对线段的细分);
  4. outputcontrolpoints:Hull着色器执行的次数,每次输出一个控制点。SV_OutputControlPointID给出输出点在Hull着色器中的索引。
  5. patchconstantfunc:常量Hull着色器函数的名称;
  6. maxtessfactor:提示驱动指定你的着色器使用的最大细分因子。这个可以让硬件有一个潜在的优化(让硬件知道最大细分因子)。D3D11硬件支持的最大细分因子是64.


3 曲面细分阶段

作为程序员,我们无法控制曲面细分阶段,它是由硬件完成的,基于常量Hull着色器程序输出的细分因子对Patch进行细分,下面是一些基于不同因子细分的例子:


3.1 方块patch的细分例子:


3.2 三角形patch的细分例子:



4 DOMAIN着色器

曲面细分阶段输出了所有新的顶点和三角形。DOMAIN着色器对每个新创建的顶点进行调用。当曲面细分开启的时候,顶点着色器运行与每个控制点,Hull着色器是每个细分patch的顶点着色器。Domain着色器中对每个细分完成的patch变换到其次裁切空间。

对于方块patch,Domain着色器输入细分因子(常量Hull着色器的输出),细分顶点位置(u, v)的坐标参数,所有从控制点hull着色器输出的控制点。Domain并不给你每个顶点的实际位置,而是patch空间的(u, v),顶点的位置通过双线性差值得到:

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); float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
}

三角形patch类似,只是坐标从(u, v)变为三维重心(u, v, w)坐标。修改为重心坐标系原因是贝塞尔三角形patches通过重心坐标系定义的。



5 细分一个平面方块

作为本章中的一个Demo,我们提交一个方块patch,然后根据和摄像机的距离进行细分,然后根据数学公式对顶点进行偏移(类似之前“hills”Demo)。

顶点缓冲保存4个控制点,创建如下:

void BasicTessellationApp::BuildQuadPatchGeometry()
{
std::array<XMFLOAT3,4> vertices =
{
XMFLOAT3(-10.0f, 0.0f, +10.0f),
XMFLOAT3(+10.0f, 0.0f, +10.0f),
XMFLOAT3(-10.0f, 0.0f, -10.0f),
XMFLOAT3(+10.0f, 0.0f, -10.0f)
};
std::array<std::int16_t, 4> indices = { 0, 1, 2, 3 }; const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t); auto geo = std::make_unique<MeshGeometry>();
geo->Name = "quadpatchGeo"; ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->VertexBufferCPU));
CopyMemory(geo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize); ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->IndexBufferCPU));
CopyMemory(geo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize); geo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices.data(),
vbByteSize, geo->VertexBufferUploader);
geo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(),
ibByteSize, geo->IndexBufferUploader); geo->VertexByteStride = sizeof(XMFLOAT3);
geo->VertexBufferByteSize = vbByteSize;
geo->IndexFormat = DXGI_FORMAT_R16_UINT;
geo->IndexBufferByteSize = ibByteSize;
SubmeshGeometry quadSubmesh;
quadSubmesh.IndexCount = 4;
quadSubmesh.StartIndexLocation = 0;
quadSubmesh.BaseVertexLocation = 0;
geo->DrawArgs["quadpatch"] = quadSubmesh; mGeometries[geo->Name] = std::move(geo);
}

渲染物体创建如下:

void BasicTessellationApp::BuildRenderItems()
{
auto quadPatchRitem = std::make_unique<RenderItem>(); quadPatchRitem->World = MathHelper::Identity4x4();
quadPatchRitem->TexTransform = MathHelper::Identity4x4();
quadPatchRitem->ObjCBIndex = 0;
quadPatchRitem->Mat = mMaterials["whiteMat"].get();
quadPatchRitem->Geo = mGeometries["quadpatchGeo"].get();
quadPatchRitem->PrimitiveType = D3D_PRIMITIVE_TOPOLOGY_4_CONTROL_POINT_PATCHLIST;
quadPatchRitem->IndexCount = quadPatchRitem->Geo->DrawArgs["quadpatch"].IndexCount;
quadPatchRitem->StartIndexLocation = quadPatchRitem->Geo->DrawArgs["quadpatch"].StartIndexLocation;
quadPatchRitem->BaseVertexLocation = quadPatchRitem->Geo->DrawArgs["quadpatch"].BaseVertexLocation; mRitemLayer[(int)RenderLayer::Opaque].push_back(quadPatchRitem.mAllRitems.push_back(std::move(quadPatchRitem));
}

Hull着色器和前面介绍的基本一致,不同的地方在于,根据和摄像机的距离决定细分多少;并且它是一个pass-through着色器:

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

最后在domain着色器中对顶点的y坐标进行偏移:

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


6 立方贝塞尔方块PATCHES

本节我们通过描述立方贝塞尔方块Patches来展示如何通过大量控制点构成一个表面。


6.1 贝塞尔曲线

有三个不共线的控制点p0, p1, 和p2定义一个贝塞尔曲线,那么如果要求曲线上的点p(t)的位置,首先对p0、p1和p1、p2根据t进行线性插值:



然后p(t)点就可以通过基于t的线性插值得到:



将上面两组方程结合起来,就得到贝塞尔曲线方程:



类似的方式,如果是4个控制点(p0, p1, p2,和p3):



第一次插值:



第二次插值:



第三次插值:



将上面方程结合起来,最终公式为:



通常情况下都只用到3个点,因为已经足够光滑,和控制表面。

针对N维的贝塞尔曲线方程是Bernstein basis functions,可以定义为:

对于三维曲线Bernstein basis functions是:

相比于之前4个控制点的最终方程,我们可以将贝塞尔曲线方程写为:

然后求出三次Bernstein basis functions的偏导数:



三次贝塞尔曲线的偏导数就是:



偏导数方程对求表面的切线方向很有用。


6.2 三次贝塞尔平面

对于一个具有4x4个控制点的patch,我可以将每一行定义为一个具有4个控制点的三次贝塞尔曲线;那么第i行的贝塞尔曲线为:



如果我们在u0的位置求这些贝塞尔曲线的值,那么我们会得到从列方向上的4个点。我们可以使用这4个点定义另一条贝塞尔曲线:



如果我们让U正常变化,我们就会扫出一组类似的贝塞尔曲线,组成一个贝塞尔平面。



它的偏导数用以求切线和法线向量:


6.3 三次贝塞尔平面求解代码

本节给出三次贝塞尔平面求解代码,为了方便,先给出完整公式:



代码直接映射到上面给出的公式:

float4 BernsteinBasis(float t)
{
float invT = 1.0f - t; return float4( invT * invT * invT,
3.0f * t * invT * invT,
3.0f * t * t * invT,
t * t * t );
} 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;
} float4 dBernsteinBasis(float t)
{
float invT = 1.0f - t; return float4( -3 * invT * invT,
3 * invT * invT - 6 * t * invT,
6 * t * invT - 3 * t * t,
3 * t * t );
}

6.4 定义Patch几何

我们的顶点缓冲保存16个控制点:

void BezierPatchApp::BuildQuadPatchGeometry()
{
std::array<XMFLOAT3,16> vertices =
{
// Row 0
XMFLOAT3(-10.0f, -10.0f, +15.0f),
XMFLOAT3(-5.0f, 0.0f, +15.0f),
XMFLOAT3(+5.0f, 0.0f, +15.0f),
XMFLOAT3(+10.0f, 0.0f, +15.0f),
// Row 1
XMFLOAT3(-15.0f, 0.0f, +5.0f),
XMFLOAT3(-5.0f, 0.0f, +5.0f),
748
XMFLOAT3(+5.0f, 20.0f, +5.0f),
XMFLOAT3(+15.0f, 0.0f, +5.0f),
// Row 2
XMFLOAT3(-15.0f, 0.0f, -5.0f),
XMFLOAT3(-5.0f, 0.0f, -5.0f),
XMFLOAT3(+5.0f, 0.0f, -5.0f),
XMFLOAT3(+15.0f, 0.0f, -5.0f),
// Row 3
XMFLOAT3(-10.0f, 10.0f, -15.0f),
XMFLOAT3(-5.0f, 0.0f, -15.0f),
XMFLOAT3(+5.0f, 0.0f, -15.0f),
XMFLOAT3(+25.0f, 10.0f, -15.0f)
}; std::array<std::int16_t, 16> indices =
{
0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11,
12, 13, 14, 15
}; const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t); auto geo = std::make_unique<MeshGeometry>();
geo->Name = "quadpatchGeo"; ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->VertexBufferCPU));
CopyMemory(geo->VertexBufferCPU->GetBufferPointer(), vertices.data(), vbByteSize); ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->IndexBufferCPU));
CopyMemory(geo->IndexBufferCPU->GetBufferPointer(), indices.data(), ibByteSize); geo->VertexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), vertices.data(),
vbByteSize, geo->VertexBufferUploader);
geo->IndexBufferGPU = d3dUtil::CreateDefaultBuffer(md3dDevice.Get(),
mCommandList.Get(), indices.data(),
ibByteSize, geo->IndexBufferUploader); geo->VertexByteStride = sizeof(XMFLOAT3);
geo->VertexBufferByteSize = vbByteSize;
geo->IndexFormat = DXGI_FORMAT_R16_UINT;
geo->IndexBufferByteSize = ibByteSize;
SubmeshGeometry quadSubmesh;
quadSubmesh.IndexCount = (UINT)indices.size();
quadSubmesh.StartIndexLocation = 0;
quadSubmesh.BaseVertexLocation = 0; geo->DrawArgs["quadpatch"] = quadSubmesh;
mGeometries[geo->Name] = std::move(geo);
}

我们的渲染物体创建和定义如下:

void BezierPatchApp::BuildRenderItems()
{
auto quadPatchRitem = std::make_unique<RenderItem>();
quadPatchRitem->World = MathHelper::Identity4x4();
quadPatchRitem->TexTransform = MathHelper::Identity4x4();
quadPatchRitem->ObjCBIndex = 0;
quadPatchRitem->Mat = mMaterials["whiteMat"].get();
quadPatchRitem->Geo = mGeometries["quadpatchGeo"].get();
quadPatchRitem->PrimitiveType = D3D11_PRIMITIVE_TOPOLOGY_16_CONTROL_POINT_PATCHLIST;
quadPatchRitem->IndexCount = quadPatchRitem->Geo->DrawArgs["quadpatch"].IndexCount;
quadPatchRitem->StartIndexLocation = quadPatchRitem->Geo->DrawArgs["quadpatch"].StartIndexLocation;
quadPatchRitem->BaseVertexLocation = quadPatchRitem->Geo->DrawArgs["quadpatch"].BaseVertexLocation;
mRitemLayer[(int)RenderLayer::Opaque].push_back(quadPatchRitem.mAllRitems.push_back(std::move(quadPatchRitem));
}


7 总结

  1. 曲面细分阶段是渲染流水线中的一个可选的阶段,它包含Hull着色器,曲面细分,Domain着色器;曲面细分完全由硬件完成,其他两个阶段是可编程的;
  2. 曲面细分可以优化内存,也可以减少物理和动画运算(在低模上计算),可以实现LOD(以前只能放到CPU);
  3. 提交曲面细分控制点要使用新的基元类型;单个基元D3D12支持1到32个控制点,由枚举D3D_PRIMITIVE_1_CONTROL_POINT_PATCH到D3D_PRIMITIVE_32_CONTROL_POINT_PATCH定义;
  4. 启用曲面细分后,顶点着色器输入控制点,对每个控制点进行传统的动画和物理计算;Hull着色器包含常量Hull着色器(Constant Hull Shader)和控制点Hull着色器(Control Point Hull Shader)。常量Hull着色器针对每个Patch执行,输出度每个Patch细分多少的细分因子(tessellation factors)(也可以添加其他可选数据)。控制点Hull着色器在每次控制点输出的时候调用一次,它修改了表面的表达方式。比如一个有3个控制点的三角形,可以输出为有10个控制点的贝塞尔三角面;
  5. Domain着色器对每个细分生成的顶点调用一次,在这里对每个顶点投射到其次裁切空间;
  6. 如果不需要细分物体,就不要开启细分阶段,因为会有性能开销。避免细分太多覆盖小于8像素的三角形。将需要细分的绘制放到一起,不要在同一帧中频繁开启和关闭细分。Hull着色器中使用背面消除和视锥体消除屏蔽看不到的Patch;
  7. 用参数方程定义的贝塞尔曲线和平面,可以用来表示平滑的曲线或表面。它们通过控制点在确定形状。为了让我们可以直接绘制平滑的曲线和表面,贝塞尔表面被很多流行的硬件细分算法使用,比如PN Triangles 和 Catmull-Clark approximations。


8 练习

本章内容我目前用不到,练习暂时不做。

最新文章

  1. android 开启本地相册选择图片并返回显示
  2. Bootstrap学习笔记
  3. 3dmax渲染插件,生成2.5d瓦片
  4. Jenkins参数化构建插件,实现构建前输入自定义参数
  5. 《C和指针(Pointer on c)》 学习笔记(转自:http://dsqiu.iteye.com/blog/1687944)
  6. Java关于md5+salt盐加密验证
  7. Spring-MVC开发之全局异常捕获全面解读
  8. MUI - Scroll插件的使用
  9. careercup-高等难度 18.5
  10. PHP面向对象编程
  11. eclipse下使用Genymotion调试Android程序出现的问题
  12. 又是玻璃效果?调用一句代码就OK了
  13. [LeetCode][Python]15:3Sum
  14. JDK和JRE的差异和区别
  15. Android: 自定义Tab样式,一种简单的方式。
  16. 使用 TRESTClient 与 TRESTRequest 作为 HTTP Client
  17. linux 添加多个网段
  18. JasperReports+iReport在eclipse中的使用(转)
  19. 如何开启GZIP
  20. 深入探索.NET内部了解CLR如何创建运行时对象

热门文章

  1. python scikit-learn计算tf-idf词语权重
  2. 关于hive表同步类型问题
  3. springboot拦截器之验证登录
  4. Docker(二)安装及常用命令
  5. 阿里云CDN上线 WAF,一站式提供分发+安全能力
  6. Mysql指定服务器访问
  7. vue/npm 错误提示&amp;解决
  8. Mysql 遇到的一些坑
  9. Boost.Asio基础
  10. 洛谷P2196 挖地雷 [2017年4月计划 动态规划13]