Basic Procedural Desert Generation and Rendering in Unity

October 13, 2018
Last modified: August 25, 2019

Avatar

Juan Fornos Co-founder

Game Designer & Programmer

Avatar

Mauricio Eguía Co-founder

Game Designer & Programmer

Source code: https://github.com/WiseShards/basic_desert

Introduction

Update: Fixed some typos

In this blog post we present a simple way to generate a desert and then render it with custom shaders in Unity.

While developing Perpetual Wizard we found ourselves in the need of generating a desert at runtime. Even though we are using pre existing techniques, we figured out this approach from scratch. We know that more realistic results can be achieved but the final look is good enough for Perpetual Wizard since it has a non realistic style (stylized low poly, heavily influenced by Lara Croft Go). We are quite happy with the results and think it's a good starting point for a more realistic look.

Mesh Generation

The idea to the mesh generation is pretty simple. We consider our desert to be a box with dimensions Width x Depth x Height. With that in mind, for each unit in the base (x and z coordinates):

  • We create a vertex and two triangles with this vertex and the vertices next to it.
  • The height (y coordinate) of the vertex is computed using Perlin Noise to simulate the desert dunes.

Perlin Noise is a technique used to create textures. The resulting patterns are pseudo random and have a natural appearance. Since Perlin Noise is a gradient noise, the values change smoothly and it works to compute our dunes height. Even though the technique works in N dimensions, in Unity is implemented in two, with the function Mathf.PerlinNoise.

List<Vector3> vertices = new List<Vector3>();
List<int> triangles = new List<int>();
List<Vector2> uv = new List<Vector2>();
for(int i = 0; i < Width; i++) {
    for(int j = 0; j < Depth; j++) {
        float xCoord = (i / (float) Width) * NoiseScale;
        float yCoord = (j / (float) Depth) * NoiseScale;
        float y = Height * Mathf.PerlinNoise(xCoord, yCoord);

        vertices.Add(new Vector3(i - Width * 0.5f, y, j - Depth * 0.5f));
        uv.Add(new Vector2((float) i / Width, (float) j / Depth));
        if(i == 0 || j == 0) continue; // First bottom and left skipped
        triangles.Add(Width * i + j); // Top right
        triangles.Add(Width * i + (j - 1)); // Bottom right
        triangles.Add(Width * (i - 1) + (j - 1)); // Bottom left - First triangle
        triangles.Add(Width * (i - 1) + (j - 1)); // Bottom left
        triangles.Add(Width * (i - 1) + j); // Top left
        triangles.Add(Width * i + j); // Top right - Second triangle
    }
}

Note how we scale the noise with NoiseScale. This value alters the speed or frequency of the noise, resulting in different dunes sizes.

Also, you might want to clamp the noise value as it may be a bit higher than 1.0 according to the documentation.

The image below illustrates how the triangles are created:

Desert Mesh GenerationDesert Mesh Generation

The first triangles are created for the red vertex (as we skip vertices in the bottom and in the left). The first created triangle is the yellow one and the second is the green one.

The next steps are creating the mesh, assigning the values computed before and setting up the MeshRenderer:

Mesh mesh = new Mesh();
mesh.vertices = vertices.ToArray();
mesh.uv = uv.ToArray();
mesh.triangles = triangles.ToArray();
mesh.RecalculateNormals();

MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
Renderer ren = gameObject.AddComponent<MeshRenderer>();
ren.shadowCastingMode = ShadowCastingMode.Off;
ren.receiveShadows = false;
ren.lightProbeUsage = LightProbeUsage.Off;
ren.reflectionProbeUsage = ReflectionProbeUsage.Off;
ren.material = Material;
meshFilter.mesh = mesh;

Using the default shader the result is something like this:

Desert Mesh with default ShaderDesert Mesh with default Shader

Rendering

Noise Texture

To simulate the sand grains we are using a noise texture. We can use use an existing texture or create one with the code below:

private const int Size = 1024;

[MenuItem("Desert/Create Noise Texture")]
static void CreateTexture() {
    Texture2D tex = new Texture2D(Size, Size);
    for(int i = 0; i < Size; ++i) {
        for(int j = 0; j < Size; ++j) {
            float random = Random.value;
            Color c = Color.white * random;
            c.a = 1.0f;
            tex.SetPixel(i, j, c);
        }
    }
    byte[] texture = tex.EncodeToPNG();
    System.IO.File.WriteAllBytes(Application.dataPath + "/Textures/DesertNoise.png", texture);
}

This texture simply creates random grey pixels. The texture could be more sophisticated to generate a better results, but for the sake of this article this is a good start. Be sure to disable the Mip Maps generation in the texture settings.

Sample Grain Noise TextureSample Grain Noise Texture

Shader

The vertex shader is quite simple. We use the built-in TRANSFORM_TEX macro so we can tweak the distribution of the noise in the mesh via the tiling parameters in the material. The built-in TRANSFER_SHADOW macro, handles the shadows casted on the mesh. We get the clip pos with UnityObjectToClipPos and the ambient color with the built in ShadeSH9.

The lighting uses a single directional light and is computed using the standard Lambert diffuse model. We just added the “wrap diffuse” technique to tweak the NdotL value, which is a generalization and simplification of the Half Lambert technique presented by Valve. The idea is simple: clamp negative values to 0, scale the NdotL by _WarpDiffuse and add (1 - _WarpDiffuse) to it. This way the final NdotL value will be in the range [1 - _WarpDiffuse, 1] brightening the zones where light incidence is normally low.

VertexOutput vert(VertexInput v) {
    VertexOutput o;

    o.uv = TRANSFORM_TEX(v.uv, _NoiseTex);

    TRANSFER_SHADOW(o)

    o.pos = UnityObjectToClipPos(v.vertex);

    fixed3 worldNormal = UnityObjectToWorldNormal(v.normal);

    fixed NdotL = max(0, dot(worldNormal, _WorldSpaceLightPos0.xyz));
    NdotL = (NdotL * _WarpDiffuse) + (1.0 - _WarpDiffuse);
    o.diff = NdotL * _LightColor0;

    o.ambient = ShadeSH9(fixed4(worldNormal,1));

    return o;
}

The fragment shader just samples the noise texture, applies the value to the _Color, and computes the lighting in the standard way (computes the final lighting by attenuating the diffuse color with built-in SHADOW_ATTENUATION marco attenuation value and adds the ambient color). We added two variables to tweak the shader:

  • _MinNoise: ensure that the noise value is in the range [_MinNoise, 1.0], interpolating linearly the original noise value.
  • _WhiteThreshold: if the noise is at least _WhiteThreshold make it full white (before lighting) to simulate shiny grains.
fixed4 frag(VertexOutput i) : SV_Target {
    fixed noise = tex2D(_NoiseTex, i.uv).r;
    noise = lerp(_MinNoise, 1.0, noise);
    fixed w = step(_WhiteThreshold, noise);
    fixed4 color = (_Color * noise) + w;

    fixed shadow = SHADOW_ATTENUATION(i);
    fixed3 lighting = i.diff * shadow + i.ambient;

    color.rgb *= lighting;
    return color;
}

This is the final look:

Desert Final ResultDesert Final Result

Final thoughts

We are quite happy with the result and hope you find it useful. It's a solid starting point for more realistic results but good enough for Perpetual Wizard. Don't hesitate to leave a comment below or to contact us directly.

comments powered by Disqus

Get in touch

hoot@wiseshards.com

Sign up for our newsletter to hear about our projects and secrets!

Sign me up