Продолжение
цикла о создании бесконечных миров, в которой мы познакомимся с основами процедурной генерации.
Кажется что довольно большая часть людей приходит в игровую индустрию из творческих побуждений. Одним из направлений такого творчества может послужить создание собственных миров, либо похожих на наш, либо сильно отличающихся. Больше того, если обратиться к интернету с вопросом по этой тематике, то найдётся довольно большое количество статей, что может служить подтверждением большого стремления людей стать демиургами. Этот пост пойдёт по стопам многих предшественников, и расскажет об одном из самых простых и базовых способов создать небольшой, но всё-таки свой кусочек вселенной, а именно “Шуме Перлина(Perlin Noise)”.
Процедурная генерация
С точки зрения программиста, для создания любого предмета есть 2 пути: сделать его руками или написать код, для его генерации. Хотя скорее есть только второй, а первым почему-то пользуются все остальные. Для начала определимся с базовыми понятиями:
Процедурная генерация (ПГ, англ. Procedural generation, сокр. PCG) - общее название для механизмов автоматического создания контента по какому-либо алгоритму. Это понятие включает в себя создание всего от музыки, до правил игры.
Генератор псевдослучайных чисел (ГСПЧ, англ. pseudorandom number generator, сокр. PRNG) - алгоритм, порождающий последовательность чисел, элементы которой почти независимы друг от друга и подчиняются заданному распределению.
Порождающий элемент(англ. seed) - определяет последовательность выдаваемых ГСПЧ чисел.
Если вам необходимо небольшое количество контента, то, конечно же, самым простым способом будет создать его руками(расставить по карте деревья или нарисовать текстуру). Однако если мы говорим про действительно большие объёмы материала, то процедурная генерация может стать более выгодным способом. Принцип довольно прост, когда вам необходимо что то, то используя ГСПЧ, порождающий элемент, дополнительные входные данные и какой-либо алгоритм создания необходимой вещи вы получаете уникальную единицу контента. Если после генерации происходят какие-либо изменения, то вы их сохраняете отдельно. Впоследствии, если вам необходимо повторить сгенерированный объект, вы, зная порождающий элемент, входные данные и внесённые изменения можете повторить его генерацию и получить данный объект.
Это общее описание схемы, которая при использовании различных алгоритмов подходит практически для любой задачи начиная от генерации музыки, продолжая текстурами огня, заканчивая генерацией животных для какой-нибудь планеты в No Man’s Sky. Мы же в данном посте рассмотрим применение процедурной генерации к пространству, а именно к поверхности земли, так что все дальнейшие примеры будут сведены к этой задаче.
Сгенерируем свои 6 соток
Одним из универсальных способов описания земной поверхности является применение карты высот. Она представляет из себя двумерный массив с данными о высоте точек на местности. Если же мы возьмём вертикальный срез с такой карты, то получим вот такой вид:
Соответственно если нам удастся сгенерировать подобную карту высот, мы сможем по ней создать некий ландшафт. Предположим, мы сгенерируем точки через обычный Random, и получим такой график:
В двухмерном виде, сгенерированная карта будет выглядеть примерно так:
По картинке должно быть понятно, что обычный ГСПЧ не очень подходит для генерации карты, т.к. белый шум не предполагает зависимости от близлежащих точек, а следовательно, мы получим большие перепады, не сильно похожие на реальную поверхность.
Шум Перлина
Для генерации связанных данных нам поможет ГСПЧ под названием Шум Перлина, который при генерации выдаст подобную картину:
Мы не будем углубляться в принципы работы самого алгоритма, за дополнительной информацией можно обратиться к этой англоязычной статье или этой статье с хабра, в которой рассказывается про генерацию облаков.
Понятия, которые нам понадобятся для дальнейшей работы
Амплитуда (amplitude) - максимальное значение смещения или изменения переменной величины от среднего значения.
Частота (frequency) - характеристика периодического процесса, равна количеству повторений или возникновения событий (процессов) в единицу времени.
Лакунарность (lacunarity) - контролирует изменение частоты.
Постоянство (persistence) - контролирует изменение амплитуды.
Октава (octave) - повторение.
Продемонстрируем принцип работы алгоритма с тремя октавами:
Lacunarity = 2;
Persistence = 0.5;
Первая октава задаёт основной перепад высот(моря-горы)
Frequency = Lacunarity ^ 0 = 1
Amplitude = Persistence ^ 0 = 1
Вторая октава задаёт средние перепады поверхности(овраги-валуны)
Frequency = Lacunarity ^ 1 = 2
Amplitude = Persistence ^ 1 = 0.5
usingUnityEngine;publicstaticclassNoiseMapGenerator{publicstaticfloat[]GenerateNoiseMap(intwidth,intheight,intseed,floatscale,intoctaves,floatpersistence,floatlacunarity,Vector2offset){// Массив данных о вершинах, одномерный вид поможет избавиться от лишних циклов впоследствии
float[]noiseMap=newfloat[width*height];// Порождающий элемент
System.Randomrand=newSystem.Random(seed);// Сдвиг октав, чтобы при наложении друг на друга получить более интересную картинку
Vector2[]octavesOffset=newVector2[octaves];for(inti=0;i<octaves;i++){// Учитываем внешний сдвиг положения
floatxOffset=rand.Next(-100000,100000)+offset.x;floatyOffset=rand.Next(-100000,100000)+offset.y;octavesOffset[i]=newVector2(xOffset/width,yOffset/height);}if(scale<0){scale=0.0001f;}// Учитываем половину ширины и высоты, для более визуально приятного изменения масштаба
floathalfWidth=width/2f;floathalfHeight=height/2f;// Генерируем точки на карте высот
for(inty=0;y<height;y++){for(intx=0;x<width;x++){// Задаём значения для первой октавы
floatamplitude=1;floatfrequency=1;floatnoiseHeight=0;floatsuperpositionCompensation=0;// Обработка наложения октав
for(inti=0;i<octaves;i++){// Рассчитываем координаты для получения значения из Шума Перлина
floatxResult=(x-halfWidth)/scale*frequency+octavesOffset[i].x*frequency;floatyResult=(y-halfHeight)/scale*frequency+octavesOffset[i].y*frequency;// Получение высоты из ГСПЧ
floatgeneratedValue=Mathf.PerlinNoise(xResult,yResult);// Наложение октав
noiseHeight+=generatedValue*amplitude;// Компенсируем наложение октав, чтобы остаться в границах диапазона [0,1]
noiseHeight-=superpositionCompensation;// Расчёт амплитуды, частоты и компенсации для следующей октавы
amplitude*=persistence;frequency*=lacunarity;superpositionCompensation=amplitude/2;}// Сохраняем точку для карты высот
// Из-за наложения октав есть вероятность выхода за границы диапазона [0,1]
noiseMap[y*width+x]=Mathf.Clamp01(noiseHeight);}}returnnoiseMap;}}
usingSystem;usingSystem.Collections.Generic;usingUnityEngine;publicclassNoiseMapRenderer:MonoBehaviour{ [SerializeField]publicSpriteRendererspriteRenderer=null;// Определение раскраски карты в зависимости от высот
[Serializable]publicstructTerrainLevel{publicstringname;publicfloatheight;publicColorcolor;} [SerializeField]publicList<TerrainLevel>terrainLevel=newList<TerrainLevel>();// В зависимости от типа отрисовываем шум либо в чёрно-белом, либо цветном варианте
publicvoidRenderMap(intwidth,intheight,float[]noiseMap,MapTypetype){if(type==MapType.Noise){ApplyColorMap(width,height,GenerateNoiseMap(noiseMap));}elseif(type==MapType.Color){ApplyColorMap(width,height,GenerateColorMap(noiseMap));}}// Создание текстуры и спрайта для отображения
privatevoidApplyColorMap(intwidth,intheight,Color[]colors){Texture2Dtexture=newTexture2D(width,height);texture.wrapMode=TextureWrapMode.Clamp;texture.filterMode=FilterMode.Point;texture.SetPixels(colors);texture.Apply();spriteRenderer.sprite=Sprite.Create(texture,newRect(0.0f,0.0f,texture.width,texture.height),newVector2(0.5f,0.5f),100.0f);;}// Преобразуем массив с данными о шуме в массив чёрно-белых цветов, для передачи в текстуру
privateColor[]GenerateNoiseMap(float[]noiseMap){Color[]colorMap=newColor[noiseMap.Length];for(inti=0;i<noiseMap.Length;i++){colorMap[i]=Color.Lerp(Color.black,Color.white,noiseMap[i]);}returncolorMap;}// Преобразуем массив с данными о шуме в массив цветов, зависящих от высоты, для передачи в текстуру
privateColor[]GenerateColorMap(float[]noiseMap){Color[]colorMap=newColor[noiseMap.Length];for(inti=0;i<noiseMap.Length;i++){// Базовый цвет с самым высоким диапазоном значений
colorMap[i]=terrainLevel[terrainLevel.Count-1].color;foreach(varlevelinterrainLevel){// Если шум попадает в более низкий диапазон, то используем его
if(noiseMap[i]<level.height){colorMap[i]=level.color;break;}}}returncolorMap;}}
Создайте объект с новыми скриптами и компонентом Sprite Renderer
Настройте палитру для карты высот
Результат
На выходе мы получили карту высот, которая похожа на отдельную часть ландшафта.
Заключение
Мы познакомились с базовыми понятиями процедурной генерации и Шумом Перлина, а также сгенерировали с их помощью карту высот, которая может напоминать реальный ландшафт. Это очень простой алгоритм, который позволяет добиться приемлемых результатов за небольшое время. Самым очевидным способом применения для которой будет наложение на объект типа Plane и создание, таким образом, 3D ландшафта, как и сделал Sebastian в своей серии видео. Однако, это всего лишь базовый механизм, который не учитывает множество параметров при создании ландшафта, как, например, климат. Также независимость расчётов, которая, с одной стороны, позволяет создать бесконечное пространство, с другой, выдаёт довольно однообразный результат и не позволяет создавать карты, учитывающие разделение на континенты или острова, для этого потребуется вводить дополнительные механизмы. Тем не менее в следующих статьях этого цикла мы попробуем применить этот базовый алгоритм для чего-то более интересного, ведь по большей части основным ограничением является лишь наша фантазия, ведь даже Minecraft изначально основывался на Шуме Перлина. Пока! =)
Bonus
Одна из моих самых любимых статей на эту же тему, да и в целом этот блог содержит много интересной информации по разработке игровых механик.