我们在上篇中已经实现了曲面细分,但是曲面细分的目的是为了自动平滑LowPoly模型,而上一个案例中,我们是使用了波动函数“凹”出了一个特定的形状,这明显是不符合我们想要自动平滑的预期,而贝塞尔曲线很好的解决了这个问题,所以我们这篇来学习一下贝塞尔曲线和贝塞尔曲面算法。
1. 三阶贝塞尔曲线
我们先从最简单的二阶贝塞尔开始说起。现有3个非共线的控制点
![v2-1e99367892b4f40a552a9a6c68dad4ae_b.gif](http://img-02.proxy.5ce.com/view/image?&type=2&guid=879b78f9-e42f-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-1e99367892b4f40a552a9a6c68dad4ae_b.gif)
同理,三阶贝塞尔曲线就是做了3轮的线性插值,如下图所示,第一轮先插值得到
![v2-1b59a143154b39df5193b87672cbe246_b.jpg](http://img-02.proxy.5ce.com/view/image?&type=2&guid=879b78f9-e42f-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-1b59a143154b39df5193b87672cbe246_b.jpg)
最后得到 点
![v2-a86726445d75076db028a8eb42ca376a_b.gif](http://img-01.proxy.5ce.com/view/image?&type=2&guid=879b78f9-e42f-eb11-8da9-e4434bdf6706&url=https://pic3.zhimg.com/v2-a86726445d75076db028a8eb42ca376a_b.gif)
简化后的三阶贝塞尔曲线参数方程:
通常来讲,三次贝塞尔曲线已经能够满足绝大部分情况了,因为它足够平滑,对曲线的控制自由度也较高。
仔细观察二阶贝塞尔和三阶贝塞尔方程,发现是有规律的,这个规律可以用著名的伯恩斯坦函数(Bernstein basis function)来阐明。伯恩斯坦函数如下所示:
其中n是几阶,i是控制点索引,t则是[0,1]范围内的阈值。将三阶贝塞尔的参数代入伯恩斯坦函数,得到的就是4个顶点前的系数。所以我们可以将三阶贝塞尔方程写作:
我们还可以对
2. 三阶贝塞尔曲面
我们先使用16个点来创建4条三阶贝塞尔曲线,如下图所示,横向的蓝色曲线即为三阶贝塞尔曲线,我们首先将上文中使用的阈值
![v2-07ada47a6bcdd59294a1b70b5ce088dd_b.jpg](http://img-02.proxy.5ce.com/view/image?&type=2&guid=879b78f9-e42f-eb11-8da9-e4434bdf6706&url=https://pic2.zhimg.com/v2-07ada47a6bcdd59294a1b70b5ce088dd_b.jpg)
三阶贝塞尔曲面参数方程:
可以看到,它是在贝塞尔曲线的基础上又做了一次贝塞尔计算,其实这和双线性插值的用法很像。
这样我们就能通过顶点的uv来计算得到最终顶点坐标。
3. 代码实现
搞清楚了三阶贝塞尔曲面的算法,我们只需将数学公式应用到代码里即可。在这之前我们先将cpu中16个控制点的面片传入GPU。
void ShapesApp::BuildQuadPatchGeometry()
{
std::vector<Vertex> vertices(16);//初始化顶点列表
//16个控制点坐标
//第一行
vertices[0].Pos = { -10.0f, -10.0f, +15.0f };
vertices[1].Pos = { -5.0f, 0.0f, +15.0f };
vertices[2].Pos = { +5.0f, 0.0f, +15.0f };
vertices[3].Pos = { +10.0f, 0.0f, +15.0f };
//第二行
vertices[4].Pos = { -15.0f, 0.0f, +5.0f };
vertices[5].Pos = { -5.0f, 0.0f, +5.0f };
vertices[6].Pos = { +5.0f, 20.0f, +5.0f };
vertices[7].Pos = { +15.0f, 0.0f, +5.0f };
//第三行
vertices[8].Pos = { -15.0f, 0.0f, -5.0f };
vertices[9].Pos = { -5.0f, 0.0f, -5.0f };
vertices[10].Pos = { +5.0f, 0.0f, -5.0f };
vertices[11].Pos = { +15.0f, 0.0f, -5.0f };
//第四行
vertices[12].Pos = { -10.0f, 10.0f, -15.0f };
vertices[13].Pos = { -5.0f, 0.0f, -15.0f };
vertices[14].Pos = { +5.0f, 0.0f, -15.0f };
vertices[15].Pos = { +25.0f, 10.0f, -15.0f };
std::vector<std::int16_t> indices = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 };//初始化索引列表
const UINT vbByteSize = vertices.size() * sizeof(Vertex);//顶点缓存大小
const UINT ibByteSize = indices.size() * sizeof(std::int16_t);//索引缓存大小
//绘制三参数
SubmeshGeometry quadPatchSubmesh;
quadPatchSubmesh.baseVertexLocation = 0;
quadPatchSubmesh.startIndexLocation = 0;
quadPatchSubmesh.indexCount = (UINT)indices.size();
auto geo = std::make_unique<MeshGeometry>();
geo->name = "quadPatchGeo";
//将顶点和索引数据复制到GPU系统内存上
geo->vertexBufferGpu = ToolFunc::CreateDefaultBuffer(d3dDevice.Get(),
cmdList.Get(), vbByteSize, vertices.data(), geo->vertexBufferUploader);
geo->indexBufferGpu = ToolFunc::CreateDefaultBuffer(d3dDevice.Get(),
cmdList.Get(), ibByteSize, indices.data(), geo->indexBufferUploader);
//顶点索引大小赋值
geo->vertexByteStride = sizeof(Vertex);
geo->vertexBufferByteSize = vbByteSize;
geo->indexBufferByteSize = ibByteSize;
geo->indexFormat = DXGI_FORMAT_R16_UINT;
//将顶点和索引数据复制到CPU系统内存上
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->DrawArgs["quadPatch"] = quadPatchSubmesh;//绘制三参数
geometries[geo->name] = std::move(geo);//存入geometries结构中
}
然后更改渲染项代码,注意要将primitiveType改为16个控制点的Patch列表D3D_PRIMITIVE_TOPOLOGY_16_CONTROL_POINT_PATCHLIST
void ShapesApp::BuildRenderItem()
{
auto quadPatchRitem = std::make_unique<RenderItem>();
quadPatchRitem->world = MathHelper::Identity4x4();
quadPatchRitem->objCBIndex = 0;//floor常量数据(world矩阵)在objConstantBuffer索引1上
//quadPatchRitem->primitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
quadPatchRitem->primitiveType = D3D_PRIMITIVE_TOPOLOGY_16_CONTROL_POINT_PATCHLIST;
quadPatchRitem->geo = geometries["quadPatchGeo"].get();
quadPatchRitem->mat = materials["bone"].get();//赋予骨头材质给骷髅头
XMStoreFloat4x4(&quadPatchRitem->texTransform, DirectX::XMMatrixScaling(1.0f, 1.0f, 1.0f));
quadPatchRitem->indexCount = quadPatchRitem->geo->DrawArgs["quadPatch"].indexCount;
quadPatchRitem->baseVertexLocation = quadPatchRitem->geo->DrawArgs["quadPatch"].baseVertexLocation;
quadPatchRitem->startIndexLocation = quadPatchRitem->geo->DrawArgs["quadPatch"].startIndexLocation;
ritemLayer[(int)RenderLayer::quadPatch].push_back(quadPatchRitem.get());
allRitems.push_back(std::move(quadPatchRitem));
}
接着我们在shader中实现三阶贝塞尔算法。首先实现伯恩斯坦函数的4个系数。
//计算伯恩斯坦基函数的4个系数(三阶)
float4 BernsteinBasis(float t)
{
float invT = 1.0f - t;//1-t
//计算伯恩斯坦基函数的4个系数(三阶)
return float4(invT * invT * invT,//B03
3.0f * t * invT * invT,//B13
3.0f * t * t * invT,//B23
t * t * t);//B33
}
然后实现贝塞尔曲面,返回顶点坐标。
//通过伯恩斯坦系数计算控制点坐标
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;
}
最后在DS中传入控制点和uv以及细分因子,就能计算出每个顶点的坐标了。
//域着色器
[domain("quad")]
DomainOut DS(PatchTess pt, //细分因子
float2 uv : SV_DomainLocation, //细分后顶点UV(位置UV,非纹理UV)
const OutputPatch<HullOut, 16> patch)//patch的16个控制点
{
DomainOut dout;
//计算伯恩斯坦系数
float4 basisU = BernsteinBasis(uv.x);
float4 basisV = BernsteinBasis(uv.y);
//计算控制点坐标
float3 p = CubicBezierSum(patch, basisU, basisV);
//将控制点坐标转换到裁剪空间
float4 posW = mul(float4(p, 1.0f), gWorld);
dout.PosH = mul(posW, gViewProj);
return dout;
}
编译运行,美妙的三阶贝塞尔曲面出现了。
![v2-eeba9114a8434edc22ab266af992a02f_b.jpg](http://img-03.proxy.5ce.com/view/image?&type=2&guid=879b78f9-e42f-eb11-8da9-e4434bdf6706&url=https://pic4.zhimg.com/v2-eeba9114a8434edc22ab266af992a02f_b.jpg)
至此,曲面细分章节就分享完了,我觉得主要还是熟悉HS和DS的实现流程以及搞明白贝塞尔曲线和曲面的原理,具体的代码应用其实并没有那么重要,在引擎中,这些都是封装好的工具函数,直接用即可。下一篇我们会分享本章课后习题,下篇见,拜拜!