Como posso implementar um renderizador que pode desenhar muitos tipos de primitivas?

8

Isso está um pouco ligado a uma pergunta que eu fiz anteriormente sobre o desenho de primitivas indexadas .

Meu problema era que eu estava desenhando apenas um cubo quando queria desenhar muitos. Foi-me dito que o problema era que eu estava sobrescrevendo os buffers de vértice e índice a cada nova instanciação Cubee que, em vez disso, eu deveria criar um na origem e depois desenhar muitos, passando por uma matriz de transformação para o shader que faz com que parecesse diferente. locais. Isso funcionou lindamente.

Agora, porém, tenho um novo problema: como eu desenharia muitos tipos diferentes de primitivos?

Aqui está o meu código da pergunta anterior:

Cube::Cube(D3DXCOLOR colour, D3DXVECTOR3 min, D3DXVECTOR3 max)
{
// create eight vertices to represent the corners of the cube
VERTEX OurVertices[] =
{
    {D3DXVECTOR3(min.x, max.y, max.z), colour},
    {D3DXVECTOR3(min.x, max.y, min.z), colour},
    {D3DXVECTOR3(min.x, min.y, max.z), colour},
    {min, colour},
    {max, colour},
    {D3DXVECTOR3(max.x, max.y, min.z), colour},
    {D3DXVECTOR3(max.x, min.y, max.z), colour},
    {D3DXVECTOR3(max.x, min.y, min.z), colour},
};

// create the vertex buffer
D3D10_BUFFER_DESC bd;
bd.Usage = D3D10_USAGE_DYNAMIC;
bd.ByteWidth = sizeof(VERTEX) * 8;
bd.BindFlags = D3D10_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE;
bd.MiscFlags = 0;

device->CreateBuffer(&bd, NULL, &pBuffer);

void* pVoid;    // the void pointer

pBuffer->Map(D3D10_MAP_WRITE_DISCARD, 0, &pVoid);    // map the vertex buffer
memcpy(pVoid, OurVertices, sizeof(OurVertices));    // copy the vertices to the buffer
pBuffer->Unmap();

// create the index buffer out of DWORDs
DWORD OurIndices[] = 
{
    0, 1, 2,    // side 1
    2, 1, 3,
    4, 0, 6,    // side 2
    6, 0, 2,
    7, 5, 6,    // side 3
    6, 5, 4,
    3, 1, 7,    // side 4
    7, 1, 5,
    4, 5, 0,    // side 5
    0, 5, 1,
    3, 7, 2,    // side 6
    2, 7, 6,
};

// create the index buffer
// D3D10_BUFFER_DESC bd;    // redefinition
bd.Usage = D3D10_USAGE_DYNAMIC;
bd.ByteWidth = sizeof(DWORD) * 36;
bd.BindFlags = D3D10_BIND_INDEX_BUFFER;
bd.CPUAccessFlags = D3D10_CPU_ACCESS_WRITE;
bd.MiscFlags = 0;

device->CreateBuffer(&bd, NULL, &iBuffer);

iBuffer->Map(D3D10_MAP_WRITE_DISCARD, 0, &pVoid);    // map the index buffer
memcpy(pVoid, OurIndices, sizeof(OurIndices));    // copy the indices to the buffer
iBuffer->Unmap();

//this is simply a single call to the update method that sets up the scale, rotation
//and translation matrices, in case the cubes are static and you don't want to have to 
//call update every frame
Update(D3DXVECTOR3(1, 1, 1), D3DXVECTOR3(0, 0, 0), D3DXVECTOR3(0, 0, 0));
}

Claramente, se eu duplicasse e modificasse o código para ser um objeto ou forma diferente, a última forma a ser inicializada substituiria o buffer de vértice, não?

Eu uso vários buffers de vértice? Anexo o novo buffer de vértice ao antigo e uso os índices apropriados para desenhá-los? Posso fazer também? Ambos?

SirYakalot
fonte

Respostas:

12

Provavelmente, é uma má idéia criar novas classes para cada tipo de geometria que você oferecerá suporte. Não é muito escalável ou sustentável. Além disso, o design da classe que você deseja agora parece conflitar as tarefas de gerenciar a própria geometria e os dados da instância para essa geometria.

Aqui está uma abordagem que você pode adotar:

Crie duas classes Meshe MeshInstance. A Meshcontém todas as propriedades da geometria compartilhada - basicamente um buffer de vértice e um buffer de índice. Se desejar, você pode criar funções auxiliares que criam malhas que contêm dados de vértice de cubo (ou dados de vértice de esfera ou o que mais você quiser). Você deve adaptar a interface pública da Meshclasse para permitir que essas funções auxiliares sejam implementadas como funções não membros e não amigas.

MeshInstance, por outro lado, deve ser construído com uma referência a a Mesh. O MeshInstancecontém as propriedades de um objeto individual - sua transformação mundial e substituições de shader usadas para renderizá-lo, e assim por diante.

Dessa forma, quando você deseja criar um novo cubo, primeiro obtém o Meshobjeto que representa um cubo de alguma biblioteca compartilhada de objetos de malha primitivos criados na inicialização. Então você cria um novo MeshInstance, atribuindo a ele esse cubo Mesh.

Ao renderizar, você cria uma lista de todos os MeshInstances que deseja desenhar e os envia. Se você agrupá-los por Meshou textura, é possível otimizar a sobrecarga de alteração de estado (ou seja, você desenha todas as instâncias de malha correspondentes à malha de cubo de uma só vez e, em seguida, todas as instâncias de malha correspondentes à malha de esfera, para ter menos SetVertexBufferchamadas o dispositivo D3D). Você também pode agrupar por outro estado, como textura e sombreador.

Dessa maneira, você evita desperdiçar memória duplicando dados de vértice e assegura que seu sistema de renderização possa ser dimensionado para qualquer conjunto arbitrário de primitivas simplesmente implementando novas (a) funções para criar malhas programaticamente ou (b) funções para carregar malhas de arquivos de um formato particular.

Uma vez que seu pipeline de renderização funcione em termos de Meshobjetos generalizados , é muito mais fácil adaptá-lo de maneira global a novas técnicas ou otimizações.

Comentários específicos:

Claramente, se eu duplicasse e modificasse o código para ser um objeto ou forma diferente, a última forma a ser inicializada substituiria o buffer de vértice. não seria?

Não. No código que você postou, a única maneira pBuffere algo semelhante seria substituído se fosse uma variável de membro estática. Se você copiasse a classe por atacado para criar (por exemplo) uma Sphereclasse, isso seria uma nova variável estática. Ainda é uma má ideia.

Eu uso vários buffers de vértice? Anexo o novo buffer de vértice ao antigo e uso os índices apropriados para desenhá-los? Posso fazer também? Ambos?

A implementação ingênua da técnica descrita acima envolve vários buffers (um para cada conjunto de geometria compartilhada). Se essa geometria for estática, é possível armazenar tudo em um buffer (ou múltiplo, pois existe um limite ideal prático para o tamanho do buffer) para minimizar ainda mais as alterações no estado do buffer. Isso deve ser considerado uma otimização e é deixado como um exercício para o leitor; primeiro faça funcionar, depois se preocupe em torná-lo rápido.


fonte
Eu sei que não devemos postar comentários de agradecimento, mas isso é muito útil! Obrigado!
SirYakalot
No momento, o pBuffer e o iBuffer são externos. Devo tornar esses membros da instância de cada objeto Mesh?
SirYakalot
11
Sim, é um bom lugar para começar.
Agora que eu implementei isso, estou com um pouco de dificuldade para pensar em como fazê-lo, apenas na parte em que você diz "Se quiser, você pode criar funções auxiliares que criam malhas contendo dados de vértice de cubo (ou dados de vértice de esfera, ou o que você quiser). Você deve adaptar a interface pública da classe Mesh para permitir que essas funções auxiliares sejam implementadas como funções de não membros e de amigos. " o que exatamente você quer dizer com funções não auxiliares e não amigas?
precisa saber é o seguinte
Uma função que não é um membro da classe (portanto, uma função global) que também não é declarada como amiga da classe. Uma função "regular", em outras palavras - que manipula o objeto de malha apenas por meio de sua API pública.