Création de cartes à partir d'entités de bruit

L'un des articles les plus populaires de mon site est consacré à la génération de cartes polygonales ( traduction en Habré). La création de telles cartes nécessite beaucoup d'efforts. Mais je n'ai pas commencé par cela, mais par une tâche beaucoup plus simple, que je décrirai ici. Cette technique simple vous permet de créer de telles cartes en moins de 50 lignes de code:


Je ne vais pas expliquer comment dessiner de telles cartes: cela dépend de la langue, de la bibliothèque graphique, de la plateforme, etc. Je vais simplement expliquer comment remplir le tableau avec des données cartographiques.

Le bruit


La méthode standard pour générer des cartes 2D consiste à utiliser le bruit avec une bande de fréquence limitée comme bloc de construction, comme le bruit Perlin ou le bruit simplex. Voici à quoi ressemble la fonction de bruit:

image

Nous attribuons un nombre de 0,0 à 1,0 à chaque point de la carte. Dans cette image, 0,0 est noir et 1,0 est blanc. Voici comment définir la couleur de chaque point de grille dans la syntaxe d'un langage de type C:

for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 

La boucle fonctionnera de la même manière en Javascript, Python, Haxe, C ++, C #, Java et la plupart des autres langages populaires, je vais donc l'afficher dans une syntaxe de type C afin que vous puissiez la convertir dans la langue dont vous avez besoin. Dans la suite du tutoriel, je montrerai comment le corps du cycle change ( value[y][x]=… ligne value[y][x]=… ) lors de l'ajout de nouvelles fonctions. La démo montrera un exemple complet.

Dans certaines bibliothèques, il sera nécessaire de décaler ou de multiplier les valeurs résultantes afin de les renvoyer dans la plage de 0,0 à 1,0.

La hauteur


Le bruit en lui-même n'est qu'une collection de chiffres. Nous devons lui donner un sens . La première chose à laquelle vous pouvez penser est de lier la valeur du bruit à la hauteur (c'est ce qu'on appelle une «carte de hauteur»). Prenons le bruit montré ci-dessus et dessinons-le en hauteur:



Le code est resté presque le même, à l'exception de la boucle intérieure. Maintenant, cela ressemble à ceci:

 elevation[y][x] = noise(nx, ny); 

Oui, et c'est tout. Les données cartographiques sont restées les mêmes, mais maintenant je vais les appeler elevation (hauteur), pas value .

Nous avons eu beaucoup de collines, mais rien de plus. Qu'est-ce qui ne va pas?

La fréquence


Le bruit peut être généré à n'importe quelle fréquence . Jusqu'à présent, je n'ai choisi qu'une seule fréquence. Voyons comment cela affecte.

Essayez de changer la valeur avec le curseur (dans l'article d'origine) et voyez ce qui se passe à différentes fréquences:


Cela change simplement l'échelle. Au début, cela ne semble pas très utile, mais ce n'est pas le cas. J'ai un autre tutoriel ( traduction en Habré), qui explique la théorie : des concepts tels que la fréquence, l'amplitude, les octaves, le bruit rose et bleu, etc.

 elevation[y][x] = noise(freq * nx, freq * ny); 

Il est également parfois utile de rappeler la longueur d'onde , qui est l'inverse de la grandeur. Lorsque la fréquence est doublée, la taille n'est que de moitié. Le doublement de la longueur d'onde double. La longueur d'onde est la distance mesurée en pixels / tuiles / mètres ou toute autre unité que vous avez sélectionnée pour les cartes. Elle est liée à la fréquence: wavelength = map_size / frequency .

Octaves


Pour rendre la carte des hauteurs plus intéressante, nous ajouterons du bruit avec différentes fréquences :



 elevation[y][x] = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 2 * ny); 

Mélangeons de grandes collines à basse fréquence avec de petites collines à haute fréquence sur une seule carte. Déplacez le curseur (dans l'article d'origine) pour ajouter de petites collines au mélange:


Maintenant, cela ressemble beaucoup plus au soulagement fractal dont nous avons besoin! Nous pouvons obtenir des collines et des montagnes accidentées, mais nous n'avons toujours pas de plaines plates. Pour ce faire, vous avez besoin d'autre chose.

Redistribution


La fonction de bruit nous donne des valeurs comprises entre 0 et 1 (ou de -1 à +1, selon la bibliothèque). Pour créer des plaines plates, nous pouvons augmenter la hauteur à une puissance . Déplacez le curseur (dans l'article d'origine) pour obtenir différents degrés.


 e = 1 * noise(1 * nx, 1 * ny); + 0.5 * noise(2 * nx, 2 * ny); + 0.25 * noise(4 * nx, 4 * ny); elevation[y][x] = Math.pow(e, exponent); 

Des valeurs élevées abaissent les hauteurs moyennes des plaines et des valeurs faibles élèvent les hauteurs moyennes vers les sommets des montagnes. Nous devons les omettre. J'utilise les fonctions de puissance car elles sont plus simples, mais vous pouvez utiliser n'importe quelle courbe; J'ai une démo plus compliquée.

Maintenant que nous avons une carte d'élévation réaliste, ajoutons des biomes!

Biomes


Le bruit donne des chiffres, mais nous avons besoin d'une carte des forêts, des déserts et des océans. La première chose que vous pouvez faire est de transformer de petites hauteurs en eau:


 function biome(e) { if (e < waterlevel) return WATER; else return LAND; } 

Wow, cela devient déjà comme un monde généré par la procédure! Nous avons de l'eau, de l'herbe et de la neige. Et si nous en avons besoin de plus? Faisons une séquence d'eau, de sable, d'herbe, de forêt, de savane, de désert et de neige:



Relief basé sur la hauteur

 function biome(e) { if (e < 0.1) return WATER; else if (e < 0.2) return BEACH; else if (e < 0.3) return FOREST; else if (e < 0.5) return JUNGLE; else if (e < 0.7) return SAVANNAH; else if (e < 0.9) return DESERT; else return SNOW; } 

Wow, ça a l'air super! Pour votre jeu, vous pouvez modifier les valeurs et les biomes. Crysis aura beaucoup plus de jungle; Skyrim a beaucoup plus de glace et de neige. Mais peu importe comment vous changez les chiffres, cette approche est assez limitée. Les types de relief correspondent aux hauteurs, forment donc des bandes. Pour les rendre plus intéressants, nous devons choisir des biomes basés sur autre chose. Créons une deuxième carte de bruit pour l'humidité.



Au-dessus, le bruit des hauteurs; fond - bruit d'humidité

Utilisons maintenant la hauteur et l'humidité ensemble . Dans la première image illustrée ci-dessous, l'axe y est la hauteur (tirée de l'image ci-dessus) et l'axe x est l'humidité (la deuxième image est plus élevée). Cela nous donne une carte convaincante:



Soulagement basé sur deux valeurs de bruit

Les petites hauteurs sont les océans et les côtes. Les grandes hauteurs sont rocheuses et neigeuses. Entre les deux, nous obtenons une large gamme de biomes. Le code ressemble à ceci:

 function biome(e, m) { if (e < 0.1) return OCEAN; if (e < 0.12) return BEACH; if (e > 0.8) { if (m < 0.1) return SCORCHED; if (m < 0.2) return BARE; if (m < 0.5) return TUNDRA; return SNOW; } if (e > 0.6) { if (m < 0.33) return TEMPERATE_DESERT; if (m < 0.66) return SHRUBLAND; return TAIGA; } if (e > 0.3) { if (m < 0.16) return TEMPERATE_DESERT; if (m < 0.50) return GRASSLAND; if (m < 0.83) return TEMPERATE_DECIDUOUS_FOREST; return TEMPERATE_RAIN_FOREST; } if (m < 0.16) return SUBTROPICAL_DESERT; if (m < 0.33) return GRASSLAND; if (m < 0.66) return TROPICAL_SEASONAL_FOREST; return TROPICAL_RAIN_FOREST; } 

Si nécessaire, vous pouvez modifier toutes ces valeurs en fonction des exigences de votre jeu.

Si nous n'avons pas besoin de biomes, des dégradés lisses (voir cet article ) peuvent créer des couleurs:



Pour les biomes et les gradients, une seule valeur de bruit n'offre pas une variabilité suffisante, mais deux suffisent.

Le climat


Dans la section précédente, j'ai utilisé l' altitude comme substitut de la température . Plus la hauteur est élevée, plus la température est basse. Cependant, la latitude géographique affecte également les températures. Utilisons à la fois la hauteur et la latitude pour contrôler la température:


Près des pôles (grandes latitudes), le climat est plus froid et sur les sommets des montagnes (grandes hauteurs), le climat est également plus froid. Jusqu'à présent, je l'ai travaillé pas très dur: pour la bonne approche de ces paramètres, vous avez besoin de beaucoup de réglages subtils.

Il y a aussi le changement climatique saisonnier . En été et en hiver, les hémisphères nord et sud deviennent plus chauds et plus froids, mais à l'équateur la situation ne change pas beaucoup. Beaucoup peut également être fait ici, par exemple, on peut simuler les vents dominants et les courants océaniques, l'effet des biomes sur le climat et l'effet moyen des océans sur les températures.

Les îles


Dans certains projets, j'avais besoin que les bordures de la carte soient de l'eau. Cela transforme le monde en une ou plusieurs îles. Il y a plusieurs façons de le faire, mais j'ai utilisé une solution assez simple dans mon générateur de carte polygonale: j'ai changé la hauteur en e = e + a - b*d^c , où d est la distance du centre (sur une échelle de 0-1). Une autre option consiste à modifier e = (e + a) * (1 - b*d^c) . La constante a élève tout, b abaisse les bords et c contrôle le taux de déclin.


Je ne suis pas entièrement satisfait de cela et beaucoup reste à explorer. Faut-il que ce soit Manhattan ou la distance euclidienne? Doit-elle dépendre de la distance au centre ou de la distance au bord? La distance doit-elle être carrée, linéaire, ou avoir un autre degré? Doit-il s'agir d'une addition / soustraction, ou d'une multiplication / division, ou autre chose? Dans l'article d'origine, essayez Ajouter, a = 0,1, b = 0,3, c = 2,0, ou essayez Multiplier, a = 0,05, b = 1,00, c = 1,5. Les options qui vous conviennent dépendent de votre projet.

Pourquoi s'en tenir aux fonctions mathématiques standard? Comme je l'ai dit dans mon article sur les dégâts en RPG ( traduction sur Habré), tout le monde (y compris moi) utilise des fonctions mathématiques, comme les polynômes, les distributions exponentielles, etc., mais sur l'ordinateur on ne peut pas se limiter à elles. Nous pouvons prendre n'importe quelle fonction de formation et l'utiliser ici, en utilisant la table de recherche e = e + height_adjust[d] . Jusqu'à présent, je n'ai pas étudié cette question.

Bruit épineux


Au lieu d'augmenter la hauteur à une puissance, nous pouvons utiliser la valeur absolue pour créer des pics nets:

 function ridgenoise(nx, ny) { return 2 * (0.5 - abs(0.5 - noise(nx, ny))); } 

Pour ajouter des octaves, nous pouvons faire varier les amplitudes des hautes fréquences afin que seules les montagnes reçoivent le bruit supplémentaire:

 e0 = 1 * ridgenoise(1 * nx, 1 * ny); e1 = 0.5 * ridgenoise(2 * nx, 2 * ny) * e0; e2 = 0.25 * ridgenoise(4 * nx, 4 * ny) * (e0+e1); e = e0 + e1 + e2; elevation[y][x] = Math.pow(e, exponent); 


Je n'ai pas beaucoup d'expérience avec cette technique, j'ai donc besoin d'expérimenter pour apprendre à bien l'utiliser. Il peut également être intéressant de mélanger du bruit de basse fréquence hérissé avec du bruit de haute fréquence non hérissé.

Terrasses


Si nous arrondissons la hauteur aux n niveaux suivants, nous obtenons des terrasses:


Ceci est le résultat de l'application de la fonction de redistribution de la hauteur sous la forme e = f(e) . Ci-dessus, nous avons utilisé e = Math.pow(e, exponent) pour aiguiser les sommets des montagnes; ici, nous utilisons e = Math.round(e * n) / n pour créer des terrasses. Si vous utilisez une fonction sans marche, les terrasses peuvent être arrondies ou se produire uniquement à certaines hauteurs.

Placement des arbres


Habituellement, nous avons utilisé le bruit fractal pour la hauteur et l'humidité, mais il peut également être utilisé pour placer des objets inégalement espacés, tels que des arbres et des pierres. Pour la hauteur, nous utilisons des amplitudes élevées avec des fréquences basses («bruit rouge»). Pour placer des objets, vous devez utiliser des amplitudes élevées avec des fréquences élevées ("bruit bleu"). À gauche, un motif de bruit bleu; à droite, les endroits où le bruit est supérieur aux valeurs adjacentes:


 for (int yc = 0; yc < height; yc++) { for (int xc = 0; xc < width; xc++) { double max = 0; //     for (int yn = yc - R; yn <= yc + R; yn++) { for (int xn = xc - R; xn <= xc + R; xn++) { double e = value[yn][xn]; if (e > max) { max = e; } } } if (value[yc][xc] == max) { //    xc,yc } } } 

En choisissant différents R pour chaque biome, nous pouvons obtenir une densité d'arbres variable:



C'est génial qu'un tel bruit puisse être utilisé pour placer des arbres, mais d'autres algorithmes sont souvent plus efficaces et créent une distribution plus uniforme: taches de Poisson, carreaux de Van ou tramage graphique.

À l'infini et au-delà


Les calculs du biome à la position (x, y) sont indépendants des calculs de toutes les autres positions. Ce calcul local a deux propriétés pratiques: il peut être calculé en parallèle et il peut être utilisé pour un terrain sans fin. Placez le curseur de la souris sur la mini-carte (dans l'article d'origine) à gauche pour générer la carte à droite. Vous pouvez générer n'importe quelle partie de la carte sans générer (et même sans stocker) la carte entière.



Implémentation


L'utilisation du bruit pour générer du terrain est une solution populaire, et sur Internet, vous pouvez trouver des didacticiels pour de nombreuses langues et plates-formes différentes. Le code pour générer des cartes dans différentes langues est approximativement le même. Voici la boucle la plus simple en trois langues différentes:

  • Javascript:

     let gen = new SimplexNoise(); function noise(nx, ny) { // Rescale from -1.0:+1.0 to 0.0:1.0 return gen.noise2D(nx, ny) / 2 + 0.5; } let value = []; for (let y = 0; y < height; y++) { value[y] = []; for (let x = 0; x < width; x++) { let nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 
  • C ++:

     module::Perlin gen; double noise(double nx, double ny) { // Rescale from -1.0:+1.0 to 0.0:1.0 return gen.GetValue(nx, ny, 0) / 2.0 + 0.5; } double value[height][width]; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { double nx = x/width - 0.5, ny = y/height - 0.5; value[y][x] = noise(nx, ny); } } 
  • Python:

     from opensimplex import OpenSimplex gen = OpenSimplex() def noise(nx, ny): # Rescale from -1.0:+1.0 to 0.0:1.0 return gen.noise2d(nx, ny) / 2.0 + 0.5 value = [] for y in range(height): value.append([0] * width) for x in range(width): nx = x/width - 0.5 ny = y/height - 0.5 value[y][x] = noise(nx, ny) 

Toutes les bibliothèques de bruit se ressemblent à peu près. Essayez opensimplex pour Python , ou libnoise pour C ++ , ou simplex-noise pour Javascript. Pour la plupart des langues populaires, il existe de nombreuses bibliothèques de bruit. Ou vous pouvez apprendre comment fonctionne le bruit Perlin ou réaliser le bruit vous-même. Je ne l'ai pas fait.

Dans différentes bibliothèques de bruit pour votre langue, les détails de l'application peuvent varier légèrement (certains nombres de retour dans la plage de 0,0 à 1,0, d'autres dans la plage de -1,0 à +1,0), mais l'idée de base est la même. Pour un vrai projet, vous devrez peut-être envelopper la fonction de noise et l'objet gen dans une classe, mais ces détails ne sont pas pertinents, donc je les ai rendus globaux.

Pour un projet aussi simple, peu importe le bruit que vous utilisez: bruit Perlin, bruit simplex, bruit OpenSimplex, bruit de valeur, décalage médian, algorithme diamant ou transformée de Fourier inverse. Chacun d'eux a ses avantages et ses inconvénients, mais pour un générateur de cartes similaire, ils créent tous plus ou moins les mêmes valeurs de sortie.

Le rendu de la carte dépend de la plateforme et du jeu, donc je ne l'ai pas implémenté; ce code n'est nécessaire que pour générer des hauteurs et des biomes, dont le rendu dépend du style utilisé dans le jeu. Vous pouvez le copier, le porter et l'utiliser dans vos projets.

Les expériences


J'ai regardé le mélange d'octaves, l'augmentation des degrés à une puissance et la combinaison des hauteurs avec l'humidité pour créer un biome. Ici, vous pouvez étudier un graphique interactif qui vous permet d'expérimenter avec tous ces paramètres, qui montre en quoi consiste le code:


Voici un exemple de code:

 var rng1 = PM_PRNG.create(seed1); var rng2 = PM_PRNG.create(seed2); var gen1 = new SimplexNoise(rng1.nextDouble.bind(rng1)); var gen2 = new SimplexNoise(rng2.nextDouble.bind(rng2)); function noise1(nx, ny) { return gen1.noise2D(nx, ny)/2 + 0.5; } function noise2(nx, ny) { return gen2.noise2D(nx, ny)/2 + 0.5; } for (var y = 0; y < height; y++) { for (var x = 0; x < width; x++) { var nx = x/width - 0.5, ny = y/height - 0.5; var e = (1.00 * noise1( 1 * nx, 1 * ny) + 0.50 * noise1( 2 * nx, 2 * ny) + 0.25 * noise1( 4 * nx, 4 * ny) + 0.13 * noise1( 8 * nx, 8 * ny) + 0.06 * noise1(16 * nx, 16 * ny) + 0.03 * noise1(32 * nx, 32 * ny)); e /= (1.00+0.50+0.25+0.13+0.06+0.03); e = Math.pow(e, 5.00); var m = (1.00 * noise2( 1 * nx, 1 * ny) + 0.75 * noise2( 2 * nx, 2 * ny) + 0.33 * noise2( 4 * nx, 4 * ny) + 0.33 * noise2( 8 * nx, 8 * ny) + 0.33 * noise2(16 * nx, 16 * ny) + 0.50 * noise2(32 * nx, 32 * ny)); m /= (1.00+0.75+0.33+0.33+0.33+0.50); /* draw biome(e, m) at x,y */ } } 

Il y a une difficulté: pour le bruit des hauteurs et de l'humidité, il est nécessaire d'utiliser une graine différente, sinon ils se révéleront identiques et les cartes n'auront pas l'air si intéressantes. En Javascript, j'utilise la bibliothèque prng-parkmiller ; en C ++, vous pouvez utiliser deux objets linear_congruential_engine distincts; en Python, vous pouvez créer deux instances distinctes d'une classe random.Random .

Réflexions


J'aime cette approche de la génération de cartes pour sa simplicité . Il est rapide et nécessite très peu de code pour produire des résultats décents.

Je n'aime pas ses limites dans cette approche. Les calculs locaux signifient que chaque point est indépendant de tous les autres. Différentes zones de la carte ne sont pas connectées les unes aux autres . Chaque endroit sur la carte «semble» le même. Il n'y a pas de restrictions mondiales, par exemple, «il devrait y avoir de 3 à 5 lacs sur la carte» ou des caractéristiques mondiales, comme une rivière qui coule du haut du plus haut sommet vers l'océan. De plus, je n'aime pas le fait que pour obtenir une bonne image, vous devez configurer les paramètres pendant longtemps.

Pourquoi est-ce que je le recommande? Je pense que c'est un bon point de départ, en particulier pour les jeux indépendants et les jams de jeu. Deux de mes amis ont écrit la version initiale de Realm of the Mad God en seulement 30 jours pour un concours de jeux . Ils m'ont demandé d'aider à créer des cartes. J'ai utilisé cette technique (plus quelques fonctionnalités supplémentaires qui se sont avérées peu utiles) et j'ai fait une carte pour elles. Quelques mois plus tard, après avoir reçu les retours des joueurs et étudié attentivement la conception du jeu, nous avons créé un générateur de carte plus avancé basé sur les polygones de Voronoï, décrit ici ( traduction en Habré). Ce générateur de cartes n'utilise pas les techniques décrites dans cet article. Il utilise le bruit pour créer des cartes d'une manière complètement différente.

Information additionnelle


Il y a beaucoup de choses intéressantes que vous pouvez faire avec les fonctions de bruit. Si vous effectuez une recherche sur Internet, vous pouvez trouver des options telles que la turbulence, la houle, le multifractal strié, l'amortissement d'amplitude, les terrasses, le bruit de voronoi, les dérivés analytiques, la déformation de domaine et autres. Vous pouvez utiliser cette page comme source d'inspiration. Je ne les considère pas ici, mon article se concentre sur la simplicité.

Ce projet a été influencé par mes précédents projets de génération de cartes:

  • J'ai utilisé le bruit global de Perlin pour mon premier générateur de cartes Realm of the Mad God . Nous l'avons utilisé pendant les six premiers mois du test alpha, puis l'avons remplacé par un générateur de carte sur les polygones Voronoi , spécialement créé pour les exigences de gameplay que nous avons déterminées lors du test alpha. Les biomes et leurs couleurs pour l'article sont issus de ces projets.
  • Lors de l'étude du traitement des signaux audio, j'ai écrit un tutoriel sur le bruit qui explique des concepts tels que la fréquence, l'amplitude, les octaves et la «couleur» du bruit. Les mêmes concepts qui fonctionnent pour le son s'appliquent également à la génération de cartes basée sur le bruit. À ce moment-là, j'ai créé une génération de secours de démonstration brute, mais je ne les ai pas terminées.
  • Parfois, j'expérimente pour trouver des limites. Je voulais savoir combien de code est minimalement nécessaire pour créer des cartes convaincantes. Dans ce mini-projet, j'ai atteint zéro ligne de code - tout se fait avec des filtres d'image (turbulence, seuils, dégradés de couleurs). Cela m'a rendu heureux et triste. Dans quelle mesure la génération de cartes peut-elle être effectuée par des filtres d'image? En assez gros. Tout ce qui est décrit ci-dessus sur le "schéma de dégradés de couleurs lisses" est tiré de cette expérience. La couche de bruit est un filtre d'image à turbulence; les octaves sont des images superposées les unes aux autres; L'outil de degré est appelé «correction de courbe» dans Photoshop.

Ce qui me dérange un peu, c'est que la plupart du code que les développeurs de jeux écrivent pour la génération de terrain basée sur le bruit (y compris le déplacement au milieu) se révèle être le même que dans les filtres de son et d'image. D'un autre côté, cela crée des résultats assez décents en quelques lignes de code, c'est pourquoi j'ai écrit cet article. Il s'agit d'un point de référence rapide et facile . Habituellement, je n'utilise pas de telles cartes depuis longtemps, mais remplacez-les par un générateur de cartes plus complexe dès que je découvre quels types de cartes conviennent mieux à la conception du jeu. Pour moi, c'est un modèle standard: commencer par quelque chose d'extrêmement simple, puis le remplacer uniquement après avoir mieux compris le système avec lequel je travaille.

Il y a beaucoup plus de choses qui peuvent être faites avec du bruit, dans l'article, je n'en ai mentionné que quelques-unes. Essayez Noise Studio pour tester de manière interactive diverses fonctionnalités.

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


All Articles