D3D11_Chili_Tutorial(3):设计一个Bindable/Drawable系统

DirectXMath(原视频P21)

在这里插入图片描述
我们直接在 Graphics.cpp 中包含头文件#include <DirectXMath.h>(这个库已经包含在Windows SDK中了)

为了方便我们先这样定义域:namespace dx = DirectX;

然后我们就这样改写我们的 constant buffer:

// create constant buffer for transformation matrix
struct ConstantBuffer
{
	dx::XMMATRIX transform;
};
const ConstantBuffer cb =
{
	{
		dx::XMMatrixTranspose(
			dx::XMMatrixRotationZ(angle) *
			dx::XMMatrixScaling(3.0f / 4.0f,1.0f,1.0f) *
			dx::XMMatrixTranslation(x,y,0.0f)
		)
	}
};
wrl::ComPtr<ID3D11Buffer> pConstantBuffer;
D3D11_BUFFER_DESC cbd;
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd.Usage = D3D11_USAGE_DYNAMIC;
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
cbd.MiscFlags = 0u;
cbd.ByteWidth = sizeof(cb);
cbd.StructureByteStride = 0u;
D3D11_SUBRESOURCE_DATA csd = {};
csd.pSysMem = &cb;
GFX_THROW_INFO(pDevice->CreateBuffer(&cbd, &csd, &pConstantBuffer));

// bind constant buffer to vertex shader
pContext->VSSetConstantBuffers(0u, 1u, pConstantBuffer.GetAddressOf());

这里的 dx::XMMATRIX 就像一个黑箱一样,里头的数据被 SIMD 优化了。我们只需要用一些接口就行了而不去触碰里头真正的data。

并且注意这里我们在 CPU 端就转置了(XMMatrixTranspose),而不是去到 GPU 端再去做。毕竟 C++ 矩阵是行主序的而 HLSL 是列主序默认的。

我们在App.cpp中传参是这样的:

wnd.Gfx().DrawTestTriangle(
	timer.Peek(), 
	wnd.mouse.GetPosX() / 400.0f - 1.0f,
	-wnd.mouse.GetPosY() / 300.0f + 1.0f
);

因为任何超过1将被视为离屏,所以我们除了和屏幕大小有关的数加以限制。这直接 hard code 了,其实不太好,不过只是测试一下,就这样得了。

这里y轴反转了一下,因为在 pixel 坐标或者说我们的 mouse 坐标,y是向下的;但是在 NDC(normalized device coordinates)中 y 是向上的。

这个库还有比如 XMVECTOR 之类的,拥有 SSE 支持是很好的(?)。

3D Cube / Z-buffer(原视频P22)

这里我们把立方体每个面的颜色绑定到 PS 中:
PS:

cbuffer CBuf
{
	float4 face_colors[6];
};

float4 main( uint tid : SV_PrimitiveID ) : SV_Target
{
	return face_colors[tid / 2];
}

C++代码中我们则是这样绑定的:

// lookup table for cube face colors
struct ConstantBuffer2
{
	struct
	{
		float r;
		float g;
		float b;
		float a;
	} face_colors[6];
};
const ConstantBuffer2 cb2 =
{
	{
		{1.0f,0.0f,1.0f},
		{1.0f,0.0f,0.0f},
		{0.0f,1.0f,0.0f},
		{0.0f,0.0f,1.0f},
		{1.0f,1.0f,0.0f},
		{0.0f,1.0f,1.0f},
	}
};
wrl::ComPtr<ID3D11Buffer> pConstantBuffer2;
D3D11_BUFFER_DESC cbd2;
cbd2.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
cbd2.Usage = D3D11_USAGE_DEFAULT;
cbd2.CPUAccessFlags = 0u;
cbd2.MiscFlags = 0u;
cbd2.ByteWidth = sizeof(cb2);
cbd2.StructureByteStride = 0u;
D3D11_SUBRESOURCE_DATA csd2 = {};
csd2.pSysMem = &cb2;
GFX_THROW_INFO(pDevice->CreateBuffer(&cbd2, &csd2, &pConstantBuffer2));

// bind constant buffer to pixel shader
pContext->PSSetConstantBuffers(0u, 1u, pConstantBuffer2.GetAddressOf());

为了支持SV_PrimitiveID,我们不能用Shader Model 4 Level 9_3 (/4_0_level_9_3),转而改为:
在这里插入图片描述
我们绘制了两个立方体,带来了一些深度上的问题,因此引入Z-buffer:
Graphics类中我们添加成员:Microsoft::WRL::ComPtr<ID3D11DepthStencilView> pDSV;
(D3D中深度缓冲和模板缓冲共享一个空间)

我们在 Graphic 类构造函数中写下相关代码:

// create depth stensil state
D3D11_DEPTH_STENCIL_DESC dsDesc = {};
dsDesc.DepthEnable = TRUE;
dsDesc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;
dsDesc.DepthFunc = D3D11_COMPARISON_LESS;
wrl::ComPtr<ID3D11DepthStencilState> pDSState;
GFX_THROW_INFO(pDevice->CreateDepthStencilState(&dsDesc, &pDSState));

// bind depth state
pContext->OMSetDepthStencilState(pDSState.Get(), 1u);

// create depth stensil texture
wrl::ComPtr<ID3D11Texture2D> pDepthStencil;
D3D11_TEXTURE2D_DESC descDepth = {};
descDepth.Width = 800u;
descDepth.Height = 600u;
descDepth.MipLevels = 1u;
descDepth.ArraySize = 1u;
descDepth.Format = DXGI_FORMAT_D32_FLOAT;
descDepth.SampleDesc.Count = 1u;
descDepth.SampleDesc.Quality = 0u;
descDepth.Usage = D3D11_USAGE_DEFAULT;
descDepth.BindFlags = D3D11_BIND_DEPTH_STENCIL;
GFX_THROW_INFO(pDevice->CreateTexture2D(&descDepth, nullptr, &pDepthStencil));

// create view of depth stensil texture
D3D11_DEPTH_STENCIL_VIEW_DESC descDSV = {};
descDSV.Format = DXGI_FORMAT_D32_FLOAT;
descDSV.ViewDimension = D3D11_DSV_DIMENSION_TEXTURE2D;
descDSV.Texture2D.MipSlice = 0u;
GFX_THROW_INFO(pDevice->CreateDepthStencilView(
	pDepthStencil.Get(), &descDSV, &pDSV
));

// bind depth stensil view to OM
pContext->OMSetRenderTargets(1u, pTarget.GetAddressOf(), pDSV.Get());

同时,在我们之前的 ClearBuffer 函数中,我们必须得添加上关于深度缓冲的 clear:

void Graphics::ClearBuffer(float red, float green, float blue) noexcept
{
	const float color[] = { red,green,blue,1.0f };
	pContext->ClearRenderTargetView(pTarget.Get(), color);
	pContext->ClearDepthStencilView(pDSV.Get(), D3D11_CLEAR_DEPTH, 1.0f, 0u);
}

20:可绑定 / 可绘制系统p1

如下图,像 InputLayout、PS、ConstantBuffer 等等都是可以绑定(到渲染管线)的,于是我们想用一个接口统一管理它们:
在这里插入图片描述
因此我们的想法是弄一个抽象接口Bindable,有一个方法Bind,可以接受参数并将其绑定到渲染管线。然后继承于它的子类将实现其接口:

class Bindable
{
public:
	virtual void Bind( Graphics& gfx ) noexcept = 0;
	virtual ~Bindable() = default;
protected:
	static ID3D11DeviceContext* GetContext( Graphics& gfx ) noexcept;
	static ID3D11Device* GetDevice( Graphics& gfx ) noexcept;
	static DxgiInfoManager& GetInfoManager( Graphics& gfx ) noexcept(!IS_DEBUG);
};

然后另一边是可绘制的实体 Drawable ,其将包含多个可绑定的实体,比如着色器、常量缓存、输入布局等。当调用绘制的时候,将把自身每个可绑定的东西绑定至渲染管线:

class Drawable
{
public:
	Drawable() = default;
	Drawable( const Drawable& ) = delete;
	virtual DirectX::XMMATRIX GetTransformXM() const noexcept = 0;
	void Draw( Graphics& gfx ) const noexcept(!IS_DEBUG);
	virtual void Update( float dt ) noexcept = 0;
	void AddBind( std::unique_ptr<Bindable> bind ) noexcept(!IS_DEBUG);
	void AddIndexBuffer( std::unique_ptr<class IndexBuffer> ibuf ) noexcept;
	virtual ~Drawable() = default;
private:
	const IndexBuffer* pIndexBuffer = nullptr;
	std::vector<std::unique_ptr<Bindable>> binds;
};

可以看到其内部有一个vector成员把每个Bindable对象存储起来了,通过AddBind方法注册:

void Drawable::AddBind( std::unique_ptr<Bindable> bind ) noexcept(!IS_DEBUG)
{
	assert( "*Must* use AddIndexBuffer to bind index buffer" && typeid(*bind) != typeid(IndexBuffer) );
	binds.push_back( std::move( bind ) );
}

void Drawable::AddIndexBuffer( std::unique_ptr<IndexBuffer> ibuf ) noexcept
{
	assert( "Attempting to add index buffer a second time" && pIndexBuffer == nullptr );
	pIndexBuffer = ibuf.get();
	binds.push_back( std::move( ibuf ) );
}

Draw 函数是 Drawable 接口中的,对于一些entity比如 Box 之类的,直接用基类的 Draw 方法就好了,所以不需要再重写,这将大大减少样板代码:

void Drawable::Draw( Graphics& gfx ) const noexcept(!IS_DEBUG)
{
	for( auto& b : binds )
	{
		b->Bind( gfx );
	}
	gfx.DrawIndexed( pIndexBuffer->GetCount() );
}

注意到因为我们DrawIndexed需要indexbuffer,所以干脆单独把它写到一个成员中了(指向其所在的内存位置),但是由于我们Draw函数内部会一一调用它们重载的Bind方法,indexbuffer也是一个需要绑定的Bindable对象,所以它还是要注册到binds数组中。

现在架构改成这个样子了:
在这里插入图片描述
在这里插入图片描述
首先是把那些处理异常之类的宏都放入头文件中。因为Bindable对象也需要访问这些宏。

然后我们直接把 Bindable 标记为 Graphics 的友元类,让这些类来访问Graphics类的私有变量。
在这里插入图片描述
可以看到这些小技巧:友元关系是不能继承的,因此只有 Bindable 类可以访问到内部成员,于是仅在类中定义接口:

ID3D11DeviceContext* Bindable::GetContext( Graphics& gfx ) noexcept
{
	return gfx.pContext.Get();
}

ID3D11Device* Bindable::GetDevice( Graphics& gfx ) noexcept
{
	return gfx.pDevice.Get();
}

DxgiInfoManager& Bindable::GetInfoManager( Graphics& gfx ) noexcept(!IS_DEBUG)
{
#ifndef NDEBUG
	return gfx.infoManager;
#else
	throw std::logic_error( "YouFuckedUp! (tried to access gfx.infoManager in Release config)" );
#endif
}

这些接口访问了 Graphics 类的私有成员 pContext、pDevice、infoManager。然后这三个函数又定义为protected,这让子类可以使用这三个接口。

接着看 VertexBuffer,为了使其更加灵活,我们使用了模板类型(模板参数就是顶点的类型):

class VertexBuffer : public Bindable
{
public:
	template<class V>
	VertexBuffer( Graphics& gfx,const std::vector<V>& vertices )
		:
		stride( sizeof( V ) )
	{
		INFOMAN( gfx );

		D3D11_BUFFER_DESC bd = {};
		bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
		bd.Usage = D3D11_USAGE_DEFAULT;
		bd.CPUAccessFlags = 0u;
		bd.MiscFlags = 0u;
		bd.ByteWidth = UINT( sizeof( V ) * vertices.size() );
		bd.StructureByteStride = sizeof( V );
		D3D11_SUBRESOURCE_DATA sd = {};
		sd.pSysMem = vertices.data();
		GFX_THROW_INFO( GetDevice( gfx )->CreateBuffer( &bd,&sd,&pVertexBuffer ) );
	}
	void Bind( Graphics& gfx ) noexcept override;
protected:
	UINT stride;
	Microsoft::WRL::ComPtr<ID3D11Buffer> pVertexBuffer;
};

然后就可以使用 sizeof(V) 来确定缓存大小了。

对于 ConstantBuffer 我们也做了类似的事情。不过因为存在两种类型的常量缓冲区——VertexConstantBuffer 和 PixelConstantBuffer,我们用了一个继承的关系,它们都继承基类ConstantBuffer。并且由于通常常数缓存在每一帧都更新,所以我们给了它一个 Update 方法:

void Update( Graphics& gfx,const C& consts )
{
	INFOMAN( gfx );

	D3D11_MAPPED_SUBRESOURCE msr;
	GFX_THROW_INFO( GetContext( gfx )->Map(
		pConstantBuffer.Get(),0u,
		D3D11_MAP_WRITE_DISCARD,0u,
		&msr
	) );
	memcpy( msr.pData,&consts,sizeof( consts ) );
	GetContext( gfx )->Unmap( pConstantBuffer.Get(),0u );
}

这里我们用了 Context 的一个 Map 方法。当映射(map)资源时,基本上就是将其锁定了,并且获得指向该内存的指针(比如这里获得 msr ,是一个 D3D11_MAPPED_SUBRESOURCE 类型,通过 msr.pData 就得到一个 pointer to data)。然后我们通过 memcpy 相当于把 consts 的数据写到了这个内存中,写完之后就可以使用 UnMap 。

最后再看一下类图:
在这里插入图片描述
有些特殊的是这个 TransformCbuf ,之前 Drawable 中的纯虚函数virtual DirectX::XMMATRIX GetTransformXM() const noexcept = 0; 也会在 TransformCbuf 使用到:

class TransformCbuf : public Bindable
{
public:
	TransformCbuf( Graphics& gfx,const Drawable& parent );
	void Bind( Graphics& gfx ) noexcept override;
private:
	VertexConstantBuffer<DirectX::XMMATRIX> vcbuf;
	const Drawable& parent;
};
TransformCbuf::TransformCbuf( Graphics& gfx,const Drawable& parent )
	:
	vcbuf( gfx ),
	parent( parent )
{}

void TransformCbuf::Bind( Graphics& gfx ) noexcept
{
	vcbuf.Update( gfx,
		DirectX::XMMatrixTranspose(
			parent.GetTransformXM() * gfx.GetProjection()
		)
	);
	vcbuf.Bind( gfx );
}

可以看到其内部有成员 VertexConstantBuffer<DirectX::XMMATRIX> ,对于当前的顶点着色器,它有一个常量缓冲区,其中包含着变换矩阵将会用于每个顶点。但这个变换矩阵是根据 Box 的世界坐标建立起来的(以测试的类 Box 为例)。因此我们要做的就是用新矩阵更新常量缓冲区,然后将其绑定至渲染管线。

但是 Drawable 虽然有大量的 Bindable 对象,但不知道究竟哪个才是 TransCbuf(就像 index buffer 一样),并且 Drawable 基类并没有产生变换矩阵需要的数据,只有子类才有。所以我们要做的就是,不让它从顶点缓存继承,而是直接从 Bindable 基类继承。然后将顶点缓存作为其中的一个组成元素。然后其中将保存一个指向 Drawable 的引用,这样调用 TransCbuf 的绑定函数的时候,就会从父类 Drawable 获取转换矩阵(上面代码的 parent.GetTransformXM(),是个纯虚函数),并用于更新自己内部的顶点着色器常数缓存(上面代码的vcbuf.Update),然后将该常量缓冲区绑定到管道。

最后我们在 App.cpp 系统中创建一堆盒子:

class App
{
public:
	App();
	// master frame / message loop
	int Go();
	~App();
private:
	void DoFrame();
private:
	Window wnd;
	ChiliTimer timer;
	std::vector<std::unique_ptr<class Box>> boxes;
};
App::App()
	:
	wnd( 800,600,"The Donkey Fart Box" )
{
	std::mt19937 rng( std::random_device{}() );
	std::uniform_real_distribution<float> adist( 0.0f,3.1415f * 2.0f );
	std::uniform_real_distribution<float> ddist( 0.0f,3.1415f * 2.0f );
	std::uniform_real_distribution<float> odist( 0.0f,3.1415f * 0.3f );
	std::uniform_real_distribution<float> rdist( 6.0f,20.0f );
	for( auto i = 0; i < 80; i++ )
	{
		boxes.push_back( std::make_unique<Box>(
			wnd.Gfx(),rng,adist,
			ddist,odist,rdist
		) );
	}
	wnd.Gfx().SetProjection( DirectX::XMMatrixPerspectiveLH( 1.0f,3.0f / 4.0f,0.5f,40.0f ) );
}

21:可绑定 / 可绘制系统p2

上一节写了那些系统,但是我们创建一堆盒子的时候,每次都创建一遍 VertexShader 之类的,非常浪费资源。所以我们希望每个盒子可以共享一些 Bindable 对象。

那么如何共享呢?自然可以使用静态对象,但是该在哪放置这些静态变量呢?如果放在子类中,比如 Box,那就以后每个子类都要创建,非常繁琐;并且在基类 Drawable 中,它的 Draw 函数只知道 Bindable 实例但无法知道子类的静态成员。

而如果放在基类中,比如基类有一个 vector 去装那些静态Bindable对象,但这样也是行不通的。因为那样所有子类都共享完全相同的Bindable对象了,而我们是想做到不同的实例有不同的Bindable对象。

这里Chili的操作非常流批,直接用一个 CRTP 的方法,在 Drawable 和 子类(比如 Box)直接搞一个模板类DrawableBase:
在这里插入图片描述
于是我们就有:

template<class T>
class DrawableBase : public Drawable
class Box : public DrawableBase<Box>

可以想象,如果再来一个Drawable子类比如球 Sphere ,我们就可以有 class Sphere : public DrawableBase<Sphere>,而 DrawableBase<Sphere>DrawableBase<Box> 是两个不同的类,相当于让编译器去复制粘贴代码了,避免每创建一个子类就要我们手动复制粘贴代码的操作,非常漂亮!

我们的Drawable则被改造如下:

class Drawable
{
	template<class T>
	friend class DrawableBase;
public:
	Drawable() = default;
	Drawable(const Drawable&) = delete;
	virtual DirectX::XMMATRIX GetTransformXM() const noexcept = 0;
	void Draw(Graphics& gfx) const noexcept(!IS_DEBUG);
	virtual void Update(float dt) noexcept = 0;
	virtual ~Drawable() = default;
protected:
	void AddBind(std::unique_ptr<Bindable> bind) noexcept(!IS_DEBUG);
	void AddIndexBuffer(std::unique_ptr<class IndexBuffer> ibuf) noexcept(!IS_DEBUG);
private:
	virtual const std::vector<std::unique_ptr<Bindable>>& GetStaticBinds() const noexcept = 0;
private:
	const class IndexBuffer* pIndexBuffer = nullptr;
	std::vector<std::unique_ptr<Bindable>> binds;
};

加了一个纯虚函数:virtual const std::vector<std::unique_ptr<Bindable>>& GetStaticBinds() const noexcept = 0;,因为Drawable需要访问这些静态Bindable,通过在Draw函数中新增绑定:

for (auto& b : GetStaticBinds())
{
	b->Bind(gfx);
}

而这个纯虚函数将在DrawableBase中定义,DrawableBase完整代码如下:

template<class T>
class DrawableBase : public Drawable
{
protected:
	static bool IsStaticInitialized() noexcept
	{
		return !staticBinds.empty();
	}
	static void AddStaticBind(std::unique_ptr<Bindable> bind) noexcept(!IS_DEBUG)
	{
		assert("*Must* use AddStaticIndexBuffer to bind index buffer" && typeid(*bind) != typeid(IndexBuffer));
		staticBinds.push_back(std::move(bind));
	}
	void AddStaticIndexBuffer(std::unique_ptr<IndexBuffer> ibuf) noexcept(!IS_DEBUG)
	{
		assert("Attempting to add index buffer a second time" && pIndexBuffer == nullptr);
		pIndexBuffer = ibuf.get();
		staticBinds.push_back(std::move(ibuf));
	}
	void SetIndexFromStatic() noexcept(!IS_DEBUG)
	{
		assert("Attempting to add index buffer a second time" && pIndexBuffer == nullptr);
		for (const auto& b : staticBinds)
		{
			if (const auto p = dynamic_cast<IndexBuffer*>(b.get()))
			{
				pIndexBuffer = p;
				return;
			}
		}
		assert("Failed to find index buffer in static binds" && pIndexBuffer != nullptr);
	}
private:
	const std::vector<std::unique_ptr<Bindable>>& GetStaticBinds() const noexcept override
	{
		return staticBinds;
	}
private:
	static std::vector<std::unique_ptr<Bindable>> staticBinds;
};

template<class T>
std::vector<std::unique_ptr<Bindable>> DrawableBase<T>::staticBinds;

然后在 Box 的构造函数中,我们通过检测if (!IsStaticInitialized())来判断要不要AddStaticBind那一堆东西。这个检测我们直接判断staticBinds数组是不是空来做到的:

static bool IsStaticInitialized() noexcept
{
	return !staticBinds.empty();
}

然后每个Box的变换方式是不一样的,否则就堆积起来了;所以我们会用AddBind(std::make_unique<TransformCbuf>(gfx, *this));

于是子类Box的构造函数全部代码如下:

Box::Box(Graphics& gfx,
	std::mt19937& rng,
	std::uniform_real_distribution<float>& adist,
	std::uniform_real_distribution<float>& ddist,
	std::uniform_real_distribution<float>& odist,
	std::uniform_real_distribution<float>& rdist)
	:
	r(rdist(rng)),
	droll(ddist(rng)),
	dpitch(ddist(rng)),
	dyaw(ddist(rng)),
	dphi(odist(rng)),
	dtheta(odist(rng)),
	dchi(odist(rng)),
	chi(adist(rng)),
	theta(adist(rng)),
	phi(adist(rng))
{
	if (!IsStaticInitialized())
	{
		struct Vertex
		{
			struct
			{
				float x;
				float y;
				float z;
			} pos;
		};
		const std::vector<Vertex> vertices =
		{
			{ -1.0f,-1.0f,-1.0f },
			{ 1.0f,-1.0f,-1.0f },
			{ -1.0f,1.0f,-1.0f },
			{ 1.0f,1.0f,-1.0f },
			{ -1.0f,-1.0f,1.0f },
			{ 1.0f,-1.0f,1.0f },
			{ -1.0f,1.0f,1.0f },
			{ 1.0f,1.0f,1.0f },
		};
		AddStaticBind(std::make_unique<VertexBuffer>(gfx, vertices));

		auto pvs = std::make_unique<VertexShader>(gfx, L"VertexShader.cso");
		auto pvsbc = pvs->GetBytecode();
		AddStaticBind(std::move(pvs));

		AddStaticBind(std::make_unique<PixelShader>(gfx, L"PixelShader.cso"));

		const std::vector<unsigned short> indices =
		{
			0,2,1, 2,3,1,
			1,3,5, 3,7,5,
			2,6,3, 3,6,7,
			4,5,7, 4,7,6,
			0,4,2, 2,4,6,
			0,1,4, 1,5,4
		};
		AddStaticIndexBuffer(std::make_unique<IndexBuffer>(gfx, indices));

		struct ConstantBuffer2
		{
			struct
			{
				float r;
				float g;
				float b;
				float a;
			} face_colors[6];
		};
		const ConstantBuffer2 cb2 =
		{
			{
				{ 1.0f,0.0f,1.0f },
				{ 1.0f,0.0f,0.0f },
				{ 0.0f,1.0f,0.0f },
				{ 0.0f,0.0f,1.0f },
				{ 1.0f,1.0f,0.0f },
				{ 0.0f,1.0f,1.0f },
			}
		};
		AddStaticBind(std::make_unique<PixelConstantBuffer<ConstantBuffer2>>(gfx, cb2));

		const std::vector<D3D11_INPUT_ELEMENT_DESC> ied =
		{
			{ "Position",0,DXGI_FORMAT_R32G32B32_FLOAT,0,0,D3D11_INPUT_PER_VERTEX_DATA,0 },
		};
		AddStaticBind(std::make_unique<InputLayout>(gfx, ied, pvsbc));

		AddStaticBind(std::make_unique<Topology>(gfx, D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST));
	}
	else
	{
		SetIndexFromStatic();
	}

	AddBind(std::make_unique<TransformCbuf>(gfx, *this));
}

这里的else中我们调用了SetIndexFromStatic函数,这个函数是这样写的:

void SetIndexFromStatic() noexcept(!IS_DEBUG)
{
	assert("Attempting to add index buffer a second time" && pIndexBuffer == nullptr);
	for (const auto& b : staticBinds)
	{
		if (const auto p = dynamic_cast<IndexBuffer*>(b.get()))
		{
			pIndexBuffer = p;
			return;
		}
	}
	assert("Failed to find index buffer in static binds" && pIndexBuffer != nullptr);
}

原因是因为,上一节我们就说过index buffer的特殊性,因为除了绑定,我们还需要在DrawIndexed函数中使用到(获取其中的数量,Draw函数中我们这样写的:gfx.DrawIndexed(pIndexBuffer->GetCount());

因此如果只是第一次AddStaticIndexBuffer了,那么之后的实例中pIndexBuffer将会没有被设置,导致出错。所以我们在这里相当于遍历了一遍staticBinds,通过dynamic_cast找出哪个对象是IndexBuffer,然后绑定给pIndexBuffer 。(这里 pIndexBuffer 是一个指向我们自己定义的 IndexBuffer 类的指针,这个类当中就保存着 IndexBuffer 实际包含的顶点个数,这个是必须在每次draw call都要记录下来的,所以要保存一个 pIndexBuffer )

这时再运行效果非常好:
在这里插入图片描述
此时这些Box都共享着VS和PS了(共享的原理我再理一遍,通过CRTP的手段,弄了个中间类 DrawableBase ,这个类里头有记录所有要共享的Bindable对象是vector容器(static std::vector<std::unique_ptr<Bindable>> staticBinds;),我们在创建第一个Box的时候就把这些要共享的信息都添加到这个容器中(通过我们写的AddStaticBind方法),然后每次调用Drawable类的Draw函数都会把staticBinds中的所有对象绑定好。唯一不共享的就是转换矩阵,每次Box构造函数中都一定会调用一次AddBind(std::make_unique<TransformCbuf>(gfx, *this));

这里还做了一点小改进,就是不同实例也有可能共享一个TransformCbuf,于是我们自然希望它也能注册进来(static void AddStaticBind(std::unique_ptr<Bindable> bind) noexcept(!IS_DEBUG)),很简单,改成static且为unique_ptr即可:

class TransformCbuf : public Bindable
{
public:
	TransformCbuf(Graphics& gfx, const Drawable& parent);
	void Bind(Graphics& gfx) noexcept override;
private:
	static std::unique_ptr<VertexConstantBuffer<DirectX::XMMATRIX>> pVcbuf;
	const Drawable& parent;
};

最后 Chili 补充了一下,设计这个系统的目的有两个:首先是解决每帧重复加载资源的问题,第二个目的是展示你们可能想不到的东西。当然这不是说框架一定要这样设计。

22:可绑定 / 可绘制系统p3

之前我们只测试了一种 Drawable 子类即 Box ,接下来将测试多种 Drawable 子类。与其将构建几何体的程序放在构造体里,Chili 更喜欢用一些不同的库函数来创建几何体:
在这里插入图片描述

为了方便,我们先搞了一个类模板IndexedTriangleList,其中有成员vertices和indices:

template<class T>
class IndexedTriangleList
{
public:
	IndexedTriangleList() = default;
	IndexedTriangleList( std::vector<T> verts_in,std::vector<unsigned short> indices_in )
		:
		vertices( std::move( verts_in ) ),
		indices( std::move( indices_in ) )
	{
		assert( vertices.size() > 2 );
		assert( indices.size() % 3 == 0 );
	}
	void Transform( DirectX::FXMMATRIX matrix )
	{
		for( auto& v : vertices )
		{
			const DirectX::XMVECTOR pos = DirectX::XMLoadFloat3( &v.pos );
			DirectX::XMStoreFloat3(
				&v.pos,
				DirectX::XMVector3Transform( pos,matrix )
			);
		}
	}

public:
	std::vector<T> vertices;
	std::vector<unsigned short> indices;
};

之后的几何体比如 Cube,就会有一个 Make 函数,返回一个 IndexedTriangleList :

class Cube
{
public:
	template<class V>
	static IndexedTriangleList<V> Make()
	{
		namespace dx = DirectX;

		constexpr float side = 1.0f / 2.0f;

		std::vector<dx::XMFLOAT3> vertices;
		vertices.emplace_back( -side,-side,-side ); // 0
		vertices.emplace_back( side,-side,-side ); // 1
		vertices.emplace_back( -side,side,-side ); // 2
		vertices.emplace_back( side,side,-side ); // 3
		vertices.emplace_back( -side,-side,side ); // 4
		vertices.emplace_back( side,-side,side ); // 5
		vertices.emplace_back( -side,side,side ); // 6
		vertices.emplace_back( side,side,side ); // 7

		std::vector<V> verts( vertices.size() );
		for( size_t i = 0; i < vertices.size(); i++ )
		{
			verts[i].pos = vertices[i];
		}
		return{
			std::move( verts ),{
				0,2,1, 2,3,1,
				1,3,5, 3,7,5,
				2,6,3, 3,6,7,
				4,5,7, 4,7,6,
				0,4,2, 2,4,6,
				0,1,4, 1,5,4
			}
		};
	}
};

IndexedTriangleList 的成员都定义成 public 的,是为了后面方便修改,比如有不同的顶点(包含颜色或者别的信息)就可以修改。

现在,我们就不需要那样定义顶点了,比如 Box 中我们直接这样:

struct Vertex
{
	dx::XMFLOAT3 pos;
};
const auto model = Cube::Make<Vertex>();

AddStaticBind(std::make_unique<VertexBuffer>(gfx, model.vertices));

...

AddStaticIndexBuffer(std::make_unique<IndexBuffer>(gfx, model.indices));

并且想要重新更改几何体形状,我们也可以方便地用 IndexedTriangleList 中的Transform方法:

void Transform( DirectX::FXMMATRIX matrix )
{
	for( auto& v : vertices )
	{
		const DirectX::XMVECTOR pos = DirectX::XMLoadFloat3( &v.pos );
		DirectX::XMStoreFloat3(
			&v.pos,
			DirectX::XMVector3Transform( pos,matrix )
		);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值