Я реализовал функцию Diamond-Square, которая создает карту высот. Реализация, кажется, работает правильно на первый взгляд.
Это только два примера, но уже сейчас видно, что выходные значения в целом довольно высоки. Есть только несколько действительно темных ценностей. я. 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>