Génération procédurale de carte du monde - Thêta Tau Tau - 25-12-2012
Génération procédurale de cartes du monde
en javascript (canvas)
Je viens ici partager un petit script de génération procédurale que j'ai fait ce week-end. Il ne gère que l'altitude donc exit les forêts, rivières etc. mais le paramétrage permet d'obtenir une grande diversité de formes : continents, îles, lacs... (mais trouver le bon jeu de paramètres n'est pas une mince affaire).
L'idée du script est de partir d'une petite image qui donne la forme générale de ce qu'on souhaite obtenir, l’algorithme diamond-square venant ajouter une part d'aléatoire à la forme finale.
Par exemple l'image ci-dessus a été obtenue en partant de celle-ci :
Le script actuel ne gère que les cartes à cases carrées, si je fini le script pour les cases hexagonales je le posterais également.
Mode d'emploi :- Compatibilité : IE et Firefox. Chrome n'est pas compatible.
- Fonctionement :
- Enregistrez le script au format .html
- Dans le même dossier, mettez une image nommée base.png (par exemple celle-ci : ).
- Ouvrez la page avec votre navigateur
- Cliquez sur générer et la carte apparaitra, clic droit dessus pour l'enregistrer.
- Utilisation : faîtes ce que vous voulez des cartes générées. Quant au script lui-même si vous souhaitez l'utiliser sur votre site, demandez moi d'abord.
- Paramétrage :
- L'image base.png doit être en niveaux de gris, le noir représentant les points les plus bas et le blanc les points les plus hauts. Si vous voulez un continent, pensez à mettre du noir tout autours.
- Le nombre d'itérations de l’algorithme diamond-square, détermine la taille finale de l'image. Sa largeur sera de (largeur de base.png - 1)^nombre d'itérations + 1. Attention, le temps d'éxécution augmente exponentiellement avec ce paramètre.
- La rugosité : augmentez la valeur pour un relief plus accidenté ou si le résultat est trop proche de l'image de base. A l'inverse réduisez la pour vous rapprocher de l'image de base.
- Les types de terrains : j’obtiens de meilleurs résultats en mettant beaucoup de mer, pour le reste c'est surtout esthétique.
Script
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Map</title>
</head>
<body>
<p> Image de base : <img id="base" src="base.png">
(Créez un fichier .pnj représentant grossièrement le relief voulu en niveaux de gris)</p>
<form id="params" method="post">
<p><label for="Iterations">Itérations</label> : <input type="text" name="Iterations" value ="5">
(A chaque itération la résolution de la carte est doublée)</p>
<p><label for="Roughness">Rugosité</label> : <input type="text" name="Roughness" value ="0.2"/>
(Mettre une forte rugosité pour un relief accidenté, et inversement)</p>
<h3>Types de terrain</h3>
<table>
<tr><td>Poids</td><td>Couleur</td></tr>
<tr><td><input type="text" name="Weigth1" value ="500"/></td><td><input type="text" name="Color1" value ="#378696"/></td></tr>
<tr><td><input type="text" name="Weigth2" value ="20"/></td><td><input type="text" name="Color2" value ="#56C5D8"/></td></tr>
<tr><td><input type="text" name="Weigth3" value ="5"/></td><td><input type="text" name="Color3" value ="#A3FFEF"/></td></tr>
<tr><td><input type="text" name="Weigth4" value ="1"/></td><td><input type="text" name="Color4" value ="#FFF76B"/></td></tr>
<tr><td><input type="text" name="Weigth5" value ="10"/></td><td><input type="text" name="Color5" value ="#43A52B"/></td></tr>
<tr><td><input type="text" name="Weigth6" value ="2"/></td><td><input type="text" name="Color6" value ="#C66531"/></td></tr>
<tr><td><input type="text" name="Weigth7" value ="0.2"/></td><td><input type="text" name="Color7" value ="#FFFFFF"/></td></tr>
<tr><td><input type="text" name="Weigth8" value ="0"/></td><td><input type="text" name="Color8" value =""/></td></tr>
<tr><td><input type="text" name="Weigth9" value ="0"/></td><td><input type="text" name="Color9" value =""/></td></tr>
</table>
<p>
Le poids représente la fréquence du terrain,
par exemple si on met 1 pour la terre et 2 pour la mer on aura 2 fois plus de mer que de terre.<br />
Rentrez les terrains du plus bas (mer) au plus haut (montagne).<br />
Les poids doivent être adaptés selon l\'image de base.
</p>
<p><input type="button" value="Générer!" onclick="run()"/></p>
</form>
<canvas id="map" width="50" height="50">
</canvas>
<p id="debug"></p>
<script>
//Transforme un objet image en une matrice contenant les niveaux de gris (enfin, de rouge, mais ça reviens au même).
//Les valeurs sont normalisée pour varier entre 0 et 1 et non entre 0 et 255.
function ImgToGreyLevelMatrix(Img){
//On met l'image dans un canvas pour pouvoir accéder aux valeurs des pixels.
var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
context.drawImage(Img, 0, 0);
var data = context.getImageData(0, 0, Img.width, Img.height).data;
//On créé une matrice représentant les niveaux de gris.
var Z = []
for(var x = 0; x < Img.width; x++){
Z[x] = [];
for(var y = 0; y < Img.height; y++){
Z[x][y] = 100 + data[(x + Img.width*y)*4]/255;
}
}
return Z;
}
//Algorithme de diamond-square
//A partir d'une matrice Z de dimentions x et y créé une matrice newZ de dimentions (x-1)*2+1 et (y-1)*2+1
//La valeur de chaque case découle de la moyenne des cases qui l'entour auquel on ajoutte de l'aléatoire
//Si la fonction est appelée plusieurs fois il faut diviser par 2 le paramètre scaledRoughness à chaque itération
//pour respecter le changement d'échelle
//Cf. google pour plus d'infos.
function diamondSquare(Z, scaledRoughness){
//On gènère une matrice de taille supérieure
newZ = []
for(var x = 0; x < (Z.length - 1)*2 + 1; x++){
newZ[x] = []
for(var y = 0; y < (Z[0].length - 1)*2 + 1; y++){
//Points déjà existants
if( x%2 == 0 && y%2 == 0){
newZ[x][y] = Z[x/2][y/2];
}else if( x%2 == 1 && y%2 == 1){//Phase du carré
newZ[x][y] = (Z[(x-1)/2][(y-1)/2]+Z[(x+1)/2][(y-1)/2]+Z[(x-1)/2][(y+1)/2]+Z[(x+1)/2][(y+1)/2])/4+scaledRoughness*(Math.random()-0.5);;
}else{
newZ[x][y] = -1;//On remplira ces cases lors de la phase diamant.
}
}
}
//phase du diamant
var width = newZ.length;
var height = newZ[0].length;
//Bords supérieurs et inférieurs (3 points)
for(var x = 1; x < width; x += 2){
newZ[x][0] = ( newZ[x-1][0] + newZ[x+1][0] + newZ[x][1] ) /3 + scaledRoughness*(Math.random()-0.5);;
newZ[x][height-1] = ( newZ[x-1][height-1] + newZ[x+1][height-1] + newZ[x][height-2] ) /3 + scaledRoughness*(Math.random()-0.5);;
}
//Pareil pour les bords gauche et droit
for(var y = 1; y < height; y += 2){
newZ[0][y] = ( newZ[0][y-1] + newZ[0][y+1] + newZ[1][y] ) /3 + scaledRoughness*(Math.random()-0.5);;
newZ[width-1][y] = ( newZ[width-1][y-1] + newZ[width-1][y+1] + newZ[width-2][y] ) /3 + scaledRoughness*(Math.random()-0.5);;
}
//Le reste (4 points donc)
for(var x = 2; x < width-2; x += 2){
for(var y = 1; y < height-1; y += 2){
newZ[x][y] = ( newZ[x-1][y] + newZ[x+1][y] + newZ[x][y-1] + newZ[x][y+1] ) /4 + scaledRoughness*(Math.random()-0.5);;
}
}
for(var x = 1; x < width-1; x += 2){
for(var y = 2; y < height-2; y += 2){
newZ[x][y] = ( newZ[x-1][y] + newZ[x+1][y] + newZ[x][y-1] + newZ[x][y+1] ) /4 + scaledRoughness*(Math.random()-0.5);;
}
}
return newZ;
}
//Défini des seuils tels qu'il y ait un nombre donné de valeurs dans la matrice qui soient entre ces seuils.
//Par exemple si weigths = [1,2,3], renvoie 3 seuils tels que :
// - 1/6 des valeurs de la matrice soient inférieures au premier seuil
// - 3/6 des valeurs de la matrice soient inférieures au deuxième seuil
// - toutes les valeurs soient inférieures au 3ème seuil.
function threshForWantedProportions(Z,weigths){
//On créé un tableau avec toutes les valeurs de la matrice
var zValues = [];
for (var x = 0; x < Z.length; x++){
for(var y = 0; y < Z[0].length; y++){
if(!Z[x][y]){alert(x+";"+y)}
zValues.push(Z[x][y])
}
}
zValues.sort(function(a,b){return a-b});
var tot = weigths.reduce(function(a,b){return a + b;});
var thresh = [];
var w = 0;
for(var t = 0; t < weigths.length; t++){
w += weigths[t];
thresh[t] = zValues[Math.floor(zValues.length*w/tot)];
}
return thresh;
}
function drawMatrix(m,canvas){
var width = m.length;
var height = m[0].length;
canvas.width = width;
canvas.height = height;
var context = canvas.getContext('2d');
for(x = 0; x < width; x++){
for(y = 0; y < height; y++){
context.fillStyle = m[x][y];
context.fillRect(x,y,1,1);
}
}
}
function run(){
var baseImg = document.images.base;
var Z = ImgToGreyLevelMatrix(baseImg);
var roughness = parseFloat(document.getElementsByName("Roughness")[0].value);
var iterations = parseInt(document.getElementsByName("Iterations")[0].value);
for(var n = 1; n <= iterations; n++){
Z = diamondSquare(Z,roughness);
roughness /= 2;
}
//On récupère les poids et les couleurs
var form = document.getElementById('params');
var weigths = [];
var colors = [];
for(var k = 1; k <= 9; k++){
if(document.getElementsByName("Weigth"+k)[0].value && document.getElementsByName("Color"+k)[0].value){
weigths.push(parseFloat(document.getElementsByName("Weigth"+k)[0].value));
colors.push(document.getElementsByName("Color"+k)[0].value);
}
}
//On fixe les seuils
var thresh = threshForWantedProportions(Z,weigths)
//On créé une matrice de couleurs grâce à ces seuils
mat = [];
for(var x = 0; x < Z.length; x++){
mat[x] = [];
for(var y = 0; y < Z[0].length; y++){
var T = 0;
while(Z[x][y] > thresh[T]){T++;}//On cherche le seuil correspondant à l'altitude
mat[x][y] = colors[T];//On met la couleur correspondant au seuil
}
}
//On affiche l'image
drawMatrix(mat,document.getElementById("map"));
}
</script>
</body>
</html>
RE: Génération procédurale de carte du monde - niahoo - 25-12-2012
Vraiment pas mal, ça et l'autre topic.
Est-ce que tu saurais extraire ton algorithme de génération de cases afin que l'on puisse librement choisir son moteur de rendu ? Je serais intéressé pour travailler avec SVG.
RE: Génération procédurale de carte du monde - Thêta Tau Tau - 25-12-2012
Je vais essayer de réorganiser un peu le code en le découpant en fonctions pour que ce soit plus clair (c'est vrai que c'est codé à un peu à l'arrache et que ça doit pas être évident de s'y retrouver/le modifier).
EDIT : voilà c'est fait, toutes les fonctions sont génériques sauf run() qui est spécifique.
|