terrain-map
ARCHIVED - repo for blog post http://www.vogt.world/writing/procedural-terrain-generation/
git clone https://git.vogt.world/terrain-map.git
Log | Files | README.md
← All files
name: landmap.js
-rw-r--r--
16119
  1function indexOfMax(arr) {
  2  if (arr.length === 0) {
  3    return -1;
  4  }
  5
  6  var max = arr[0];
  7  var maxIndex = 0;
  8
  9  for (var ind = 1; ind < arr.length; ind++) {
 10    if (arr[ind] > max) {
 11      maxIndex = ind;
 12      max = arr[ind];
 13    }
 14  }
 15  return maxIndex;
 16}
 17
 18function indexOfMin(arr) {
 19  if (arr.length === 0) {
 20    return -1;
 21  }
 22
 23  var min = arr[0];
 24  var minIndex = 0;
 25
 26  for (var ind = 1; ind < arr.length; ind++) {
 27    if (arr[ind] < min) {
 28      minIndex = ind;
 29      min = arr[ind];
 30    }
 31  }
 32  return minIndex;
 33}
 34
 35// options can be a serialized LandMap
 36function LandMap(options) {
 37  var level = 8;
 38  this.containerId = options.containerId;
 39  this.size = Math.pow(2, level) + 1;
 40  this.max = this.size - 1;
 41  this.maps = options.maps || {};
 42  this.meta = options.meta || {}
 43}
 44
 45LandMap.prototype.get = function(which, x, y) {
 46  if (x < 0 || x > this.max || y < 0 || y > this.max) {
 47    return -1;
 48  } else {
 49    return this.maps[which][x + this.size * y];
 50  }
 51};
 52
 53LandMap.prototype.set = function(which, x, y, value) {
 54  this.maps[which][(x + this.size * y)] = value;
 55};
 56
 57LandMap.prototype.generate = function(options) {
 58  var deviationAmount = options.deviation,
 59    feature = options.feature;
 60  this.meta[options.feature] = options;
 61  var self = this;
 62
 63  if (!(feature in self.maps)) {
 64    this.maps[feature] = new Array(this.size * this.size);
 65  }
 66
 67  this.set(feature, 0, 0, Math.random() * self.max);
 68  this.set(feature, this.max, 0, Math.random() * self.max);
 69  this.set(feature, this.max, this.max, Math.random() * self.max);
 70  this.set(feature, 0, this.max, Math.random() * self.max);
 71
 72  subdivide(this.max);
 73
 74  function subdivide(size) {
 75    var x, y, half = size / 2;
 76    var scale = deviationAmount * size;
 77    if (half < 1) return;
 78
 79    for (y = half; y < self.max; y += size) {
 80      for (x = half; x < self.max; x += size) {
 81        square(feature, x, y, half, Math.random() * scale * 2 - scale);
 82      }
 83    }
 84    for (y = 0; y <= self.max; y += half) {
 85      for (x = (y + half) % size; x <= self.max; x += size) {
 86        diamond(feature, x, y, half, Math.random() * scale * 2 - scale);
 87      }
 88    }
 89    subdivide(size / 2);
 90  }
 91
 92  function average(values) {
 93    var valid = values.filter(function(val) {
 94      return val !== -1;
 95    });
 96    var total = valid.reduce(function(sum, val) {
 97      return sum + val;
 98    }, 0);
 99    return total / valid.length;
100  }
101
102  function square(which, x, y, size, offset) {
103    var ave = average([
104      self.get(which, x - size, y - size), // upper left
105      self.get(which, x + size, y - size), // upper right
106      self.get(which, x + size, y + size), // lower right
107      self.get(which, x - size, y + size) // lower left
108    ]);
109    self.set(which, x, y, ave + offset);
110  }
111
112  function diamond(which, x, y, size, offset) {
113    var ave = average([
114      self.get(which, x, y - size), // top
115      self.get(which, x + size, y), // right
116      self.get(which, x, y + size), // bottom
117      self.get(which, x - size, y) // left
118    ]);
119    self.set(which, x, y, ave + offset);
120  }
121};
122
123LandMap.prototype.smooth = function(options) {
124  var amount = options.amount,
125    featureFrom = options.from,
126    featureTo = options.to;
127
128  this.meta[featureTo] = options;
129
130  if (!(featureTo in this.maps)) {
131    this.maps[featureTo] = new Array(this.size * this.size);
132  }
133
134  var self = this;
135  var MARGIN = amount;
136  for (var y = 0; y < this.size; y++) {
137    for (var x = 0; x < this.size; x++) {
138      var nextValue = self.get(featureFrom, x, y);
139      var nextValueCount = 0;
140      for (var xRange = Math.max(x - Math.round(MARGIN), 0); xRange < Math.min(x + Math.round(MARGIN), this.size); xRange++) {
141        for (var yRange = Math.max(y - Math.round(MARGIN), 0); yRange < Math.min(y + Math.round(MARGIN), this.size); yRange++) {
142          if (distanceBetween(x, y, xRange, yRange) <= MARGIN) {
143            var value = self.get(featureFrom, xRange, yRange);
144            nextValue = nextValue + value;
145            nextValueCount++;
146          }
147        }
148      }
149      var finalVal = ((nextValue / nextValueCount));
150      self.set(featureTo, x, y, finalVal);
151    }
152  }
153
154  function distanceBetween(ax, ay, bx, by) {
155    return Math.abs(Math.sqrt((ax - bx) * (ax - bx) + (ay - by) * (ay - by)));
156  }
157};
158
159LandMap.prototype.combine = function(options) {
160  var one = options.one,
161    two = options.two,
162    three = options.three,
163    result = options.result;
164
165  this.meta[result] = options;
166
167  function percent(value, max, min) {
168    return value / Math.abs((max - min));
169  }
170
171  if (!(result in this.maps)) {
172    this.maps[result] = new Array(this.size * this.size);
173  }
174
175  var max = Number.MIN_SAFE_INTEGER;
176  var min = Number.MAX_SAFE_INTEGER;
177  for (var y = 0; y < this.size; y++) {
178    for (var x = 0; x < this.size; x++) {
179      var val = this.get(two, x, y);
180      if (val !== undefined) {
181        max = Math.max(max, val);
182        min = Math.min(min, val);
183      }
184    }
185  }
186
187  for (var y = 0; y < this.size; y++) {
188    for (var x = 0; x < this.size; x++) {
189      var featureOne = this.get(one, x, y);
190      var featureTwo = this.get(two, x, y);
191      var featureThree = this.get(three, x, y);
192      if (featureOne !== undefined && featureTwo !== undefined && featureThree !== undefined) {
193        var featureThreePercent = percent(featureThree, min, max);
194        var val = (1 - featureThreePercent) * featureOne + featureThreePercent * featureTwo;
195        this.set(result, x, y, val);
196      }
197    }
198  }
199};
200
201LandMap.prototype.grd = function(options) {
202  var amount = options.amount,
203    percent = options.percent,
204    featureFrom = options.from,
205    featureTo = options.to;
206
207  this.meta[featureTo] = options;
208
209  this.maps[featureTo] = new Array(this.size * this.size);
210
211  for (var y = 0; y < this.size; y++) {
212    for (var x = 0; x < this.size; x++) {
213      this.set(featureTo, x, y, this.get(featureFrom, x, y));
214    }
215  }
216
217  var operationAray = new Array(this.size * this.size);
218  for (var y = 0; y < this.size; y++) {
219    for (var x = 0; x < this.size; x++) {
220      operationAray[(x + this.size * y)] = [0];
221    }
222  }
223
224  var MARGIN = amount;
225  for (var i = 0; i < MARGIN; i++) {
226    operationAray[(x + this.size * y)] = [0];
227    var max = Number.MIN_SAFE_INTEGER;
228    var min = Number.MAX_SAFE_INTEGER;
229    for (var y = 0; y < this.size; y++) {
230      for (var x = 0; x < this.size; x++) {
231        var val = this.get(featureTo, x, y);
232        if (val !== undefined) {
233          max = Math.max(max, val);
234          min = Math.min(min, val);
235        }
236      }
237    }
238
239    // iterate through all
240    // for (var xRange = Math.max(x - Math.round(MARGIN), 0); xRange < Math.min(x + Math.round(MARGIN), this.size); xRange++) {
241    //   for (var yRange = Math.max(y - Math.round(MARGIN), 0); yRange < Math.min(y + Math.round(MARGIN), this.size); yRange++) {
242    for (var y = 1; y < this.size-2; y++) {
243      for (var x = 1; x < this.size-2; x++) {
244        var neighbors = [
245          this.get(featureTo, x - 1, y),
246          this.get(featureTo, x + 1, y),
247          this.get(featureTo, x, y - 1),
248          this.get(featureTo, x, y + 1)
249        ];
250        operationAray[(x + this.size * y)].push(this.get(featureTo, x, y));
251        var index = indexOfMax(featureTo);
252        var thisValue = this.get(featureTo, x, y);
253        if (neighbors[index] > thisValue) {
254          if (index == 0) {
255            // WEST (left)
256            operationAray[(x + this.size * y)].push(this.get(featureTo, x - 1, y) * percent);
257            operationAray[((x - 1) + this.size * y)].push(this.get(featureTo, x - 1, y) * (percent) * (-1));
258          } else if (index == 1) {
259            // EAST (right)
260            operationAray[(x + this.size * y)].push(this.get(featureTo, x + 1, y) * percent);
261            operationAray[((x + 1) + this.size * y)].push(this.get(featureTo, x + 1, y) * (percent) * (-1));
262          } else if (index == 2) {
263            // NORTH (up)
264            operationAray[(x + this.size * y)].push(this.get(featureTo, x, y - 1) * percent);
265            operationAray[(x + this.size * (y - 1))].push(this.get(featureTo, x - 1, y) * (percent) * (-1));
266          } else if (index == 3) {
267            // SOUTH (down)
268            operationAray[(x + this.size * y)].push(this.get(featureTo, x, y + 1) * percent);
269            operationAray[(x + this.size * (y + 1))].push(this.get(featureTo, x, y + 1) * (percent) * (-1));
270          }
271        }
272      }
273    }
274    //iterate through summing the operationAray, and setting it
275    for (var y = 1; y < this.size - 2; y++) {
276      for (var x = 1; x < this.size - 2; x++) {
277        var value = operationAray[(x + this.size * y)].reduce(function(a, b) {
278          return a + b;
279        }, 0);
280        this.set(featureTo, x, y, value);
281        operationAray[(x + this.size * y)] = [value];
282      }
283    }
284  }
285};
286
287LandMap.prototype.simpleErosion = function(options) {
288  var Kq = options.carryingCapacity;
289  var Kd = options.depositionSpeed;
290  var iterations = options.iterations;
291  var drops = options.drops;
292  var one = options.from;
293  var two = options.to;
294
295  this.meta[two] = options;
296
297  var HeightMap = new Array(this.size * this.size);
298  for (var y = 0; y < this.size; y++) {
299    for (var x = 0; x < this.size; x++) {
300      HeightMap[(x + this.size * y)] = this.get(one, x, y);
301    }
302  }
303
304  var HMAP_SIZE = this.size;
305
306  function HMAP_INDEX(x, y) {
307    var val = (x + HMAP_SIZE * y)
308    return val;
309  }
310
311  function HMAP_VALUE(x, y) {
312    return HeightMap[(x + HMAP_SIZE * y)];
313  }
314
315  function DEPOSIT_AT(X, Y) {
316    var c = 0.0;
317    var v = 1.05;
318    var maxVelocity = 10.0;
319
320    // For the number of iterations
321    for (var iter = 0; iter < iterations; iter++) {
322      v = Math.min(v, maxVelocity); // limiting velocity
323      var val = HMAP_VALUE(X, Y);
324      var nv = [
325        HMAP_VALUE(X, Y - 1), //NORTH
326        HMAP_VALUE(X, Y + 1), //SOUTH
327        HMAP_VALUE(X + 1, Y), //EAST
328        HMAP_VALUE(X - 1, Y) //WEST
329      ];
330
331      var minInd = indexOfMin(nv);
332      // if the lowest neighbor is NOT greater than the current value
333      if (nv[minInd] < val) {
334        //deposit or erode
335        var vtc = Kd * v * Math.abs(nv[minInd]); // value to steal is depositionSpeed * velocity * abs(slope);
336        // if carrying amount is greater than Kq
337        if (c > Kq) {
338          //DEPOSIT
339          c -= vtc;
340          HeightMap[HMAP_INDEX(X, Y)] += vtc;
341        } else {
342          //ERODE
343          // if carrying + value to steal > carrying cap
344          if (c + vtc > Kq) {
345            var delta = c + vtc - Kq;
346            c += delta;
347            HeightMap[HMAP_INDEX(X, Y)] -= delta;
348          } else {
349            c += vtc;
350            HeightMap[HMAP_INDEX(X, Y)] -= vtc;
351          }
352        }
353
354        // move to next value
355        if (minInd == 0) {
356          //NORTH
357          Y -= 1
358        }
359        if (minInd == 1) {
360          //SOUTH
361          Y += 1
362        }
363        if (minInd == 2) {
364          //EAST
365          X += 1
366        }
367        if (minInd == 3) {
368          //WEST
369          X -= 1
370        }
371
372        // limiting to edge of map
373        if (X > this.size - 1) {
374          X = this.size;
375        }
376        if (Y > this.size - 1) {
377          Y = this.size;
378        }
379        if (Y < 0) {
380          Y = 0;
381        }
382        if (X < 0) {
383          X = 0;
384        }
385      }
386    }
387  }
388
389  for (var drop = 0; drop < drops; drop++) {
390    DEPOSIT_AT(Math.floor(Math.random() * this.size), Math.floor(Math.random() * this.size));
391    this.maps[two] = HeightMap;
392  }
393};
394
395LandMap.prototype.complexErosion = function(options) {
396  var Kq = options.carryingCapacity;
397  var Kd = options.depositionSpeed;
398  var iterations = options.iterations;
399  var drops = options.drops;
400  var one = options.from;
401  var two = options.to;
402
403  this.meta[two] = options;
404
405  var HeightMap = new Array(this.size * this.size);
406  for (var y = 0; y < this.size; y++) {
407    for (var x = 0; x < this.size; x++) {
408      HeightMap[(x + this.size * y)] = this.get(one, x, y);
409    }
410  }
411
412  var HMAP_SIZE = this.size;
413
414  function HMAP_INDEX(x, y) {
415    var val = (x + HMAP_SIZE * y)
416    return val;
417  }
418
419  function HMAP_VALUE(x, y) {
420    return HeightMap[(x + HMAP_SIZE * y)];
421  }
422
423  function DEPOSIT_AT(X, Y) {
424    var c = 0.0;
425    var v = 1.05;
426    var minSlope = 1.15;
427    var maxVelocity = 10.0;
428
429    // For the number of iterations
430    for (var iter = 0; iter < iterations; iter++) {
431      v = Math.min(v, maxVelocity); // limiting velocity
432      var val = HMAP_VALUE(X, Y);
433      var nv = [
434        HMAP_VALUE(X, Y - 1), //NORTH
435        HMAP_VALUE(X, Y + 1), //SOUTH
436        HMAP_VALUE(X + 1, Y), //EAST
437        HMAP_VALUE(X - 1, Y) //WEST
438      ];
439
440      var minInd = indexOfMin(nv);
441      // if the lowest neighbor is NOT greater than the current value
442      if (nv[minInd] < val) {
443        //deposit or erode
444        var slope = Math.min(minSlope, val - nv[minInd])
445        var vtc = Kd * v * slope; // value to steal is depositionSpeed * velocity * abs(slope);
446        // if carrying amount is greater than Kq
447        if (c > Kq) {
448          //DEPOSIT
449          c -= vtc;
450          HeightMap[HMAP_INDEX(X, Y)] += vtc;
451        } else {
452          //ERODE
453          // if carrying + value to steal > carrying cap
454          if (c + vtc > Kq) {
455            var delta = c + vtc - Kq;
456            c += delta;
457            HeightMap[HMAP_INDEX(X, Y)] -= delta;
458          } else {
459            c += vtc;
460            HeightMap[HMAP_INDEX(X, Y)] -= vtc;
461          }
462        }
463
464        // move to next value
465        if (minInd == 0) {
466          //NORTH
467          Y -= 1
468        }
469        if (minInd == 1) {
470          //SOUTH
471          Y += 1
472        }
473        if (minInd == 2) {
474          //EAST
475          X += 1
476        }
477        if (minInd == 3) {
478          //WEST
479          X -= 1
480        }
481
482        // limiting to edge of map
483        if (X > this.size - 1) {
484          X = this.size;
485        }
486        if (Y > this.size - 1) {
487          Y = this.size;
488        }
489        if (Y < 0) {
490          Y = 0;
491        }
492        if (X < 0) {
493          X = 0;
494        }
495      }
496    }
497  }
498
499  for (var drop = 0; drop < drops; drop++) {
500    DEPOSIT_AT(Math.floor(Math.random() * this.size), Math.floor(Math.random() * this.size));
501    this.maps[two] = HeightMap;
502  }
503};
504
505LandMap.prototype.draw = function() {
506  var html = '<div class="row">';
507  var featureCount = 0;
508  for (feature in this.maps) {
509    html += '<div class="four columns"><strong>' + feature + '</strong><br><div class="box"><span id="' + this.containerId + feature + '"></span><pre>' + JSON.stringify(this.meta[feature], null, 2) + '</pre></div></div>';
510    featureCount++;
511    if (featureCount == 3) {
512      html += '</div><div class="row">'
513      featureCount = 0;
514    }
515  }
516  document.getElementById(this.containerId).innerHTML = html;
517  for (feature in this.maps) {
518    var display = document.getElementById("tmp");
519    var ctx = display.getContext('2d');
520    var width = display.width = 256;
521    var height = display.height = 256;
522    var self = this;
523
524    var cellWidth = 1;
525
526    function drawCell(x, y, inensity) {
527      ctx.fillStyle = inensity;
528      ctx.fillRect(x * cellWidth, y * cellWidth, cellWidth, cellWidth);
529    }
530
531    // finding max, min
532    var max = Number.MIN_SAFE_INTEGER;
533    var min = Number.MAX_SAFE_INTEGER;
534    for (var y = 0; y < this.size; y++) {
535      for (var x = 0; x < this.size; x++) {
536        var val = this.get(feature, x, y);
537        if (val !== undefined) {
538          max = Math.max(max, val);
539          min = Math.min(min, val);
540        }
541      }
542    }
543    // Drawing each cell
544    for (var y = 0; y < this.size; y++) {
545      for (var x = 0; x < this.size; x++) {
546        var val = this.get(feature, x, y);
547        if (val !== undefined) {
548          drawCell(x, y, brightness(val, max, min));
549        }
550      }
551    }
552
553    document.getElementById(this.containerId + feature).innerHTML = '<img src="' + display.toDataURL("image/png") + '" class="u-max-full-width map"/>';
554  }
555
556  function brightness(value, max, min) {
557    var delta = Math.abs((max - min));
558    var b = Math.floor((value / delta) * 255);
559    return 'rgba(' + b + ',' + b + ',' + b + ',1)';
560  }
561};