En ce qui concerne Java vs C ++, j'ai écrit un moteur voxel dans les deux versions (version C ++ ci-dessus). J'écris aussi des moteurs de voxels depuis 2004 (quand ils n'étaient pas à la mode). :) Je peux dire sans hésiter que les performances du C ++ sont de loin supérieures (mais il est également plus difficile de coder). Il s’agit moins de la vitesse de calcul que de la gestion de la mémoire. Sans aucun doute, lorsque vous allouez / libérez autant de données que dans un monde de voxels, C (++) est le langage à battre. pourtant, vous devriez penser à votre objectif. Si les performances sont votre priorité absolue, optez pour C ++. Si vous voulez juste écrire un jeu sans performances démesurées, Java est définitivement acceptable (comme en témoigne Minecraft). Il existe de nombreux cas triviaux / marginaux, mais en général, vous pouvez vous attendre à ce que Java s'exécute environ 1,75 à 2,0 fois plus lentement que le C ++ (bien écrit). Vous pouvez voir une ancienne version de mon moteur mal optimisée en action ici (EDIT: une version plus récente ici ). Bien que la génération de morceaux puisse sembler lente, gardez à l'esprit qu'elle génère des diagrammes 3D de voronoï de manière volumétrique, en calculant les normales à la surface, l'éclairage, les objets en sortie et les ombres sur le processeur à l'aide de méthodes à force brute. J'ai essayé diverses techniques et je peux obtenir une génération de blocs environ 100x plus rapide en utilisant diverses techniques de mise en cache et d'instanciation.
Pour répondre au reste de votre question, vous pouvez améliorer le rendement de nombreuses manières.
- Caching Chaque fois que vous le pouvez, vous devez calculer les données une fois. Par exemple, je fais cuire la lumière dans la scène. Il pourrait utiliser un éclairage dynamique (dans l’écran, en post-traitement), mais cuire au four dans cet éclairage signifie que je n’ai pas à passer aux normales pour les triangles, c’est-à-dire ....
Passez le moins de données possible sur la carte vidéo. Une chose que les gens ont tendance à oublier est que plus vous transmettez de données au GPU, plus cela prend du temps. Je passe dans une seule couleur et une position de sommet. Si je veux faire des cycles jour / nuit, je peux simplement faire un étalonnage des couleurs, ou je peux recalculer la scène à mesure que le soleil change.
La transmission de données au GPU étant très coûteuse, il est possible d'écrire un moteur dans un logiciel plus rapide à certains égards. L'avantage du logiciel est qu'il peut effectuer toutes sortes de manipulations de données / d'accès à la mémoire, ce qui n'est tout simplement pas possible sur un GPU.
Jouez avec la taille du lot. Si vous utilisez un GPU, les performances peuvent varier considérablement en fonction de la taille de chaque tableau de vertex que vous transmettez. En conséquence, jouez avec la taille des morceaux (si vous utilisez des morceaux). J'ai trouvé que les morceaux 64x64x64 fonctionnent plutôt bien. Quoi qu'il en soit, gardez vos morceaux cubiques (pas de prismes rectangulaires). Cela facilitera le codage et diverses opérations (comme les transformations) et, dans certains cas, le rendra plus performant. Si vous ne stockez qu'une valeur pour la longueur de chaque dimension, gardez à l'esprit que deux registres de moins sont intervertis lors du calcul.
Considérez les listes d’affichage (pour OpenGL). Même s'ils sont "à l'ancienne", ils peuvent être plus rapides. Vous devez transformer une liste d’affichage en variable ... si vous appelez des opérations de création de liste d’affichage en temps réel, le processus sera lent. Comment une liste d'affichage est-elle plus rapide? Il ne met à jour que les attributs état, vs attributs par sommet. Cela signifie que je peux passer jusqu'à six faces, puis une couleur (par rapport à une couleur pour chaque sommet du voxel). Si vous utilisez GL_QUADS et des voxels cubiques, vous pouvez économiser jusqu'à 20 octets (160 bits) par voxel! (15 octets sans alpha, bien que vous souhaitiez généralement que les éléments soient alignés sur 4 octets.)
J'utilise une méthode brute-force de rendu des "morceaux", ou des pages de données, qui est une technique courante. Contrairement aux octrees, il est beaucoup plus facile / rapide de lire / traiter les données, bien que beaucoup moins convivial en mémoire (cependant, de nos jours, vous pouvez obtenir 64 gigaoctets de mémoire pour 200 $ à 300 $) ... pas que l'utilisateur moyen l'ait. De toute évidence, vous ne pouvez pas allouer un grand tableau pour le monde entier (un ensemble de voxels 1024x1024x1024 correspond à 4 Go de mémoire, en supposant qu'un int de 32 bits est utilisé par voxel). Donc, vous allouez / désaffectez de nombreux petits tableaux, en fonction de leur proximité avec le spectateur. Vous pouvez également affecter les données, obtenir la liste d’affichage nécessaire, puis vider les données pour économiser de la mémoire. Je pense que l'idéal serait d'utiliser une approche hybride d'octrees et de tableaux - stocker les données dans un tableau lors de la génération procédurale du monde, de l'éclairage, etc.
Rendre près de loin ... un pixel coupé est un gain de temps. Le GPU lancera un pixel s'il ne réussit pas le test du tampon de profondeur.
Rendu uniquement les morceaux / pages dans la fenêtre d'affichage (explicite). Même si le gpu sait comment clipser les polgyons en dehors de la fenêtre d'affichage, le transfert de ces données prend encore du temps. Je ne sais pas quelle serait la structure la plus efficace pour cela ("honteusement", je n'ai jamais écrit d'arborescence BSP), mais même un simple raycast par morceau pourrait améliorer les performances, et des tests contre le tronc de visualisation seraient évidemment gagner du temps.
Informations évidentes, mais pour les débutants: supprimez tous les polygones qui ne sont pas à la surface - c'est-à-dire si un voxel est composé de six faces, supprimez les faces qui ne sont jamais restituées (touchent un autre voxel).
En règle générale, tout ce que vous faites dans la programmation: CACHE LOCALITY! Si vous parvenez à conserver des éléments locaux en mémoire cache (même pendant un court laps de temps, cela fera une différence énorme. Cela signifie que vos données doivent rester congruentes (dans la même région de mémoire) et que les zones de mémoire ne doivent pas trop souvent être modifiées. dans l’idéal, travaillez sur un bloc par thread et conservez cette mémoire exclusive dans le thread (cela ne s’applique pas uniquement au cache du processeur): pensez à la hiérarchie du cache comme ceci (la plus lente à la plus rapide): réseau (nuage / base de données / etc.) -> disque dur (obtenez un disque SSD si vous n'en avez pas déjà un), une mémoire RAM (obtenez un canal triple ou une RAM supérieure si vous ne l'avez pas déjà), un ou plusieurs caches de processeur, enregistrez-vous. Essayez de conserver vos données sur dernier point, et ne l’échangez pas plus que nécessaire.
Filetage Fais le. Les mondes Voxel sont bien adaptés au filetage, car chaque partie peut être calculée (la plupart du temps) indépendamment des autres. routines pour le filetage.
N'utilisez pas les types de données char / byte. Ou des shorts. Votre consommateur moyen aura un processeur AMD ou Intel moderne (comme vous probablement). Ces processeurs ne disposent pas de registres 8 bits. Ils calculent les octets en les plaçant dans un emplacement de 32 bits, puis les reconvertissent (peut-être) en mémoire. Votre compilateur peut faire toutes sortes de vaudous, mais utiliser un nombre de 32 ou 64 bits vous donnera les résultats les plus prévisibles (et les plus rapides). De même, une valeur "bool" ne prend pas 1 bit; le compilateur utilisera souvent 32 bits complets pour un booléen. Il peut être tentant de faire certains types de compression sur vos données. Par exemple, vous pouvez stocker 8 voxels sous forme d'un nombre unique (2 ^ 8 = 256 combinaisons) s'ils étaient tous du même type / couleur. Cependant, vous devez penser aux conséquences de ceci - cela pourrait économiser beaucoup de mémoire, mais cela peut également nuire aux performances, même avec un petit temps de décompression, car même une petite quantité de temps supplémentaire est proportionnelle à la taille de votre monde. Imaginez-vous en train de calculer un raycast; pour chaque étape du raycast, vous devrez exécuter l'algorithme de décompression (à moins que vous ne trouviez un moyen intelligent de généraliser le calcul de 8 voxels par pas de rayon).
Comme Jose Chavez le mentionne, le modèle de conception flyweight peut être utile. Tout comme vous utiliseriez une image bitmap pour représenter une tuile dans un jeu 2D, vous pouvez créer votre monde à partir de plusieurs types de tuiles 3D (ou blocs). L'inconvénient est la répétition de textures, mais vous pouvez améliorer cela en utilisant des textures de variance qui s'emboîtent. En règle générale, vous souhaitez utiliser l’instanciation chaque fois que vous le pouvez.
Évitez de traiter les vertex et les pixels dans le shader lors de la sortie de la géométrie. Dans un moteur Voxel, vous aurez inévitablement beaucoup de triangles, de sorte que même un simple pixel shader peut considérablement réduire votre temps de rendu. Il est préférable de rendre le rendu dans un tampon, puis vous pixel shader en post-traitement. Si vous ne pouvez pas faire cela, essayez de faire des calculs dans votre vertex shader. Les autres calculs doivent être intégrés aux données de sommet si possible. Des passes supplémentaires deviennent très coûteuses si vous devez restituer toute la géométrie (telle que le mappage des ombres ou le mappage de l'environnement). Parfois, il vaut mieux abandonner une scène dynamique au profit de détails plus riches. Si votre jeu contient des scènes modifiables (terrain destructible, par exemple), vous pouvez toujours recalculer la scène au fur et à mesure de la destruction des objets. La recompilation n'est pas chère et devrait prendre moins d'une seconde.
Détendez vos boucles et gardez les tableaux à plat! Ne fais pas ça:
for (i = 0; i < chunkLength; i++) {
for (j = 0; j < chunkLength; j++) {
for (k = 0; k < chunkLength; k++) {
MyData[i][j][k] = newVal;
}
}
}
//Instead, do this:
for (i = 0; i < chunkLengthCubed; i++) {
//figure out x, y, z index of chunk using modulus and div operators on i
//myData should have chunkLengthCubed number of indices, obviously
myData[i] = newVal;
}
EDIT: Grâce à des tests plus approfondis, j'ai trouvé que cela pouvait être faux. Utilisez le cas qui convient le mieux à votre scénario. En règle générale, les tableaux doivent être plats, mais l'utilisation de boucles multi-index peut souvent être plus rapide, selon le cas.