Реализация Diamond-Square дает слишком высокие значения

Я реализовал функцию Diamond-Square, которая создает карту высот. Реализация, кажется, работает правильно на первый взгляд.

enter image description here

enter image description here

Это только два примера, но уже сейчас видно, что выходные значения в целом довольно высоки. Есть только несколько действительно темных ценностей. я. E. Если вы посмотрите на карту высот (созданную квадратом алмаза) в этой статье, вы увидите, что они не так однородны, как мои. Между разными регионами намного больше смещений. Есть регионы, похожие на кратеры.

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

Я работаю над плоским типизированным массивом. Параметры, которые я передаю функции:

  • sideLength
    • Поскольку у меня есть плоский массив, представляющий 2D матрицу, я передаю длину стороны сетки для дальнейших вычислений. Я передаю значение 257 здесь.
  • maxHeight
    • Максимально возможное выходное значение. Я передаю 255 здесь, потому что позже я использую выходные данные для отображения карты высот на холсте.
  • roughness
    • Это значение смещения, которое я использую в квадратном шаге, чтобы получить больше случайных смещений высоты. Здесь я обычно принимаю значение около 50 здесь.

Я Heightmap функцию Heightmap, чтобы получить вывод:

/**
 * Creates a heightmap based on parameters passed.
 * @param {number} sideLength - Side length of a the resulting grid array. Diamond-Square can only have a size (2^n)+1.
 * @param {number} maxHeight - Max height value for the heightmap values.
 * @param {number} roughness - A factor which is used as offset value for the heightmap. Defines the roughness of a heightmap.
 * @returns {Float32Array} - A flat 'Float32Array' representing a 2D-grid with size 'sideLength * sideLength'.
 */
static HeightMap(sideLength, maxHeight, roughness) {

    const n = Math.log(sideLength - 1) / Math.log(2);
    if (n < 0 || n % 1 != 0) {
        throw "Invalid side length in Diamond Square: Side Length has to be in range of '(2^n) + 1'.";
    }

    let gridArray = new Float32Array(sideLength * sideLength);
    this._initGrid(gridArray, sideLength, maxHeight);
    this._seed(gridArray, sideLength, roughness);

    return gridArray;
}

Здесь сначала начинается "сетка":

/**
 * Sets the initial corner values for a Diamond-Square grid.
 * @param {Float32Array} gridArray - An 'Float32Array' with its values (ideally) set to '0'.
 * @param {number} sideLength - Side length of a the resulting grid array. Diamond-Square can only have a size '(2^n)+1'.
 * @param {number} maxHeight - Max height value for the heightmap values.
 * @returns {Float32Array} - A flat 'Float32Array' representing a 2D-grid with its NW, NE, SE and SW values initialized.
 */
static _initGrid(gridArray, sideLength, maxHeight) {

    gridArray[0] = MathHelper.RandomInt(0, maxHeight); // NW
    gridArray[sideLength - 1] = MathHelper.RandomInt(0, maxHeight); // NE
    gridArray[sideLength * sideLength - 1] = MathHelper.RandomInt(0, maxHeight); // SE
    gridArray[sideLength * sideLength - sideLength] = MathHelper.RandomInt(0, maxHeight); // SW

    return gridArray;
}

После этого функция HeightMap вызывает _seed которая в основном является циклом Diamond-Square:

/**
 * Performs the Diamond Square (aka. Midpoint displacement) algorithm on a given flat TypedArray.
 * @param {Float32Array} gridArray - An (Diamond-Square-initialized) 'Float32Array'.
 * @param {number} sideLength - Side length of a the resulting grid array.
 * @param {number} roughness - A factor which is used as offset value for the heightmap. Defines the roughness of a heightmap.
 * @returns {Float32Array} - Returns a ready to use heightmap produced by the Diamond-Square algorithm.
 */
static _seed(gridArray, sideLength, roughness) {
    let step = Math.sqrt(gridArray.length) - 1;
    let size = Math.sqrt(gridArray.length) - 1;
    let currentRoughness = roughness;

    while (step / 2 >= 1) {

        let numSquares = (Math.pow(size, 2)) / (Math.pow(step, 2));
        let perRowSquares = Math.floor(Math.sqrt(numSquares));
        for (let i = 0; i < perRowSquares; i++) {
            for (let j = 0; j < perRowSquares; j++) {
                const nwIndex = this._getNWIndex(i, j, step, sideLength);
                const cornerValues = this._getCornerValues(nwIndex, gridArray, sideLength, step);
                this._diamondStep(nwIndex, cornerValues, gridArray, sideLength, step, currentRoughness);
                this._squareStep(nwIndex, cornerValues, gridArray, sideLength, step, currentRoughness);
            }
        }

        currentRoughness /= 2.0;
        step /= 2;
    }

    return gridArray;
}

Примечание. Я рассчитываю индексы позиции на основе индекса текущего северо-западного индекса. Для этого у меня есть функция:

/**
 * Returns the array index for the north-west value for the current step.
 * @param {number} i - Current row, I guess.
 * @param {number} j - Current column, I guess.
 * @param {number} stepSize - Current step size.
 * @param {number} sideLength - Grid side length.  
 * @returns {number} - Returns the index for current north-west value.
 */
static _getNWIndex(i, j, stepSize, sideLength) {
    return (i * (stepSize * sideLength)) + j * stepSize;
}

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

/**
 * Return an array holding the north-west, north-east, south-west and south-east values for the current step.
 * @param {number} nwIndex - North-West index for current step. 
 * @param {Float32Array} gridArray - The corner values for the current step.  
 * @param {number} sideLength - Grid side length. 
 * @param {number} stepSize - Current step size.  
 * @returns {Float32Array} - Returns the typed array the function of operating on.
 */
static _getCornerValues(nwIndex, gridArray, sideLength, stepSize) {
    return [
        gridArray[nwIndex], // NW
        gridArray[nwIndex + stepSize], // NE
        gridArray[nwIndex + stepSize * sideLength], // SW
        gridArray[nwIndex + stepSize + stepSize * sideLength] // SE
    ];
}

И последнее, но не менее важное: у меня есть _diamondStep и _sqaureStep:

/**
 * Performs the Diamond Step by setting the center value for the current step.
 * @param {number} nwIndex - North-West index for current step.
 * @param {number[]} cornerValues - The corner values for the current step.
 * @param {Float32Array} gridArray - Array holding heightmap data. Function will write to this array.
 * @param {number} sideLength - Grid side length. 
 * @param {number} stepSize - Current step size.
 * @returns {Float32Array} - Returns the typed array the function of operating on.
 */
static _diamondStep(nwIndex, cornerValues, gridArray, sideLength, stepSize, roughness) {

    // Center point. Calculated from "East - 'stepSize / 2'"
    gridArray[(((nwIndex + stepSize * sideLength) + stepSize) - (stepSize * sideLength) / 2) - stepSize / 2]
        = (cornerValues[0] + cornerValues[1] + cornerValues[2] + cornerValues[3]) / 4 + (roughness * MathHelper.RandomInt(-1, 1));

    return gridArray;
}

/**
 * Performs the Square Step by setting the north, east, south and west values for the current step.
 * @param {number} nwIndex - North-West index for current step.
 * @param {number[]} cornerValues - The corner values for the current step. 
 * @param {Float32Array} gridArray - Array holding heightmap data. Function will write to this array. 
 * @param {number} sideLength - Grid side length.  
 * @param {number} stepSize - Current step size. 
 * @param {number} roughness - Roughness factor for the current step.
 * @returns {Float32Array} - Returns the typed array the function of operating on.
 */
static _squareStep(nwIndex, cornerValues, gridArray, sideLength, stepSize, roughness) {

    const average = (cornerValues[0] + cornerValues[1] + cornerValues[2] + cornerValues[3]) / 4;
    const value = average + (roughness * MathHelper.RandomInt(-1, 1));

    // N
    gridArray[nwIndex + (stepSize / 2)] = value;
    // E
    gridArray[((nwIndex + stepSize * sideLength) + stepSize) - (stepSize * sideLength) / 2] = value;
    // S
    gridArray[(nwIndex + stepSize * sideLength) + stepSize / 2] = value;
    // W
    gridArray[(nwIndex + stepSize * sideLength) - (stepSize * sideLength) / 2] = value;

    return gridArray;
}

Как я упоминал ранее, реализация, похоже, работает. Тем не менее, мне интересно, вызвана ли общая "белизна" неправильной параметризацией или неправильной реализацией?

Вот рабочая скрипка:

function HeightMap(sideLength, maxHeight, roughness) {

  const n = Math.log(sideLength - 1) / Math.log(2);
  if (n < 0 || n % 1 != 0) {
    throw "Invalid side length in Diamond Square: Side Length has to be in range of '(2^n) + 1'.";
  }

  let gridArray = new Float32Array(sideLength * sideLength);
  _initGrid(gridArray, sideLength, maxHeight);
  _seed(gridArray, sideLength, roughness);

  return gridArray;
}


function _initGrid(gridArray, sideLength, maxHeight) {

  gridArray[0] = RandomInt(0, maxHeight); // NW
  gridArray[sideLength - 1] = RandomInt(0, maxHeight); // NE
  gridArray[sideLength * sideLength - 1] = RandomInt(0, maxHeight); // SE
  gridArray[sideLength * sideLength - sideLength] = RandomInt(0, maxHeight); // SW

  return gridArray;
}


function _seed(gridArray, sideLength, roughness) {
  let step = Math.sqrt(gridArray.length) - 1;
  let size = Math.sqrt(gridArray.length) - 1;
  let currentRoughness = roughness;

  while (step / 2 >= 1) {

    let numSquares = (Math.pow(size, 2)) / (Math.pow(step, 2));
    let perRowSquares = Math.floor(Math.sqrt(numSquares));
    for (let i = 0; i < perRowSquares; i++) {
      for (let j = 0; j < perRowSquares; j++) {
        const nwIndex = _getNWIndex(i, j, step, sideLength);
        const cornerValues = _getCornerValues(nwIndex, gridArray, sideLength, step);
        _diamondStep(nwIndex, cornerValues, gridArray, sideLength, step, currentRoughness);
        _squareStep(nwIndex, cornerValues, gridArray, sideLength, step, currentRoughness);
      }
    }

    currentRoughness /= 2.0;
    step /= 2;
  }

  return gridArray;
}


function _diamondStep(nwIndex, cornerValues, gridArray, sideLength, stepSize, roughness) {
  gridArray[(((nwIndex + stepSize * sideLength) + stepSize) - (stepSize * sideLength) / 2) - stepSize / 2] =
    (cornerValues[0] + cornerValues[1] + cornerValues[2] + cornerValues[3]) / 4 + (roughness * RandomInt(-1, 1));

  return gridArray;
}

function _squareStep(nwIndex, cornerValues, gridArray, sideLength, stepSize, roughness) {

  const average = (cornerValues[0] + cornerValues[1] + cornerValues[2] + cornerValues[3]) / 4;
  const value = average + (roughness * RandomInt(-1, 1));

  // N
  gridArray[nwIndex + (stepSize / 2)] = value;
  // E
  gridArray[((nwIndex + stepSize * sideLength) + stepSize) - (stepSize * sideLength) / 2] = value;
  // S
  gridArray[(nwIndex + stepSize * sideLength) + stepSize / 2] = value;
  // W
  gridArray[(nwIndex + stepSize * sideLength) - (stepSize * sideLength) / 2] = value;

  return gridArray;
}

function _getCornerValues(nwIndex, gridArray, sideLength, stepSize) {
  return [
    gridArray[nwIndex], // NW
    gridArray[nwIndex + stepSize], // NE
    gridArray[nwIndex + stepSize * sideLength], // SW
    gridArray[nwIndex + stepSize + stepSize * sideLength] // SE
  ];
}

function _getNWIndex(i, j, stepSize, sideLength) {
  return (i * (stepSize * sideLength)) + j * stepSize;
}

function GenerateIterations(max) {
  let iterations = [];
  for (let n = 0; n < max; n++) {
    iterations.push(Math.pow(2, n) + 1);
  }
  return iterations;
}

function Grayscale(canvasName, data, rows, cols) {
  let canvas = document.getElementById(canvasName);
  let ctx = canvas.getContext("2d");

  let imageData = ctx.createImageData(cols, rows);

  for (let i = 0; i < data.length; i++) {
    const color = data[i];
    imageData.data[i * 4] = color;
    imageData.data[i * 4 + 1] = color;
    imageData.data[i * 4 + 2] = color;
    imageData.data[i * 4 + 3] = 255;
  }

  ctx.putImageData(imageData, 0, 0);
}

function RandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

let terrainGrid = HeightMap(257, 255, 50);
Grayscale('grayscaleCanvas', terrainGrid, 257, 257);
.greyscaleCanvas {
  border: solid 1px black;
}
<canvas id="grayscaleCanvas" class="greyscaleCanvas" width="257px" height="257px"></canvas>

Ответы

Ответ 1

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

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

function HeightMap(sideLength, maxHeight, roughness) {

  const n = Math.log(sideLength - 1) / Math.log(2);
  if (n < 0 || n % 1 != 0) {
    throw "Invalid side length in Diamond Square: Side Length has to be in range of '(2^n) + 1'.";
  }

  let gridArray = new Array(sideLength);
  for (var i = 0; i < gridArray.length; i++) {
    gridArray[i] = new Float32Array(sideLength);
    }
  gridArray = _initGrid(gridArray, sideLength, maxHeight);
    gridArray = _seed(gridArray, sideLength, roughness);
  return gridArray;
}


function _initGrid(gridArray, sideLength, maxHeight) {

  gridArray[0][0] = RandomInt(0, maxHeight); // NW
  gridArray[0][sideLength-1] = RandomInt(0, maxHeight); // NE
  gridArray[sideLength-1][sideLength-1] = RandomInt(0, maxHeight); // SE
  gridArray[sideLength-1][0] = RandomInt(0, maxHeight); // SW

  return gridArray;
}


function _seed(gridArray, sideLength, roughness) {
  let step = sideLength - 1;
  let size = sideLength - 1;
  let currentRoughness = roughness;

    let run_num = 0
  while (step / 2 >= 1) {
    console.log(run_num)
        run_num = run_num + 1
    let numSquares = Math.pow(size, 2) / Math.pow(step, 2);
    let perRowSquares = Math.floor(Math.sqrt(numSquares));
    for (let i = 0; i < perRowSquares; i++) {
      for (let j = 0; j < perRowSquares; j++) {
        row = i*step
        col = j*step
        const squareCornerValues = _getSquareCornerValues(gridArray, row, col, step)
        gridArray = _diamondStep(squareCornerValues, row, col, step, gridArray, currentRoughness);
        gridArray = _squareStep(row, col, step, gridArray, sideLength, currentRoughness)
       // _squareStep(diamondMidPoints, gridArray, step, currentRoughness);
      }
    }

    currentRoughness /= 2.0;
    step /= 2;
  }
  return gridArray;
}


function _diamondStep(squareCornerValues, i, j, step, gridArray, currentRoughness) {
  gridArray[row+step/2][col+step/2] = (squareCornerValues[0] + squareCornerValues[1] + squareCornerValues[2] + squareCornerValues[3] ) / 4  + (currentRoughness * RandomInt(-1, 1));
  return gridArray;
}

function _squareStep(row, col, step, gridArray, sideLength, currentRoughness) {

  let diamondMidPoints = [[row, col+step/2], //top
                                                [row+step/2, col], //left
                            [row+step, col+step/2], //right
                            [row+step/2, col+step] //bottom
                             ];
  for (let z = 0; z < diamondMidPoints.length; z++){
    corners = _get_diamond_corners(diamondMidPoints[z], step, sideLength, gridArray);
    gridArray[diamondMidPoints[z][0]][diamondMidPoints[z][1]] = (corners[0] + corners[1] + corners[2] + corners[3]) /4  + (currentRoughness * RandomInt(-1, 1));
  }

  return gridArray;
}

function _getSquareCornerValues(gridArray, row, col, step) {
  return [
    gridArray[row][col], // NW
    gridArray[row][col+step], // NE
    gridArray[row+step][col], // SW
    gridArray[row+step][col+step] // SE
  ];
}

function _get_diamond_corners(diamondMidPoints, step, sideLength, gridArray){
    row = diamondMidPoints[0];
  col = diamondMidPoints[1];
  top_coord = [(row  - step/2 + sideLength) % sideLength, col];
  left_coord = [row, (col - step/2 + sideLength) % sideLength];
  right_coord = [row, (col + step/2 + sideLength) % sideLength];
  bottom_coord = [(row + step/2 + sideLength) % sideLength, col];
  return [gridArray[top_coord[0]][top_coord[1]],
                gridArray[left_coord[0]][left_coord[1]],
          gridArray[right_coord[0]][right_coord[1]],
          gridArray[bottom_coord[0]][bottom_coord[1]]
             ];
}

function Grayscale(canvasName, data, rows, cols) {
  let canvas = document.getElementById(canvasName);
  let ctx = canvas.getContext("2d");

  let imageData = ctx.createImageData(cols, rows);

  for (let i = 0; i < data.length; i++) {
    const color = data[i];
    imageData.data[i * 4] = color;
    imageData.data[i * 4 + 1] = color;
    imageData.data[i * 4 + 2] = color;
    imageData.data[i * 4 + 3] = 255;
  }

  ctx.putImageData(imageData, 0, 0);
}

function RandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}


let terrainGrid = HeightMap(257, 255, 50);
terrainList = []
for (let q=0; q < 257; q++) {
  terrainList.push.apply(terrainList, terrainGrid[q])
}
Grayscale('grayscaleCanvas', terrainList, 257, 257);

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

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

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

Здесь JSFiddle со всем, надеюсь, это больше, чем вы хотите. (Примечание: если что-то не делается идиоматически, это потому, что я не очень хорош в Javascript): https://jsfiddle.net/z6so4xyc/15/