Abgleichen eines Teils der prozedural erzeugten Welt mit einem Teil der anderen Welt

18

Haben Sie Die Chroniken des Bernsteins von Roger Zelazny gelesen?

Stellen Sie sich vor, Sie spielen im 3rd Person MMO-Spiel. Du laichst in der Welt und fängst an herumzulaufen. Nach einiger Zeit, wenn Sie denken, dass Sie die Karte gelernt haben, stellen Sie fest, dass Sie sich an einem Ort befinden, den Sie noch nie zuvor gesehen haben. Sie kehren zu dem letzten Ort zurück, von dem Sie sicher waren, dass Sie ihn kennen und der immer noch dort ist. Aber der Rest der Welt hat sich verändert und Sie haben nicht einmal bemerkt, wie es passiert ist.

Ich habe über prozedurale Welterzeugung gelesen. Ich habe über Perlin-Rauschen und Oktaven, Simplex-Rauschen, Diamantquadrat-Algorithmus, Simulation von tektonischen Platten und Wassererosion gelesen. Ich glaube, ich habe ein vages Verständnis der allgemeinen Herangehensweise an die prozedurale Welterzeugung.

Und mit diesem Wissen habe ich keine Ahnung, wie man so etwas machen kann wie oben geschrieben. Jede Idee, die mir in den Sinn kommt, stößt auf einige theoretische Probleme. Hier sind einige Ideen, die mir einfallen:

1) "Reversible" Weltgeneration mit einer Startnummer als Eingabe und einer vollständig beschreibenden Blocknummer

Ich bezweifle, dass es überhaupt möglich ist, aber ich stelle mir eine Funktion vor, die einen Keim erhält und eine Zahlenmatrix erzeugt, auf der Brocken aufgebaut sind. Und für jede eindeutige Nummer gibt es einen eindeutigen Block. Und eine zweite Funktion, die diese eindeutige Chunk-Nummer erhält und einen Startwert erzeugt, der diese Nummer enthält. Ich habe versucht, ein Schema im Bild unten zu machen:

Bildbeschreibung hier eingeben

2) Brocken komplett zufällig machen und einen Übergang zwischen ihnen machen.

Wie Aracthor vorschlug. Der Vorteil dieses Ansatzes ist, dass es möglich ist und keine Magie erforderlich ist Vorteil Wirkung benötigt :)

Der Nachteil dieses Ansatzes ist meiner Meinung nach, dass es wahrscheinlich nicht möglich ist, eine vielfältige Welt zu haben. Wenn Sie sagen, dass sowohl der Archipel als auch ein Kontinent, der nur durch eine Zahl dargestellt wird, und die angrenzenden Abschnitte gleich sind, wäre die Größe eines Abschnitts nicht gleich dem Kontinent. Und ich bezweifle, dass es möglich ist, gut aussehende Übergänge zwischen Stücken zu machen. Vermisse ich etwas?

Mit anderen Worten, Sie entwickeln ein MMO mit prozedural generierter Welt. Aber anstatt eine Welt zu haben, hast du viele . Welchen Ansatz würdest du wählen, um Welten zu generieren, und wie würdest du den Übergang des Spielers von einer Welt zur anderen implementieren, ohne dass der Spieler den Übergang bemerkt.

Jedenfalls glaube ich, dass Sie die allgemeine Idee haben. Wie hättest du das gemacht?

Netaholic
quelle
Daher habe ich hier einige Probleme mit den Antworten. @Aracthor Ich habe mit Ihnen schon über glatte Verteiler gesprochen, das gilt hier. Allerdings gibt es 2 ziemlich hohe Antworten, also frage ich mich, ob es einen Punkt gibt ...
Alec Teal
@AlecTeal Wenn Sie etwas hinzufügen möchten, tun Sie dies bitte. Über Anregungen und Verbesserungsvorschläge würde ich mich freuen.
Netaholic

Antworten:

23

Verwenden Sie ein Stück Rauschen höherer Ordnung. Wenn Sie zuvor 2D-Rauschen für eine Höhenkarte verwendet haben, verwenden Sie stattdessen 3D-Rauschen mit fester letzter Koordinate. Jetzt können Sie die Position in der letzten Dimension langsam ändern, um das Gelände zu ändern. Da Perlin-Rauschen in allen Dimensionen kontinuierlich ist, erhalten Sie weiche Übergänge, solange Sie die Position ändern, an der Sie die Rauschfunktion abtasten.

Wenn Sie zum Beispiel nur das vom Abstand zum Spieler entfernte Gelände als Versatz ändern möchten. Sie können auch den Versatz für jede Koordinate auf der Karte speichern und ihn nur erhöhen, aber niemals verringern. Auf diese Weise wird die Karte nur neuer, aber niemals älter.

Diese Idee funktioniert auch, wenn Sie bereits 3D-Rauschen verwenden. Dann probieren Sie einfach 4D aus. Schauen Sie sich auch das Simplex-Rauschen an. Es ist die verbesserte Version von Perlin Noise und funktioniert besser für mehr Dimensionen.

danijar
quelle
2
Das ist interessant. Verstehe ich richtig, dass Sie vorschlagen, ein 3D-Rauschen zu erzeugen, ein XY-Slice an einem bestimmten Z als Höhenkarte zu verwenden und einen sanften Übergang zu einem anderen Slice vorzunehmen, indem Sie die Z-Koordinate ändern, wenn der Abstand zum Spieler zunimmt?
Netaholic
@netaholic Genau. Es ist eine sehr gute Intuition, es als Scheibe zu beschreiben. Außerdem können Sie den höchsten Wert für die letzte Koordinate überall auf der Karte verfolgen und nur erhöhen, aber niemals verringern.
Danijar
1
Das ist eine geniale Idee. Grundsätzlich wäre Ihre Geländekarte ein parabolischer Schnitt (oder eine andere Kurve) durch ein 3D-Volumen.
Fake Name
Das ist eine wirklich clevere Idee.
user253751
5

Ihre Idee, die Welt in mehrere Teile aufzuteilen, ist nicht schlecht. Es ist einfach unvollständig.

Das einzige Problem sind Verbindungsstellen zwischen Stücken. Wenn Sie beispielsweise Perlin-Rauschen verwenden, um Entlastung zu erzeugen, und für jeden Block einen anderen Startwert festlegen, und dies riskieren, geschieht Folgendes:

Chunk Relief Bug

Eine Lösung wäre, nicht nur aus dem Perlin-Rausch-Samen, sondern auch aus anderen Brocken um ihn herum eine Brockenentlastung zu erzeugen.

Perlin-Algorithmus verwendet zufällige Kartenwerte, um sich selbst zu "glätten". Wenn sie eine gemeinsame Karte verwenden, würde es zusammen geglättet.

Das einzige Problem ist, wenn Sie einen Chunk-Samen ändern, um ihn zu ändern, wenn der Spieler zurücktritt, müssen Sie auch Chunks neu laden, da sich auch deren Ränder ändern sollten.

Dies würde die Größe der Chunks nicht ändern, aber den minimalen Abstand vom Player zum Laden / Entladen vergrößern, da ein Chunk geladen werden muss, wenn der Player dies sieht, und bei dieser Methode auch benachbarte Chunks .

AKTUALISIEREN:

Wenn jeder Teil Ihrer Welt von einem anderen Typ ist, wächst das Problem. Hier geht es nicht nur um Erleichterung. Eine kostspielige Lösung wäre:

Stücke schneiden

Nehmen wir an, grüne Brocken sind Waldwelten, blaue Archipele und gelbe flache Wüsten.
Die Lösung besteht darin, "Übergangszonen" zu schaffen, in denen sich Ihr Relief und Ihre Bodennatur (sowie geerdete Objekte oder alles andere, was Sie wollen) nach und nach von einem Typ zum anderen wandeln.

Und wie Sie auf diesem Bild sehen können, wären kleine Quadrate in den Ecken der Blöcke der Hölle zu codieren: Sie müssen eine Verbindung zwischen 4 Blöcken herstellen, möglicherweise unterschiedlicher Natur.

Ich denke, für diese Komplexitätsstufe können klassische Generationen der 2D-Welt wie Perlin2D einfach nicht verwendet werden. Ich verweise Sie auf @ Danijar Antwort dafür.

Aracthor
quelle
Schlagen Sie vor, die "Mitte" eines Blocks aus einem Samen zu erzeugen und seine Kanten basierend auf benachbarten Blöcken "geglättet" zu machen? Es ist sinnvoll, aber es wird die Größe eines Blocks erhöhen, da es die Größe eines Bereichs sein sollte, den der Spieler beobachten kann, plus die doppelte Breite eines Übergangsbereichs zu benachbarten Blöcken. Und je vielfältiger die Welt ist, desto größer wird die Brockenfläche.
Netaholic
@netaholic Es wäre nicht größer, aber irgendwie. Ich habe einen Absatz hinzugefügt.
Aracthor
Ich habe meine Frage aktualisiert. Versucht, einige Ideen zu beschreiben, die ich habe
Netaholic
Die andere Antwort hier verwendet also eine (nicht ganz) dritte Dimension als Diagramme. Auch du siehst das Flugzeug als eine Mannigfaltigkeit an, und ich mag deine Ideen. Um es ein bisschen weiter auszudehnen, möchten Sie wirklich eine glatte Mannigfaltigkeit. Sie müssen sicherstellen, dass Ihre Übergänge glatt sind. Sie könnten dann eine Unschärfe oder ein Rauschen darauf anwenden, und die Antwort wäre perfekt.
Alec Teal
0

Während die Idee von danijar ziemlich solide ist, könnten Sie am Ende eine Menge Daten speichern, wenn Sie möchten, dass der lokale Bereich und die Entfernung gleich sind. Und fordern immer mehr Scheiben von immer komplexerem Lärm. Sie können all dies auf eine standardmäßige 2D-Art und Weise erhalten.

Ich entwickelte einen Algorithmus zur prozeduralen Erzeugung von zufälligem fraktalem Rauschen, der teilweise auf dem Algorithmus des Diamantquadrats basierte , den ich als unendlich und deterministisch festlegte. Diamantquadrat kann also eine unendliche Landschaft erzeugen, ebenso wie mein eigener ziemlich blockartiger Algorithmus.

Die Idee ist im Grunde die gleiche. Anstatt jedoch höherdimensionales Rauschen abzutasten, können Sie Werte auf verschiedenen iterativen Ebenen iterieren.

Sie speichern also immer noch die Werte, die Sie zuvor angefordert haben, und speichern sie im Cache (dieses Schema kann unabhängig verwendet werden, um einen bereits superschnellen Algorithmus zu beschleunigen). Und wenn ein neuer Bereich angefordert wird, wird er mit einem neuen y-Wert erstellt. und jeder Bereich, der in dieser Anforderung nicht angefordert wurde, wird entfernt.

Also anstatt durch andere Räume in zusätzlichen Dimensionen zu blättern. Wir speichern ein zusätzliches Stück monotoner Daten, um sie in verschiedenen (zunehmend größeren Mengen auf verschiedenen Ebenen) zu mischen.

Wenn der Benutzer in eine Richtung fährt, werden die Werte entsprechend verschoben (und auf jeder Ebene) und neue Werte an den neuen Kanten generiert. Wenn der top iterative Startwert geändert wird, verschiebt sich die gesamte Welt drastisch. Wenn die abschließende Iteration ein anderes Ergebnis liefert, ist der Änderungsbetrag sehr gering, etwa + -1 Block. Aber der Hügel wird immer noch da sein und das Tal usw., aber die Ecken und Winkel werden sich verändert haben. Es sei denn, Sie gehen weit genug, und dann wird der Hügel verschwunden sein.

Wenn wir also 100x100 Chunk-Werte pro Iteration gespeichert haben. Dann könnte sich beim 100x100 vom Player nichts ändern. Bei 200x200 können sich die Dinge jedoch um einen Block ändern. Bei 400x400 könnten sich die Dinge um 2 Blöcke ändern. Bei einer Entfernung von 800x800 können sich die Dinge um 4 Blöcke ändern. Also werden sich die Dinge ändern und sie werden sich immer mehr ändern, je weiter Sie gehen. Wenn du zurückgehst, werden sie anders sein, wenn du zu weit gehst, werden sie komplett verändert und gehen komplett verloren, da alle Samen aufgegeben würden.

Eine andere Dimension hinzuzufügen, um diesen stabilisierenden Effekt zu erzielen, würde sicherlich funktionieren und das y in der Ferne verschieben, aber Sie würden eine Menge Daten für sehr viele Blöcke speichern, wenn Sie dies nicht müssten. In deterministischen Algorithmen für fraktales Rauschen können Sie denselben Effekt erzielen, indem Sie einen sich ändernden Wert (mit einem anderen Betrag) hinzufügen, wenn sich die Position über einen bestimmten Punkt hinaus bewegt.

https://jsfiddle.net/rkdzau7o/

var SCALE_FACTOR = 2;
//The scale factor is kind of arbitrary, but the code is only consistent for 2 currently. Gives noise for other scale but not location proper.
var BLUR_EDGE = 2; //extra pixels are needed for the blur (3 - 1).
var buildbuffer = BLUR_EDGE + SCALE_FACTOR;

canvas = document.getElementById('canvas');
ctx = canvas.getContext("2d");
var stride = canvas.width + buildbuffer;
var colorvalues = new Array(stride * (canvas.height + buildbuffer));
var iterations = 7;
var xpos = 0;
var ypos = 0;
var singlecolor = true;


/**
 * Function adds all the required ints into the ints array.
 * Note that the scanline should not actually equal the width.
 * It should be larger as per the getRequiredDim function.
 *
 * @param iterations Number of iterations to perform.
 * @param ints       pixel array to be used to insert values. (Pass by reference)
 * @param stride     distance in the array to the next requestedY value.
 * @param x          requested X location.
 * @param y          requested Y location.
 * @param width      width of the image.
 * @param height     height of the image.
 */

function fieldOlsenNoise(iterations, ints, stride, x, y, width, height) {
  olsennoise(ints, stride, x, y, width, height, iterations); //Calls the main routine.
  //applyMask(ints, stride, width, height, 0xFF000000);
}

function applyMask(pixels, stride, width, height, mask) {
  var index;
  index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) {
    for (var j = 0, m = width - 1; j <= m; j++) {
      pixels[index + j] |= mask;
    }
  }
}

/**
 * Converts a dimension into the dimension required by the algorithm.
 * Due to the blurring, to get valid data the array must be slightly larger.
 * Due to the interpixel location at lowest levels it needs to be bigger by
 * the max value that can be. (SCALE_FACTOR)
 *
 * @param dim
 * @return
 */

function getRequiredDim(dim) {
  return dim + BLUR_EDGE + SCALE_FACTOR;
}

//Function inserts the values into the given ints array (pass by reference)
//The results will be within 0-255 assuming the requested iterations are 7.
function olsennoise(ints, stride, x_within_field, y_within_field, width, height, iteration) {
  if (iteration == 0) {
    //Base case. If we are at the bottom. Do not run the rest of the function. Return random values.
    clearValues(ints, stride, width, height); //base case needs zero, apply Noise will not eat garbage.
    applyNoise(ints, stride, x_within_field, y_within_field, width, height, iteration);
    return;
  }

  var x_remainder = x_within_field & 1; //Adjust the x_remainder so we know how much more into the pixel are.
  var y_remainder = y_within_field & 1; //Math.abs(y_within_field % SCALE_FACTOR) - Would be assumed for larger scalefactors.

  /*
  Pass the ints, and the stride for that set of ints.
  Recurse the call to the function moving the x_within_field forward if we actaully want half a pixel at the start.
  Same for the requestedY.
  The width should expanded by the x_remainder, and then half the size, with enough extra to store the extra ints from the blur.
  If the width is too long, it'll just run more stuff than it needs to.
  */

  olsennoise(ints, stride,
    (Math.floor((x_within_field + x_remainder) / SCALE_FACTOR)) - x_remainder,
    (Math.floor((y_within_field + y_remainder) / SCALE_FACTOR)) - y_remainder,
    (Math.floor((width + x_remainder) / SCALE_FACTOR)) + BLUR_EDGE,
    (Math.floor((height + y_remainder) / SCALE_FACTOR)) + BLUR_EDGE, iteration - 1);

  //This will scale the image from half the width and half the height. bounds.
  //The scale function assumes you have at least width/2 and height/2 good ints.
  //We requested those from olsennoise above, so we should have that.

  applyScaleShift(ints, stride, width + BLUR_EDGE, height + BLUR_EDGE, SCALE_FACTOR, x_remainder, y_remainder);

  //This applies the blur and uses the given bounds.
  //Since the blur loses two at the edge, this will result
  //in us having width requestedX height of good ints and required
  // width + blurEdge of good ints. height + blurEdge of good ints.
  applyBlur(ints, stride, width + BLUR_EDGE, height + BLUR_EDGE);

  //Applies noise to all the given ints. Does not require more or less than ints. Just offsets them all randomly.
  applyNoise(ints, stride, x_within_field, y_within_field, width, height, iteration);
}



function applyNoise(pixels, stride, x_within_field, y_within_field, width, height, iteration) {
  var bitmask = 0b00000001000000010000000100000001 << (7 - iteration);
  var index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) { //iterate the requestedY positions. Offsetting the index by stride each time.
    for (var j = 0, m = width - 1; j <= m; j++) { //iterate the requestedX positions through width.
      var current = index + j; // The current position of the pixel is the index which will have added stride each, requestedY iteration
      pixels[current] += hashrandom(j + x_within_field, k + y_within_field, iteration) & bitmask;
      //add on to this pixel the hash function with the set reduction.
      //It simply must scale down with the larger number of iterations.
    }
  }
}

function applyScaleShift(pixels, stride, width, height, factor, shiftX, shiftY) {
  var index = (height - 1) * stride; //We must iteration backwards to scale so index starts at last Y position.
  for (var k = 0, n = height - 1; k <= n; n--, index -= stride) { // we iterate the requestedY, removing stride from index.
    for (var j = 0, m = width - 1; j <= m; m--) { // iterate the requestedX positions from width to 0.
      var pos = index + m; //current position is the index (position of that scanline of Y) plus our current iteration in scale.
      var lower = (Math.floor((n + shiftY) / factor) * stride) + Math.floor((m + shiftX) / factor); //We find the position that is half that size. From where we scale them out.
      pixels[pos] = pixels[lower]; // Set the outer position to the inner position. Applying the scale.
    }
  }
}

function clearValues(pixels, stride, width, height) {
  var index;
  index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) { //iterate the requestedY values.
    for (var j = 0, m = width - 1; j <= m; j++) { //iterate the requestedX values.
      pixels[index + j] = 0; //clears those values.
    }
  }
}

//Applies the blur.
//loopunrolled box blur 3x3 in each color.
function applyBlur(pixels, stride, width, height) {
  var index = 0;
  var v0;
  var v1;
  var v2;

  var r;
  var g;
  var b;

  for (var j = 0; j < height; j++, index += stride) {
    for (var k = 0; k < width; k++) {
      var pos = index + k;

      v0 = pixels[pos];
      v1 = pixels[pos + 1];
      v2 = pixels[pos + 2];

      r = ((v0 >> 16) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v2 >> 16) & 0xFF);
      g = ((v0 >> 8) & 0xFF) + ((v1 >> 8) & 0xFF) + ((v2 >> 8) & 0xFF);
      b = ((v0) & 0xFF) + ((v1) & 0xFF) + ((v2) & 0xFF);
      r = Math.floor(r / 3);
      g = Math.floor(g / 3);
      b = Math.floor(b / 3);
      pixels[pos] = r << 16 | g << 8 | b;
    }
  }
  index = 0;
  for (var j = 0; j < height; j++, index += stride) {
    for (var k = 0; k < width; k++) {
      var pos = index + k;
      v0 = pixels[pos];
      v1 = pixels[pos + stride];
      v2 = pixels[pos + (stride << 1)];

      r = ((v0 >> 16) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v2 >> 16) & 0xFF);
      g = ((v0 >> 8) & 0xFF) + ((v1 >> 8) & 0xFF) + ((v2 >> 8) & 0xFF);
      b = ((v0) & 0xFF) + ((v1) & 0xFF) + ((v2) & 0xFF);
      r = Math.floor(r / 3);
      g = Math.floor(g / 3);
      b = Math.floor(b / 3);
      pixels[pos] = r << 16 | g << 8 | b;
    }
  }
}


function hashrandom(v0, v1, v2) {
  var hash = 0;
  hash ^= v0;
  hash = hashsingle(hash);
  hash ^= v1;
  hash = hashsingle(hash);
  hash ^= v2;
  hash = hashsingle(hash);
  return hash;
}

function hashsingle(v) {
  var hash = v;
  var h = hash;

  switch (hash & 3) {
    case 3:
      hash += h;
      hash ^= hash << 32;
      hash ^= h << 36;
      hash += hash >> 22;
      break;
    case 2:
      hash += h;
      hash ^= hash << 22;
      hash += hash >> 34;
      break;
    case 1:
      hash += h;
      hash ^= hash << 20;
      hash += hash >> 2;
  }
  hash ^= hash << 6;
  hash += hash >> 10;
  hash ^= hash << 8;
  hash += hash >> 34;
  hash ^= hash << 50;
  hash += hash >> 12;
  return hash;
}


//END, OLSEN NOSE.



//Nuts and bolts code.

function MoveMap(dx, dy) {
  xpos -= dx;
  ypos -= dy;
  drawMap();
}

function drawMap() {
  //int iterations, int[] ints, int stride, int x, int y, int width, int height
  console.log("Here.");
  fieldOlsenNoise(iterations, colorvalues, stride, xpos, ypos, canvas.width, canvas.height);
  var img = ctx.createImageData(canvas.width, canvas.height);

  for (var y = 0, h = canvas.height; y < h; y++) {
    for (var x = 0, w = canvas.width; x < w; x++) {
      var standardShade = colorvalues[(y * stride) + x];
      var pData = ((y * w) + x) * 4;
      if (singlecolor) {
        img.data[pData] = standardShade & 0xFF;
        img.data[pData + 1] = standardShade & 0xFF;
        img.data[pData + 2] = standardShade & 0xFF;
      } else {
        img.data[pData] = standardShade & 0xFF;
        img.data[pData + 1] = (standardShade >> 8) & 0xFF;
        img.data[pData + 2] = (standardShade >> 16) & 0xFF;
      }
      img.data[pData + 3] = 255;
    }
  }
  ctx.putImageData(img, 0, 0);
}

$("#update").click(function(e) {
  iterations = parseInt($("iterations").val());
  drawMap();
})
$("#colors").click(function(e) {
  singlecolor = !singlecolor;
  drawMap();
})

var m = this;
m.map = document.getElementById("canvas");
m.width = canvas.width;
m.height = canvas.height;

m.hoverCursor = "auto";
m.dragCursor = "url(data:image/vnd.microsoft.icon;base64,AAACAAEAICACAAcABQAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAA/AAAAfwAAAP+AAAH/gAAB/8AAAH/AAAB/wAAA/0AAANsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////////////////////////////////////////////////////////////////////////////////////gH///4B///8Af//+AD///AA///wAH//+AB///wAf//4AH//+AD///yT/////////////////////////////8=), default";
m.scrollTime = 300;

m.mousePosition = new Coordinate;
m.mouseLocations = [];
m.velocity = new Coordinate;
m.mouseDown = false;
m.timerId = -1;
m.timerCount = 0;

m.viewingBox = document.createElement("div");
m.viewingBox.style.cursor = m.hoverCursor;

m.map.parentNode.replaceChild(m.viewingBox, m.map);
m.viewingBox.appendChild(m.map);
m.viewingBox.style.overflow = "hidden";
m.viewingBox.style.width = m.width + "px";
m.viewingBox.style.height = m.height + "px";
m.viewingBox.style.position = "relative";
m.map.style.position = "absolute";

function AddListener(element, event, f) {
  if (element.attachEvent) {
    element["e" + event + f] = f;
    element[event + f] = function() {
      element["e" + event + f](window.event);
    };
    element.attachEvent("on" + event, element[event + f]);
  } else
    element.addEventListener(event, f, false);
}

function Coordinate(startX, startY) {
  this.x = startX;
  this.y = startY;
}

var MouseMove = function(b) {
  var e = b.clientX - m.mousePosition.x;
  var d = b.clientY - m.mousePosition.y;
  MoveMap(e, d);
  m.mousePosition.x = b.clientX;
  m.mousePosition.y = b.clientY;
};

/**
 * mousedown event handler
 */
AddListener(m.viewingBox, "mousedown", function(e) {
  m.viewingBox.style.cursor = m.dragCursor;

  // Save the current mouse position so we can later find how far the
  // mouse has moved in order to scroll that distance
  m.mousePosition.x = e.clientX;
  m.mousePosition.y = e.clientY;

  // Start paying attention to when the mouse moves
  AddListener(document, "mousemove", MouseMove);
  m.mouseDown = true;

  event.preventDefault ? event.preventDefault() : event.returnValue = false;
});

/**
 * mouseup event handler
 */
AddListener(document, "mouseup", function() {
  if (m.mouseDown) {
    var handler = MouseMove;
    if (document.detachEvent) {
      document.detachEvent("onmousemove", document["mousemove" + handler]);
      document["mousemove" + handler] = null;
    } else {
      document.removeEventListener("mousemove", handler, false);
    }

    m.mouseDown = false;

    if (m.mouseLocations.length > 0) {
      var clickCount = m.mouseLocations.length;
      m.velocity.x = (m.mouseLocations[clickCount - 1].x - m.mouseLocations[0].x) / clickCount;
      m.velocity.y = (m.mouseLocations[clickCount - 1].y - m.mouseLocations[0].y) / clickCount;
      m.mouseLocations.length = 0;
    }
  }

  m.viewingBox.style.cursor = m.hoverCursor;
});

drawMap();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="500" height="500">
</canvas>
<fieldset>
  <legend>Height Map Properties</legend>
  <input type="text" name="iterations" id="iterations">
  <label for="iterations">
    Iterations(7)
  </label>
  <label>
    <input type="checkbox" id="colors" />Rainbow</label>
</fieldset>

Tatarisieren
quelle