Генерация летающих островов шумом перлина.

37
1
7
По просьбе Liahim пишу гайд по генерации летающих островов шумом перлина.


[Дисклаймер]Раньше я ни разу не писал своих уроков, так что могу понаделать кучу ошибок, прошу принять это во внимание.[/Дисклаймер]


Начнем с небольшой теории. Шум перлина - это функция типа f(x,y) = z, ну или в случае с майном - f(x,z) = y, где два параметра - координаты на плоскости, а результат - высота. В майне уже есть готовая функция для генерации шума перлина, так что я не буду рассказывать о том, как сделать шум собственноручно, тем более до меня об этом отлично рассказали здесь.

Давайте попробуем ради интереса создать простенькую генерацию этим шумом.


Код:
import net.minecraft.world.World;
import net.minecraft.world.chunk.IChunkGenerator;
import net.minecraft.world.chunk.IChunkProvider;
import net.minecraftforge.fml.common.IWorldGenerator;

import java.util.Random;

/**
* Created by Defernus on 07.06.2017.
*/
public class MyGenerator implements IWorldGenerator {

   @Override
   public void generate(Random rnd, int chunkX, int chunkZ, World world, IChunkGenerator iChunkGenerator, IChunkProvider iChunkProvider) {
       if(!canGenerateHere(world, chunkX, chunkZ)) {//прирываем ф-ю, если не можем генерировать в этом чанке
           return;
       }
       
       //ну а тут будет сам генератор
       
   }

   //Проверяем чанк на возможность генерации в нем
   private static boolean canGenerateHere(World world, int chunkX, int chunkZ) {
       Random rnd = world.setRandomSeed(chunkX, chunkZ, 651246235);
       // в нашем случае берется число от 0, до 9 и если оно равно 0,
       // то мы можем генерировать в нем(т.е. на будут подходить примерно
       // 10% всех чанков)
       return rnd.nextInt(10)==0;
   }
}


[size=medium]Ну и не забудьте зарегать генератор в ServerProxy(ну или CommonProxy как у меня) в preInit
[/size]



Код:
GameRegistry.registerWorldGenerator(new MyGenerator(), 0);

Наш генератор еще ни чего не генерирует, и если вы запустите его, то ни чего нового не увидите, но сейчас мы это исправим.

Давайте создадим простенькие кубики размером в 16 блоков, в которых будут генерироваться наши мини-ландшафты.


Код:
@Override
   public void generate(Random rnd, int chunkX, int chunkZ, World world, IChunkGenerator iChunkGenerator, IChunkProvider iChunkProvider) {
       if(!canGenerateHere(world, chunkX, chunkZ)) {//прирываем ф-ю, если не можем генерировать в этом чанке
           return;
       }

       //Сама генерация

       int x = chunkX*16;
       int z = chunkZ*16;
       int y = 100;

       //создаем шум перлина, в который передаем рандом и глубину(о ней будет пара строк ниже)
       NoiseGeneratorPerlin perlin = new NoiseGeneratorPerlin(rnd, 2);

       for(int i = 0;  i < 16; i++) {
           for(int j = 0; j < 16; j++) {
               //получаем высоту ландшафта из шума
               int h = (int)perlin.getValue(x+i, z+j);

               BlockPos pos = new BlockPos(x+i, y+h, z+j);
               //ставим блок по координатам(в нашем случае - блок травы)
               world.setBlockState(pos, Blocks.GRASS.getDefaultState());
           }
       }

   }

2017-06-07_19.23.48.png
Получилось не очень, ибо шум надо было растянуть, что мы и сделаем.

Код:
//заменяем
int h = (int)perlin.getValue(x+i, z+j);
//на
int h = (int)perlin.getValue((x+i)/10., (z+j)/10.);
//"." посте десятки нужна для получения double результата

2017-06-07_19.24.10.png
Вот это уже похоже на правду.

Конец первой части.
 
Последнее редактирование модератором:
37
1
7
Часть вторая

Настало время впервые поговорить о том, как генерировать острова.

Алгоритм достаточно прост, надо взять два разных шума перлина и залить все, что находится выше первого и ниже второго вот так
Безымянный.png
(паинт мастер!)

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

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

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

Код:
@Override
   public void generate(Random rnd, int chunkX, int chunkZ, World world, IChunkGenerator iChunkGenerator, IChunkProvider iChunkProvider) {
       if(!canGenerateHere(world, chunkX, chunkZ)) {
           return;
       }

       int r = 16;//радиус острова

       //берем центр чанка в качестве центра острова
       int x = chunkX*16+8;
       int z = chunkZ*16+8;
       int y = 100;

       NoiseGeneratorPerlin perlin = new NoiseGeneratorPerlin(rnd, 2);

       for(int i = -r;  i <= r; i++) {
           for(int j = -r; j <= r; j++) {
               int hT = (int)perlin.getValue((x+i)/10., (z+j)/10.);//верхний шум
               int hB = (int)perlin.getValue((x+i), (z+j))*5;//нижний шум можно не растягивать, но стоит увеличить котраст
               hB += Math.sqrt(i*i+j*j)-r;//собсна добавляем ту самую третью ф-ю к нашему нижнему шуму

               for(int k = hB; k <= hT; k++) {
                   BlockPos pos = new BlockPos(x+i, y+k, z+j);

                   world.setBlockState(pos, Blocks.GRASS.getDefaultState());
               }
           }
       }

   }

   private static boolean canGenerateHere(World world, int chunkX, int chunkZ) {
       Random rnd = world.setRandomSeed(chunkX, chunkZ, 651246235);
       return rnd.nextInt(50)==0;
   }


Безымянныйй.png
Так же на первое время мы выйдем за границы чанка в нашем генераторе (Так лучше не делать и мы исправим это чуть дальше).
И надо не забыть уменьшить шанс спавна острова, т.к. сейчас они спавнятся слишком часто и будут пересекаться друг с другом
2017-06-07_20.31.50.png
Получаем нечто отдаленно похожее на конечный результат, но остров остров кишит дырками, да и состоит из блоков травы, что не есть хорошо.

Давайте исправим это.
Изменить ситуацию с травой легко.
Код:
       for(int i = -r;  i <= r; i++) {
           for(int j = -r; j <= r; j++) {
               int hT = (int)perlin.getValue((x+i)/10., (z+j)/10.);//верхний шум
               int hB = (int)perlin.getValue((x+i), (z+j))*5;//нижний шум можно не растягивать, но стоит увеличить котраст
               hB += Math.sqrt(i*i+j*j)-r;//собсна добавляем ту самую третью ф-ю к нашему нижнему шуму

               for(int k = hB; k <= hT; k++) {

                   BlockPos pos = new BlockPos(x+i, y+k, z+j);

                   IBlockState block;

                   if(k == hT) {
                       block = Blocks.GRASS.getDefaultState();
                   }else if(k > hT-4) {
                       block = Blocks.DIRT.getDefaultState();
                   }else {
                       block = Blocks.STONE.getDefaultState();
                   }
                   world.setBlockState(pos, block);
               }
           }
       }

А с дырками все еще проще, надо просто уменьшить интенсивность нижнего шума в зависимости от близости к центру.

2017-06-07_20.52.29.png

Ну так остров выглядит на много лучше.

Осталась еще пара мелочей, к примеру мы можем улучшить поверхность острова немного изменив верхний шум
Код:
//растянем его еще сильнее и увеличим контраст, а так же добавим возвышение к центру
int hT = (int)(perlin.getValue((x+i)/20., (z+j)/20.)*2 + r*(1.-(i*i+j*j)/(r*r))/10.);
2017-06-07_21.05.32.png
Вот тат на много лучше.
Конец второй части.
 
Последнее редактирование модератором:
37
1
7
Часть третья, заключительная(наверное)
В этой части мы подчистим за собой.
Сейчас у нашего генератора есть несколько проблем, во первых мы при генерации одного чанка затрагиваем сразу восемь соседних(что на самом деле очень не хорошо), а так же некоторые острова обрезаются, так как границы генерации были заданы слишком маленькими, да, чуть не забыл, еще встречаются вот такие вот ребята:
2017-06-07_21.03.53.png
Это острова, которые сгенерировались в соседних чанках.
Давайте исправим сразу все эти проблемы, еще и увеличив размеры островов.

Начнем с конца, её исправить легче всего, на самом деле я просто взял код из генератора меншонов и переписал под себя))

Код:
   private static boolean canGenerateHere(World world, int chunkX, int chunkZ) {
       int i = chunkX;
       int j = chunkZ;

       if (chunkX < 0) {
           i = chunkX - 9;
       }

       if (chunkZ < 0) {
           j = chunkZ - 9;
       }

       i /= 15;
       j /= 15;
       i *= 15;
       j *= 15;

       Random random = new Random((long)i * 341873128712L + (long)j * 132897987541L + world.getWorldInfo().getSeed() + (long)27644437);
       i += random.nextInt(5);
       j += random.nextInt(5);

       if( i == chunkX && j == chunkZ ){
           return true;
       }

       return false;
   }
Мы просто берем квадрат 15x15 чанков и генерируем в нем один остров(больше в нем островов не будет). Если кому не будет понятен этот алгоритм, пишите, разжую))

Теперь надо исправить первую проблему, а именно генерировать только в том чанке, в котором нужно. она фиксится чуть сложнее

Код:
@Override
   public void generate(Random rnd, int chunkX, int chunkZ, World world, IChunkGenerator iChunkGenerator, IChunkProvider iChunkProvider) {
       //корды центрального чанка острова
       int cchunkX = 0;
       int cchunkZ = 0;

       boolean cg = false;
       for(int k = 0; k < 49; k++) {
           //проходим по квадрату 7x7 в поисках центра острова, если такой находится, мы генерируем чанк.
           int i = k%7-3;
           int j = k/7-3;
           if(canGenerateHere(world, chunkX+i, chunkZ+j)){
               cchunkX = chunkX + i;
               cchunkZ = chunkZ + j;

               cg = true;
               break;
           }

       }

       if(!cg)return;//если центр найден не был прекращаем генерацию

       //берем центр чанка в качестве центра острова
       int x = cchunkX*16+8;
       int z = cchunkZ*16+8;
       int y = 100;

       //здесь нужен свой рандом, зависящий от центра острова
       rnd = new Random((long)x * 341873128712L + (long)z * 132897987541L + world.getWorldInfo().getSeed() + (long)27644437);

       int r = 16+rnd.nextInt(17);//радиус острова от 16 до 32 блоков

       NoiseGeneratorPerlin perlin = new NoiseGeneratorPerlin(rnd, 2);

       //Теперь мы будем проходить цикл только в пределах нужного чанка
       int iMin = ( chunkX-cchunkX ) * 16-8;
       int iMax = ( chunkX-cchunkX+1 ) * 16-8;
       int jMin = ( chunkZ-cchunkZ ) * 16-8;
       int jMax = ( chunkZ-cchunkZ+1 ) * 16-8;

       for(int i = iMin;  i <= iMax; i++) {
           for(int j = jMin; j <= jMax; j++) {
               double l = Math.sqrt(i*i+j*j);

               int hT = (int)( perlin.getValue((x+i)/40., (z+j)/40.)*2 - (i*i+j*j)/(10.*r) );//верхний шум
               int hB = (int)( ( perlin.getValue((x+i), (z+j))-1 )*( ((r-l)/(double)r)*10>0?((r-l)/r)*10:0 ) );

               hB += (int)l-r;

               for(int k = hB; k <= hT; k++) {

                   BlockPos pos = new BlockPos(x+i, y+k, z+j);

                   IBlockState block;

                   if(k == hT) {
                       block = Blocks.GRASS.getDefaultState();
                   }else if(k > hT-4) {
                       block = Blocks.DIRT.getDefaultState();
                   }else {
                       block = Blocks.STONE.getDefaultState();
                   }
                   world.setBlockState(pos, block);
               }
           }
       }

   }
Тут я чуток увлекся и изменил некоторые параметры генерации, от чего остров стал выглядеть не много лучше
2017-06-07_22.36.42.png

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

Код:
int hB = (int)( perlin.getValue((x+i)/50., (z+j)/50.)*((r-l)/5.+7) + ( perlin.getValue((x+i), (z+j))-1 )*( ((r-l)/(double)r)*10>0?((r-l)/r)*10:0 ) );
2017-06-07_22.55.54.png
Ну вот и все, остров выглядит вполне не плохо.
 
Последнее редактирование модератором:
4,045
63
645
Ахрененно!
Прочувствовать бы теперь всё это )
Небольшой вопрос: почему в методе canGenerateHere() ты не используешь рандом из начального метода generate(), а создаёшь свой?

P.S. По мне, сливающиеся "ребята" вполне себе хорошо смотрятся и только разнообразят ландшафт )
Я бы выдал это за фишку )
 
37
1
7
Liahim написал(а):
Ахрененно!
Прочувствовать бы теперь всё это )
Небольшой вопрос: почему в методе canGenerateHere() ты не используешь рандом из начального метода generate(), а создаёшь свой?

P.S. По мне, сливающиеся "ребята" вполне себе хорошо смотрятся и только разнообразят ландшафт )
Я бы выдал это за фишку )

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

А по поводу сливающихся ребят не прокатит, ибо у них высота разная и получаются резкие обрывы ландшафта.
 
7,099
324
1,509
Мы просто берем квадрат 15x15 чанков и генерируем в нем один остров(больше в нем островов не будет). Если кому не будет понятен этот алгоритм, пишите, разжую))
Да, разъясни, пожалуйста, не очень понимаю, как это работает
 
Сверху