This post continues a
cycle about endless world creation, in which we will get acquainted with the basics of procedural generation.
It seems that quite a large part of people come to the gaming industry for creative reasons. One of the directions of such creativity can be the creation of the worlds. Moreover, if you turn to the Internet with a question on this topic, you will find a relatively large number of articles, which can serve as confirmation of the great desire of people to become demiurges. This post will follow in the footsteps of many predecessors and talk about one of the simplest and most basic ways to create a small, but still your piece of the universe, named “Perlin Noise.”
Procedural Generation
From the programmer view, there are two ways to create any subject: to make it with your hands or write code to generate it. Although most likely, there is only the second and the first, for some reason, used by everyone else. First, let’s define basic concepts:
Procedural generation(PCG) - a common name for mechanisms for automatically creating content using an algorithm. This concept includes the creation of everything from music to the rules of the game.
Pseudorandom number generator(PRNG) - an algorithm that generates a sequence of numbers whose elements are almost independent of each other and obey a given distribution.
Seed - determines the sequence of numbers to be issued by the PRNG.
If you need a small amount of content, then, of course, the easiest way is to create it with your hands (arrange trees on a map or draw a texture). However, if we are talking about vast volumes of material, then procedural generation can be a more profitable way. The principle is quite simple when you need something you get the PRNG, the seed, additional input data, and some algorithm for creating the necessary thing and generate a unique unit of content. If any changes occur after generation, then you save them separately. Subsequently, if you need to repeat the object, knowing the generating element, the input data, and the performed changes, you can reproduce its generation and get the given object.
It is a general description of the scheme, which, using various algorithms, is suitable for almost any task, starting from music generation, continuing with fire textures, and ending with animal generation for some planet in No Man’s Sky. In this post, we will consider the purpose of procedural generation to the surface of the earth.
Make our land
One of the universal ways to describe the earth’s surface is to use a heightmap. It is a two-dimensional array with data on the height of points on the ground. If we take a vertical slice from such a map, we get this view:
Accordingly, if we manage to generate such a map of heights, we can create a landscape from it. Suppose we generate points through a regular Random and get a graph like this:
If you look in a two-dimensional form, the generated map will look something like this:
It should be clear from the picture that the usual PRNG is not very suitable for creating a map because white noise does not imply dependence on nearby points. Therefore, we get significant surface-level differences that are not very similar to the real surface.
Perlin Noise
To generate the related data, we will use a PRNG called Perlin Noise, which will create a picture like that:
We will not delve into the principles of the algorithm itself; for more information, refer to this article or this one.
Concepts that we will need for further work:
Amplitude - the maximum value of the offset or change of the variable from the average value.
Frequency - the characteristic of a periodic process is equal to the number of repetitions or occurrence of events (operations) per unit time.
Lacunarity - controls the change in frequency.
Persistence - controls the change in amplitude.
Octave - samples with different frequencies.
We will demonstrate the principle of operation of the three-octave algorithm:
Lacunarity = 2;
Persistence = 0.5;
The first octave sets the main elevation (sea-mountains)
Frequency = Lacunarity ^ 0 = 1
Amplitude = Persistence ^ 0 = 1
The second octave sets the average surface drops (boulders)
Frequency = Lacunarity ^ 1 = 2
Amplitude = Persistence ^ 1 = 0.5
The third octave defines small surface drops (small stones)
Frequency = Lacunarity ^ 2 = 4
Amplitude = Persistence ^ 2 = 0.25
We put the octaves on top of each other, and as a result, we get something similar to a real height map in section:
usingUnityEngine;publicstaticclassNoiseMapGenerator{publicstaticfloat[]GenerateNoiseMap(intwidth,intheight,intseed,floatscale,intoctaves,floatpersistence,floatlacunarity,Vector2offset){// An array of vertex data, a one-dimensional view will help get rid of unnecessary cycles later
float[]noiseMap=newfloat[width*height];// Seed element
System.Randomrand=newSystem.Random(seed);// Octave shift to get a more interesting picture with overlapping
Vector2[]octavesOffset=newVector2[octaves];for(inti=0;i<octaves;i++){// Also use external position shift
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;}// For a more visually pleasant zoom shift our view to the center
floathalfWidth=width/2f;floathalfHeight=height/2f;// Generate points for a heightmap
for(inty=0;y<height;y++){for(intx=0;x<width;x++){// Set the values for the first octave
floatamplitude=1;floatfrequency=1;floatnoiseHeight=0;floatsuperpositionCompensation=0;// Octave Overlay Processing
for(inti=0;i<octaves;i++){// Calculate the coordinates to get from Noise Perlin
floatxResult=(x-halfWidth)/scale*frequency+octavesOffset[i].x*frequency;floatyResult=(y-halfHeight)/scale*frequency+octavesOffset[i].y*frequency;// Obtaining Altitude from the PRNG
floatgeneratedValue=Mathf.PerlinNoise(xResult,yResult);// Octave overlay
noiseHeight+=generatedValue*amplitude;// Compensate octave overlay to stay in a range [0,1]
noiseHeight-=superpositionCompensation;// Calculation of amplitude, frequency and superposition compensation for the next octave
amplitude*=persistence;frequency*=lacunarity;superpositionCompensation=amplitude/2;}// Save heightmap point
// Due to the superposition of octaves, there is a chance of going out of the range [0,1]
noiseMap[y*width+x]=Mathf.Clamp01(noiseHeight);}}returnnoiseMap;}}
usingUnityEngine;publicenumMapType{Noise,Color}publicclassNoiseMap:MonoBehaviour{// Input data for our noise generator
[SerializeField]publicintwidth; [SerializeField]publicintheight; [SerializeField]publicfloatscale; [SerializeField]publicintoctaves; [SerializeField]publicfloatpersistence; [SerializeField]publicfloatlacunarity; [SerializeField]publicintseed; [SerializeField]publicVector2offset; [SerializeField]publicMapTypetype=MapType.Noise;privatevoidStart(){GenerateMap();}publicvoidGenerateMap(){// Generate a map
float[]noiseMap=NoiseMapGenerator.GenerateNoiseMap(width,height,seed,scale,octaves,persistence,lacunarity,offset);// Pass the map to the renderer
NoiseMapRenderermapRenderer=FindObjectOfType<NoiseMapRenderer>();mapRenderer.RenderMap(width,height,noiseMap,type);}}
NoiseMapRenderer - creates texture for map display
usingSystem;usingSystem.Collections.Generic;usingUnityEngine;publicclassNoiseMapRenderer:MonoBehaviour{ [SerializeField]publicSpriteRendererspriteRenderer=null;// Determining the coloring of the map depending on the height
[Serializable]publicstructTerrainLevel{publicstringname;publicfloatheight;publicColorcolor;} [SerializeField]publicList<TerrainLevel>terrainLevel=newList<TerrainLevel>();// Depending on the type, we draw noise either in black and white or in color
publicvoidRenderMap(intwidth,intheight,float[]noiseMap,MapTypetype){if(type==MapType.Noise){ApplyColorMap(width,height,GenerateNoiseMap(noiseMap));}elseif(type==MapType.Color){ApplyColorMap(width,height,GenerateColorMap(noiseMap));}}// Create texture and sprite to display
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);;}// Convert an array with noise data into an array of black and white colors, for transmission to the texture
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;}// Convert an array with noise data into an array of colors, depending on the height, for transmission to the texture
privateColor[]GenerateColorMap(float[]noiseMap){Color[]colorMap=newColor[noiseMap.Length];for(inti=0;i<noiseMap.Length;i++){// Base color with the highest value range
colorMap[i]=terrainLevel[terrainLevel.Count-1].color;foreach(varlevelinterrainLevel){// If the noise falls into a lower range, then use it
if(noiseMap[i]<level.height){colorMap[i]=level.color;break;}}}returncolorMap;}}
Create an object with new scripts and the Sprite Renderer component
Set palette for heightmap
Result
As a result, we got a heightmap that looks like a separate part of the landscape.
Conclusion
We got acquainted with the basic concepts of procedural generation and Perlin Noise, and also generated a map of heights with it, which can resemble a real landscape. It is a straightforward algorithm that allows you to achieve acceptable results in a short time. The most obvious application for which would be overlaying its values on an object like Plane and thus creating a 3D landscape, as Sebastian did in his video series. However, this is just a basic algorithm that does not count many parameters when creating a landscape, climate, for example. Also, the independence of calculations, allows you to create infinite space but produces a fairly similar result. In the end, it does not allow you to create maps divided into continents or islands without additional mechanisms. Nevertheless, in the following articles of this series, we will try to apply this basic algorithm for something more interesting, because for the most part, the main limitation is only our imagination. After all, even Minecraft was originally based on Perlin Noise. See you next time! =)
Bonus
One of my favorite articles on the same topic, and in general, this blog contains a lot of interesting information on the development of game mechanics.