Алгоритм Midpoint Displacement 2D, создающий необычные узоры

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

Сначала создайте массив, представляющий пустую карту. Вы начинаете с присвоения четырем углам случайного значения.

В этом квадрате создайте среднюю точку, усредняя четыре угла и добавляя небольшую «ошибку» или случайное значение. Затем создайте середины 4 сторон, усредняя два угла, между которыми находится каждый. После этих действий у вас останется 4 квадрата. Повторите шаги:

  1. Создайте среднюю точку, усредняя четыре угла и добавляя небольшую «ошибку».

  2. Создайте среднюю точку каждой стороны, усредняя два угла, между которыми находится каждая точка.

На каждой итерации уменьшайте диапазон ГСЧ. Таким образом, исходные несколько точек могут иметь довольно большие вариации, но более поздние точки получают только крошечные корректировки. Это обеспечивает правильное количество деталей в изображении.

Вот функция, которую я написал для выполнения этих шагов и последующей нормализации значений:

public static function generateFloatMatrix(Columns:Int, Rows:Int, RangeModifier:Float = 0.65):Array<Array<Float>>
{
    //Blank 2D Array
    var matrix:Array<Array<Float>> = InitFloatMatrix(Columns, Rows);
    var range:Float = 1;
    
    //Set Values for all four corners
    matrix[0][0] = Math.random() * range;
    matrix[Rows-1][0] = Math.random() * range;
    matrix[0][Columns-1] = Math.random() * range;
    matrix[Rows - 1][Columns - 1] = Math.random() * range;
    
    //Calculates the amount of segments in base 2
    var length = Math.sqrt((Columns * Columns) + (Rows * Rows));
    var power:Int = Std.int(Math.pow(2, Math.ceil(Math.log(length) / Math.log(2))));
    
    //Stores largest calculated value for normalization
    var max:Float = 0;
    
    var width:Int = Std.int(Columns);
    var height:Int = Std.int(Rows);
    
    var i:Int = 1;
    while (i < power)
    {
        //Segment Size
        width = Std.int(Columns / i);
        height = Std.int(Rows / i);
        
        for (y in 0...i)
        {
            for (x in 0...i)
            {
                //Top Left Coordinates per segment
                var left = width * x;
                var top = height * y;
                
                //Find Midpoint
                var xMid = Math.ceil(left + (width / 2));
                var yMid = Math.ceil(top + (height / 2));
                
                //Make sure right and bottom do not go out of bounds
                var right:Int = (left + width < Columns ? left + width : Columns - 1);
                var bottom:Int = (top + height < Rows ? top + height : Rows - 1);
                
                //Sets midpoint value to average of all four corners.
                matrix[yMid][xMid] = 
                    (matrix[top][left] + 
                        matrix[bottom][left] + 
                        matrix[bottom][right] + 
                        matrix[top][right]) / 4;
                        
                //trace ("Top: " + top + " - Left: " + left + " - Bottom: " + bottom + " - Right: " + right);
                
                //Adds random value to midpoint
                matrix[yMid][xMid] += Math.random() * range;
                
                //Set side values to average of adjacent corners
                matrix[top][xMid] = (matrix[top][left] + matrix[top][right]) / 2;
                matrix[bottom][xMid] = (matrix[bottom][left] + matrix[bottom][right]) / 2;
                matrix[yMid][left] = (matrix[top][left] + matrix[bottom][left]) / 2;
                matrix[yMid][right] = (matrix[top][right] + matrix[bottom][right]) / 2;
                
                max = Math.max(matrix[top][left], max);
            }
        }
        
        //Reduces range
        range *= RangeModifier;
        i *= 2;
    }
    
    //Normalizes all values in matrix
    for (y in 0...Rows)
    {
        for (x in 0...Columns)
        {
            matrix[y][x] /= max;
        }
    }
    
    return matrix;
}

Это изображения, которые он создает, если я использую каждое значение для рендеринга каждого пикселя с указанной координатой. Все пиксели, отображаемые белым цветом, имеют значение 0, а черные — значение 1.

Визуализируется как 8x8 пикселей

Визуализировано как 1x1 пиксель


person Tim Stoddard    schedule 12.11.2014    source источник


Ответы (1)


Ваша проблема в том, что вы не обязательно попадаете в уже заполненные пиксели своими вычислениями, если размеры вашей карты не являются степенью двойки. Например, если ваша карта имеет ширину 30 единиц, ширина вашей сетки составляет 15 при первом проходе и 7 при втором проходе, где он основывает свои расчеты на еще нетронутой единице 14.

Решение состоит в том, чтобы выполнять все вычисления с арифметикой с плавающей запятой, пока вы не определите единичные индексы, которые, конечно, должны быть целыми числами:

while (i < power)
{
    var width:Float = Columns / i;    // Floating-point division
    var height:Float = Rows / i;

    for (y in 0...i)
    {
        for (x in 0...i)
        {
            var left:Int = Math.floor(width * x);
            var top:Int = Math.floor(height * y);

            var xMid:Int = Math.floor(width * (x + 0.5));
            var yMid:Int = Math.floor(height * (y + 0.5));

            var right:Int = Math.floor(width * (x +1));
            var bottom:Int = Math.floor(height * (y + 1));

            //Make sure right and bottom do not go out of bounds
            if (right > Columns - 1) right = Columns - 1;
            if (bottom > Rows - 1) bottom = Rows - 1;

            // Do offset and interpolation stuff
        }
    }
}

Это должно дать вам случайную карту, эффект миллиметровки и все такое.

(Предостережение: я не знаком с Haxe, но проверял это на Javascript, который не имеет целочисленного типа. Я использовал Math-floor везде, где вы захотите сделать это способом Haxe.)

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

person M Oehm    schedule 12.11.2014
comment
Применил сделанные вами предложения, теперь все выглядит намного лучше, спасибо! Результат Есть ли способ убрать эффект миллиметровки? Например, вторая интерполяция? - person Tim Stoddard; 13.11.2014
comment
Спасибо, что поделились результатом. Эффект миллиметровки — типичный артефакт алгоритма смещения средней точки. Это происходит потому, что вы добавляете шум только к середине каждого квадрата, а не к средним точкам краев. Алгоритм также чувствителен к модификатору диапазона; возможно, калибровка дает лучшие результаты. Это распространенная проблема, взгляните на соответствующие вопросы на правой боковой панели. - person M Oehm; 13.11.2014
comment
Вы также можете добавить шум в два этапа: сначала добавьте шум в середину каждого квадрата. Затем добавьте шум к краям квадратов, рассматривая их как середины квадрата, повернутого на 45°, углы которого являются конечными точками краев и серединами двух квадратов, соединенных краями. Таким образом, вы усредняете и добавляете шум в две сетки, прямолинейную и диагональную, попеременно. После каждого этапа следует умножать диапазон на квадратный корень из модификатора диапазона. Это уменьшит эффект, но, вероятно, не избавит от него полностью. - person M Oehm; 13.11.2014
comment
Поиграв немного со смещением средней точки, я обнаружил, что ваш эффект миллиметровки особенно заметен. Он присутствует в большинстве случаев, как в примерах на странице Википедии, но гораздо слабее. Я предполагаю, что ваши складки возникают в результате нормализации данных, где max намного больше 1. Вы только добавляете рельеф, а ваши углы начинаются со значения от 0 до 1. Лучшей стратегией может быть начать со значений около 0,5 (например, 0.25 + random() ) и позволить средней точке идти в обе стороны: m += range * (random() - 0.5). - person M Oehm; 13.11.2014
comment
Я создал небольшую игрушечную веб-страницу, где вы можете поиграться с некоторыми настройками. Настройки по умолчанию позволяют ландшафту подниматься или опускаться в модпойнтах; они дают разумный рельеф со случайными слабыми складками. Если вы установите затухание диапазона на 0,3 и отметите нормализацию, вы получите гладкий ландшафт с сильными складками, похожими на звездообразование. Отметьте двухэтапный алгоритм, и складки исчезнут. (Однако пики все еще есть.) Таким образом, решения заключаются в том, чтобы сначала выбрать разумные диапазоны значений и использовать двухпроходный алгоритм для точной настройки. - person M Oehm; 14.11.2014
comment
Хорошо, я сделал ваше первое предложение добавить шум к средним точкам краев, который получил это. Я не был полностью уверен в вашем двухэтапном методе, я думал, что вы упомянули бы об изменении точек для усреднения конечных точек, но в итоге это дало мне действительно темные и блочные паттерны. Наконец, я попробовал вашу стратегию работы с нормализацией, начав четыре угла с (0,25 + random()) и позволив средней точке идти в обоих направлениях, что дало это, который выглядит так, как будто он значительно уменьшил эффект миллиметровой бумаги, но все еще блочный. - person Tim Stoddard; 14.11.2014
comment
Похоже, MPD — очень чувствительный алгоритм. Ваша блочность происходит из-за разницы в высоте и ширине. Поскольку вы равномерно увеличиваете шаг и должны учитывать наибольшее измерение, шум будет многократно применяться к одной и той же линии в другом измерении. В большинстве примеров реализации используются квадратные сетки и обычно также используются измерения, равные 2^k + 1, что избавит вас от всех арифметических операций с плавающей запятой. Пример, на который вы ссылаетесь, делает это, как и мой код. Это дает достаточно реалистичные результаты. - person M Oehm; 14.11.2014
comment
Итак, что вы можете сделать, так это: разверните карту до квадрата и работайте с ней. Очевидно, что вы собираетесь выбросить часть карты после генерации. В качестве альтернативы вы можете разделить свою карту на квадраты, например. 3x4 плитки размером 160x160 пикселей каждая и сместите их. Однако вы должны создать бесшовные переходы между тайлами, чтобы вы могли рассматривать карту тайлов как расширенное состояние в алгоритме с одинаковой шириной шага в каждом направлении, но с разным размером. - person M Oehm; 14.11.2014
comment
Понятно, я помню шаги, в которых упоминалось, что он ограничен фиксированными размерами, и говорилось, что для обработки прямоугольников необходимо сделать дополнительные шаги. Досадно, что не сказано, какими будут дополнительные шаги. Думаю, я мог бы вычислить наибольший общий делитель обоих значений и использовать его для определения размера сетки? - person Tim Stoddard; 14.11.2014
comment
Да, GCD кажется хорошим вариантом, но он будет работать только в том случае, если ваши размеры не являются относительно простыми или когда GCD мал. Думаю, достаточно, чтобы размер сетки был примерно одинаковым в обоих направлениях. Вы, вероятно, знаете размеры своей карты, поэтому вы также можете оценить хороший начальный размер сетки и сделать его параметром функции. - person M Oehm; 14.11.2014
comment
Я понимаю. Я намеревался использовать это для общей функции, но это, вероятно, усложнит мою ситуацию. - person Tim Stoddard; 14.11.2014
comment
Давайте продолжим это обсуждение в чате. - person Tim Stoddard; 14.11.2014
comment
Похоже, что метод отсечения дает наилучшие результаты, несмотря на экспоненциальное снижение скорости алгоритма. Вот результаты: i.imgur.com/hXsez9q.gif - person Tim Stoddard; 14.11.2014