手続き的に生成された世界のチャンクを他の世界のチャンクに一致させる


18

ロジャー・ゼラズニーの 『アンバーのクロニクル』を読んだことがありますか?

自分が3人称MMOゲームでプレイしていると想像してください。あなたは世界に出現し、さまよい始めます。しばらくして、マップを学習したと思ったとき、あなたはその場所にいること、今まで見たことのないことを悟ります。あなたが知っていると確信していた最後の場所に戻り、それはまだそこにあります。しかし、世界のその他の地域は変化しており、あなたはそれがどのように起こったかにさえ気づきませんでした。

手続き型の世界世代について読んだことがあります。パーリンノイズとオクターブ、シンプレックスノイズ、ダイアモンドスクエアアルゴリズム、構造プレートのシミュレーションと水の侵食について読みました。手続き型の世界生成における一般的なアプローチについて、漠然と理解していると思います。

そして、この知識があれば、上記のようなことをどのように行うことができるかについてはわかりません。私の頭に浮かぶすべてのアイデアは、いくつかの理論的な問題に遭遇します。ここに私が考えることができるいくつかのアイデアがあります:

1)入力としてシード番号といくつかの完全に記述されたチャンク番号を持つ「リバーシブル」世界世代

私はそれが可能でさえあるとは思いませんが、シードを受け取り、チャンクが構築される数のマトリックスを生成する関数を想像します。また、一意の番号ごとに一意のチャンクがあります。そして、この一意のチャンク番号を取得し、この番号を含むシードを生成する2番目の関数。私は下の写真のスキームを作成しようとしました:

ここに画像の説明を入力してください

2)チャンクを完全にランダムにし、それらの間にトランジションを作成します。

Aracthorが示唆されました。このアプローチの利点は、それが可能であり、魔法の機能を必要としないことです:)

私の意見では、このアプローチの短所は、多様な世界を持つことはおそらく不可能だということです。群島と大陸の両方が1つの数字で表され、隣接するチャンクであるとすると、チャンクのサイズは大陸と等しくなりません。そして、チャンク間で見栄えの良い遷移を行うことができるとは思えません。何か不足していますか?

つまり、手続き的に生成された世界を持つMMOを開発しているということです。しかし、1つの世界を持っている代わりに、あなたは多くを持ってます。ワールドを生成するためにどのようなアプローチをとり、プレイヤーが遷移に気付かないで、ある世界から別の世界へのプレイヤーの移行をどのように実装しますか。

とにかく、私はあなたが一般的な考えを持っていると信じています。どうしましたか?


したがって、ここでの回答にはいくつか問題があります。@Aracthor滑らかな多様体について以前に話したことがありますが、そのようなことはここで当てはまります。ポイントがあります場合、私は思ったんだけどのでしかし2つのかなり高い答えは...ある
アレック・ティール

@AlecTeal何か追加することがあれば、してください。アイデアや提案を聞いてうれしいです。
ネタホリック

回答:


23

高次ノイズのスライスを使用します。以前に高さマップに2Dノイズを使用した場合は、代わりに最後の座標が固定された3Dノイズを使用します。これで、最後の次元の位置をゆっくりと変更して、地形を修正できます。パーリンノイズはすべての次元で連続的であるため、ノイズ関数をサンプリングする位置をスムーズに変更する限り、スムーズな遷移が得られます。

たとえば、プレーヤーまでの距離から遠く離れた地形のみをオフセットとして変更する場合。また、マップ上の各座標のオフセットを保存し、増加させることはできますが、減少させることはできません。この方法では、マップは新しくなりますが、古くなることはありません。

このアイデアは、すでに3Dノイズを使用している場合にも機能し、4Dからサンプリングするだけです。また、シンプレックスノイズも見てください。これは、Perlinノイズの改良バージョンであり、より多くの次元でより適切に機能します。


2
これは面白い。3Dノイズを生成し、その特定のzでxyスライスをハイトマップとして使用し、プレーヤーからの距離が増加するにつれてz座標を変更して別のスライスにスムーズに移行することをお勧めします。
ネタホリック

@netaholicまさに。スライスとして記述することは非常に良い直観です。さらに、マップ上のあらゆる場所で最後の座標の最高値を追跡し、増加させるだけで、減少させることはできません。
danijar

1
これは素晴らしいアイデアです。基本的に、地形マップは3Dボリュームを通る放物線(または他の曲線)スライスになります。
偽の名前

これは本当に賢いアイデアです。
user253751

5

世界をいくつかのチャンクに分割するというアイデアは悪くありません。ただ不完全です。

唯一の問題は、チャンク間のジャンクションです。たとえば、perlinノイズを使用してレリーフを生成し、各チャンクに対して異なるシードを使用し、これが発生するリスクがある場合:

チャンクリリーフバグ

解決策は、Perlinノイズシードからだけでなく、周囲の他のチャンクからもチャンクレリーフを生成することです。

Perlinアルゴリズムは、周囲のランダムマップの値を使用して、自身を「スムーズ」にします。共通のマップを使用する場合、一緒に平滑化されます。

唯一の問題は、プレイヤーが後退したときにチャンクシードを変更して異なるものにする場合、境界線も変更する必要があるため、チャンクもリロードする必要があることです。

これはチャンクのサイズを変更しませんが、プレーヤーから見たときにチャンクをロードする必要があるため、プレーヤーからロード/アンロードするまでの最小距離を増やします。 。

更新:

ワールドの各チャンクのタイプが異なる場合、問題は大きくなります。これは安reliefだけではありません。コストのかかるソリューションは次のとおりです。

チャンクカット

緑の塊は森林の世界、青の塊は群島、黄色の塊は平らな砂漠だとしましょう。
ここでの解決策は、「遷移」ゾーンを作成することです。このゾーンでは、レリーフと地面の性質(および接地されたオブジェクト、または必要なもの)が次第にタイプから次へと変化します。

この写真でわかるように、コードの地獄の部分は、チャンクコーナーの小さな四角になります。4つのチャンク間でリンクを作成する必要があり、性質が異なる可能性があります。

そのため、この複雑さのレベルでは、Perlin2Dのような古典的な2Dワールド世代使用できないと思います。そのために、@ danijarの回答を参照してください。


シードからチャンクの「中心」を生成し、そのエッジを隣接するチャンクに基づいて「平滑化」することを提案しますか?それは理にかなっていますが、チャンクのサイズを大きくします。エリアのサイズである必要があるため、プレイヤーは隣接するチャンクへの移行エリアの幅に2倍を加えて観察できます。そして、チャンク領域は、世界が多様になるほどさらに大きくなります。
ネタホリック

@netaholicそれはもっと大きくはありませんが、ある種のものです。段落を追加しました。
アラクトール

質問を更新しました。私が持っているいくつかのアイデアを説明しようとした
ネタ中毒

したがって、ここでのもう1つの答えは、チャートとして3番目の次元を使用します(完全ではありません)。また、飛行機を多様体とみなし、あなたのアイデアが好きです。さらに拡張するには、スムーズな多様体が必要です。移行がスムーズであることを確認する必要があります。次に、これにぼかしやノイズを適用すると、答えは完璧になります。
アレックティール

0

danijarのアイデアはかなり堅実ですが、ローカルエリアを同じにし、距離をシフトしたい場合は、多くのデータを格納することになります。そして、ますます複雑なノイズのスライスを要求します。これらはすべて、より標準的な2D形式で取得できます。

ランダムでフラクタルノイズを手続き的に生成するためのアルゴリズムを開発しまし。これは、無限で決定論的であるように修正したダイヤモンドスクエアアルゴリズムに一部基づいています。そのため、ダイアモンドスクエアは、無限のランドスケープを作成でき、また独自のかなりブロッキーなアルゴリズムを作成できます。

考え方は基本的に同じです。ただし、高次元のノイズをサンプリングするのではなく、異なる反復レベルで値を反復できます。

そのため、以前に要求した値を保存し、キャッシュします(このスキームを単独で使用して、既に超高速のアルゴリズムを高速化できます)。新しい領域が要求されると、新しいy値で作成されます。そのリクエストでリクエストされていないエリアは削除されます。

そのため、追加の次元で異なるスペースをスコーピングするよりも。異なるデータを混合するために、単調なデータを余分に格納します(異なるレベルで徐々に大きくなります)。

ユーザーが方向に移動すると、値はそれに応じて(および各レベルで)移動し、新しいエッジで新しい値が生成されます。最上位の反復シードが変更されると、世界全体が大幅にシフトします。最後の反復に異なる結果が与えられた場合、変更量は非常に小さい+ -1ブロック程度になります。しかし、丘はまだそこにあり、谷などはありますが、隅と割れ目は変わりました。十分に行かない限り、丘は消えてしまいます。

したがって、各反復で100x100の値のチャンクを格納した場合。その後、プレーヤーから100x100で変化するものはありません。しかし、200x200では、1ブロック変化する可能性があります。400x400では、2ブロック変化する可能性があります。800x800離れると、物事は4ブロックずつ変化します。したがって、物事は変化し、あなたが進むにつれてますます変化します。戻るとそれらは異なり、行き過ぎるとそれらは完全に変わり、すべての種が捨てられるので完全に失われます。

この安定化効果を提供するために異なる次元を追加することは確かに機能し、yを遠くにシフトしますが、必要のないときに非常に多くのブロックの多くのデータを保存することになります。決定論的なフラクタルノイズアルゴリズムでは、位置が特定のポイントを超えて移動するときに変化する値(異なる量)を追加することで、同じ効果を得ることができます。

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>

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.