It’s fairly common to find Perlin Noise being used to generate procedural terrain, among other things. Perlin Noise can be used to generate a heightmap, which in turn can be used to control the height of a chunk of terrain… and it would then be common to layer those heightmaps at differing frequencies to create the sort of surface detail that you would associate with real world terrain.
A single Perlin Noise heightmap might look like this.
I’m going to describe a quick test here where I’ll build a grid of terrain cells in Unity using some custom shaders to make the rendering a bit more interesting. What I’m going to end up with is a simple interface defined via a C# script, that in the Inspector looks something like this, that will allow me to quickly build and edit the properties of my terrain.
The way I’ve set this up is such that clicking the Build button cleans out all children of the game-object that the script is attached to, and then generates a set of new children representing terrain tiles and the water. The surface area of the terrain is defined by the tile count and tile size.
For each tile I’ll generate six sets of Perlin Noise, using different frequencies (input scales) and weights, which I’ll then sum and normalize, before raising to a power and finally scaling to fit the desired world height. We raise to a power in order to flatten out the lower terrain while leaving the taller features more or less untouched… which I find yields more interesting results. Once we have out heights we then create polygon meshes per tile to represent the terrain.
The important part of all this is that deep inside the inner loop of the script we do the following per vertex calculation, where the s and w values are taken direct from the UI we’ve created.
// sum the weighted heights. float y = 0.0f; y += Mathf.PerlinNoise(x / s0, z / s0) * w0; y += Mathf.PerlinNoise(x / s1, z / s1) * w1; y += Mathf.PerlinNoise(x / s2, z / s2) * w2; y += Mathf.PerlinNoise(x / s3, z / s3) * w3; y += Mathf.PerlinNoise(x / s4, z / s4) * w4; y += Mathf.PerlinNoise(x / s5, z / s5) * w5; y /= weightSum; // raising the height to a power gives a much nicer result // - more flat areas... recognizable mountains... etc // also factor in a height scale. y = Mathf.Pow(y, WorldHeightPower); y *= WorldHeightScale;
With those meshes built and attached to a MeshFilter component (and a MeshCollider too) I’ll then link them to a custom shader. Assuming I’ve a shader in my asset library this is easy. I just need to add a MeshRenderer component and link it to my shader like so, where since I’ve based my shader on the standard Unity surface shader I also need to enable to metallic workflow and set the glossiness to prevent the terrain looking like shiny plastic.
meshRenderer.sharedMaterial = new Material(Shader.Find("Custom/GOAT_Terrain")); meshRenderer.sharedMaterial.EnableKeyword("_SPECGLOSSMAP"); meshRenderer.sharedMaterial.SetFloat("_Glossiness", 0.1f);
My custom shader samples two textures, one for grass and one for cliffs and I also provide a scale for each. Whenever these are changed via the inspector I can easily connect the new values to my components by doing the following for each terrain tile, bearing in mind that I’ve named that _GrassTex and _CliffTex in my shader code.
meshRenderer.sharedMaterial.SetTexture("_GrassTex", GrassTexture); meshRenderer.sharedMaterial.SetTexture("_CliffTex", CliffTexture); meshRenderer.sharedMaterial.SetTextureScale("_GrassTex", new Vector2(GrassScale, GrassScale)); meshRenderer.sharedMaterial.SetTextureScale("_CliffTex", new Vector2(CliffScale, CliffScale));
Finally… my shader uses the following code in place of the usual code that samples _MainTex, allowing for the two textures to be sampled and blended based on the surface normal, meaning you automatically get the grass texture on the flatter areas and the cliff texture otherwise…
float lerpFactor = saturate(saturate(o.Normal.y - 0.8f) * 8.5f); fixed4 grassColor = tex2D(_GrassTex, IN.uv_GrassTex); fixed4 cliffColor = tex2D(_CliffTex, IN.uv_CliffTex); fixed4 textureColor = grassColor * lerpFactor + cliffColor * (1.0f - lerpFactor); o.Albedo = textureColor.rgb * IN.color.rgb;
The end result looks something like this…
The result thus far isn’t really what I was after. I was hoping to create something a little more stylized with more interesting and not necessarily natural features, and I’m not sure this is really taking me in the right direction, so while I could carry on and try to make this look super realistic, I’m going to leave this experiment there and look into other approaches.