Зіставлення шматка процесуально сформованого світу до шматка іншого світу


18

Ви читали «Хроніки бурштину» Роджера Зелазного?

Уявіть, що ви граєте в гру MMO від третьої особи. Ви нерестуєтесь у світі і починаєте бродити навколо. Через деякий час, коли ти думаєш, що ти дізнався карту, ти розумієш, що ти в такому місці, якого ти ще ніколи не бачив. Ви повертаєтесь до останнього місця, про яке ви впевнені, що знаєте, і воно все ще є. Але решта світу змінилася, і ви навіть не помітили, як це сталося.

Я читав про процесуальне покоління світу. Я читав про шум і октави Перліна, шум симплекс, алгоритм алмазного квадрата, про моделювання тектонічних плит та водної ерозії. Я вважаю, що я маю певне розуміння загального підходу в поколінні процедурного світу.

І маючи ці знання, я не маю уявлення про те, як можна зробити щось на кшталт написаного вище. Кожна ідея, яка мені спадає на думку, стикається з деякими теоретичними проблемами. Ось кілька ідей, про які я можу придумати:

1) "Реверсивне" світове покоління з числом насіння в якості вхідного даних та деяким повністю описуючим числом

Сумніваюся, що це навіть можливо, але я уявляю функцію, яка отримає насіння і створить матрицю чисел, на якій будуються шматки. І для кожного унікального номера є унікальний шматок. І друга функція, яка отримує це унікальне число фрагмента і дає насіння, яке містить це число. Я спробував скласти схему на малюнку нижче:

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

2) Зробити шматки абсолютно випадковими і зробити перехід між ними.

Як запропонував Арактор . Переваги такого підходу полягають у тому, що це можливо і він не вимагає магічного злиття :)

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

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

У будь-якому випадку, я вважаю, що ти маєш загальну думку. Як би ти це зробив?


Тож у мене тут є деякі проблеми з відповідями. @Aracthor Я говорив з вами про гладкі колектори раніше, такі стосунки стосуються тут. Однак є 2 досить високих відповіді, тому мені цікаво, чи є сенс ...
Alec Teal

@AlecTeal, якщо вам є що додати, будь ласка, зробіть. Буду радий почути будь-які ідеї та пропозиції.
netaholic

Відповіді:


23

Використовуйте шматочок шуму вищого порядку. Якщо раніше ви використовували 2d шум для відображення висоти, замість цього використовуйте 3D-шум із останньою координатою. Тепер ви можете повільно змінювати положення в останньому вимірі, щоб змінити місцевість. Оскільки шум Перліна безперервний у всіх вимірах, ви отримаєте плавні переходи до тих пір, поки ви плавно зміните положення, в якому відбираєте вибірку функції шуму.

Якщо ви хочете, наприклад, змінити місцевість далеко від відстані до гравця, як компенсувати. Ви також можете зберігати зміщення кожної координати на карті і лише збільшувати, але ніколи не зменшувати її. Таким чином карта стає лише новішою, але ніколи не старіє.

Ця ідея також працює, якщо ви вже використовуєте 3D-шум, просто зразок із 4D потім. Також погляньте на шум Simplex. Це вдосконалена версія шуму Perlin і працює краще для інших габаритів.


2
Це цікаво. Я правильно розумію, що ви пропонуєте генерувати 3d-шум, використовувати xy-фрагмент на певній з нього як мапу висоти та зробити плавний перехід на інший фрагмент, змінюючи координату z, оскільки відстань від гравця збільшується?
нетаголік

@netaholic Рівно. Охарактеризувати його як шматочок - це дуже гарна інтуїція. Крім того, ви можете відслідковувати найвище значення для останньої координати скрізь на карті та лише збільшувати її, але ніколи не зменшувати.
danijar

1
Це геніальна ідея. В основному, ваша карта місцевості буде параболічним (або іншим кривим) фрагментом через 3D-об'єм.
Підроблене ім’я

Це дійсно розумна ідея.
користувач253751

5

Ваша ідея розділити світ на кілька шматочків - непогана. Це просто неповно.

Єдина проблема - стики між шматками. Наприклад, якщо ви використовуєте перлин-шум для створення рельєфу, а також насіння, що відрізняється для кожного куска, і ризикуєте цього статися:

Кусковий рельєфний клоп

Рішенням було б створити полегшення шматка не тільки з його перменного шуму, але й з інших шматочків навколо нього.

Алгоритм Перліна використовує значення випадкової карти навколо них, щоб "згладити" себе. Якщо вони використовують загальну карту, вони згладжуються разом.

Єдина проблема - якщо ви поміняєте насіння шматка, щоб зробити його різним, коли гравець відступає, вам доведеться також перезавантажувати шматки навколо, тому що їхні межі повинні змінюватися.

Це не змінило б розмір шматочків, але це збільшило б мінімальну відстань від гравця до завантаження / вивантаження, тому що шматок повинен бути завантажений, коли гравець бачить це, і, за допомогою цього методу, суміжні шматки повинні бути занадто .

ОНОВЛЕННЯ:

Якщо кожен шматок вашого світу різного типу, проблема наростає. Мова йде не лише про полегшення. Дорогим рішенням було б таке:

Шматки нарізати

Припустимо, зелені куски - це лісові світи, сині архіпелаги та жовті плоскі пустелі.
Вирішення тут полягає у створенні "перехідних" зон, де ваша рельєфна та наземна природа (а також обґрунтовані об'єкти або все, що завгодно) поступово перетворюватимуться з одного типу на інший.

І як ви бачите на цій фотографії, пекельна частина для кодування мала б маленькі квадрати в куточках кутів: вони повинні зробити зв’язок між 4 шматками, потенційно різними натурами.

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


Ви пропонуєте створити «центр» шматка з насіння, а його краї «згладити» на основі сусідніх шматочків? Це має сенс, але це збільшить розмір шматка, оскільки це повинен бути розмір площі, яку гравець може спостерігати плюс подвоїти ширину перехідної області до сусідніх шматок. А площа шматка стає ще більшою, чим різноманітнішим є світ.
нетаголік

@netaholic Це було б не більше, а своєрідне. Я додав на ньому абзац.
Арактхор

Я оновив своє запитання. Намагався описати деякі ідеї, які я маю
нетаголік

Тож інша відповідь тут використовує (начебто, не зовсім) третій вимір як графіки. Також ви також розглядаєте літак як колектор, і мені подобаються ваші ідеї. Щоб розширити його трохи далі, ви дуже хочете плавного колектора. Вам потрібно забезпечити плавні переходи. Потім ви можете застосувати розмиття або шум до цього, і відповідь була б ідеальною.
Alec Teal

0

Незважаючи на те, що ідея даніяра є досить міцною, ви можете в кінцевому підсумку зберігати багато даних, якщо хочете, щоб місцева територія була однаковою і зсув відстані. І вимагаючи все більше фрагментів все більш складного шуму. Ви можете отримати все це більш стандартним способом 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>

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.