Stockage de voxels pour un moteur de voxel en C ++

9

J'essaie d'écrire un petit moteur de voxels parce que c'est amusant, mais j'ai du mal à trouver la meilleure façon de stocker les voxels réels. Je suis conscient que j'aurai besoin de morceaux en quelque sorte, donc je n'ai pas besoin d'avoir le monde entier en mémoire, et je suis conscient que j'ai besoin de les rendre avec des performances raisonnables.

J'ai lu des octrees et d'après ce que je comprends, cela commence par 1 cube, et dans ce cube peut être 8 cubes de plus, et dans tous ces 8 cubes peut être encore 8 cubes etc. Mais je ne pense pas que cela correspond à mon moteur de voxel parce que mes cubes / articles de voxel auront tous exactement la même taille.

Donc, une autre option consiste à créer simplement un tableau de taille 16 * 16 * 16 et à avoir un seul bloc, et vous le remplissez d'éléments. Et les pièces où il n'y a pas d'articles auront 0 comme valeur (0 = air). Mais je crains que cela ne gaspille beaucoup de mémoire et ne soit pas très rapide.

Ensuite, une autre option est un vecteur pour chaque morceau et remplissez-le de cubes. Et le cube tient sa position dans le morceau. Cela économise de la mémoire (pas de blocs d'air), mais rend la recherche d'un cube à un emplacement spécifique beaucoup plus lente.

Je ne peux donc pas vraiment trouver de bonne solution et j'espère que quelqu'un pourra m'aider. Alors, que voudriez-vous utiliser et pourquoi?

Mais un autre problème est le rendu. Il suffit de lire chaque morceau et de l'envoyer au GPU à l'aide d'OpenGL, mais c'est très lent. Générer un maillage par morceau serait mieux, mais cela signifie que chaque fois que je casse un bloc, je dois reconstruire le morceau entier, ce qui pourrait prendre un peu de temps, provoquant un hoquet mineur mais perceptible, ce que je ne veux évidemment pas non plus. Ce serait donc plus difficile. Alors, comment pourrais-je rendre les cubes? Il suffit de créer tous les cubes dans un tampon de vertex par bloc et de le rendre et peut-être d'essayer de le mettre dans un autre thread, ou y a-t-il une autre façon?

Merci!

Clonkex
la source
1
Vous devez utiliser l'instanciation pour rendre vos cubes. Vous pouvez trouver un tutoriel ici learnopengl.com/Advanced-OpenGL/Instancing . Pour stocker les cubes: avez-vous de fortes contraintes de mémoire sur le matériel? 16 ^ 3 cubes ne semblent pas trop de mémoire.
Turms
@Turms Merci pour votre commentaire! Je n'ai pas de fortes contraintes de mémoire, c'est juste un PC ordinaire. Mais je pensais que si chaque partie la plus importante est composée à 50% d'air et que le monde est très grand, il doit y avoir pas mal de mémoire perdue. Mais ce n'est probablement pas beaucoup comme vous le dites. Alors, devrais-je opter pour des blocs 16 * 16 * 16 avec une quantité statique de blocs? Et aussi, vous dites que je devrais utiliser l'instanciation, est-ce vraiment nécessaire? Mon idée était de générer un maillage pour chaque morceau, car de cette façon, je peux laisser de côté tous les triangles invisibles.
6
Je ne recommande pas d'utiliser l'instanciation pour les cubes comme Turms le décrit.Cela ne fera que réduire vos appels de tirage, mais ne fera rien pour le sur-tirage et les faces cachées - en fait, cela vous empêche de résoudre ce problème, car pour l'instanciation de travailler tous les cubes doivent être les mêmes - vous ne pouvez pas supprimer les faces cachées de certains cubes ou fusionner des faces coplanaires en polygones simples plus grands.
DMGregory
Choisir le meilleur moteur voxel peut être un défi. La grande question à vous poser est "quelles opérations dois-je faire sur mes voxels?" Cela guide les opérations. Par exemple, vous vous demandez combien il est difficile de déterminer quel voxel se trouve dans un oct-tree. Les algorithmes oct-tree sont parfaits pour les problèmes qui peuvent générer ces informations au besoin en parcourant l'arbre (souvent de manière récursive). Si vous rencontrez des problèmes spécifiques où cela coûte trop cher, vous pouvez envisager d'autres options.
Cort Ammon
Une autre grande question est de savoir à quelle fréquence les voxels sont mis à jour. Certains algorithmes sont excellents s'ils peuvent prétraiter des données pour les stocker efficacement, mais moins efficaces si les données sont constamment mises à jour (comme les données pourraient le faire dans une simulation fluide basée sur des particules)
Cort Ammon

Réponses:

23

Le stockage des blocs en tant que positions et valeurs est en fait très inefficace. Même sans surcharge due à la structure ou à l'objet que vous utilisez, vous devez stocker 4 valeurs distinctes par bloc. Cela n'aurait de sens que de l'utiliser sur la méthode de "stockage de blocs dans des tableaux fixes" (celle que vous avez décrite plus tôt) lorsque seulement un quart des blocs sont solides, et de cette façon vous ne prenez même aucune autre méthode d'optimisation en Compte.

Les octrees sont en fait parfaits pour les jeux basés sur les voxels, car ils sont spécialisés dans le stockage de données avec des fonctionnalités plus grandes (par exemple, les correctifs du même bloc). Pour illustrer cela, j'ai utilisé un quadtree (essentiellement des octrees en 2d):

Ceci est mon jeu de départ contenant des tuiles 32x32, ce qui équivaudrait à 1024 valeurs: entrez la description de l'image ici

Le stockage de 1024 valeurs distinctes ne semble pas inefficace, mais une fois que vous atteignez des tailles de carte similaires à des jeux, tels que Terraria , le chargement des écrans prend plusieurs secondes. Et si vous l'augmentez jusqu'à la troisième dimension, il commence à utiliser tout l'espace du système.

Les quadtre (ou octrees en 3D) peuvent aider la situation. Pour en créer un, vous pouvez soit partir des tuiles et les regrouper, soit partir d'une énorme cellule et la diviser jusqu'à atteindre les tuiles. J'utiliserai la première approche, car elle est plus facile à visualiser.

Donc, dans la première itération, vous regroupez tout en cellules 2x2, et si une cellule ne contient que des tuiles du même type, vous déposez les tuiles et stockez simplement le type. Après une itération, notre carte ressemblera à ceci:

entrez la description de l'image ici

Les lignes rouges marquent ce que nous stockons. Chaque carré n'a qu'une valeur. Cela a ramené la taille de 1024 à 439, soit une diminution de 57%.

Mais vous connaissez le mantra . Allons plus loin et regroupons-les en cellules:

entrez la description de l'image ici

Cela a réduit le nombre de valeurs stockées à 367. Cela ne représente que 36% de la taille d'origine.

Vous devez évidemment faire cette division jusqu'à ce que toutes les 4 cellules adjacentes (8 blocs adjacents en 3d) à l'intérieur d'un bloc soient stockées dans une cellule, convertissant essentiellement un bloc en une grande cellule.

Cela a également d'autres avantages, principalement lors d'une collision, mais vous pouvez créer un octree séparé pour cela, qui ne se soucie que de savoir si un seul bloc est solide ou non. De cette façon, au lieu de vérifier la collision pour chaque bloc à l'intérieur d'un bloc, vous pouvez simplement le faire contre les cellules.

Bálint
la source
Merci pour votre réponse! Il semble que l'octree soit la voie à suivre. (Puisque mon moteur de voxel sera en 3D) J'ai une question libre que je voudrais poser: votre dernière photo montre les parties noires pour avoir des carrés plus grands, car j'ai l'intention d'avoir un minecraft comme moteur où vous pouvez modifier le terrain du voxel, je préférerais garder tout ce qui a un bloc de la même taille, car sinon cela rendrait les choses très compliquées, c'est possible non? (Je simplifierais toujours les fentes vides / aériennes bien sûr.) Deuxième , existe-t-il une sorte de tutoriel sur la façon de programmer un octree? Merci!
7
@ appmaker1358 ce n'est pas un problème du tout. Si le joueur essaie de modifier un grand bloc, vous le divisez en blocs plus petits à ce moment . Il n'est pas nécessaire de stocker des valeurs 16x16x16 de "rock" quand vous pourriez dire à la place "tout ce morceau est du rock solide" jusqu'à ce que ce ne soit plus vrai.
DMGregory
1
@ appmaker1358 Comme l'a dit DMGregory, la mise à jour des données stockées dans un octree est relativement facile. Tout ce que vous avez à faire est de diviser la cellule dans laquelle le changement s'est produit jusqu'à ce que chaque sous-cellule ne contienne qu'un seul type de bloc. Voici un exemple interactif avec un quadtree . En générer un est également simple. Vous créez une grande cellule, qui contient complètement le morceau, puis vous parcourez récursivement chaque cellule feuille (cellules qui n'ont pas d'enfants), vérifiez si la partie du terrain qu'elle représente contient plusieurs types de blocs, si oui, subdivisez la cellulaire
Bálint
@ appmaker1358 Le plus gros problème est en fait l'inverse - s'assurer que l'octree ne se remplit pas de feuilles avec un seul bloc, ce qui peut facilement se produire dans un jeu de style Minecraft. Cependant, il existe de nombreuses solutions au problème - il s'agit simplement de choisir ce que vous trouvez approprié. Et cela ne devient un vrai problème que lorsqu'il y a beaucoup de construction en cours.
Luaan
Les octrees ne sont pas nécessairement le meilleur choix. voici une lecture intéressante: 0fps.net/2012/01/14/an-analysis-of-minecraft-like-engines
Polygnome
7

Des octrois existent pour résoudre exactement le problème que vous décrivez, permettant un stockage dense de données éparses sans temps de recherche importants.

Le fait que vos voxels soient de la même taille signifie simplement que votre octree a une profondeur fixe. par exemple. pour un bloc 16x16x16, vous avez besoin d'au plus 5 niveaux d'arbre:

  • racine de morceau (16x16x16)
    • octant de premier niveau (8x8x8)
      • octant de deuxième niveau (4x4x4)
        • octant de troisième niveau (2x2x2)
          • voxel unique (1x1x1)

Cela signifie que vous avez au plus 5 étapes pour savoir s'il y a un voxel à une position particulière dans le morceau:

  • racine de bloc: le bloc entier a-t-il la même valeur (par exemple, tout l'air)? Si oui, nous avons terminé. Si non...
    • premier niveau: l'octant qui contient cette position a-t-il la même valeur? Si non...
      • deuxième niveau...
        • troisième niveau ...
          • maintenant nous nous adressons à un seul voxel et pouvons retourner sa valeur.

Beaucoup plus court que la numérisation, même 1% du chemin à travers un tableau de jusqu'à 4096 voxels!

Notez que cela nous permet de compresser les données partout où il y a un octant complet de la même valeur - que cette valeur soit entièrement aérienne ou entièrement rocheuse ou autre. C'est seulement là où les octants contiennent des valeurs mixtes que nous devons subdiviser davantage, jusqu'à la limite des nœuds foliaires à voxel unique.


Pour s'adresser aux enfants d'un morceau, nous procédons généralement dans l' ordre de Morton , quelque chose comme ceci:

  1. X- Y- Z-
  2. X- Y- Z +
  3. X- Y + Z-
  4. X- Y + Z +
  5. X + Y- Z-
  6. X + Y- Z +
  7. X + Y + Z-
  8. X + Y + Z +

Ainsi, notre navigation dans les nœuds Octree pourrait ressembler à ceci:

GetOctreeValue(OctreeNode node, int depth, int3 nodeOrigin, int3 queryPoint) {
    if(node.IsAllOneValue)
        return node.Value;

    int childIndex =  0;
    childIndex += (queryPoint.x > nodeOrigin.x) ? 4 : 0;
    childIndex += (queryPoint.y > nodeOrigin.y) ? 2 : 0;
    childIndex += (queryPoint.z > nodeOrigin.z) ? 1 : 0;

    OctreeNode child = node.GetChild(childIndex);

    return GetOctreeValue(
                child, 
                depth + 1,
                nodeOrigin + childOffset[depth, childIndex],
                queryPoint
    );
}
DMGregory
la source
Merci pour votre réponse! Il semble que les octree soient la voie à suivre. Mais j'ai 2 questions cependant, vous dites que les octree sont plus rapides que de parcourir un tableau, ce qui est correct. Mais je n'aurais pas besoin de le faire car le tableau pourrait être statique, ce qui signifie que je peux calculer où se trouve le cube dont j'ai besoin. Alors pourquoi aurais-je besoin de scanner? Deuxième question, dans le dernier niveau de l'octree (le 1x1x1), comment puis-je savoir quel cube est où, puisque si je le comprends bien, et que le nœud d'octree a 8 autres nœuds, comment savez-vous quel nœud appartient à quelle position 3d ? (Ou dois-je m'en souvenir moi-même?)
Oui, vous avez déjà couvert le cas d'un tableau exhaustif de voxels 16x16x16 dans votre question, et avez semblé rejeter l'encombrement de la mémoire 4K par bloc (en supposant que chaque ID de voxel est un octet) comme excessif. La recherche que vous avez mentionnée survient lors du stockage d'une liste de voxels avec une position, vous obligeant à parcourir la liste pour trouver le voxel à votre position cible. 4096 est ici la limite supérieure de cette longueur de liste - généralement elle sera plus petite que cela, mais généralement toujours plus profonde qu'une recherche d'octree correspondante.
DMGregory