Comment puis-je implémenter un moteur de rendu capable de dessiner de nombreux types de primitives?

8

Ceci est quelque peu lié à une question que j'ai posée précédemment concernant le dessin des primitives indexées .

Mon problème était que je ne dessinais qu'un seul cube quand j'en voulais en dessiner plusieurs. On m'a dit que le problème était que j'écrasais les tampons de sommet et d'index à chaque nouvelle instanciation de Cubeet qu'au lieu de cela, je devrais en créer un à l'origine puis en dessiner plusieurs, en passant par une matrice de transformation vers le shader qui le fait apparaître dans différents des endroits. Cela a fonctionné à merveille.

J'ai maintenant un nouveau problème: comment dessiner différents types de primitives?

Voici mon code de la question précédente:

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

De toute évidence, si je dupliquais et modifiais le code pour en faire un objet ou une forme différent, la dernière forme à initialiser écraserait le tampon de vertex, n'est-ce pas?

Dois-je utiliser plusieurs tampons de vertex? Dois-je ajouter le nouveau tampon de vertex à l'ancien et utiliser les indices appropriés pour les dessiner? Puis-je faire non plus? Tous les deux?

SirYakalot
la source

Réponses:

12

C'est probablement une mauvaise idée de créer de nouvelles classes pour chaque type de géométrie que vous allez prendre en charge. Ce n'est pas très évolutif ou maintenable. De plus, la conception de classe que vous semblez vouloir maintenant semble confondre les tâches de gestion de la géométrie elle-même et les données d'instance pour cette géométrie.

Voici une approche que vous pouvez adopter:

Créez deux classes Meshet MeshInstance. A Meshcontient toutes les propriétés de la géométrie partagée - essentiellement un tampon de sommet et un tampon d'index. Si vous le souhaitez, vous pouvez créer des fonctions d'assistance qui créent des maillages contenant des données de sommet de cube (ou des données de sommet de sphère, ou tout ce que vous voulez). Vous devez adapter l'interface publique de la Meshclasse pour permettre à ces fonctions d'assistance d'être implémentées en tant que fonctions non membres et non amis.

MeshInstance, d'autre part, doit être construit en référence à a Mesh. Le MeshInstancecontient les propriétés d'un objet individuel - c'est la transformation du monde, et les remplacements de shader utilisés pour le rendre, etc.

De cette façon, lorsque vous souhaitez créer un nouveau cube, vous obtenez d'abord l' Meshobjet représentant un cube à partir d'une bibliothèque partagée d'objets de maillage primitifs que vous avez créés au démarrage. Ensuite, vous créez un nouveau MeshInstance, en lui affectant ce cube Mesh.

Lorsque vous effectuez le rendu, vous créez une liste de tous les MeshInstances que vous souhaitez dessiner et vous les soumettez. Si vous les regroupez par Meshou texture, vous pouvez optimiser la surcharge de changement d'état (c'est-à-dire que vous dessinez toutes les instances de maillage correspondant au maillage du cube à la fois, puis toutes les instances de maillage correspondant au maillage de la sphère, de sorte que vous avez moins d' SetVertexBufferappels sur l'appareil D3D). Vous pouvez également regrouper par autre état, comme la texture et le shader.

De cette façon, vous évitez de gaspiller de la mémoire en dupliquant les données de vertex et vous vous assurez que votre système de rendu peut s'adapter à n'importe quel ensemble arbitraire de primitives simplement en implémentant de nouvelles fonctions (a) pour créer des maillages par programme ou (b) des fonctions pour charger des maillages à partir de fichiers de un format particulier.

Une fois que votre pipeline de rendu fonctionne en termes d' Meshobjets généralisés , il est beaucoup plus facile de l'adapter de manière globale à de nouvelles techniques ou optimisations.

Commentaires spécifiques:

De toute évidence, si je dupliquais et modifiais le code pour en faire un objet ou une forme différent, la dernière forme à initialiser écraserait le tampon de vertex. non?

Non. Dans le code que vous avez publié, le seul moyen pBufferet similaire serait écrasé était s'il s'agissait d'une variable membre statique. Si vous copiez la classe en gros pour créer (par exemple) une Sphereclasse, ce serait une nouvelle variable statique. C'est encore une mauvaise idée.

Dois-je utiliser plusieurs tampons de vertex? Dois-je ajouter le nouveau tampon de vertex à l'ancien et utiliser les indices appropriés pour les dessiner? Puis-je faire non plus? Tous les deux?

L'implémentation naïve de la technique que je décris ci-dessus implique plusieurs tampons (un pour chaque ensemble de géométrie partagée). Si cette géométrie est statique, il est possible de tout stocker dans un (ou plusieurs, car il existe une limite optimale pratique à la taille du tampon) pour minimiser encore plus les changements d'état du tampon. Cela devrait être considéré comme une optimisation et est laissé comme un exercice pour le lecteur; faites-le d'abord fonctionner, puis craignez de le rendre rapide.


la source
Je sais que nous ne sommes pas censés publier des commentaires de «merci», mais c'est tellement utile! Je vous remercie!
SirYakalot
pour le moment, pBuffer et iBuffer sont externes. Dois-je créer ces membres d'instance de chaque objet Mesh?
SirYakalot
1
Oui, c'est un bon point de départ.
Maintenant que je viens de l'implémenter, j'ai un peu de mal à réfléchir à la façon de le faire, juste la partie où vous dites "Si vous le souhaitez, vous pouvez créer des fonctions d'aide qui créent des maillages contenant des données de sommet de cube (ou des données de sommet de sphère, ". Vous devez personnaliser l'interface publique de la classe Mesh pour permettre à ces fonctions d'assistance d'être implémentées en tant que fonctions non membres et non amis." qu'entendez-vous exactement par fonctions non assistantes et non amies?
SirYakalot
Une fonction qui n'est pas membre de la classe (donc une fonction globale) qui n'est pas non plus déclarée comme amie de la classe. Une fonction "régulière", en d'autres termes - qui manipule l'objet maillé uniquement via son API publique.