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