一个恒定缓冲区可以用于许多对象吗?

时间:2014-03-04 22:29:30

标签: c++ directx directx-11

我是Direct3D 11的新手,我在理解如何基于每个对象更新常量(和其他缓冲区)方面遇到了一些麻烦。我在一些简单的代码,我试图让两个四边形绘制到屏幕,但在不同的位置。这是我用来绘制它们的代码。

// ------------------------------------------------------------------------------------------------------------------------------
void QuadShape::UpdateBuffers(ID3D11DeviceContext* pContext)
{
  // We need to know about our verts + set the constant buffers...
  // NOTE: We only really need to do this when the buffer data actually changes...
  XMMATRIX translate = XMMatrixTranspose(XMMatrixTranslation(X, Y, 0));
  XMStoreFloat4x4(&ConstData.World, translate);

  D3D11_MAPPED_SUBRESOURCE mappedResource;
    ZeroMemory(&mappedResource, sizeof(D3D11_MAPPED_SUBRESOURCE));

  pContext->Map(ConstBuf, 0, D3D11_MAP_WRITE_DISCARD, 0, &mappedResource);
  memcpy(mappedResource.pData, &ConstData, sizeof(ObjectConstBuffer));
  pContext->Unmap(ConstBuf, 0);

}

// ------------------------------------------------------------------------------------------------------------------------------
void QuadShape::Draw(ID3D11DeviceContext* pContext)
{
  UpdateBuffers(pContext);
  pContext->DrawIndexed(_VertCount, _StartVert, 0);
}

您可以看到我正在根据对象的当前X / Y位置计算转换矩阵,并将其映射到对象的常量缓冲区,表示为“ConstBuf”。我遇到的问题来自于这样一个事实,即尽管我已经验证了为每个四边形计算的矩阵确实不同,并且缓冲区确实是动态的,但所有四边形都最终被绘制在同一位置。

我猜测正在发生的事情是,映射的资源只是被最后一个矩阵所覆盖,但我认为MAP_WRITE_DISCARD应该避免这种情况。我很困惑,我怎样才能为每个对象使用不同的常量缓冲区,让它们显示在不同的位置?

3 个答案:

答案 0 :(得分:9)

您应该按更新频率对常量缓冲区进行分组,因此如果您有一些每个对象更改的数据,请将其放在一个常量缓冲区中。如果你有其他数据 - 比如一个投影矩阵 - 只有在调整窗口大小时才会改变,把它放在另一个常量缓冲区中。

因此,例如,定义两个这样的缓冲区,如:

struct CBPerObject
{
    XMMATRIX mWorld;    // world matrix
};

struct CBChangeOnResize
{
    XMMATRIX mProjection;     // projection matrix
};

然后创建常量缓冲区并在成员变量中保留对它们的引用:

CComPtr<ID3D11Buffer> m_pCBPerObject;        // dx11 constant buffer (per object)
CComPtr<ID3D11Buffer> m_pCBChangeOnResize;   // dx11 constant buffer (change on resize)

创建代码(为清晰起见,省略了错误处理):

// create the constant buffers
D3D11_BUFFER_DESC pBuffDesc;
ZeroMemory(&pBuffDesc, sizeof(pBuffDesc));
pBuffDesc.Usage = D3D11_USAGE_DEFAULT;
pBuffDesc.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
pBuffDesc.CPUAccessFlags = 0;

// per object changes
pBuffDesc.ByteWidth = sizeof(CBPerObject);
m_pDevice->CreateBuffer(&pBuffDesc, nullptr, &m_pCBPerObject);

// on resize changes
pBuffDesc.ByteWidth = sizeof(CBChangeOnResize);
m_pDevice->CreateBuffer(&pBuffDesc, nullptr, &m_pCBChangeOnResize);

现在你可以在初始化期间将它们绑定一次(假设布局不会改变):

// constant buffers never change in shaders
pContext->VSSetConstantBuffers(0, 1, &m_pCBPerObject.p);
pContext->VSSetConstantBuffers(1, 1, &m_pCBChangeOnResize.p);

您可能还需要绑定到像素着色器,而是使用PSSetConstantBuffers

现在,您只需要在需要时更新常量缓冲区。例如,调整窗口大小时:

void CMyClass::OnSize()
{
    // update projection matrix
    CBChangeOnResize cbBuffer;

    // calculate the projection matrix - for example:
    XMMATRIX mProjection = XMMatrixOrthographicOffCenterLH(fLeft, fRight, fBottom, 
                                                           fTop, 0.1f, 1000.0f);
    cbBuffer.mProjection = XMMatrixTranspose(mProjection);

    // update the constant buffer
    pContext->UpdateSubresource(m_pCBChangeOnResize, 0, nullptr, &cbBuffer, 0, 0);
}

同样,在绘图时,更新每个对象的世界矩阵:

void CMyClass::DrawScene()
{
    // draw the complete scene
    CBPerObject cbBuffer;

    // ... clear render target etc. 

    // for each object ... 
    {
        cbBuffer.mWorld = XMLoadFloat4x4(pObject->GetWorld());

        // update cb and draw object
        pContext->UpdateSubresource(m_pCBPerObject, 0, nullptr, &cbBuffer, 0, 0);
        pContext->DrawIndexed(6, 0, 0);
    }

    // ... etc.
}

因此除非布局发生变化,否则无需重新绑定常量缓冲区,并且您可以使用UpdateSubresource代替Map / Unmap,如上所示

在顶点(像素)着色器中,根据它们绑定的槽来定义常量缓冲区:

cbuffer cbPerObject : register (b0)
{
    matrix mWorld;
};

cbuffer cbChangeOnResize : register (b1)
{
    matrix mProjection;
};

在语法register (b0)register (b1)中,b0b1引用提供给VSSetConstantBuffers的第一个参数。

答案 1 :(得分:0)

您需要更新着色器中的常量缓冲区。在UpdateBuffers方法结束时添加:

pContext->VSSetConstantBuffers(0, 1, ConstBuf);

还要确保缓冲区是16字节对齐的: http://msdn.microsoft.com/en-us/library/bb509632%28v=vs.85%29.aspx

性能损失可能不像你想象的那么大,特别是与你每帧进行的其他昂贵的操作相比(我实际上是出于好奇而测试它并让你知道结果),这个这是非常标准的做法。如果你不想接受我的话,请看看Frank Luna的这篇文章,其中包括很好地解释常量缓冲区,并提供处理它们的常用策略:

http://d3dcoder.net/Data/Resources/d3d11Metro.pdf

如果您真的担心基于每个对象调用*SetConstantBuffers(例如,您有很多独立移动的对象),那么您可以每帧绑定一次,然后使用指向每个对象的绑定缓冲区的指针

一种方法是使用Shader类来封装所有与着色器相关的资源以及绘制对象,因此在我的主要更新方法中,我会调用Shader::bindBuffers()一次然后调用Shader::Render(Object*)为每个对象及其参数(包括矩阵)。在Shader::Render()内部,我使用之前绑定的缓冲区并使用map / unmap等更新它们。

另一种方法是让对象自己绘制,但是将指针作为调用类的成员存储的常量缓冲区传递给render方法(在你的情况下为UpdateBuffers)。您可以在Microsoft的此示例中看到类似的方法:

http://code.msdn.microsoft.com/windowsapps/Metro-style-DirectX-7c64aa8d/sourcecode?fileId=50992&pathId=642179348

然而,这些都是优化,你不应该过早担心它们。

答案 2 :(得分:0)

在这种情况下,您必须为每个对象“update-bind-draw”。伪代码:

foreach(object in objects)
{
   constantBuffer.update(object.position);
   device.bind(constantBuffer);
   device.draw();
}

您的代码缺少绑定调用。

这样,对于N个对象,你有N个缓冲区更新,N个绑定和N个绘制调用,如果你有很多对象(你在驱动程序上花了很多时间,有CPU-GPU同步点,占用内存带宽,这是非常低效的)通过冗余数据传输)。但是,通过合适的驱动程序实现可能会消除不必要的绑定调用。

更新
存在几种不同的方法,它们将内存和代码复杂性交换为运行时性能。值得注意的是,有“批处理”和一系列技术,称为“Geometry Instancing”

1。 批处理可以描述为合并顶点缓冲区中所有对象的预转换顶点,而不是在着色器中转换它们。您可以大大节省顶点着色器时间,但是如果对象的任何属性(例如位置)发生更改,则需要重新计算CPU (!)上对象的所有顶点并部分更新顶点缓冲区。因此,它最适用于静态对象(例如,不经常移动/根本不移动)和小对象(没有多少顶点要更新)。通常用于绘制2D内容:精灵,GUI和文本。

2。 硬件几何实例化。您需要额外的顶点缓冲区,它将存储实例属性而不是顶点属性(我们将其称为实例缓冲区)。您必须更改输入布局,添加每个实例属性。您将实例缓冲区更新为更改其属性的对象,就像使用任何顶点缓冲区一样。您使用DrawInstanced*()来电进行绘制。这是一种流行的技术,可以优化许多动态物体的绘制,这些物体非常相似(草地,树叶,陨石,道路交通,足球场上的球迷等)。几个高级技巧允许应用它甚至绘制非常不同的对象。

3。 软件几何实例化。手动实现硬件几何实例化。如果您的目标硬件不支持硬件实例化(非常旧的桌面和某些移动设备),如果您想要一些高级控制,则可以使用。 您可以只为每个顶点添加一个索引属性,并将大型常量缓冲区(对于旧设备)或纹理或结构缓冲区(在较新的设备上)等用作实例信息源。

参考文献:
Geometry instancing wiki
Chapter 3. Inside Geometry Instancing(警告:包含一些旧东西)
rastertek.com - Tutorial 37: Instancing
braynzarsoft.net - Lesson 32: Direct3D 11 Instancing
NVIDIA Direct3D SDK 10 Code Samples:实例测试,皮肤实例化(下载SDKs
DirectX SDK sample“Instancing10”

更新
注意,虽然实例化和批处理很棒,但这并不意味着你必须删除常量缓冲区。正如 Roger Rowland 的回答中所解释的那样,常量的智能分组在几种情况下会给你很好的结果,比如不可变常量,许多对象共享的常量,比如摄像机的常量等。参见“{{3} “NVIDIA关于我们究竟如何管理缓冲区的介绍