指针和固定大小缓冲区_DX12绘制篇:动态顶点缓冲区

今天我们继续搞定"山川湖泊"。在上篇已经创建了"山川",这篇来创建"湖泊"模型和波纹涟漪动画。

如何让“湖泊”产生波浪涟漪效果呢?最直观的想法就是不断改变顶点坐标,只要改变一次所需的时间越短,那这个顶点动画看起来就会越平滑。那如何不断改变顶点坐标呢?我们可以通过如渲染到纹理,计算着色器以及顶点纹理拾取等在GPU上计算的方法,但这些方法涉及比较高级的应用,我们在深入之后章节会学习。今天我们使用CPU计算的方法,每帧(或者每几帧)改变顶点缓冲区中顶点的坐标,实现顶点动画效果,这就涉及到动态顶点缓冲区了。还记得我们之前是如何每帧更新常量缓冲区的吗?我们封装了UploadBuffer类来实现的,因为上传堆是可以被CPU写入的,并且封装时也做了非常量缓冲区的扩展,所以我们可以使用这个类来实现动态顶点缓冲区。

1.MeshGeometry类

现在的问题是,“山川”模型没有动画,是静态的,顶点数据存在默认堆中(当然,具体实现是先存上传堆再传至默认堆)。而“湖泊”模型又是动态的,顶点数据存在上传堆中。因为所处不同类型的堆,所以我们就不能共用vertexBufferGpu和indexBufferGpu,于是我们封装一个小结构,专门存放每个几何体顶点数据在传输过程中用到的CPU系统内存和GPU缓存的指针变量,并将顶点缓冲区描述符(vbv)和索引缓冲区描述符(ibv)的创建结构体也放入其中(因为需要用到堆地址)。我们将这个结构命名为MeshGeometry。

struct SubmeshGeometry
{
	UINT indexCount;
	UINT startIndexLocation;
	UINT baseVertexLocation;
};
struct MeshGeometry
{
	std::string name;

	ComPtr<ID3DBlob> vertexBufferCpu = nullptr;	//CPU系统内存上的顶点数据
	ComPtr<ID3D12Resource> vertexBufferUploader = nullptr;	//GPU上传堆中的顶点缓冲区
	ComPtr<ID3D12Resource> vertexBufferGpu = nullptr;	//GPU默认堆中的顶点缓冲区(最终的GPU顶点缓冲区)

	ComPtr<ID3DBlob> indexBufferCpu = nullptr;	//CPU系统内存上的索引数据
	ComPtr<ID3D12Resource> indexBufferUploader = nullptr;	//GPU上传堆中的索引缓冲区
	ComPtr<ID3D12Resource> indexBufferGpu = nullptr;	//GPU默认堆中的索引缓冲区(最终的GPU索引缓冲区)

	UINT vertexBufferByteSize = 0;
	UINT vertexByteStride = 0;
	UINT indexBufferByteSize = 0;
	DXGI_FORMAT indexFormat = DXGI_FORMAT_R16_UINT;

	std::unordered_map<std::string, SubmeshGeometry> DrawArgs;//对应不同子物体绘制三参数的无序列表

	D3D12_VERTEX_BUFFER_VIEW GetVbv()const
	{
		D3D12_VERTEX_BUFFER_VIEW vbv;
		vbv.BufferLocation = vertexBufferGpu->GetGPUVirtualAddress();//顶点缓冲区资源虚拟地址
		vbv.SizeInBytes = vertexBufferByteSize;	//顶点缓冲区大小(所有顶点数据大小)
		vbv.StrideInBytes = vertexByteStride;	//每个顶点元素所占用的字节数

		return vbv;
	}

	D3D12_INDEX_BUFFER_VIEW GetIbv() const
	{
		D3D12_INDEX_BUFFER_VIEW ibv;
		ibv.BufferLocation = indexBufferGpu->GetGPUVirtualAddress();
		ibv.Format = indexFormat;
		ibv.SizeInBytes = indexBufferByteSize;

		return ibv;
	}

	//等上传堆资源传至默认堆后,释放上传堆里的内存
	void DisposeUploaders()
	{
		vertexBufferUploader = nullptr;
		indexBufferUploader = nullptr;
	}
};

接着我们在RenderItem结构体中加入MeshGeometry类指针,因为RenderItem是MeshGeometry的上层结构。

struct RenderItem
{
	......

	MeshGeometry* geo = nullptr;

	......
};

然后我们修改“山川”模型的顶点和索引列表代码,将顶点数据赋值MeshGeometry类中对应变量。注意:在最后我们将grid的绘制三参数放入一个映射表,再将grid的MeshGeometry放入另一个映射表,这样我们就能将山川和湖泊的顶点数据区分开来了。

std::unordered_map<std::string, std::unique_ptr<MeshGeometry>> geometries;

//创建索引缓存
std::vector<std::uint16_t> indices = grid.GetIndices16();
//计算顶点缓存和索引缓存大小
//MeshGeometry* geo = nullptr;
auto geo = std::make_unique<MeshGeometry>();
geo->name = "landGeo";
const UINT vbByteSize = (UINT)vertices.size() * sizeof(Vertex);
const UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);
geo->vertexBufferByteSize = vbByteSize;
geo->indexBufferByteSize = ibByteSize;
geo->vertexByteStride = sizeof(Vertex);
geo->indexFormat = DXGI_FORMAT_R16_UINT;

//顶点、索引数据传至GPU缓存
ThrowIfFailed(D3DCreateBlob(vbByteSize, &geo->vertexBufferCpu));	//创建顶点数据内存空间
ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->indexBufferCpu));	//创建索引数据内存空间
CopyMemory(geo->vertexBufferCpu->GetBufferPointer(), vertices.data(), vbByteSize);	//将顶点数据拷贝至顶点系统内存中
CopyMemory(geo->indexBufferCpu->GetBufferPointer(), indices.data(), ibByteSize);	//将索引数据拷贝至索引系统内存中
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);

//将之前封装好的几何体的SubmeshGeometry对象赋值给无序映射表
geo->DrawArgs["landGrid"] = gridSubmesh;
//将“山川”的MeshGeometry装入总的几何体映射表
geometries["landGeo"] = std::move(geo);

接着,我们实现BuildRenderItem的“山川”部分。利用上面创建好的无序表landGeo赋值当前MeshGeoetry,然后就可以使用对应的MeshGeoetry里面的数据了。最后将“山川”渲染项放入总渲染项中。

auto landRitem = std::make_unique<RenderItem>();
landRitem->world = MathHelper::Identity4x4();
landRitem->objCBIndex = 0;//grid常量数据(world矩阵)在objConstantBuffer索引0上
landRitem->geo = geometries["landGeo"].get();	//赋值当前MeshGeoetry
landRitem->primitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
landRitem->indexCount = landRitem->geo->DrawArgs["landGrid"].indexCount;
landRitem->baseVertexLocation = landRitem->geo->DrawArgs["landGrid"].baseVertexLocation;
landRitem->startIndexLocation = landRitem->geo->DrawArgs["landGrid"].startIndexLocation;
allRitems.push_back(std::move(landRitem));

2.波动算法

具体网格上的顶点是怎样运动的呢?这就涉及到波动算法,最经典的当然是波动方程了,但是我们本篇不会涉及具体的算法,只是看下大致的框架。我们新建一个Waves.h文件,并创建一个Waves类,Disturb函数就是波动方程函数,而Update函数则是每帧更新波动方程计算出来的顶点坐标。

#pragma once
#ifndef WAVES_H
#define WAVES_H

#include <vector>
#include <DirectXMath.h>

//执行波浪模拟的计算。在之后进行的模拟更新后,客户端必须将当前解决方案复制到顶点缓冲区中进行渲染。
class Waves
{
public:
    Waves(int m, int n, float dx, float dt, float speed, float damping);
    Waves(const Waves& rhs) = delete;
    Waves& operator=(const Waves& rhs) = delete;
    ~Waves();

    int RowCount()const;
    int ColumnCount()const;
    int VertexCount()const;
    int TriangleCount()const;
    float Width()const;
    float Depth()const;

    // 返回计算后的网格顶点坐标
    const DirectX::XMFLOAT3& Position(int i)const { return mCurrSolution[i]; }

    // 返回计算后的网格顶点法线
    const DirectX::XMFLOAT3& Normal(int i)const { return mNormals[i]; }

    // 返回计算后的网格顶点切线
    const DirectX::XMFLOAT3& TangentX(int i)const { return mTangentX[i]; }

    void Update(float dt);
    void Disturb(int i, int j, float magnitude);

private:
    int mNumRows = 0;
    int mNumCols = 0;

    int mVertexCount = 0;
    int mTriangleCount = 0;

    // Simulation constants we can precompute.
    float mK1 = 0.0f;
    float mK2 = 0.0f;
    float mK3 = 0.0f;

    float mTimeStep = 0.0f;
    float mSpatialStep = 0.0f;

    std::vector<DirectX::XMFLOAT3> mPrevSolution;
    std::vector<DirectX::XMFLOAT3> mCurrSolution;
    std::vector<DirectX::XMFLOAT3> mNormals;
    std::vector<DirectX::XMFLOAT3> mTangentX;
};

#endif // WAVES_H

3.创建并每帧更新顶点动态缓冲区

上面提到,我们使用UploadBuffer类来实现动态缓冲区的创建,并且应该在帧资源里创建,因为每个帧资源有自己的commandAllocator,每帧都会重置它,所以为了避免缓冲区资源被提前释放,要把顶点动态缓冲区创建放在帧资源里。可以看到wavesVB就是我们的顶点动态缓冲区了,由于它不属于常量缓冲,所以第三个参数是false。

FrameResources::FrameResources(ID3D12Device* device, UINT passCount, UINT objCount, UINT wavesVertCount)
{
	ThrowIfFailed(device->CreateCommandAllocator(
		D3D12_COMMAND_LIST_TYPE_DIRECT, 
		IID_PPV_ARGS(&cmdAllocator)));

	objCB = std::make_unique<UploadBufferResource<ObjectConstants>>(device, objCount, true);
	passCB = std::make_unique<UploadBufferResource<PassConstants>>(device, passCount, true);

	wavesVB = std::make_unique<UploadBufferResource<Vertex>>(device, wavesVertCount, false);//一个顶点即一个子缓冲区
}

然后我们修改BuildFrameResources函数,加入第4个参数,即顶点动态缓冲区的子缓冲区数量(等于顶点数)。

void LandAndWave::BuildFrameResources()
{
	for (int i = 0; i < frameResourcesCount; i++)
	{
		FrameResourcesArray.push_back(std::make_unique<FrameResources>(
			d3dDevice.Get(),
			1, //pass子缓冲数量
			(UINT)allRitems.size(),//子物体缓冲数量
			waves->VertexCount()));//顶点子缓冲数量
	}
}

然后我们要在主文件的Init函数中创初始化wave类中数据,实例化“湖泊”模型,并准备好波动数据。

bool LandAndWave::Init(HINSTANCE hInstance, int nShowCmd, std::wstring customCaption)
{
	......

	//初始化wave类
	waves = std::make_unique<Waves>(128, 128, 1.0f, 0.03f, 4.0f, 0.2f);

	......
}

然后我们新建一个UpdateWaves函数,来更新每帧的顶点数据。可以看到我们先用波动方程计算出波动的位置和大小,然后更新顶点数据,最后将更新的顶点数据存入CPU系统内存并最终传至GPU的上传堆中。

void LandAndWave::UpdateWaves(const GameTime& gt)
{
	static float t_base = 0.0f;
	if ((gameTime.TotalTime() - t_base) >= 0.25f)
	{
		t_base += 0.25f;	//0.25秒生成一个波浪
		//随机生成横坐标
		int i = MathHelper::Rand(4, waves->RowCount() - 5);
		//随机生成纵坐标
		int j = MathHelper::Rand(4, waves->ColumnCount() - 5);
		//随机生成波的半径
		float r = MathHelper::RandF(0.2f, 0.5f);//float用RandF函数
		//使用波动方程函数生成波纹
		waves->Disturb(i, j, r);
	}

	//每帧更新波浪模拟(即更新顶点坐标)
	waves->Update(gt.DeltaTime());

	//将更新的顶点坐标存入GPU上传堆中
	auto currWavesVB = currFrameResources->wavesVB.get();
	for (int i = 0; i < waves->VertexCount(); i++)
	{
		Vertex v;
		v.Pos = waves->Position(i);
		v.Color = XMCOLOR(DirectX::Colors::Blue);

		currWavesVB->CopyData(i, v);
	}
	//赋值湖泊的GPU上的顶点缓存
	wavesRitem->geo->vertexBufferGpu = currWavesVB->Resource();
}

4.构建“湖泊”索引缓冲区

由于我们的波动只是顶点坐标的改变,并不会改变顶点的索引,所以索引缓存还是静态的。我们用之前的静态存储即可。可以看到,我们先运用grid的程序算法,得到索引列表,并将索引列表存入CPU系统内存,接着使用CreateDefaultBuffer函数经上传堆传入默认堆中。这些代码我们已经很熟悉了,之前一直是这么做的。最后赋值对应MeshGeometry类中的数据(类似于上面的landGeo)。注意:我们规定了索引数不能大于65536,否则中止程序。

void LandAndWave::BuildLakeIndexBuffer()
{
	
	//初始化索引列表(每个三角形3个索引)
	std::vector<std::uint16_t> indices(3 * waves->TriangleCount());
	assert(waves->VertexCount() < 0x0000ffff);//顶点索引数大于65536则中止程序

	//填充索引列表
	int m = waves->RowCount();
	int n = waves->ColumnCount();
	int k = 0;
	for (int i = 0; i < m - 1; i++)
	{
		for (int j = 0; j < n - 1; j++)
		{
			indices[k] = i * n + j;
			indices[k + 1] = i * n + j + 1;
			indices[k + 2] = (i + 1) * n + j;

			indices[k + 3] = (i + 1) * n + j;
			indices[k + 4] = i * n + j + 1;
			indices[k + 5] = (i + 1) * n + j + 1;

			k += 6;
		}
	}
	//计算顶点和索引缓存大小
	UINT vbByteSize = waves->VertexCount() * sizeof(Vertex);
	UINT ibByteSize = (UINT)indices.size() * sizeof(std::uint16_t);

	auto geo = std::make_unique<MeshGeometry>();
	geo->name = "lakeGeo";

	geo->vertexBufferCpu = nullptr;
	geo->vertexBufferGpu = nullptr;
	//创建索引的CPU系统内存
	ThrowIfFailed(D3DCreateBlob(ibByteSize, &geo->indexBufferCpu));
	//将索引列表存入CPU系统内存
	CopyMemory(geo->indexBufferCpu->GetBufferPointer(), indices.data(), ibByteSize);
	//将索引数据通过上传堆传至默认堆
	geo->indexBufferGpu = ToolFunc::CreateDefaultBuffer(d3dDevice.Get(), 
		cmdList.Get(), 
		ibByteSize, 
		indices.data(), 
		geo->indexBufferUploader);

	//赋值MeshGeomety中相关属性
	geo->vertexBufferByteSize = vbByteSize;
	geo->vertexByteStride = sizeof(Vertex);
	geo->indexFormat = DXGI_FORMAT_R16_UINT;
	geo->indexBufferByteSize = ibByteSize;

	SubmeshGeometry LakeSubmesh;
	LakeSubmesh.baseVertexLocation = 0;
	LakeSubmesh.startIndexLocation = 0;
	LakeSubmesh.indexCount = (UINT)indices.size();
	//使用grid几何体
	geo->DrawArgs["lakeGrid"] = LakeSubmesh;
	//湖泊的MeshGeometry
	geometries["lakeGeo"] = std::move(geo);
}

5.构建渲染项

之前已经实现了渲染项中的“山川”部分,这里再加入“湖泊”部分。记得将objCBIndex改为1,因为是第2个模型。

void LandAndWave::BuildRenderItem()
{
        //"山川"部分
        ......
        //"湖泊"部分
        auto lakeRitem = std::make_unique<RenderItem>();
        lakeRitem->world = MathHelper::Identity4x4();
        lakeRitem->objCBIndex = 1;//湖泊的常量数据(world矩阵)在objConstantBuffer索引1上
        lakeRitem->geo = geometries["lakeGeo"].get();	//赋值当前的“湖泊”MeshGeoetry
        lakeRitem->primitiveType = D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST;
        lakeRitem->indexCount = lakeRitem->geo->DrawArgs["lakeGrid"].indexCount;
        lakeRitem->baseVertexLocation = lakeRitem->geo->DrawArgs["lakeGrid"].baseVertexLocation;
        lakeRitem->startIndexLocation = lakeRitem->geo->DrawArgs["lakeGrid"].startIndexLocation;

        wavesRitem = lakeRitem.get();

        allRitems.push_back(std::move(lakeRitem));//push_back后,智能指针自动释放内存
}

编译运行,可以看到,湖面已经动起来了。

ff133d28140f7bac0bb9c8602d6578c4.gif

由于现在没有光照,动画效果不太明显,看下线框模式的效果。

7521ad12f343ddba66d621c159faa896.gif

我们实现了顶点动态缓冲区的数据更新。这个方法相对复杂,而且因为是CPU计算的,所以效率也比较低,但在诸如物理模拟计算和碰撞检测的粒子系统等领域,还是会比较多得用到动态缓冲区。好了,到今天为止我们的绘制几何体部分就结束了,完成习题后,我们就要开启光照篇了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值