0阶贝塞尔函数_DX12曲面细分:贝塞尔(Bezier)

我们在上篇中已经实现了曲面细分,但是曲面细分的目的是为了自动平滑LowPoly模型,而上一个案例中,我们是使用了波动函数“凹”出了一个特定的形状,这明显是不符合我们想要自动平滑的预期,而贝塞尔曲线很好的解决了这个问题,所以我们这篇来学习一下贝塞尔曲线和贝塞尔曲面算法。

1. 三阶贝塞尔曲线

我们先从最简单的二阶贝塞尔开始说起。现有3个非共线的控制点

,我们先求
上一点
其中
,这其实就是个线性插值,随着t的变化,
线段上滑动。同理,我们可以得到
上一点
其中
。这时候对
再次使用
进行线性插值,得到一点
,当
从0变化到1,点
所形成的轨迹就叫做二阶贝塞尔曲线。如下图所示,绿色线条即
连线,上面那个黑点即是点
,可以很明显的看到,点
所划过的轨迹是一条曲线,因为做了两轮的线性插值,所以这条曲线就叫做二阶贝塞尔曲线。公式推导我们直接看书,这里就不详细写了。

v2-1e99367892b4f40a552a9a6c68dad4ae_b.gif

同理,三阶贝塞尔曲线就是做了3轮的线性插值,如下图所示,第一轮先插值得到

,第二轮插值得到
,第三轮插值得到

v2-1b59a143154b39df5193b87672cbe246_b.jpg

最后得到 点

的运动轨迹形成的曲线我们称作三阶贝塞尔曲线,如下图所示。

v2-a86726445d75076db028a8eb42ca376a_b.gif

简化后的三阶贝塞尔曲线参数方程:

,

通常来讲,三次贝塞尔曲线已经能够满足绝大部分情况了,因为它足够平滑,对曲线的控制自由度也较高。

仔细观察二阶贝塞尔和三阶贝塞尔方程,发现是有规律的,这个规律可以用著名的伯恩斯坦函数(Bernstein basis function)来阐明。伯恩斯坦函数如下所示:

其中n是几阶,i是控制点索引,t则是[0,1]范围内的阈值。将三阶贝塞尔的参数代入伯恩斯坦函数,得到的就是4个顶点前的系数。所以我们可以将三阶贝塞尔方程写作:

我们还可以对

求导,得出点p处的切线方程。

2. 三阶贝塞尔曲面

我们先使用16个点来创建4条三阶贝塞尔曲线,如下图所示,横向的蓝色曲线即为三阶贝塞尔曲线,我们首先将上文中使用的阈值

改成
,贝塞尔曲线记作
,再使用一个
,其中
,求得贝塞尔曲线上的一点
,将4条曲线上的
连起来的线段记作
,在
范围内, 所有
的集合构成一个曲面,这个曲面就叫做贝塞尔曲面,由于它是由三阶贝塞尔曲线所形成的,所以叫做三阶贝塞尔曲面。

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

至此,曲面细分章节就分享完了,我觉得主要还是熟悉HS和DS的实现流程以及搞明白贝塞尔曲线和曲面的原理,具体的代码应用其实并没有那么重要,在引擎中,这些都是封装好的工具函数,直接用即可。下一篇我们会分享本章课后习题,下篇见,拜拜!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值