Генерация биомов (часть 2)

Генерация биомов (часть 2)

Версия(и) Minecraft
1.12.2
Содержание

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

Оглавление

  • Генерация биомов в мире (часть 2) - в данном разделе мы закончим знакомство со стандартным алгоритмом генератором биомов.
  • Собственные слои - разберем несколько различных алгоритмов генерации биомов.
1 Генерация биомов в мире (часть 2)

1.2 Масштабирование слоев
Первое, что следует разобрать подробнее - это, конечно же, масштабирование слоев. В предыдущем руководстве уже оговаривалось, что такие слои, как GenLayerZoom, GenLayerFuzzyZoom и GenLayerVoronoiZoom масштабируют входящий массив чисел следуя определенным алгоритмам.

Масштабирование очень сильно влияет на итоговый результат генерации. В качестве примера рассмотрим стандартный алгоритм генерации биомов и простой вариант алгоритма, который генерирует почти такую же поверхность (об этом во второй главе руководства), которые будем использовать для генерации одной и той же области мира (без рек и редких вариаций биомов):
  • Первый слой GenLayerIsland. Чтобы определить насколько сильно масштабируется каждый кусочек поверхности сразу же покрашен в уникальный цвет.
Масштабирование слоев_1.png

Масштабирование слоев_2.png

  • Последний слой GenLayerVoronoiZoom. Целиком влезает только розовая область, которая появилась из одного единственного пикселя. Сразу можно понять, что она внушительных размеров. Размер сгенерированной области: 1200 x 1200 чанков. Розовая область покрывает ~536 x 536 чанков.
Масштабирование слоев_3.png

Масштабирование слоев_4.png
Но возникает вопрос:
  • Как генерировать маленькие биомы типа BEACH (Пляж)?
И на него следует очевидный, но не совсем точный ответ:
  • Чем меньше масштабирующих слоев влияет на область, тем меньше эта область будет в конечном итоге.
То есть, если результат работы слоя GenLayerIsland масштабируется 11 раз, то GenLayerShore - всего 3 раза. Поэтому в конечном результате BEACH (Пляж) по сравнению с общей поверхностью, образованной один единственным розовым пикселем, занимает очень мало места.

А теперь обратите внимание на белую область, которая также присутствует в больших количествах, но которую не генерировал слой GenLayerIsland. Это - поверхность, которая образуется в результате работы слоя GenLayerRemoveTooMuchOcean. Слой не масштабирует, но создает не менее масштабные области посреди океанов. Это не согласуется с ответом, который приведен выше, потому что данный слой во время своей работы создает новую поверхность в количествах часто даже больших, чем получается после GenLayerIsland и трех масштабирований. Из всего этого можно сделать вывод:
  • Область, образующаяся после работы масштабируемых слоев, будет тем больше, чем больше масштабируемых слоев используется в алгоритме при условии, что изначальное количество масштабируемых пикселей не меняется.
1.3 GenLayerZoom
Настало время разобраться с самым важным элементом конструктора - алгоритмом, который масштабирует слои. В данном разделе я рассмотрю следующий массив данных, который и буду масштабировать:
Родитель1.png
И выглядит алгоритм так:
Java:
public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
    int i = areaX >> 1;
    int j = areaY >> 1;
    int k = (areaWidth >> 1) + 2;
    int l = (areaHeight >> 1) + 2;
    int[] aint = this.parent.getInts(i, j, k, l);
    int i1 = k - 1 << 1;
    int j1 = l - 1 << 1;
    int[] aint1 = IntCache.getIntCache(i1 * j1);

    for (int k1 = 0; k1 < l - 1; ++k1) {
        int l1 = (k1 << 1) * i1;
        int i2 = 0;
        int j2 = aint[i2 + 0 + (k1 + 0) * k];

        for (int k2 = aint[i2 + 0 + (k1 + 1) * k]; i2 < k - 1; ++i2) {
            this.initChunkSeed((long) (i2 + i << 1), (long) (k1 + j << 1));
            int l2 = aint[i2 + 1 + (k1 + 0) * k];
            int i3 = aint[i2 + 1 + (k1 + 1) * k];
            aint1[l1] = j2;
            aint1[l1++ + i1] = this.selectRandom(new int[] { j2, k2 });
            aint1[l1] = this.selectRandom(new int[] { j2, l2 });
            aint1[l1++ + i1] = this.selectModeOrRandom(j2, l2, k2, i3);
            j2 = l2;
            k2 = i3;
        }
    }

    int[] aint2 = IntCache.getIntCache(areaWidth * areaHeight);

    for (int j3 = 0; j3 < areaHeight; ++j3) {
        System.arraycopy(aint1, (j3 + (areaY & 1)) * i1 + (areaX & 1), aint2, j3 * areaWidth, areaWidth);
    }

    return aint2;
}
Читабельность кода оставляет желать лучшего. Немного подредактируем и получим более приятный результат:
Java:
public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
    int i = areaX >> 1;
    int j = areaY >> 1;
    int k = (areaWidth >> 1) + 2;
    int l = (areaHeight >> 1) + 2;
    int[] aint = this.parent.getInts(i, j, k, l);
    int k_support = k - 1 << 1;
    int l_support = l - 1 << 1;
    int[] aint_support = IntCache.getIntCache(k_support * l_support);

    for (int l1 = 0; l1 < l - 1; l1++) {
        int id = (l1 << 1) * k_support;
        int a1 = aint[0 + 0 + (l1 + 0) * k];
        int a2 = aint[0 + 0 + (l1 + 1) * k];

        for (int k1 = 0; k1 < k - 1; k1++) {
            this.initChunkSeed((long) (k1 + i << 1), (long) (l1 + j << 1));
            int a3 = aint[k1 + 1 + (l1 + 0) * k];
            int a4 = aint[k1 + 1 + (l1 + 1) * k];
            aint_support[id] = a1;
            aint_support[id++ + k_support] = this.selectRandom(new int[] { a1, a2 });
            aint_support[id] = this.selectRandom(new int[] { a1, a3 });
            aint_support[id++ + k_support] = this.selectModeOrRandom(a1, a3, a2, a4);
            a1 = a3;
            a2 = a4;
        }
    }

    int[] aint_result = IntCache.getIntCache(areaWidth * areaHeight);

    for (int j3 = 0; j3 < areaHeight; j3++) {
        System.arraycopy(aint_support, (areaX & 1) + (j3 + (areaY & 1)) * k_support, aint_result, j3 * areaWidth, areaWidth);
    }

    return aint_result;
}
Сначала разберемся что из себя представляют объявленные переменные:
  • areaX и areaY - собственные смещения, areaWidth и areaHeight - собственные длины массива.
  • aint - массив данных, соответствующий родителю.
  • aint_support - вспомогательный массив данных. Из него будет вырезаться определенная часть данных, которая и станет окончательным aint_result.
  • aint_result - собственный массив данных.
  • i и j так и остались смещениями родителя, а k и l - длинами массива родителя.
  • k_support и l_support - длины вспомогательного массива.
  • k1 и l1 - используются при чтении aint.
  • id - эта переменная является индентификатором определенного элемента массива aint_support.
  • a1, a2, a3 и a4 - хранят какие-то определенные значения массива aint.
А теперь разберем сам алгоритм. Он увеличивает массив данных приблизительно в 2 раза и делает это по довольной четкой структуре. Родитель масштабируемого слоя будет иметь приблизительно в 2 раза меньшие длины aint (особенность побитовых операций, причем чем больше длины aint_result, тем сильнее ощущается уменьшения длин aint). Тем не менее, из-за использования побитовых операций возникают некоторые ограничения, которые исправляются путем подведения всех слоев к каким-то определенным размерам и с которыми алгоритм сможет работать. Из-за этого в алгоритме присутствует вспомогательный aint_support.

Получение массива данных родителя. В данном коде стоит отметить только слагаемые + 2 - последствия использования побитовых операций. Увеличение длины aint необходимо, чтобы длины будущего aint_support были гарантировано больше, чем у aint_result
Java:
int i = areaX >> 1;
int j = areaY >> 1;
int k = (areaWidth >> 1) + 2;
int l = (areaHeight >> 1) + 2;
int[] aint = this.parent.getInts(i, j, k, l);
Определение размеров и инициализация вспомогательный aint_support. Сравнив его размеры с aint можно понять, что он более, чем в 2 раза превосходит его по размеру и, соответственно, больше aint_result. По факту весь процесс масштабирования будет проходить в этом буфером массиве, после чего необходимую часть массива мы просто сделаем окончательным aint_result. Обязательно имеет длины, кратные 2 (необходимо для последующих операций), поэтому код соответствующий:
Java:
int k_support = k - 1 << 1;
int l_support = l - 1 << 1;
int[] aint_support = IntCache.getIntCache(k_support * l_support);
Два цикла для считывания данных aint. Считывание начинается в нижнем левом углу сначала по вертикали (координата X), а потом по горизонтали (координата Z):
Java:
for (int l1 = 0; l1 < l - 1; l1++) {
    ...
    for (int k1 = 0; k1 < k - 1; k1++) {
        ...
    }
}
Красным выделены все элементы массива, которые будут прочитаны. Думаю проблем с пониманием данной части кода возникнуть не должно (aint_support в 2 раза больше, чем красный квадрат):
Родитель1_1.png
Каждый раз, когда мы будем считать новый столбец в красном квадрате, данный идентификатор будет принимать значение, равное номеру элемента aint_support, соответствующему точке на плоскости (0, 2 * l1):
Java:
int id = (l1 << 1) * k_support;
Буфер1_1.png
Помимо этого каждый раз, когда мы будем считывать новый столбец в красном квадрате, переменные a1 и a2 будут принимать значения массива aint, соответствующие точкам на плоскости (0, l1) и (0, l1 + 1):
Java:
int a1 = aint[0 + 0 + (l1 + 0) * k];
int a2 = aint[0 + 0 + (l1 + 1) * k];
Родитель1_2.png
Инициализация чанкового сида. Необходимо для генерации псевдослучайных чисел. Несмотря на то, что он называется чанковым сидом, относится именно к точке a1.
Java:
this.initChunkSeed((long) (k1 + i << 1), (long) (l1 + j << 1));
Инициализация еще двух переменных a3 и a4, которые соответствуют точкам на плоскости (k1 + 1, l1) и (k1 + 1, l1 + 1):
Java:
int a3 = aint[k1 + 1 + (l1 + 0) * k];
int a4 = aint[k1 + 1 + (l1 + 1) * k];
Родитель1_3.png
Самая интересная часть алгоритма. Сразу стоит отметить тот факт, что при увеличении id мы будем перемещаться по столбцу вверх и когда достигнем предела, перейдем вниз на новый столбец.
Java:
aint_support[id] = a1;
aint_support[id++ + k_support] = this.selectRandom(new int[] { a1, a2 });
aint_support[id] = this.selectRandom(new int[] { a1, a3 });
aint_support[id++ + k_support] = this.selectModeOrRandom(a1, a3, a2, a4);
Я забегу вперед и сразу отмечу каким точкам в aint_support соответствуют точки a1, a2, a3 и a4:
Буфер1_2.png
Думаю теперь код стал более очевидным.
  • aint_support[id] - точка a1.
  • aint_support[id++ + k_support] - точка между точками a1 и a2 и она принимает случайное из двух значений a1 и a2.
  • aint_support[id] - поскольку в прошлом присваивании переменной значение id было увеличено на 1, то теперь эта точка будет находится между a1 и a3 и принимать случайное из двух значений a1 и a3.
  • aint_support[id++ + k_support] - точка между всеми четырьмя точками. Данной точке будет соответствовать значение, которое встречается чаще всего между точками a1, a2, a3 и a4. Если такого значения нет, то выбирает случайное среди всех.
Поскольку мы двигаемся вверх по столбцу, переменные a1 и a2 нам также нужно переопределять:
Java:
a1 = a3;
a2 = a4;
После первой итерации цикла k1 мы получим первый кусочек массива aint_support. На картинке нет a3 и a4 потому что их места занимают a1 и a2:
Буфер1_3.png
А вот что получим после второй итерации вложенного массива:
Буфер1_4.png
Причем если взглянуть на массив родителя aint, то на нем также будут точки передвигаться по столбцу вверх. Например, при работы второй итерации цикла k1 брались вот такие точки:
Родитель1_4.png
После завершения первой итерации цикла l1 получим готовый первый столбец. Далее все последующие итерации сгенерируют оставшиеся столбцы:
Родитель1_5.png

Буфер1_5.png
В итоге получим такой массив aint_support:
Буфер1.png
Далее следует инициализация собственного массива данных aint_result:
Java:
int[] aint_result = IntCache.getIntCache(areaWidth * areaHeight);
Сам процесс копирования части массива aint_support. Мы подошли с самой непонятной части данного алгоритма, а именно: (areaX & 1) и (areaY & 1). Каждое побитовое "И" проверяет на нечетность смещения и если они нечетные, то весь копирующийся кусок массива aint_support будет сдвинут по соответствующим осям. Смысл данного действия в том, что внутри массива aint_support находится целых 4 массива, которые по размерам соответствуют размеру собственного массива aint_result (включая его самого) и у всех массив родителя одинаковый - aint. Тем не менее, эти 4 массива имеют отличные друг от друга смещения, а значит не могут быть равны друг другу. Этот ньюанс возникает из-за работы побитовых сдвигов и решается только соответствующим сдвигом копирующейся области для каждого из этих четырех массивов данных.
Java:
for (int j3 = 0; j3 < areaHeight; j3++) {
    System.arraycopy(aint_support, (areaX & 1) + (j3 + (areaY & 1)) * k_support, aint_result, j3 * areaWidth, areaWidth);
}
Если бы оба смещения были нечетные, то область сместилась бы по двум осям:
Буфер1_6.png
Но в нашем случае я брал четные значения смещений, поэтому будет скопированная следующая область:
Буфер1_7.png
И, собственно, окончательный результат:
Результат1.png

Также есть возможность вызова метода GenLayerZoom.magnify(...), который масштабирует выбранную область указанное количество раз.

1.4 GenLayerFuzzyZoom
Отличается от GenLayerZoom только тем, что точка, находящаяся между a1, a2, a3 и a4, не проверяет на большинство какое-то значение, а сразу выбирает случайное.

1.5 GenLayerVoronoiZoom
Название говорит само за себя. В основе лежит разбиение Вороного, о котором можно прочитать в Википедии (обязательно к ознакомлению). Структура алгоритма практически ничем не отличается от GenLayerZoom за исключением того, что родительский массив в 4 раза меньше собственного и как следствие точки a1, a2, a3 и a4 будут определять сразу 16 точек, а не 4. Будем масштабировать такой массив данных родителя:
Родитель2.png
Сам код алгоритма:
Java:
public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
    areaX = areaX - 2;
    areaY = areaY - 2;
    int i = areaX >> 2;
    int j = areaY >> 2;
    int k = (areaWidth >> 2) + 2;
    int l = (areaHeight >> 2) + 2;
    int[] aint = this.parent.getInts(i, j, k, l);
    int i1 = k - 1 << 2;
    int j1 = l - 1 << 2;
    int[] aint1 = IntCache.getIntCache(i1 * j1);

    for (int k1 = 0; k1 < l - 1; ++k1) {
        int l1 = 0;
        int i2 = aint[l1 + 0 + (k1 + 0) * k];

        for (int j2 = aint[l1 + 0 + (k1 + 1) * k]; l1 < k - 1; ++l1) {
            double d0 = 3.6D;
            this.initChunkSeed((long) (l1 + i << 2), (long) (k1 + j << 2));
            double d1 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0;
            double d2 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0;
            this.initChunkSeed((long) (l1 + i + 1 << 2), (long) (k1 + j << 2));
            double d3 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0 + 4.0D;
            double d4 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0;
            this.initChunkSeed((long) (l1 + i << 2), (long) (k1 + j + 1 << 2));
            double d5 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0;
            double d6 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0 + 4.0D;
            this.initChunkSeed((long) (l1 + i + 1 << 2), (long) (k1 + j + 1 << 2));
            double d7 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0 + 4.0D;
            double d8 = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * d0 + 4.0D;
            int k2 = aint[l1 + 1 + (k1 + 0) * k] & 255;
            int l2 = aint[l1 + 1 + (k1 + 1) * k] & 255;

            for (int i3 = 0; i3 < 4; ++i3) {
                int j3 = ((k1 << 2) + i3) * i1 + (l1 << 2);

                for (int k3 = 0; k3 < 4; ++k3) {
                    double d9 = ((double) i3 - d2) * ((double) i3 - d2) + ((double) k3 - d1) * ((double) k3 - d1);
                    double d10 = ((double) i3 - d4) * ((double) i3 - d4) + ((double) k3 - d3) * ((double) k3 - d3);
                    double d11 = ((double) i3 - d6) * ((double) i3 - d6) + ((double) k3 - d5) * ((double) k3 - d5);
                    double d12 = ((double) i3 - d8) * ((double) i3 - d8) + ((double) k3 - d7) * ((double) k3 - d7);

                    if (d9 < d10 && d9 < d11 && d9 < d12) {
                        aint1[j3++] = i2;
                    } else if (d10 < d9 && d10 < d11 && d10 < d12) {
                        aint1[j3++] = k2;
                    } else if (d11 < d9 && d11 < d10 && d11 < d12) {
                        aint1[j3++] = j2;
                    } else {
                        aint1[j3++] = l2;
                    }
                }
            }

            i2 = k2;
            j2 = l2;
        }
    }

    int[] aint2 = IntCache.getIntCache(areaWidth * areaHeight);

    for (int l3 = 0; l3 < areaHeight; ++l3) {
        System.arraycopy(aint1, (l3 + (areaY & 3)) * i1 + (areaX & 3), aint2, l3 * areaWidth, areaWidth);
    }

    return aint2;
}
И более читабельная версия:
Java:
public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        areaX = areaX - 2;
        areaY = areaY - 2;
        int i = areaX >> 2;
        int j = areaY >> 2;
        int k = (areaWidth >> 2) + 2;
        int l = (areaHeight >> 2) + 2;
        int[] aint = this.parent.getInts(i, j, k, l);
        int k_support = k - 1 << 2;
        int l_support = l - 1 << 2;
        int[] aint_support = IntCache.getIntCache(k_support * l_support);

        for (int l1 = 0; l1 < l - 1; ++l1) {
            int a1 = aint[0 + 0 + (l1 + 0) * k];
            int a2 = aint[0 + 0 + (l1 + 1) * k];

            for (int k1 = 0; k1 < k - 1; ++k1) {
                double offset = 3.6D;
                this.initChunkSeed((long) (k1 + i << 2), (long) (l1 + j << 2));
                double pointA_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
                double pointA_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
                this.initChunkSeed((long) (k1 + i + 1 << 2), (long) (l1 + j << 2));
                double pointC_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
                double pointC_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
                this.initChunkSeed((long) (k1 + i << 2), (long) (l1 + j + 1 << 2));
                double pointB_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
                double pointB_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
                this.initChunkSeed((long) (k1 + i + 1 << 2), (long) (l1 + j + 1 << 2));
                double pointD_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
                double pointD_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
                int a3 = aint[k1 + 1 + (l1 + 0) * k] & 255;
                int a4 = aint[k1 + 1 + (l1 + 1) * k] & 255;

                for (int l2 = 0; l2 < 4; ++l2) {
                    int id = (k1 << 2) + ((l1 << 2) + l2) * k_support;

                    for (int k2 = 0; k2 < 4; ++k2) {
                        double distance_A = ((double) l2 - pointA_Z) * ((double) l2 - pointA_Z) + ((double) k2 - pointA_X) * ((double) k2 - pointA_X);
                        double distance_C = ((double) l2 - pointC_Z) * ((double) l2 - pointC_Z) + ((double) k2 - pointC_X) * ((double) k2 - pointC_X);
                        double distance_B = ((double) l2 - pointB_Z) * ((double) l2 - pointB_Z) + ((double) k2 - pointB_X) * ((double) k2 - pointB_X);
                        double distance_D = ((double) l2 - pointD_Z) * ((double) l2 - pointD_Z) + ((double) k2 - pointD_X) * ((double) k2 - pointD_X);

                        if (distance_A < distance_C && distance_A < distance_B && distance_A < distance_D) {
                            aint_support[id++] = a1;
                        } else if (distance_C < distance_A && distance_C < distance_B && distance_C < distance_D) {
                            aint_support[id++] = a3;
                        } else if (distance_B < distance_A && distance_B < distance_C && distance_B < distance_D) {
                            aint_support[id++] = a2;
                        } else {
                            aint_support[id++] = a4;
                        }
                    }
                }
         
                a1 = a3;
                a2 = a4;
            }
        }

        int[] aint_result = IntCache.getIntCache(areaWidth * areaHeight);

        for (int l3 = 0; l3 < areaHeight; ++l3) {
            System.arraycopy(aint_support, (l3 + (areaY & 3)) * k_support + (areaX & 3), areaWidth);
        }

        return aint_result;
    }
Разберем новые переменные:
  • offset - величина, которая определяет насколько сильно будут смещаться вспомогательные точки относительно изначального положения.
  • pointA_X и pointA_Z - координаты вспомогательной точки А, соответствующей точке a1.
  • pointB_X и pointB_Z - координаты вспомогательной точки B, соответствующей точке a2.
  • pointC_X и pointC_Z - координаты вспомогательной точки C, соответствующей точке a3.
  • pointD_X и pointD_Z - координаты вспомогательной точки D, соответствующей точке a4.
  • l2 и k2 - используются при создании области 4 * 4.
  • distance_A, distance_B, distance_C и distance_D - расстояния от точки из области 4 * 4 до точек A, B, C и D соответственно.
В самом начале алгоритма смещения уменьшаются 2. Делается это для того, чтобы результат не был очень сильно похож на родителя, потому что если GenLayerZoom почти в точности повторяет своего родителя, то GenLayerVoronoiZoom из-за данного сдвига будет выдавать и немножечко другой результат. Если бы мы брали произвольные смещения, то толку от такого кода было бы немного, но в нашем случае координаты блоков рассчитываются с помощью побитового смещения, поэтому в данный код вполне эффективен (причем для любых чанков скопированная область будет одной и той же - со смещениями 2 и 2):
Java:
areaX = areaX - 2;
areaY = areaY - 2;
В отличии от GenLayerZoom, где смежные точки зависели только от a1 и использовалась всего одна инициализация чанкового сида, соответствующая данной точке, в GenLayerVoronoiZoom смежные точки зависят от всех четырех, поэтому при расчете координат вспомогательных точек чанковый сид инициализируется по новой для a1, a2, a3 и a4.
Java:
this.initChunkSeed((long) (k1 + i << 2), (long) (l1 + j << 2));
this.initChunkSeed((long) (k1 + i + 1 << 2), (long) (l1 + j << 2));
this.initChunkSeed((long) (k1 + i << 2), (long) (l1 + j + 1 << 2));
this.initChunkSeed((long) (k1 + i + 1 << 2), (long) (l1 + j + 1 << 2));
Теперь дело дошло до вспомогательных точек A, B, C и D, а также до генерации вспомогательной области 4 * 4 (которая на самом деле потом станет частью массива aint_support). Все координаты вспомогательных точек вычисляются по одному алгоритму:
координата вспомогательной точки = смещение + изначальная координата точки
Соответственно, отбросив первое слагаемое координаты вспомогательных точек A, B, C и D будут соответствовать координатам точек a1, a2, a3 и a4 в массиве данных aint_support, поэтому изначальная координата точки в нашем случае будет равна либо 0, либо 4. Само же смещение принимает случайное значение в диапазоне [-1.8, 1.8].
Java:
double offset = 3.6D;
double pointA_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
double pointA_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
double pointC_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
double pointC_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
double pointB_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset;
double pointB_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
double pointD_X = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
double pointD_Z = ((double) this.nextInt(1024) / 1024.0D - 0.5D) * offset + 4.0D;
Если схематично нарисовать пока что пустую вспомогательную область 4 * 4 и все возможные варианты расположения точек, то выглядеть это будет так (обратите внимание: на картинке координатные оси, которые нужны для вспомогательных точек):
Вспом2_1.png
Единственное отличие от GenLayerZoom в побитовой операции. Поскольку GenLayerVoronoiZoom в стандартном алгоритме генерации биомов - последний слой, в нем реализован алгоритм, который бы не позволял генерировать биомы с точно не существующими идентификаторами. Как правило такого и не должно происходить, но раз уж проверка есть, то стоило объяснить:
Java:
int a3 = aint[k1 + 1 + (l1 + 0) * k] & 255;
int a4 = aint[k1 + 1 + (l1 + 1) * k] & 255;
Два цикла, которые перебирают все точки вспомогательного массива:
Java:
for (int l2 = 0; l2 < 4; ++l2) {
    ...
    for (int k2 = 0; k2 < 4; ++k2) {
        ...
    }
}
В GenLayerZoom мы просто присваивали значение идентификатора по новой каждый раз, когда переходили к новому столбцу, а потом увеличивали его значение по мере необходимости. Но сейчас у нас вспомогательная область не 2 * 2, а 4 * 4, поэтому нужно перезаписывать переменную при переходе и на новый столбец, и на новый столбец во вспомогательном массиве:
Java:
int id = (k1 << 2) + ((l1 << 2) + l2) * k_support;
На картинке изображены идентификаторы только для всех вспомогательных области (внутри областей все считается аналогично):
Буфер2_1.png
Вычисляем расстояние до каждой из вспомогательных точек:
Java:
double distance_A = ((double) l2 - pointA_Z) * ((double) l2 - pointA_Z) + ((double) k2 - pointA_X) * ((double) k2 - pointA_X);
double distance_C = ((double) l2 - pointC_Z) * ((double) l2 - pointC_Z) + ((double) k2 - pointC_X) * ((double) k2 - pointC_X);
double distance_B = ((double) l2 - pointB_Z) * ((double) l2 - pointB_Z) + ((double) k2 - pointB_X) * ((double) k2 - pointB_X);
double distance_D = ((double) l2 - pointD_Z) * ((double) l2 - pointD_Z) + ((double) k2 - pointD_X) * ((double) k2 - pointD_X);
Присваиваем точке вспомогательной области 4 * 4 значение самой близлежащей точки:
Java:
if (distance_A < distance_C && distance_A < distance_B && distance_A < distance_D) {
    aint_support[id++] = a1;
} else if (distance_C < distance_A && distance_C < distance_B && distance_C < distance_D) {
    aint_support[id++] = a3;
} else if (distance_B < distance_A && distance_B < distance_C && distance_B < distance_D) {
    aint_support[id++] = a2;
} else {
    aint_support[id++] = a4;
}
Например, для первых итераций l1 и k1 значения a1, a2, a3 и a4 будут следующие:
Родитель2_1.png
А вспомогательные точки A, B, C и D будут расположены таким образом (розовые точки соответствуют точкам, до которых рассчитываются расстояния):
Вспом2_2.png
Если условно применить разбиение Вороного для данных точек, то получится такая картина:
Вспом2_3.png
Учитывая, что точки A, B и C - вода, а D - поверхность, результат будет соответствующий:
Вспом2_4.png
Естественно мы не вводили никаких переменных под вспомогательную область, все результаты вычислений сразу записываются в массив aint_support (само понятие вспомогательной области 4 * 4 нужно лишь для понимания самого процесса):
Буфер2_2.png
Как итог получим следующий массив данных aint_support:
Буфер2.png
Внутри массива aint_support находится 16 массивов, которые по размерам соответствуют aint_result, поэтому побитовые операции также были изменены на areaX & 3 и areaY & 3. Как результат в нашем случае (areaX и areaY равны -2) будет скопирована данная область:
Буфер2_3.png
И итоговый результат:
Результат2.png


2 Собственные слои

2.1 Алгоритм генерации оболочки (АГО)
Хорошим каркасом для первых алгоритмов вполне подойдет стандартный алгоритм генерации биомов (далее - САГБ), который генерирует биомы в Обычном мире. Но для того, чтобы в точности повторить именно ту поверхность, которая САГБ, нужно добавить много лишних классов, которые нам по большому счету не нужны, потому что самое главное в алгоритме генерации оболочки (далее - АГО) - генерация очень похожей на настоящую поверхности. Сам АГО довольно простой и будет использоваться мной как каркас для всех алгоритмов генерации в данном руководстве:
Java:
public static GenLayer standartIsland(long seed) {
        GenLayer genlayer = new GenLayerIsland(1L);
        genlayer = new GenLayerFuzzyZoom(2000L, genlayer);
        genlayer = new GenLayerAddIsland(1L, genlayer);
        genlayer = new GenLayerZoom(2001L, genlayer);
        genlayer = new GenLayerAddIsland(2L, genlayer);
        genlayer = new GenLayerAddIsland(50L, genlayer);
        genlayer = new GenLayerAddIsland(70L, genlayer);
        genlayer = new GenLayerRemoveTooMuchOcean(2L, genlayer);
        genlayer = new GenLayerAddIsland(3L, genlayer);
        genlayer = new GenLayerZoom(2002L, genlayer);
        genlayer = new GenLayerZoom(2003L, genlayer);
        GenLayer pre_river = new GenLayerAddIsland(4L, genlayer);
        genlayer = GenLayerZoom.magnify(1000L, pre_river, 2);
        int biomeSize = 4;

        for (int k = 0; k < biomeSize; k++) {
            genlayer = new GenLayerZoom((long) (1000 + k), genlayer);

            if (k == 0) {
                genlayer = new GenLayerAddIsland(3L, genlayer);
            }
        }

        genlayer = new GenLayerSmooth(1000L, genlayer);
        // ГЕНЕРАЦИЯ РЕК
        int riverSize = biomeSize;
        GenLayer river = new GenLayerRiverInit(100L, pre_river);
        river = GenLayerZoom.magnify(1000L, river, 2);
        river = GenLayerZoom.magnify(1000L, river, riverSize);
        river = new GenLayerRiver(1L, river);
        river = new GenLayerSmooth(1000L, river);
        genlayer = new GenLayerRiverMix(100L, genlayer, river);
        // ГЕНЕРАЦИЯ РЕК
        genlayer = new GenLayerVoronoiZoom(10L, genlayer);
        genlayer.initWorldGenSeed(seed);
        return genlayer;
    }
Вот пример уже знакомой генерации двумя алгоритмами:
АГО_1.png
АГО_2.png
Если говорить более конкретно о том, какие именно различия ожидают нас при использовании данного алгоритма, то их не так много:
  • Алгоритм генерирует исключительно поверхность и реки
  • Алгоритм не генерирует поверхность в океане (островки)
  • В некоторых местах есть океан вместо поверхности, а вместо поверхности - океан. Причиной тому то, что в процессе генерации массива чисел каждый слоем эти массивы отличаются по размеру друг от друга. Если конечный результат по размерам будет одинаковый, то, например, изначальный слой может и будет отличаться.

2.2 Скалистое побережье
Представим, что нам нужно добавить генерацию небольших скоплений скалистых шпилей рядом с побережьем. Самым простым вариантом будет добавить генерацию биома, в котором уже и будут генерироваться эти скалистые шпили. В качестве основы будем использовать АГО (без рек):
Java:
public static GenLayer[] rockyCoast(long seed, WorldType worldType) {
    GenLayer genlayer = new GenLayerIsland(1L);
    genlayer = new GenLayerFuzzyZoom(2000L, genlayer);
    genlayer = new GenLayerAddIsland(1L, genlayer);
    genlayer = new GenLayerZoom(2001L, genlayer);
    genlayer = new GenLayerAddIsland(2L, genlayer);
    genlayer = new GenLayerAddIsland(50L, genlayer);
    genlayer = new GenLayerAddIsland(70L, genlayer);
    genlayer = new GenLayerRemoveTooMuchOcean(2L, genlayer);
    genlayer = new GenLayerAddIsland(3L, genlayer);
    genlayer = new GenLayerZoom(2002L, genlayer);
    genlayer = new GenLayerZoom(2003L, genlayer);
    GenLayer pre_river = new GenLayerAddIsland(4L, genlayer);
    genlayer = GenLayerZoom.magnify(1000L, pre_river, 2);
    int biomeSize = 4;

    for (int k = 0; k < biomeSize; k++) {
        genlayer = new GenLayerZoom((long) (1000 + k), genlayer);

        if (k == 0) {
            genlayer = new GenLayerAddIsland(3L, genlayer);
        }
    }

    GenLayer genlayer_world = new GenLayerSmooth(1000L, genlayer);
    genlayer = new GenLayerVoronoiZoom(10L, genlayer_world);
    genlayer_world.initWorldGenSeed(seed);
    genlayer.initWorldGenSeed(seed);
    return new GenLayer[] { genlayer_world, genlayer, genlayer_world };
}
АГО создаст следующий массив данных:
СП_1.png
Нам необходимо сгенерировать биом ROCKY_COAST, на котором расположатся скалистые шпили. Генерировать данный биом будем вместо самого побережья, а не океана. Поэтому изначально сгенерируем в массиве данных индентификаторы, равные -100, а затем сгенерируем вокруг них на побережье биом ROCKY_COAST. Делается это все в 2 этапа с помощью слоя GenLayerRockyCoastIsland.Mode:
  • PRE - создаем область из идентификаторов -100
  • POST - вокруг этой области генерируем биом ROCKY_COAST
Java:
public class GenLayerRockyCoastIsland extends GenLayer {

    private final Mode mode;

    public GenLayerRockyCoastIsland(long seed, GenLayer parent, GenLayerRockyCoastIsland.Mode mode) {
        super(seed);
        this.parent = parent;
        this.mode = mode;
    }

    public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        switch (this.mode) {
        case PRE:
            return pre_getInts(areaX, areaY, areaWidth, areaHeight);
        default:
            return post_getInts(areaX, areaY, areaWidth, areaHeight);
        }
    }

    private int[] pre_getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        int i = areaX - 1;
        int j = areaY - 1;
        int k = areaWidth + 2;
        int l = areaHeight + 2;
        int[] aint = this.parent.getInts(i, j, k, l);
        int[] aint1 = IntCache.getIntCache(areaWidth * areaHeight);

        for (int i1 = 0; i1 < areaHeight; ++i1) {
            for (int j1 = 0; j1 < areaWidth; ++j1) {
                int k1 = aint[j1 + 0 + (i1 + 0) * k];
                int l1 = aint[j1 + 2 + (i1 + 0) * k];
                int i2 = aint[j1 + 0 + (i1 + 2) * k];
                int j2 = aint[j1 + 2 + (i1 + 2) * k];
                int k2 = aint[j1 + 1 + (i1 + 1) * k];
                this.initChunkSeed((long) (j1 + areaX), (long) (i1 + areaY));

                if (k2 == Biome.getIdForBiome(Biomes.PLAINS) && (k1 == Biome.getIdForBiome(Biomes.OCEAN) || l1 == Biome.getIdForBiome(Biomes.OCEAN) || i2 == Biome.getIdForBiome(Biomes.OCEAN) || j2 == Biome.getIdForBiome(Biomes.OCEAN)) && this.nextInt(10) == 0) {
                    aint1[j1 + i1 * areaWidth] = -100;
                } else {
                    aint1[j1 + i1 * areaWidth] = k2;
                }
            }
        }

        return aint1;
    }

    private int[] post_getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        int i = areaX - 1;
        int j = areaY - 1;
        int k = areaWidth + 2;
        int l = areaHeight + 2;
        int[] aint = this.parent.getInts(i, j, k, l);
        int[] aint1 = IntCache.getIntCache(areaWidth * areaHeight);

        for (int i1 = 0; i1 < areaHeight; ++i1) {
            for (int j1 = 0; j1 < areaWidth; ++j1) {
                int k1 = aint[j1 + 1 + (i1 + 0) * k];
                int l1 = aint[j1 + 0 + (i1 + 1) * k];
                int i2 = aint[j1 + 1 + (i1 + 2) * k];
                int j2 = aint[j1 + 2 + (i1 + 1) * k];
                int k2 = aint[j1 + 1 + (i1 + 1) * k];
                this.initChunkSeed((long) (j1 + areaX), (long) (i1 + areaY));

                if (k2 == Biome.getIdForBiome(Biomes.PLAINS)
                        && (k1 == -100 || l1 == -100 || i2 == -100 || j2 == -100)) {
                    aint1[j1 + i1 * areaWidth] = Biome.getIdForBiome(BiomeInit.ROCKY_COAST);
                } else {
                    aint1[j1 + i1 * areaWidth] = k2;
                }
            }
        }

        return aint1;
    }

    public static enum Mode {
        PRE, POST;
    }
}
Сами этапы будет реализовывать не сразу, а с разницей в 2 масштабирования для того, чтобы не получить биом ROCKY_COAST, который находится на побережье, слишком большим:
Java:
public static GenLayer[] rockyCoast(long seed, WorldType worldType) {
        GenLayer genlayer = new GenLayerIsland(1L);
        genlayer = new GenLayerFuzzyZoom(2000L, genlayer);
        genlayer = new GenLayerAddIsland(1L, genlayer);
        genlayer = new GenLayerZoom(2001L, genlayer);
        genlayer = new GenLayerAddIsland(2L, genlayer);
        genlayer = new GenLayerAddIsland(50L, genlayer);
        genlayer = new GenLayerAddIsland(70L, genlayer);
        genlayer = new GenLayerRemoveTooMuchOcean(2L, genlayer);
        genlayer = new GenLayerAddIsland(3L, genlayer);
        genlayer = new GenLayerZoom(2002L, genlayer);
        genlayer = new GenLayerZoom(2003L, genlayer);
        GenLayer pre_river = new GenLayerAddIsland(4L, genlayer);
        genlayer = new GenLayerRockyCoastIsland(1L, pre_river, GenLayerRockyCoastIsland.Mode.PRE); // Генерируем область из идентификаторов -100
        genlayer = GenLayerZoom.magnify(1000L, ganlayer, 2);
        genlayer = new GenLayerRockyCoastIsland(1L, genlayer, GenLayerRockyCoastIsland.Mode.POST); // Генерируем биом ROCKY_COAST на побережье около идентификаторов -100
        int biomeSize = 4;

        for (int k = 0; k < biomeSize; k++) {
            genlayer = new GenLayerZoom((long) (1000 + k), genlayer);

            if (k == 0) {
                genlayer = new GenLayerAddIsland(3L, genlayer);
            }
        }

    GenLayer genlayer_world = new GenLayerSmooth(1000L, genlayer);
    genlayer = new GenLayerVoronoiZoom(10L, genlayer_world);
    genlayer_world.initWorldGenSeed(seed);
    genlayer.initWorldGenSeed(seed);
    return new GenLayer[] { genlayer_world, genlayer, genlayer_world };
    }
Оранжевая область уже соответствует нашему биому ROCKY_COAST на побережье, а с черной областью еще предстоит поработать (идентификаторы -100). В пределах данной области мы также сгенерируем биомы ROCKY_COAST, после чего заменим оставшиеся идентификаторы на биом Biome.OCEAN.
СП_2.png
Нужно добавить слой, который будет генерировать биом ROCKY_COAST внутри черной области. За основу такого слоя взят GenLayerZoronoiZoom без смещений, а также с алгоритмом, который будет эффективно работать для небольшого количества точек (не обязательно четырех). А нашем случае используется 5 точек, причем 5 точка (центральная) - это точка со смещениями [2, 2] и она соответствует биому ROCKY_COAST. Появляется 5 точка только с небольшим шансом при условии, что остальные 4 точки - черная область:
Java:
public class GenLayerRockyCoastOcean extends GenLayer {

    public GenLayerRockyCoastOcean(long seed, GenLayer parent) {
        super(seed);
        super.parent = parent;
    }

    public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        int i = areaX >> 2;
        int j = areaY >> 2;
        int k = (areaWidth >> 2) + 2;
        int l = (areaHeight >> 2) + 2;
        int[] aint = this.parent.getInts(i, j, k, l);
        int k_support = k - 1 << 2;
        int l_support = l - 1 << 2;
        int[] aint_support = IntCache.getIntCache(k_support * l_support);

        for (int l1 = 0; l1 < l - 1; ++l1) {
            int a1 = aint[0 + 0 + (l1 + 0) * k];
            int a2 = aint[0 + 0 + (l1 + 1) * k];

            for (int k1 = 0; k1 < k - 1; ++k1) {
                int a3 = aint[k1 + 1 + (l1 + 0) * k];
                int a4 = aint[k1 + 1 + (l1 + 1) * k];
                List<Integer> alist = new ArrayList<Integer>();
                alist.add(a1);
                alist.add(a2);
                alist.add(a3);
                alist.add(a4);
                List<Point> points = new ArrayList<Point>();

                for (int k3 = 0; k3 < 2; k3++) {
                    for (int l3 = 0; l3 < 2; l3++) {
                        this.initChunkSeed((long) (k1 + i + k3 << 2), (long) (l1 + j + l3 << 2));
                        int x = this.nextInt(1025);
                        int z = this.nextInt(1025);
                        Point point = new Point((double) k3 * 4.0D, (double) l3 * 4.0D, 3.6D, alist.get(l3 + k3 * 2));
                        point.rand(x, z);
                        points.add(point);

                    }
                }

                if (a1 == -100 && a2 == -100 && a3 == -100 && a4 == -100) {
                    this.initChunkSeed((long) (k1 + i << 2), (long) (l1 + j << 2));

                    if (this.nextInt(3) == 0) {
                        int x = this.nextInt(1025);
                        int z = this.nextInt(1025);
                        Point point = new Point(2.0D, 2.0D, 4.0D, Biome.getIdForBiome(BiomeInit.ROCKY_COAST));
                        point.rand(x, z);
                        points.add(point);
                    }
                }

                for (int l2 = 0; l2 < 4; ++l2) {
                    int id = (k1 << 2) + ((l1 << 2) + l2) * k_support;

                    for (int k2 = 0; k2 < 4; ++k2) {
                        int calc = calculateRocks(k2, l2, points);

                        if (calc == -100) {
                            calc = Biome.getIdForBiome(Biomes.OCEAN);
                        }

                        aint_support[id++] = calc;
                    }
                }

                a1 = a3;
                a2 = a4;
            }
        }

        int[] aint_result = IntCache.getIntCache(areaWidth * areaHeight);

        for (int l3 = 0; l3 < areaHeight; ++l3) {
            System.arraycopy(aint_support, (l3 + (areaY & 3)) * k_support + (areaX & 3), aint_result, l3 * areaWidth, areaWidth);
        }

        return aint_result;
    }

    private int calculateRocks(int k2, int l2, List<Point> points) {
        Point current = new Point(k2, l2);
        double distance = 100D;
        int calc = 0;

        for (Point point : points) {
            double d0 = (current.posX - point.posX) * (current.posX - point.posX)
                    + (current.posZ - point.posZ) * (current.posZ - point.posZ);

            if (d0 < distance) {
                distance = d0;
                calc = point.id;
            }
        }

        return calc;
    }

    private class Point {

        double posX;

        double posZ;

        double offset;

        int id;

        Point(double posX, double posZ) {
            this(posX, posZ, 0, 0);
        }

        Point(double posX, double posZ, double offset, int id) {
            this.posX = posX;
            this.posZ = posZ;
            this.offset = offset;
            this.id = id;
        }

        public void rand(int x, int z) {
            this.posX += ((double) x / 1024.0D - 0.5D) * offset;
            this.posZ += ((double) z / 1024.0D - 0.5D) * offset;
        }
    }
}
Поскольку данный слой масштабирует массив данных, если мы просто его вставим в удобное для нас место, то весь мир увеличится в 4 раза в масштабе, поэтому добавив данный слой, нужно удалить 2 слоя GenLayerZoom. Самым удачным решением будет замена данного кода:
Java:
int biomeSize = 4;

for (int k = 0; k < biomeSize; k++) {
    genlayer = new GenLayerZoom((long) (1000 + k), genlayer);

    if (k == 0) {
        genlayer = new GenLayerAddIsland(3L, genlayer);
    }
}
На новый код:
Java:
genlayer = new GenLayerZoom(1000L, genlayer);
genlayer = new GenLayerAddIsland(3L, genlayer);
genlayer = new GenLayerRockyCoastOcean(1L, genlayer); // Генерируем биом ROCKY_COAST в пределах черной области и заменяем черную область на OCEAN
genlayer = new GenLayerZoom(1003L, genlayer);
Итоговый результат:
Java:
public static GenLayer[] rockyCoast(long seed, WorldType worldType) {
    GenLayer genlayer = new GenLayerIsland(1L);
    genlayer = new GenLayerFuzzyZoom(2000L, genlayer);
    genlayer = new GenLayerAddIsland(1L, genlayer);
    genlayer = new GenLayerZoom(2001L, genlayer);
    genlayer = new GenLayerAddIsland(2L, genlayer);
    genlayer = new GenLayerAddIsland(50L, genlayer);
    genlayer = new GenLayerAddIsland(70L, genlayer);
    genlayer = new GenLayerRemoveTooMuchOcean(2L, genlayer);
    genlayer = new GenLayerAddIsland(3L, genlayer);
    genlayer = new GenLayerZoom(2002L, genlayer);
    genlayer = new GenLayerZoom(2003L, genlayer);
    genlayer = new GenLayerAddIsland(4L, genlayer);
    genlayer = new GenLayerRockyCoastIsland(1L, pre_river, GenLayerRockyCoastIsland.Mode.PRE); // Генерируем область из идентификаторов -100
        genlayer = GenLayerZoom.magnify(1000L, ganlayer, 2);
        genlayer = new GenLayerRockyCoastIsland(1L, genlayer, GenLayerRockyCoastIsland.Mode.POST); // Генерируем биом ROCKY_COAST на побережье около идентификаторов -100
    genlayer = new GenLayerZoom(1000L, genlayer);
    genlayer = new GenLayerAddIsland(3L, genlayer);
    genlayer = new GenLayerRockyCoastOcean(1L, genlayer); // Генерируем биом ROCKY_COAST в пределах черной области и заменяем черную область на OCEAN
    genlayer = new GenLayerZoom(1003L, genlayer);
    GenLayer genlayer_world = new GenLayerSmooth(1000L, genlayer);
    genlayer = new GenLayerVoronoiZoom(10L, genlayer_world);
    genlayer_world.initWorldGenSeed(seed);
    genlayer.initWorldGenSeed(seed);
    return new GenLayer[] { genlayer_world, genlayer, genlayer_world };
}
СП_3.png

2.3 Эффект диффузии
А теперь добавим биом REDSERT с границей REDSERT_EDGE, который будет проникать в окружающие его биомы. Используем АГО (без рек). Добавим слой GenLayerDiffusionIsland, который будет генерировать биом REDSERT:
Java:
public class GenLayerDiffusionIsland extends GenLayer {

    public GenLayerDiffusionIsland(long seed, GenLayer parent) {
        super(seed);
        this.parent = parent;
    }

    public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        int i = areaX - 1;
        int j = areaY - 1;
        int k = areaWidth + 2;
        int l = areaHeight + 2;
        int[] aint = this.parent.getInts(i, j, k, l);
        int[] aint1 = IntCache.getIntCache(areaWidth * areaHeight);

        for (int i1 = 0; i1 < areaHeight; ++i1) {
            for (int j1 = 0; j1 < areaWidth; ++j1) {
                int k1 = aint[j1 + 1 + (i1 + 0) * k];
                int l1 = aint[j1 + 0 + (i1 + 1) * k];
                int i2 = aint[j1 + 1 + (i1 + 2) * k];
                int j2 = aint[j1 + 2 + (i1 + 1) * k];
                int k2 = aint[j1 + 1 + (i1 + 1) * k];
                this.initChunkSeed((long) (j1 + areaX), (long) (i1 + areaY));

                if (k2 == Biome.getIdForBiome(Biomes.PLAINS) && k1 != Biome.getIdForBiome(Biomes.OCEAN) && l1 != Biome.getIdForBiome(Biomes.OCEAN) && i2 != Biome.getIdForBiome(Biomes.OCEAN) && j2 != Biome.getIdForBiome(Biomes.OCEAN) && this.nextInt(10) == 0) {
                    aint1[j1 + i1 * areaWidth] = Biome.getIdForBiome(BiomeInit.REDSERT);
                } else {
                    aint1[j1 + i1 * areaWidth] = k2;
                }
            }
        }

        return aint1;
    }
}
Добавим слой GenLayerDiffusionEffect, который и предаст эффект диффузии биому REDSERT. В основе слоя будет опять же лежать GenLayerVoronoiZoom. Но теперь алгоритм будет отличаться: если биом REDSERT будет лежать только на одной из диагоналей (a1 и a4 или a2 и a3), то пятая (центральная) точка будет тоже REDSERT, иначе точки не будет и алгоритм станет обычным разбиением Вороного:
Java:
public class GenLayerDiffusionEffect extends GenLayer {

    public GenLayerDiffusionEffect(long seed, GenLayer parent) {
        super(seed);
        super.parent = parent;
    }

    public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        int i = areaX >> 2;
        int j = areaY >> 2;
        int k = (areaWidth >> 2) + 2;
        int l = (areaHeight >> 2) + 2;
        int[] aint = this.parent.getInts(i, j, k, l);
        int k_support = k - 1 << 2;
        int l_support = l - 1 << 2;
        int[] aint_support = IntCache.getIntCache(k_support * l_support);

        for (int l1 = 0; l1 < l - 1; ++l1) {
            int a1 = aint[0 + 0 + (l1 + 0) * k];
            int a2 = aint[0 + 0 + (l1 + 1) * k];

            for (int k1 = 0; k1 < k - 1; ++k1) {
                int a3 = aint[k1 + 1 + (l1 + 0) * k];
                int a4 = aint[k1 + 1 + (l1 + 1) * k];
                List<Integer> alist = new ArrayList<Integer>();
                alist.add(a1);
                alist.add(a2);
                alist.add(a3);
                alist.add(a4);
                List<Point> points = new ArrayList<Point>();

                for (int k3 = 0; k3 < 2; k3++) {
                    for (int l3 = 0; l3 < 2; l3++) {
                        this.initChunkSeed((long) (k1 + i + k3 << 2), (long) (l1 + j + l3 << 2));
                        int x = this.nextInt(1025);
                        int z = this.nextInt(1025);
                        Point point = new Point((double) k3 * 4.0D, (double) l3 * 4.0D, 3.6D, alist.get(l3 + k3 * 2));
                        point.rand(x, z);
                        points.add(point);

                    }
                }
            
                int mesa = 0;
                int plains = 0;
            
                for (int a : alist) {
                    if (a == Biome.getIdForBiome(BiomeInit.REDSERT)) {
                        mesa++;
                    } else if (a == Biome.getIdForBiome(Biomes.PLAINS)) {
                        plains++;
                    }
                }

                boolean b1 = mesa == 3 && plains == 1;
                boolean b2 = mesa == 1 && plains == 3;
            
                if (b1 || b2) {
                    Point point = new Point(2.0D, 2.0D, 0.0D, b2 ? Biome.getIdForBiome(BiomeInit.REDSERT) : Biome.getIdForBiome(Biomes.PLAINS));
                    points.add(point);
                }

                for (int l2 = 0; l2 < 4; ++l2) {
                    int id = (k1 << 2) + ((l1 << 2) + l2) * k_support;

                    for (int k2 = 0; k2 < 4; ++k2) {
                        aint_support[id++] = calculateRocks(k2, l2, points);
                    }
                }

                a1 = a3;
                a2 = a4;
            }
        }

        int[] aint_result = IntCache.getIntCache(areaWidth * areaHeight);

        for (int l3 = 0; l3 < areaHeight; ++l3) {
            System.arraycopy(aint_support, (l3 + (areaY & 3)) * k_support + (areaX & 3), aint_result, l3 * areaWidth, areaWidth);
        }

        return aint_result;
    }

    private int calculateRocks(int k2, int l2, List<Point> points) {
        Point current = new Point(k2, l2);
        double distance = 100D;
        int calc = 0;

        for (Point point : points) {
            double d0 = (current.posX - point.posX) * (current.posX - point.posX)
                    + (current.posZ - point.posZ) * (current.posZ - point.posZ);

            if (d0 < distance) {
                distance = d0;
                calc = point.id;
            }
        }

        return calc;
    }

    private class Point {

        double posX;

        double posZ;

        double offset;

        int id;

        Point(double posX, double posZ) {
            this(posX, posZ, 0, 0);
        }

        Point(double posX, double posZ, double offset, int id) {
            this.posX = posX;
            this.posZ = posZ;
            this.offset = offset;
            this.id = id;
        }

        public void rand(int x, int z) {
            this.posX += ((double) x / 1024.0D - 0.5D) * offset;
            this.posZ += ((double) z / 1024.0D - 0.5D) * offset;
        }
    }
}
Третий слой будет создавать границу REDSERT_EDGE:
Java:
public class GenLayerDiffusionEdge extends GenLayer {

    public GenLayerDiffusionEdge(long seed, GenLayer parent) {
        super(seed);
        this.parent = parent;
    }

    public int[] getInts(int areaX, int areaY, int areaWidth, int areaHeight) {
        int i = areaX - 1;
        int j = areaY - 1;
        int k = areaWidth + 2;
        int l = areaHeight + 2;
        int[] aint = this.parent.getInts(i, j, k, l);
        int[] aint1 = IntCache.getIntCache(areaWidth * areaHeight);

        for (int i1 = 0; i1 < areaHeight; ++i1) {
            for (int j1 = 0; j1 < areaWidth; ++j1) {
                int k1 = aint[j1 + 1 + (i1 + 0) * k];
                int l1 = aint[j1 + 0 + (i1 + 1) * k];
                int i2 = aint[j1 + 1 + (i1 + 2) * k];
                int j2 = aint[j1 + 2 + (i1 + 1) * k];
                int k2 = aint[j1 + 1 + (i1 + 1) * k];
                this.initChunkSeed((long) (j1 + areaX), (long) (i1 + areaY));

                if (k2 != Biome.getIdForBiome(BiomeInit.REDSERT) && (k1 == Biome.getIdForBiome(BiomeInit.REDSERT) || l1 == Biome.getIdForBiome(BiomeInit.REDSERT) || i2 == Biome.getIdForBiome(BiomeInit.REDSERT) || j2 == Biome.getIdForBiome(BiomeInit.REDSERT))) {
                    aint1[j1 + i1 * areaWidth] = Biome.getIdForBiome(BiomeInit.REDSERT_EDGE);
                } else {
                    aint1[j1 + i1 * areaWidth] = k2;
                }
            }
        }

        return aint1;
    }
}
Сам же весь алгоритм генерации в результате вышел таким:
Java:
public static GenLayer[] diffusion(long seed, WorldType worldType) {
    GenLayer genlayer = new GenLayerIsland(1L);
    genlayer = new GenLayerFuzzyZoom(2000L, genlayer);
    genlayer = new GenLayerAddIsland(1L, genlayer);
    genlayer = new GenLayerZoom(2001L, genlayer);
    genlayer = new GenLayerAddIsland(2L, genlayer);
    genlayer = new GenLayerAddIsland(50L, genlayer);
    genlayer = new GenLayerAddIsland(70L, genlayer);
    genlayer = new GenLayerRemoveTooMuchOcean(2L, genlayer);
    genlayer = new GenLayerAddIsland(3L, genlayer);
    genlayer = new GenLayerZoom(2002L, genlayer);
    genlayer = new GenLayerDiffusionIsland(1L, genlayer); // Создаем REDSERT
    genlayer = new GenLayerZoom(2003L, genlayer);
    genlayer = new GenLayerAddIsland(4L, genlayer);
    genlayer = GenLayerZoom.magnify(1000L, genlayer, 2);
    genlayer = new GenLayerZoom(1000L, genlayer);
    genlayer = new GenLayerAddIsland(3L, genlayer);
    genlayer = new GenLayerDiffusionEffect(1L, genlayer); // Эффект диффузии
    genlayer = new GenLayerDiffusionEdge(1L, genlayer); // Граница
    genlayer = new GenLayerZoom(1001L, genlayer);
    GenLayer genlayer_world = new GenLayerSmooth(1000L, genlayer);
    genlayer = new GenLayerVoronoiZoom(10L, genlayer_world);
    genlayer_world.initWorldGenSeed(seed);
    genlayer.initWorldGenSeed(seed);
    return new GenLayer[] { genlayer_world, genlayer, genlayer_world };
}
Для сравнения выкладываю две генерации: без диффузии и с диффузией.
ЭД_1.png

ЭД_2.png
  • ЭД_2.png
    ЭД_2.png
    34.9 KB · Просмотры: 442
Автор
yoloven
Просмотры
3,709
Первый выпуск
Обновление
Оценка
5.00 звёзд 2 оценок

Другие ресурсы пользователя yoloven

Последние рецензии

Это шикарный гайд, спасибо, очень пригодится. Написан очень хорошо, с выносками важных моментов, комментариями и форматированием. Тебе бы надо поручить учебник на сайте писать, было бы замечательно.
Ничего не понял, но очень интересно )
На досуге ознакомлюсь тщательнее, за старание и разъяснения огромный респект.
yoloven
yoloven
Начни с первой части)
Сверху