Сопоставление части процедурно сгенерированного мира с частью другого мира

18

Вы читали «Хроники янтаря» Роджера Желязны?

Представьте, что вы играете в MMO от третьего лица. Вы появляетесь в мире и начинаете бродить вокруг. Через некоторое время, когда вы думаете, что изучили карту, вы понимаете, что находитесь в месте, которое вы никогда раньше не видели. Вы возвращаетесь в последнее место, в котором вы были уверены, что знаете, и оно все еще там. Но остальной мир изменился, и вы даже не заметили, как это произошло.

Я читал о процессуальном поколении мира. Я читал о перлинском шуме и октавах, симплексном шуме, алгоритме Алмазного квадрата, о симуляции тектонических плит и водной эрозии. Я полагаю, что у меня есть смутное понимание общего подхода в процессуальном мире.

И с этим знанием я понятия не имею, как вы можете сделать что-то, как написано выше. Каждая идея, которая приходит мне в голову, сталкивается с некоторыми теоретическими проблемами. Вот некоторые идеи, которые я могу придумать:

1) «Обратимое» поколение мира с начальным номером в качестве входного и некоторым полностью описывающим номером чанка

Я сомневаюсь, что это даже возможно, но я представляю себе функцию, которая получит начальное число и создаст матрицу чисел, на которой построены фрагменты. И для каждого уникального номера есть уникальный кусок. И вторая функция, которая получает этот уникальный номер чанка и производит начальное число, которое содержит этот номер. Я попытался сделать схему на картинке ниже:

введите описание изображения здесь

2) Делаем куски совершенно случайными и делаем переход между ними.

Как предположил Арактор . Преимущества такого подхода в том, что он возможен и не требует магической функции :)

Минусы этого подхода, на мой взгляд, в том, что, вероятно, невозможно иметь разнообразный мир. Если у вас есть, скажем, и архипелаг, и континент, представленный только одним числом, и его соседние куски, то размер кусочка не будет равен континенту. И я сомневаюсь, что можно сделать красивый переход между кусками. Я что-то пропустил?

Другими словами, вы разрабатываете MMO с процедурно сгенерированным миром. Но вместо одного мира у вас много . Какой подход вы бы использовали для генерации миров и как бы вы осуществили переход игрока из одного мира в другой, не заметив этого перехода.

Во всяком случае, я думаю, у вас есть общая идея. Как бы вы это сделали?

netaholic
источник
Поэтому у меня есть некоторые проблемы с ответами здесь. @Aracthor Я говорил с вами о гладких коллекторах раньше, это относится и к этому. Однако есть 2 довольно высоких ответа, поэтому мне интересно, есть ли смысл ...
Алек Тил
@AlecTeal, если у вас есть что добавить, пожалуйста, сделайте. Буду рад услышать любые идеи и предложения.
Нетахолик

Ответы:

23

Используйте кусочек шума более высокого порядка. Если вы ранее использовали 2D-шум для карты высот, используйте 3D-шум с фиксированной последней координатой. Теперь вы можете медленно менять положение в последнем измерении, чтобы изменить местность. Поскольку шум Перлина непрерывен во всех измерениях, вы будете получать плавные переходы, пока вы плавно меняете положение, в котором вы выбираете функцию шума.

Если вы хотите изменить местность только на расстоянии от игрока в качестве смещения, например. Вы также можете сохранить смещение для каждой координаты на карте и только увеличивать, но никогда не уменьшать его. Таким образом, карта только обновляется, но никогда не стареет.

Эта идея также работает, если вы уже используете 3D-шум, просто сэмплируйте из 4D. Также обратите внимание на симплексный шум. Это улучшенная версия шума Perlin, которая работает лучше для больших размеров.

Данияр
источник
2
Это интересно. Правильно ли я понимаю, что вы предлагаете генерировать трехмерный шум, использовать xy-срез в определенном z из него в качестве карты высот и плавно переходить к другому срезу, изменяя координату z при увеличении расстояния от игрока?
Нетахолик
@netaholic Точно. Описание его как среза - очень хорошая интуиция. Кроме того, вы можете отслеживать самое высокое значение для последней координаты повсюду на карте и только увеличивать его, но никогда не уменьшать.
Данияр
1
Это блестящая идея. По сути, ваша карта местности будет параболической (или другой кривой) частью трехмерного объема.
Фальшивое имя
Это действительно умная идея.
user253751
5

Ваша идея разделить мир на несколько кусков неплоха. Это просто неполноценно.

Единственная проблема - соединения между кусками. Например, если вы используете перлин-шум для генерации рельефа, а семена различны для каждого куска и рискуете это произойти:

Ошибка рельефа

Решением было бы создание рельефа фрагментов не только из его шумов Perlin, но и из других фрагментов вокруг него.

Алгоритм Перлина использует значения случайной карты вокруг них, чтобы «сгладить» себя. Если они используют общую карту, то они будут сглажены вместе.

Единственная проблема заключается в том, что если вы измените чанк, чтобы он изменился, когда игрок отступит, вам также придется перезагрузить чанки, потому что их границы также должны измениться.

Это не изменит размер чанков, но увеличит минимальное расстояние от игрока до загрузки / выгрузки, потому что чанк должен быть загружен, когда игрок его видит, и, с помощью этого метода, как соседние чанки должны быть слишком ,

ОБНОВИТЬ:

Если каждый кусок вашего мира имеет другой тип, проблема возрастает. Это не просто облегчение. Дорогое решение будет следующим:

Куски нарезать

Давайте предположим, что зеленые куски - это лесные миры, синие - архипелаги, а желтые - плоские пустыни.
Решение здесь состоит в том, чтобы создать «переходные» зоны, в которых ваш рельеф и наземная природа (а также заземленные объекты или все, что вы хотите) будут постепенно переходить от одного типа к другому.

И как вы можете видеть на этой картинке, адская часть кода - это маленькие квадраты в углах чанков: они должны создавать связь между 4 чанками, потенциально разной природы.

Так что для этого уровня сложности я думаю, что классические поколения 2D-мира, такие как Perlin2D, просто не могут быть использованы. Я отсылаю вас к ответу @danijar за это.

Aracthor
источник
Предлагаете ли вы создать «центр» фрагмента из семени, а его края «сглаживать» на основе соседних фрагментов? Это имеет смысл, но это увеличит размер чанка, поскольку он должен быть размером области, которую может наблюдать игрок, плюс удвоить ширину области перехода к смежным чанкам. И область чанка становится тем больше, чем разнообразнее мир.
Нетахолик
@netaholic Это не будет больше, но вроде. Я добавил параграф на это.
Aracthor
Я обновил свой вопрос. Пытался описать некоторые идеи, которые у меня есть
netaholic
Так что другой ответ здесь использует (вроде, не совсем) третье измерение в качестве диаграмм. Также вы также рассматриваете самолет как многообразие, и мне нравятся ваши идеи. Чтобы продлить его немного дальше, вам действительно нужен гладкий коллектор. Вы должны убедиться, что ваши переходы гладкие. Затем вы можете применить размытие или шум, и ответ будет идеальным.
Алек Тил
0

Хотя идея Данияра довольно солидна, в конечном итоге вы можете хранить много данных, если хотите, чтобы локальная область была одинаковой, а расстояние сдвигалось. И запрашивать все больше и больше кусков все более и более сложного шума. Вы можете получить все это более стандартным способом.

Я разработал алгоритм для процедурного генерирования случайного фрактального шума, частично основанный на алгоритме алмазного квадрата, который я установил как бесконечный и детерминированный. Таким образом, бриллиантовый квадрат может создавать бесконечный пейзаж, а также мой собственный довольно блочный алгоритм.

Идея в основном та же самая. Но вместо выборки шумов с более высокой размерностью вы можете перебирать значения на разных итерационных уровнях.

Таким образом, вы по-прежнему сохраняете запрошенные ранее значения и кешируете их (эту схему можно независимо использовать для ускорения уже сверхбыстрого алгоритма). И когда запрашивается новая область, она создается с новым значением 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>

Tatarize
источник