Créez votre Minecraft: générez des niveaux 3D à partir de cubes


En partie en raison de la popularité de Minecraft , l'idée d'un jeu qui se déroule dans un monde en cubes construit en relief 3D et rempli d'éléments tels que des grottes, des falaises, etc., a récemment suscité un intérêt croissant. Un tel monde est une application idéale pour le bruit généré dans le style de ma bibliothèque ANL . Cet article est né de discussions sur mes précédentes tentatives de mise en œuvre de cette technique. Depuis lors, des changements mineurs sont apparus dans la structure de la bibliothèque.

Dans les articles précédents, j'ai parlé de l'utilisation de fonctionnalités de bruit 3D pour implémenter un terrain de style Minecraft. Après cela, la bibliothèque a évolué un peu, j'ai donc décidé de revenir sur ce sujet. Comme j'ai dû répondre à de nombreuses questions sur ce système, je vais essayer de parler davantage des concepts impliqués. Pour rendre les concepts de base plus clairs, je vais commencer par l'idée de générer un terrain 2D utilisé dans des jeux comme Terraria et King Arthur's Gold, puis étendre le système à des exemples 3D comme Minecraft. Cela me permettra de démontrer plus efficacement les concepts en utilisant des images comme exemple.

Ce système a été développé en tenant compte de l'objectif abstrait suivant: nous devrions être en mesure de transmettre les coordonnées d'un certain point ou d'une certaine cellule au système, et déterminer quel type de bloc devrait être à cet emplacement. Nous voulons que le système soit une «boîte noire»: nous lui passons un point, retournons le type de bloc. Bien sûr, cela ne s'applique qu'à la génération initiale du monde. Les blocs de ces jeux peuvent être modifiés par les actions du joueur, et il sera peu pratique d'essayer de décrire ces changements en utilisant le même système. Ces changements doivent être suivis d'une autre manière. Ce système génère le monde d'origine, vierge et intact par les mains du joueur et d'autres personnages.

Cette technique n'est peut-être pas adaptée à la modélisation de systèmes tels que l'herbe ou d'autres entités biologiques, étant donné que ces systèmes eux-mêmes sont des entités complexes qui ne sont pas si faciles à modéliser implicitement. Il en va de même pour les systèmes tels que les chutes de neige, la formation de glace, etc. ... La technique décrite dans l'article est une méthode implicite , c'est-à-dire celui qui peut être estimé en un point, et dont la valeur en un point donné ne dépend pas des valeurs environnantes. Les types de systèmes biologiques et autres pour effectuer des simulations précises doivent généralement tenir compte des valeurs environnementales. Par exemple: combien de soleil tombe sur un bloc? Y a-t-il de l'eau à proximité? Il faut répondre à ces questions et à d'autres pour simuler la croissance et la propagation des systèmes biologiques, ainsi que, dans une moindre mesure, d'autres types de systèmes liés au climat. De plus, cette technique n'est pas adaptée à la modélisation de l'eau. Dans ce système, il n'y a pas de concept d'écoulement, de connaissance de la mécanique des fluides ou de la gravité. L'eau est un sujet complexe qui nécessite de nombreux calculs complexes.

Donc, nous modélisons simplement la terre et les pierres. Nous avons besoin d'une fonction qui vous indique quel doit être l'emplacement donné: terre, sable, air, or, fer, charbon, etc. ... Mais nous allons commencer par le plus simple. Nous avons besoin d'une fonction qui dira si le bloc est solide ou creux (rempli d'air). Cette fonction devrait simuler la terre qui nous entoure. Autrement dit, le ciel est au-dessus, la terre est en dessous. Alors, prenons la tâche biblique et séparons le ciel de la terre. Pour ce faire, nous étudions la fonction Gradient . La fonction Gradient reçoit un segment de ligne dans un espace à N dimensions (c'est-à-dire dans n'importe quel espace de coordonnées, qu'il soit 2D, 3D ou supérieur), et elle calcule le champ de gradient le long de ce segment. Les coordonnées entrantes sont projetées sur ce segment et leur valeur de gradient est calculée en fonction de leur position par rapport aux points d'extrémité du segment. Les points projetés reçoivent des valeurs dans l'intervalle (-1,1). Et ce sera un bon début pour nous. Nous pouvons définir la fonction Gradient le long de l'axe Y. En haut de l'intervalle, nous comparons le champ de gradient avec -1 (air), et en bas avec 1 (terre).

 terraintree =
 {
	 {name = "ground_gradient", type = "gradient", x1 = 0, x2 = 0, y1 = 0, y2 = 1}
 } 

(Je vais expliquer brièvement l'entrée. Le code des exemples est écrit dans le tableau de déclaration Lua. Pour plus d'informations sur le format, consultez la section sur l' intégration Lua . En substance, le format est conçu pour être analysé par une classe spéciale qui lit les annonces et les transforme en arborescences d'instance de module de bruit. Je préfère cela le format C ++ est plus détaillé étape par étape, car il est plus compact et plus propre. À mon avis, le code source est plus lisible et compressé que le code C ++. Pour la plupart, les déclarations sont faciles à lire et à comprendre. Les modules ont des noms, les sources sont spécifiées nom ou valeur. Le code Lua utilisé pour analyser les déclarations de table est inclus dans le code source au cas où vous souhaiteriez utiliser ces déclarations directement.)

Dans le cas de 2D, la fonction Dégradé reçoit un segment de ligne droite sous la forme (x1, x2, y1, y2), et dans le cas de 3D, le format est étendu à (x1, x2, y1, y2, z1, z2). Le point formé par (x1, y1) indique le début du segment de ligne associé à 0. Le point formé (x2, y2) est la fin du segment associé à 1. Autrement dit, nous cartographions le segment de ligne (0,1) -> ( 0,0) avec un gradient. Par conséquent, le gradient sera compris entre les régions de la fonction Y = 1 et Y = 0. C'est-à-dire que cette bande forme les dimensions du monde en Y. N'importe quelle partie du monde sera dans cette bande. Nous pouvons accrocher n'importe quelle région le long de X (presque à l'infini, mais ici la double précision nous limite), mais tout est intéressant, c'est-à-dire la surface de la terre sera dans cette bande. Ce comportement peut être changé, mais en son sein, nous avons une grande flexibilité. N'oubliez pas que toutes les valeurs supérieures ou inférieures à cette bande sont les plus susceptibles d'être inintéressantes, car les valeurs ci-dessus sont le plus souvent l'air et les valeurs ci-dessous sont au sol. (Comme vous le verrez bientôt, cette déclaration pourrait bien s'avérer erronée.) Pour la plupart des images de cette série, je correspondrai à la région carrée donnée par le carré (0,1) -> (1,0) dans l'espace 2D. Par conséquent, au début, notre monde ressemble à ceci:


Rien d'intéressant jusqu'à présent; De plus, cette image ne répond pas à la question «le point donné est-il solide ou creux?». Pour répondre à cette question, nous devons appliquer la fonction Step (fonction définie par morceaux). Au lieu d'un dégradé lisse, nous avons besoin d'une séparation claire, dans laquelle tous les emplacements d'un côté sont creux et tous les emplacements de l'autre côté sont solides. Dans ANL, cela peut être implémenté à l'aide de la fonction Select . La fonction Select reçoit deux fonctions ou valeurs entrantes (dans ce cas, elles seront égales à «solide» et «Hollow» (ouvert)), et les sélectionne en fonction de la valeur de la fonction de contrôle (dans ce cas, Gradient). Le module Select a deux paramètres supplémentaires, seuil et atténuation , qui affectent ce processus. À ce stade, l' atténuation n'est pas souhaitable, nous allons donc la rendre égale à 0. Le paramètre de seuil décide où ira la ligne de démarcation entre Solid et Open. Tout ce qui sera supérieur à cette valeur dans la fonction Dégradé deviendra Solide et tout ce qui est inférieur au seuil deviendra Ouvert. Étant donné que Gradient compare l'intervalle avec des valeurs de 0 et 1, il serait logique de placer le seuil à 0,5. Nous divisons donc l'espace exactement en deux. La valeur 1 sera un emplacement solide et la valeur 0 sera creuse. Autrement dit, nous définissons la fonction du plan terrestre comme suit:

 terraintree =
 {
	 {name = "ground_gradient", type = "gradient", x1 = 0, x2 = 0, y1 = 0, y2 = 1},
	 {name = "ground_select", type = "select", low = 0, high = 1, threshold = 0.5, control = "ground_gradient"}
 }

En comparant la même zone de la fonction que précédemment, nous obtenons quelque chose de similaire:


Cette image répond clairement à la question de savoir si le point donné est solide ou creux. Nous pouvons appeler la fonction avec n'importe quelle coordonnée possible de l'espace 2D, et son résultat sera soit 1 ou 0, selon l'endroit où le point est relatif à la surface de la terre. Cependant, une telle fonction n'est pas particulièrement intéressante, c'est juste une ligne plate s'étendant à l'infini. Pour raviver l'image, nous utilisons une technique appelée «turbulence».

La «turbulence» est une désignation complexe du concept d'ajout de valeurs aux coordonnées entrantes d'une fonction. Imaginez que nous appelons la fonction ci-dessus de la Terre avec la coordonnée (0,1). Il se situe au-dessus du plan du sol, car à Y = 1, le gradient a une valeur de 0, ce qui est inférieur au seuil = 0,5. Autrement dit, ce point sera calculé comme ouvert. Mais que se passe-t-il si, avant d'invoquer la fonction de la terre, nous transformons en quelque sorte ce point? Supposons que nous soustrayions une valeur aléatoire de la coordonnée Y, par exemple 3. Nous soustrayons 3 et obtenons la coordonnée (0, -2). Si nous appelons maintenant la fonction sol pour ce point, alors le point sera considéré comme solide, car Y = -2 se situe en dessous du segment Gradient correspondant à 1. Soudain, le point creux (0,1) se transforme en solide. Nous obtiendrons un bloc de pierre solide suspendu dans l'air. Cela peut être fait avec n'importe quel point de la fonction en ajoutant ou en soustrayant un nombre aléatoire de la coordonnée Y du point entrant avant d'appeler la fonction ground_select. Voici une image de la fonction ground_select montrant cela. Avant d'appeler la fonction ground_select, la valeur de l'intervalle (-0,25, 0,25) est ajoutée à la coordonnée Y de chaque point.


C'est plus intéressant qu'une ligne plate, mais pas très similaire à la terre, car chaque point se déplace vers une valeur complètement aléatoire, ce qui crée un motif chaotique. Cependant, si nous utilisons une fonction aléatoire continue, par exemple, Fractal de la bibliothèque ANL , alors au lieu d'un modèle aléatoire, nous obtenons quelque chose de plus contrôlable. Par conséquent, connectons une fractale au plan de la terre et voyons ce qui se passe.

 terraintree =
 {
	 {name = "ground_gradient", type = "gradient", x1 = 0, x2 = 0, y1 = 0, y2 = 1},
	 {name = "ground_shape_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 6, fréquence = 2},
	 {name = "ground_scale", type = "scaleoffset", scale = 0.5, offset = 0, source = "ground_shape_fractal"},
	 {name = "ground_perturb", type = "translomain", source = "ground_gradient", ty = "ground_scale"},
	
	 {name = "ground_select", type = "select", bas = 0, haut = 1, seuil = 0,5, control = "ground_perturb"}
 }

Ici, il convient de noter quelques aspects. Tout d'abord, nous définissons le module Fractal et le chaînons au module ScaleOffset . Le module ScaleOffset met à l'échelle les valeurs fractales de sortie à un niveau plus pratique. Une partie du relief peut être montagneuse et nécessiter une plus grande échelle, et une autre partie - plus plate et à plus petite échelle. Nous parlerons plus tard de différents types de terrain, mais pour l'instant nous les utiliserons pour la démonstration. Les valeurs de sortie de la fonction donneront maintenant l'image suivante:


C'est plus intéressant qu'un simple bruit aléatoire, non? Au moins, cela ressemble plus à de la terre, bien qu'une partie du paysage semble inhabituelle, et les îles volantes sont complètement étranges. La raison en était que chaque point individuel de la carte de sortie est décalé de manière aléatoire d'une valeur différente déterminée par la fractale. Pour illustrer cela, montrez la sortie fractale qui effectue la distorsion:


Dans l'image ci-dessus, tous les points noirs ont une valeur de -0,25 et tous les points blancs ont une valeur de 0,25. Autrement dit, lorsque la fractale est noire, le point correspondant de la fonction de la Terre sera décalé "vers le bas" de 0,25. (0,25 signifie 1/4 de l'écran.) Puisqu'un point peut être légèrement déplacé et que l'autre point au-dessus de lui dans l'espace peut être déplacé davantage, il existe une possibilité de protubérances de roches et d'îles volantes. Les saillies dans la nature sont assez naturelles, contrairement aux îles volantes. (À moins que nous ne soyons dans le film "Avatar".) Si votre jeu a besoin d'un paysage aussi fantastique, c'est génial, mais si vous avez besoin d'un modèle plus réaliste, nous devons ajuster un peu la fonction fractale. Heureusement, la fonction ScaleDomain peut le faire.

Nous voulons que la fonction se comporte comme une fonction de carte de hauteur. Imaginez une carte de hauteur 2D où chaque point de la carte représente la hauteur d'un point dans la grille de points de grille qui sont relevés vers le haut ou vers le bas. Les valeurs blanches de la carte indiquent de hautes collines, des vallées noires à basses. Nous avons besoin d'un comportement similaire, mais pour y parvenir, nous devons essentiellement nous débarrasser de l'une des dimensions. Dans le cas d'une carte de hauteur, nous créons une élévation 3D à partir d'une carte de hauteur 2D. De même, dans le cas d'un terrain 2D, nous avons besoin d'une carte de hauteur 1D. Après avoir fait en sorte que tous les points d'une fractale avec la même coordonnée Y aient la même valeur, nous pouvons déplacer tous les points avec la même coordonnée X de la même quantité, de sorte que les îles volantes disparaissent. Pour ce faire, vous pouvez utiliser ScaleDomain, en réinitialisant le coefficient scaley. Autrement dit, avant d'appeler la fonction ground_shape_fractal, nous appelons ground_scale_y pour définir la coordonnée y sur 0. Cela garantit que la valeur Y n'affecte pas la sortie de la fractale, la transformant essentiellement en une fonction de bruit 1D. Pour ce faire, nous apporterons les modifications suivantes:

 terraintree =
 {
	 {name = "ground_gradient", type = "gradient", x1 = 0, x2 = 0, y1 = 0, y2 = 1},
	 {name = "ground_shape_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 6, fréquence = 2},
	 {name = "ground_scale", type = "scaleoffset", scale = 0.5, offset = 0, source = "ground_shape_fractal"},
	 {name = "ground_scale_y", type = "scaledomain", source = "ground_scale", scaley = 0},
	 {name = "ground_perturb", type = "translomain", source = "ground_gradient", ty = "ground_scale_y"},
	
	 {name = "ground_select", type = "select", bas = 0, haut = 1, seuil = 0,5, control = "ground_perturb"}
 }

Nous enchaînerons la fonction ScaleDomain avec ground_scale, puis modifierons les données ground_perturb d'origine pour qu'elles soient une fonction ScaleDomain. Cela changera la fractale qui déplace la terre et la transforme en quelque chose comme ceci:


Maintenant, si nous regardons la sortie, nous obtenons le résultat:


Bien mieux. Les îles volantes ont complètement disparu et le relief ressemble plus à des montagnes et des collines. Malheureusement, nous avons perdu des saillies et des falaises. Maintenant, la terre entière est continue et en pente. Si vous le souhaitez, vous pouvez résoudre ce problème de plusieurs manières.

Tout d'abord, vous pouvez utiliser une autre fonction TranslateDomain , couplée à une autre fonction Fractal . Si nous appliquons une petite quantité de turbulence fractale à la direction X, nous pouvons légèrement déformer les bords et les surfaces des montagnes, et cela sera probablement suffisant pour former des précipices et des rebords. Regardons-le en action.

 terraintree =
 {
	 {name = "ground_gradient", type = "gradient", x1 = 0, x2 = 0, y1 = 0, y2 = 1},
	 {name = "ground_shape_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 6, fréquence = 2},
	 {name = "ground_scale", type = "scaleoffset", scale = 0.5, offset = 0, source = "ground_shape_fractal"},
	 {name = "ground_scale_y", type = "scaledomain", source = "ground_scale", scaley = 0},
	 {name = "ground_perturb", type = "translomain", source = "ground_gradient", ty = "ground_scale_y"},
	 {name = "ground_overhang_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 6, fréquence = 2},
	 {name = "ground_overhang_scale", type = "scaleoffset", source = "ground_overhang_fractal", scale = 0,2, offset = 0},
	 {name = "ground_overhang_perturb", type = "translomain", source = "ground_perturb", tx = "ground_overhang_scale"},
	
	 {name = "ground_select", type = "select", bas = 0, haut = 1, seuil = 0,5, control = "ground_overhang_perturb"}
 }

Et voici le résultat:


La deuxième façon: vous pouvez simplement définir le paramètre scaley de la fonction ground_scale_y à une valeur supérieure à 0. Si vous laissez une petite échelle en Y, nous obtiendrons une fraction de la variabilité, cependant, plus l'échelle est grande, plus le relief ressemblera à la version précédente sans mise à l'échelle.


Les résultats semblent beaucoup plus intéressants que les montagnes en pente ordinaires. Cependant, peu importe leur intérêt, le joueur s'ennuiera toujours à explorer le relief avec le même motif, s'étendant sur plusieurs kilomètres. De plus, un tel soulagement serait très irréaliste. Dans le monde réel, il y a beaucoup de variabilité qui rend le terrain plus intéressant. Voyons ce qui peut être fait pour rendre le monde plus diversifié.

En regardant l'exemple de code précédent, vous pouvez y voir un modèle spécifique. Nous avons une fonction de gradient, qui est contrôlée par des fonctions qui donnent à la terre une forme, après quoi une fonction définie par morceaux est appliquée et la terre devient pleine. Autrement dit, il sera plus logique de compliquer le relief au stade de la mise en forme de la terre. Au lieu d'un fractal se déplaçant le long de Y et d'un autre se déplaçant le long de X, nous pouvons atteindre le degré de complexité requis (en tenant compte des performances: chaque fractale nécessite des coûts de calcul supplémentaires, nous devons donc essayer d'être conservateurs.) Nous pouvons spécifier les formes de la terre, qui sont des montagnes, des contreforts. , plaines plates, friches, etc ... et utilisez la sortie des différentes fonctions Select enchaînées avec des fractales basse fréquence pour délimiter les zones de chaque type. Voyons donc comment mettre en œuvre différents types de terrain.

Pour illustrer le principe, nous distinguons trois types de relief: les plateaux (collines à pente douce), les montagnes et les plaines (principalement plates). Pour basculer entre eux, nous utilisons un système basé sur la sélection et les combinons dans un canevas complexe. Alors c'est parti ...

Contreforts:

Avec eux, tout est simple. Nous pouvons prendre le schéma utilisé ci-dessus, réduire légèrement l'amplitude des collines, peut-être même les rendre plus soustractives qu'additives. pour abaisser les hauteurs moyennes. Nous pouvons également réduire le nombre d'octaves pour les lisser.

 {name = "lowland_shape_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 2, fréquence = 1},
 {name = "lowland_autocorrect", type = "autocorrect", source = "lowland_shape_fractal", low = 0, high = 1},
 {name = "lowland_scale", type = "scaleoffset", source = "lowland_autocorrect", scale = 0.2, offset = -0.25},
 {name = "lowland_y_scale", type = "scaledomain", source = "lowland_scale", scaley = 0},
 {name = "lowland_terrain", type = "translomain", source = "ground_gradient", ty = "lowland_y_scale"},

Highlands:

Avec eux aussi, tout est simple. (En fait, aucun de ces types de terrain n'est difficile.) Cependant, nous utilisons une base différente pour faire ressembler les collines aux dunes.

 {name = "highland_shape_fractal", type = "fractal", fractaltype = anl.RIDGEDMULTI, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 2, fréquence = 2},
 {name = "highland_autocorrect", type = "autocorrect", source = "highland_shape_fractal", low = 0, high = 1},
 {name = "highland_scale", type = "scaleoffset", source = "highland_autocorrect", scale = 0.45, offset = 0},
 {name = "highland_y_scale", type = "scaledomain", source = "highland_scale", scaley = 0},
 {name = "highland_terrain", type = "translomain", source = "ground_gradient", ty = "highland_y_scale"},

Montagnes:

 {name = "mountain_shape_fractal", type = "fractal", fractaltype = anl.BILLOW, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 4, fréquence = 1},
 {name = "mountain_autocorrect", type = "autocorrect", source = "mountain_shape_fractal", low = 0, high = 1},
 {name = "mountain_scale", type = "scaleoffset", source = "mountain_autocorrect", scale = 0,75, offset = 0,25},
 {name = "mountain_y_scale", type = "scaledomain", source = "mountain_scale", scaley = 0,1},
 {name = "mountain_terrain", type = "translomain", source = "ground_gradient", ty = "mountain_y_scale"},

Bien sûr, vous pouvez aborder ce processus de manière encore plus créative, mais en général, le modèle sera le même. Nous mettons en évidence les caractéristiques du type de relief et sélectionnons pour elles des fonctions de bruit. Pour tout cela, les mêmes principes s'appliquent; Les principales différences sont l'échelle. Maintenant, pour les connecter ensemble, nous allons préparer des fractales supplémentaires qui contrôleront la fonction Select . Ensuite, nous enchaînons les modules Select pour générer tout le terrain.

 {name = "terrain_type_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 3, fréquence = 0,5},
 {name = "terrain_autocorrect", type = "autocorrect", source = "terrain_type_fractal", faible = 0, élevé = 1},
 {name = "terrain_type_cache", type = "cache", source = "terrain_autocorrect"},
 {name = "highland_mountain_select", type = "select", low = "highland_terrain", high = "mountain_terrain", control = "terrain_type_cache", seuil = 0,55, falloff = 0,15},
 {name = "highland_lowland_select", type = "select", low = "lowland_terrain", high = "highland_mountain_select", control = "terrain_type_cache", seuil = 0,25, falloff = 0,15},

Nous définissons donc ici trois types principaux de terrain: les plaines, les hautes terres et les montagnes. Nous utilisons une fractale pour en sélectionner une, afin qu'il y ait des transitions naturelles (plaines-> hautes terres-> montagnes). Ensuite, nous utilisons une autre fractale pour insérer au hasard des badlands dans la carte. Voici à quoi ressemble la chaîne de modules terminée:

 terraintree =
 {
	 {name = "lowland_shape_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 2, fréquence = 1},
	 {name = "lowland_autocorrect", type = "autocorrect", source = "lowland_shape_fractal", low = 0, high = 1},
	 {name = "lowland_scale", type = "scaleoffset", source = "lowland_autocorrect", scale = 0.2, offset = -0.25},
	 {name = "lowland_y_scale", type = "scaledomain", source = "lowland_scale", scaley = 0},
	 {name = "lowland_terrain", type = "translomain", source = "ground_gradient", ty = "lowland_y_scale"},
	 {name = "ground_gradient", type = "gradient", x1 = 0, x2 = 0, y1 = 0, y2 = 1},
	 {name = "highland_shape_fractal", type = "fractal", fractaltype = anl.RIDGEDMULTI, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 2, fréquence = 2},
	 {name = "highland_autocorrect", type = "autocorrect", source = "highland_shape_fractal", low = 0, high = 1},
	 {name = "highland_scale", type = "scaleoffset", source = "highland_autocorrect", scale = 0.45, offset = 0},
	 {name = "highland_y_scale", type = "scaledomain", source = "highland_scale", scaley = 0},
	 {name = "highland_terrain", type = "translomain", source = "ground_gradient", ty = "highland_y_scale"},

	 {name = "mountain_shape_fractal", type = "fractal", fractaltype = anl.BILLOW, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 4, fréquence = 1},
	 {name = "mountain_autocorrect", type = "autocorrect", source = "mountain_shape_fractal", low = 0, high = 1},
	 {name = "mountain_scale", type = "scaleoffset", source = "mountain_autocorrect", scale = 0,75, offset = 0,25},
	 {name = "mountain_y_scale", type = "scaledomain", source = "mountain_scale", scaley = 0,1},
	 {name = "mountain_terrain", type = "translomain", source = "ground_gradient", ty = "mountain_y_scale"},

	 {name = "terrain_type_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 3, fréquence = 0,5},
	 {name = "terrain_autocorrect", type = "autocorrect", source = "terrain_type_fractal", faible = 0, élevé = 1},
	 {name = "terrain_type_cache", type = "cache", source = "terrain_autocorrect"},
	 {name = "highland_mountain_select", type = "select", low = "highland_terrain", high = "mountain_terrain", control = "terrain_type_cache", seuil = 0,55, falloff = 0,15},
	 {name = "highland_lowland_select", type = "select", low = "lowland_terrain", high = "highland_mountain_select", control = "terrain_type_cache", seuil = 0,25, falloff = 0,15},
	 {name = "ground_select", type = "select", bas = 0, haut = 1, seuil = 0,5, control = "highland_lowland_select"}
 }

Voici quelques exemples des reliefs résultants:




Vous remarquerez peut-être qu'une variabilité assez élevée est obtenue. Dans certains endroits, des montagnes brisées imposantes apparaissent, dans d'autres il y a des plaines en pente douce. Maintenant, nous devons ajouter des grottes pour pouvoir explorer les merveilles des enfers.

Pour les grottes, j'utilise le système multiplicatif appliqué à ground_select . Autrement dit, je crée une fonction qui génère 1 ou 0 et je les multiplie par la sortie de ground_select . Grâce à cela, tout point de la fonction devient creux pour lequel la valeur de la fonction des grottes est 0. Autrement dit, là où je veux obtenir la grotte, la fonction des grottes devrait retourner 0, et où la grotte ne devrait pas être, la fonction devrait être 1. Quant à la forme grottes, je veux établir un système de grottes basé sur le 1 octave Ridged Multifractal .

 {name = "cave_shape", type = "fractal", fractaltype = anl.RIDGEDMULTI, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 1, fréquence = 2},

Le résultat est quelque chose comme ceci:


Si nous appliquons la fonction Select en tant que fonction définie par morceaux, comme nous l'avons fait avec le gradient terrestre, en l'implémentant de sorte que la partie inférieure du seuil de sélection soit 1 (il n'y a pas de grotte) et la partie supérieure est 0 (il y a une grotte), le résultat ressemblera à ceci :

 {name = "cave_shape", type = "fractal", fractaltype = anl.RIDGEDMULTI, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 1, fréquence = 2},
 {name = "cave_select", type = "select", low = 1, high = 0, control = "cave_shape", threshold = 0.8, falloff = 0},

Résultat:


Bien sûr, il semble assez lisse, alors ajoutez du bruit fractal pour déformer la zone.

 {name = "cave_shape", type = "fractal", fractaltype = anl.RIDGEDMULTI, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 1, fréquence = 2},
 {name = "cave_select", type = "select", low = 1, high = 0, control = "cave_shape", threshold = 0.8, falloff = 0},
 {name = "cave_perturb_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 6, fréquence = 3},
 {name = "cave_perturb_scale", type = "scaleoffset", source = "cave_perturb_fractal", scale = 0,25, offset = 0},
{name = "cave_perturb", type = "translomain", source = "cave_select", tx = "cave_perturb_scale"},

Résultat:


Cela rend les grottes un peu bruyantes et les rend moins douces. Voyons maintenant ce qui se passe si vous appliquez les grottes au relief:


En expérimentant la valeur de seuil dans cave_select , nous pouvons rendre les grottes plus minces ou plus épaisses. Mais l'essentiel que nous devons essayer est de nous assurer que les grottes ne rongent pas ces énormes fragments du relief de surface. Pour ce faire, nous pouvons revenir à la fonction highland_lowland_select , qui, rappelons-le, est la dernière fonction de relief qui déforme le gradient de la terre. Ce qui est utile dans cette fonction, c'est qu'il s'agit toujours d'un gradient, augmentant la valeur lorsque la fonction s'approfondit dans le sol. Nous pouvons utiliser le gradient pour affaiblir la fonction des grottes afin que les grottes augmentent à mesure qu'elles s'enfoncent dans le sol. Heureusement pour nous, cette atténuation peut être obtenue simplement en multipliant la sortie de la fonction highland_lowland_selectà la sortie de cave_shape , puis passez le résultat au reste de la chaîne de fonctions. Ensuite, nous apporterons un changement important ici - nous ajouterons la fonction Cache . La fonction de mise en cache enregistre le résultat de la fonction pour une coordonnée entrante donnée, et si la fonction est appelée à plusieurs reprises avec la même coordonnée, elle renverra une copie en cache et ne calculera plus le résultat. Ceci est utile dans les situations où une fonction complexe ( highland_lowland_select ) dans une chaîne de fonctions est appelée plusieurs fois. Sans cache, toute la chaîne d'une fonction complexe est recalculée à chaque appel. Pour ajouter le cache, nous devons d'abord apporter les modifications suivantes:

{name = "highland_lowland_select", type = "select", low = "lowland_terrain", high = "highland_mountain_select", control = "terrain_type_cache", seuil = 0,25, falloff = 0,15},
{name = "highland_lowland_select_cache", type = "cache", source = "highland_lowland_select"},
{name = "ground_select", type = "select", bas = 0, haut = 1, seuil = 0,5, control = "highland_lowland_select_cache"},

Nous avons donc ajouté Cache, puis redirigé l'entrée vers ground_select afin qu'elle soit prise dans le cache, et non directement dans la fonction. Ensuite, nous pouvons changer le code des grottes pour ajouter une atténuation:

{name = "cave_shape", type = "fractal", fractaltype = anl.RIDGEDMULTI, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 1, fréquence = 4},
{name="cave_attenuate_bias", type="bias", source="highland_lowland_select_cache", bias=0.45},
{name="cave_shape_attenuate", type="combiner", operation=anl.MULT, source_0="cave_shape", source_1="cave_attenuate_bias"},
{name="cave_perturb_fractal", type="fractal", fractaltype=anl.FBM, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=6, frequency=3},
{name="cave_perturb_scale", type="scaleoffset", source="cave_perturb_fractal", scale=0.5, offset=0},
{name="cave_perturb", type="translatedomain", source="cave_shape_attenuate", tx="cave_perturb_scale"},
{name="cave_select", type="select", low=1, high=0, control="cave_perturb", threshold=0.48, falloff=0},

Tout d'abord, nous avons ajouté la fonction Bias . C'est pour plus de commodité, car cela nous permet d'ajuster l'intervalle de la fonction d'atténuation du gradient. Ensuite, la fonction cave_shape_attenuate est ajoutée , qui est un combinateur de type anl :: MULT . Elle multiplie le gradient par cave_shape . Le résultat de cette opération est ensuite transmis à la fonction cave_perturb . Le résultat ressemble à ceci:


Nous voyons que plus près de la surface de la terre est devenue plus mince. (Ne faites pas attention au sommet, ce n'est qu'un artefact de valeurs de gradient négatives, cela n'affecte pas les grottes finies. Si cela devient un problème - disons que si nous utilisons cette fonction pour autre chose, nous pouvons limiter le gradient à l'intervalle (0, 1).) Il est un peu difficile de voir comment cela fonctionne par rapport au terrain, alors continuons et mettons tout en place pour voir ce qui se passe. Voici toute la chaîne de fonctions que nous avons créées jusqu'à présent.

terraintree =
 {
	{name = "ground_gradient", type = "gradient", x1 = 0, x2 = 0, y1 = 0, y2 = 1},
	
	{name = "lowland_shape_fractal", type = "fractal", fractaltype = anl.BILLOW, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 2, fréquence = 0,25},
	{name="lowland_autocorrect", type="autocorrect", source="lowland_shape_fractal", low=0, high=1},
	{name="lowland_scale", type="scaleoffset", source="lowland_autocorrect", scale=0.125, offset=-0.45},
	{name="lowland_y_scale", type="scaledomain", source="lowland_scale", scaley=0},
	{name="lowland_terrain", type="translatedomain", source="ground_gradient", ty="lowland_y_scale"},
	
	{name="highland_shape_fractal", type="fractal", fractaltype=anl.FBM, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=4, frequency=2},
	{name="highland_autocorrect", type="autocorrect", source="highland_shape_fractal", low=-1, high=1},
	{name="highland_scale", type="scaleoffset", source="highland_autocorrect", scale=0.25, offset=0},
	{name="highland_y_scale", type="scaledomain", source="highland_scale", scaley=0},
	{name="highland_terrain", type="translatedomain", source="ground_gradient", ty="highland_y_scale"},

	{name="mountain_shape_fractal", type="fractal", fractaltype=anl.RIDGEDMULTI, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=8, frequency=1},
	{name="mountain_autocorrect", type="autocorrect", source="mountain_shape_fractal", low=-1, high=1},
	{name="mountain_scale", type="scaleoffset", source="mountain_autocorrect", scale=0.45, offset=0.15},
	{name="mountain_y_scale", type="scaledomain", source="mountain_scale", scaley=0.25},
	{name="mountain_terrain", type="translatedomain", source="ground_gradient", ty="mountain_y_scale"},

	{name="terrain_type_fractal", type="fractal", fractaltype=anl.FBM, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=3, frequency=0.125},
	{name="terrain_autocorrect", type="autocorrect", source="terrain_type_fractal", low=0, high=1},
	{name="terrain_type_y_scale", type="scaledomain", source="terrain_autocorrect", scaley=0},
	{name="terrain_type_cache", type="cache", source="terrain_type_y_scale"},
	{name="highland_mountain_select", type="select", low="highland_terrain", high="mountain_terrain", control="terrain_type_cache", threshold=0.55, falloff=0.2},
	{name="highland_lowland_select", type="select", low="lowland_terrain", high="highland_mountain_select", control="terrain_type_cache", threshold=0.25, falloff=0.15},
	{name="highland_lowland_select_cache", type="cache", source="highland_lowland_select"},
	{name="ground_select", type="select", low=0, high=1, threshold=0.5, control="highland_lowland_select_cache"},
	
	{name="cave_shape", type="fractal", fractaltype=anl.RIDGEDMULTI, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=1, frequency=4},
	{name="cave_attenuate_bias", type="bias", source="highland_lowland_select_cache", bias=0.45},
	{name="cave_shape_attenuate", type="combiner", operation=anl.MULT, source_0="cave_shape", source_1="cave_attenuate_bias"},
	{name="cave_perturb_fractal", type="fractal", fractaltype=anl.FBM, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=6, frequency=3},
	{name="cave_perturb_scale", type="scaleoffset", source="cave_perturb_fractal", scale=0.5, offset=0},
	{name="cave_perturb", type="translatedomain", source="cave_shape_attenuate", tx="cave_perturb_scale"},
	{name="cave_select", type="select", low=1, high=0, control="cave_perturb", threshold=0.48, falloff=0},
	
	{name = "ground_cave_multiply", type = "combiner", operation = anl.MULT, source_0 = "cave_select", source_1 = "ground_select"}
 }

Voici des exemples de cartes randomisées dérivées de cette fonction:




Maintenant, tout semble plutôt bien. Toutes les grottes sont des cavernes assez grandes et profondément souterraines, mais plus près de la surface, elles se transforment généralement en petits tunnels. Cela contribue à créer une atmosphère de mystère. En explorant la surface, vous trouvez une petite entrée dans la grotte. Où va-t-elle? Quelle est sa profondeur? Nous ne pouvons pas le savoir, mais au cours de son étude, il commence à s'étendre, se transformant en un vaste système de cavernes remplies d'obscurité et de dangers. Et le butin, bien sûr. Il y a toujours beaucoup de butin.

Vous pouvez modifier ce système de différentes manières et obtenir des résultats différents. Nous pouvons changer les paramètres de seuil pour cave_select et les paramètres de cave_attenuate_bias , ou remplacer cave_attenuate_biasd'autres fonctions pour faire correspondre l'intervalle de gradient à d'autres valeurs mieux adaptées à vos besoins. Vous pouvez également ajouter une autre fractale qui déforme le système de grottes le long de l'axe Y pour éliminer la possibilité de tunnels anormalement lisses le long de l'axe X (causés par le fait que la forme de la grotte n'est déformée que le long de l'axe X). Vous pouvez également ajouter une nouvelle fractale comme source d'atténuation supplémentaire, définir une troisième source pour cave_shape_attenuate , qui met à l'échelle l'atténuation en fonction des régions, afin que les grottes dans certaines zones soient plus denses (par exemple, dans les montagnes), et moins souvent ou complètement absentes dans d'autres. Cette sélection régionale peut être créée à partir de la fonction terrain_type_fractalpour savoir où se trouvent les zones de montagne. Tout se résume à penser à ce que vous voulez, à déterminer quel effet les différentes fonctions auront sur la sortie et à expérimenter les paramètres jusqu'à ce que vous obteniez le résultat souhaité. Ce n'est pas une science exacte, et souvent l'effet souhaité peut être atteint de différentes manières.

Inconvénients


Cette méthode de génération de terrain présente des inconvénients. Le processus de génération de bruit peut être assez lent. Il est important de réduire le nombre de fractales, le nombre d'octaves de ces fractales que vous utilisez et d'autres opérations lentes, si possible. Essayez d'utiliser les fractales plusieurs fois et mettez en cache toutes les fonctions appelées plusieurs fois. Dans cet exemple, j'ai librement utilisé des fractales, en créant une pour chacun des trois types de relief. En utilisant ScaleOffset pour changer les intervalles et en prenant une fractale comme base pour tous, je gagnerais beaucoup de temps processeur. En 2D, tout n'est pas si mal, mais lorsque vous arrivez en 3D et essayez de comparer les quantités de données, le temps de traitement augmentera considérablement.

Aller en 3D


Tout cela est super si vous créez un jeu comme Terraria ou King Arthur's Gold , mais que faire si vous avez besoin de quelque chose comme Minecraft ou Infiniminer? Quels changements devons-nous apporter à la chaîne de fonctions? En fait, il n'y en a pas beaucoup. La fonction ci-dessus fonctionnera presque sans modification pour le relief 3D. Il vous suffira de comparer le volume 3D en utilisant les variations 3D du générateur, et aussi de comparer l'axe Y avec l'axe vertical du volume, et non la région 2D. Cependant, un changement sera néanmoins nécessaire, à savoir un moyen de réaliser les grottes. Comme vous l'avez vu, Ridged Multifractal est idéal pour un système de grottes 2D, mais en 3D, il coupe beaucoup de coquilles incurvées, pas de tunnels, et son effet se révèle être faux. Autrement dit, en 3D, il est nécessaire de spécifier deux formes fractales de grottes, les deux sont des bruits multifractaux à 1 octave, mais avec des germes différents. À l'aide de Select, définissez-les sur 1 ou 0 et multipliez-les. Ainsi, à l'intersection des fractales, une grotte apparaîtra,et tout le reste restera solide, et l'apparence des tunnels deviendra plus naturelle que l'utilisation d'une seule fractale.

terraintree3d=
 {
	{name="ground_gradient", type="gradient", x1=0, x2=0, y1=0, y2=1},
	
	{name="lowland_shape_fractal", type="fractal", fractaltype=anl.BILLOW, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=2, frequency=0.25},
	{name="lowland_autocorrect", type="autocorrect", source="lowland_shape_fractal", low=0, high=1},
	{name="lowland_scale", type="scaleoffset", source="lowland_autocorrect", scale=0.125, offset=-0.45},
	{name="lowland_y_scale", type="scaledomain", source="lowland_scale", scaley=0},
	{name="lowland_terrain", type="translatedomain", source="ground_gradient", ty="lowland_y_scale"},
	
	{name="highland_shape_fractal", type="fractal", fractaltype=anl.FBM, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=4, frequency=2},
	{name="highland_autocorrect", type="autocorrect", source="highland_shape_fractal", low=-1, high=1},
	{name="highland_scale", type="scaleoffset", source="highland_autocorrect", scale=0.25, offset=0},
	{name="highland_y_scale", type="scaledomain", source="highland_scale", scaley=0},
	{name="highland_terrain", type="translatedomain", source="ground_gradient", ty="highland_y_scale"},

	{name="mountain_shape_fractal", type="fractal", fractaltype=anl.RIDGEDMULTI, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=8, frequency=1},
	{name="mountain_autocorrect", type="autocorrect", source="mountain_shape_fractal", low=-1, high=1},
	{name="mountain_scale", type="scaleoffset", source="mountain_autocorrect", scale=0.45, offset=0.15},
	{name="mountain_y_scale", type="scaledomain", source="mountain_scale", scaley=0.25},
	{name="mountain_terrain", type="translatedomain", source="ground_gradient", ty="mountain_y_scale"},

	{name="terrain_type_fractal", type="fractal", fractaltype=anl.FBM, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=3, frequency=0.125},
	{name="terrain_autocorrect", type="autocorrect", source="terrain_type_fractal", low=0, high=1},
	{name="terrain_type_y_scale", type="scaledomain", source="terrain_autocorrect", scaley=0},
	{name="terrain_type_cache", type="cache", source="terrain_type_y_scale"},
	{name="highland_mountain_select", type="select", low="highland_terrain", high="mountain_terrain", control="terrain_type_cache", threshold=0.55, falloff=0.2},
	{name="highland_lowland_select", type="select", low="lowland_terrain", high="highland_mountain_select", control="terrain_type_cache", threshold=0.25, falloff=0.15},
	{name="highland_lowland_select_cache", type="cache", source="highland_lowland_select"},
	{name="ground_select", type="select", low=0, high=1, threshold=0.5, control="highland_lowland_select_cache"},
	
	{name="cave_attenuate_bias", type="bias", source="highland_lowland_select_cache", bias=0.45},
	{name="cave_shape1", type="fractal", fractaltype=anl.RIDGEDMULTI, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=1, frequency=4},
	{name="cave_shape2", type="fractal", fractaltype=anl.RIDGEDMULTI, basistype=anl.GRADIENT, interptype=anl.QUINTIC, octaves=1, frequency=4},
	{name="cave_shape_attenuate", type="combiner", operation=anl.MULT, source_0="cave_shape1", source_1="cave_attenuate_bias", source_2="cave_shape2"},             
	{name = "cave_perturb_fractal", type = "fractal", fractaltype = anl.FBM, basistype = anl.GRADIENT, interptype = anl.QUINTIC, octaves = 6, fréquence = 3},
	{name = "cave_perturb_scale", type = "scaleoffset", source = "cave_perturb_fractal", scale = 0.5, offset = 0},
	{name = "cave_perturb", type = "translomain", source = "cave_shape_attenuate", tx = "cave_perturb_scale"},
	{name = "cave_select", type = "select", bas = 1, haut = 0, control = "cave_perturb", seuil = 0,48, chute = 0},
	
	{name = "ground_cave_multiply", type = "combiner", operation = anl.MULT, source_0 = "cave_select", source_1 = "ground_select"}
 }

Exemples de résultats:



Il semble que certains paramètres nécessitent un réglage. Il peut être utile de réduire l'atténuation ou d'amincir les grottes, de réduire le nombre d'octaves dans la fractale du relief, de sorte que le relief devienne plus lisse, etc. ... Je répète, tout dépend du résultat que vous souhaitez obtenir.

Source: https://habr.com/ru/post/fr472574/


All Articles