Procedural Planet

Author: Luca Ruiters (ru1t3rl)

Modelling beautifully coloured planets with a nice atmosphere could take quite some time. But what if we can procedurally generate and create lots of planets that look great from far away and while being on the planet. In this article, I will take you on my journey of making these effects. The document’s structure is built with multiple small(er) dev-logs.

Table of Content

  1. Introduction
    1. Requirements
  2. Mesh Generation
    1. Sphere Type
      1. Development
      2. Result
    2. Noise
      1. Development
      2. Result
  3. Shaders
    1. Research
    2. Development
      1. Colours and tessellation
      2. Atmosphere
  4. Result
  5. Future

1. Introduction

Generating earth-like planets is a lot of fun and will save some time for the artist. But what do we need to generate an Earth-like planet? Looking at the image on the right, we can clearly see some characteristics of planet Earth. For example, the different biomes. You can see that the closer you get to the north and south people the area changes from dirt/desert-like to, forests and eventually a cold north and the South Pole. Besides this, the height of a certain position will also determine the terrain.

Another important characteristic is the blue atmosphere, which slowly fades away when it gets darker. I will go into more detail about this in the Shaders chapter.

Each chapter has been split into four parts. A short introduction, the research needed to implement that feature, the actual development and of course finally the result.

1.1. Requirements

Based on the introduction, the requirements are very straightforward. The mesh generation should contain settings for the number of vertices and the shape. To make the planet more attractive we also need a field for the planet colours, and different atmosphere parameters (colour, density fall-off and scatter strength). As the result, we should get a nice-looking planet which attracts the player and has earth characteristics.

2. Mesh Generation

Of course, the most important part of creating planets is the mesh/shape of the object. So let’s start by generating the mesh based on noise. For the mesh generation, we should be able to adjust the number of vertices. As the designer, you should be able to modify noise layers and combine them at the end of the process using different blend methods.

2.1. Sphere Type

Before I start generating the planet mesh. I will have to decide what kind of sphere to use. This could be an uv-sphere, normalized cube, spherified cube or an Icosahedron. Based on some examples and pros and cons, I decided to go with the spherified cube. At the bottom of the source (Cajaraville, 2019) there are multiple examples. One of the main reasons was that you can easily tweak the detail of one region of the sphere; when using a spherified cube. Why not use a normalized cube, you would think. This has to do with the size of the faces when getting close to an edge as visible in the image below.

2.1.1. Development

The code for generating the mesh is almost the for both the normalized and spherified cube; since we are basically first generating the cube and afterwards translate the vertex. The code snippet generates a basic cube based on a resolution value. When the resolution is equal to 4, the actual number of vertices of a single face is 4×4.

axisA = new Vectori(localUp.y, localUp.z, localUp.x);
axisB = Vectori.Cross(localUp, axisA);

for (int y = 0, i = 0; y < resolution; y++)
{
    for (int x = 0; x < resolution; x++, i++)
    {
        Vector2 percent = new Vector2(x, y) / (resolution - 1);
        Vectori pointOnUnitCube = localUp + axisA * (percent.x - .5f) * 2 + axisB * (percent.y - .5f) * 2;
        vertices[i] = pointOnUnitCube;

        if (x != resolution - 1 && y != resolution - 1)
        {
            triangles[triIndex] = i;
            triangles[triIndex + 1] = i + resolution + 1;
            triangles[triIndex + 2] = i + resolution;

            triangles[triIndex + 3] = i;
            triangles[triIndex + 4] = i + 1;
            triangles[triIndex + 5] = i + resolution + 1;
            triIndex += 6;
        }

        float offsetX = ((localUp.x + localUp.z) + 1) * (1 / 3);
        float offsetY = localUp.y / 2;

        uvs[i] = new Vector2(
            (x / resolution) / 3f + offsetX,
            (y / resolution) / 2f + offsetY
        );
    }
}

To transform the cube into a normalized sphere, we have to add one line.

// When creating a normalized sphere 
Vectori pointOnUnitSphere = pointOnUnitCube.normalized;

// For creating a spherified cube (p2 = p*p)
// Based on the math/psuedo code from https://medium.com/@oscarsc/four-ways-to-create-a-mesh-for-a-sphere-d7956b825db4
Vectori pointOnUnitSphere = Vectori.zero;
pointOnUnitSphere.x = p.x * Mathf.Sqrt(1f - p2.y * .5f - p2.z * .5f + p2.y * p2.z / 3f);
pointOnUnitSphere.y = p.y * Mathf.Sqrt(1f - p2.x * .5f - p2.z * .5f + p2.z * p2.x / 3f);
pointOnUnitSphere.z = p.z * Mathf.Sqrt(1f - p2.x * .5f - p2.y * .5f + p2.x * p2.y / 3f);

2.1.2. Result

The result of both algorithms is almost equal. The benefit of the normalized sphere is a more equal size of all faces. Which can be seen in the image below. The left sphere is the normalized one, and on the right, we have the spherified cube.

2.2. Noise

While doing some research, I came across multiple types of noise (i.e. Perlin noise, Simplex, Value & Voronoi). Simplex is a newer version of Perlin noise and is often used when working with 3D models. Before I knew this, I already started working on a Perlin noise script. The result was great, but I had a hard time applying this 2D noise to a 3D model. Eventually, I decided to switch to Simplex and used a script from libnoise-dotnet. Which is based on the example code of this paper (Gustavson, 2005).

I used the two sources for my old Perlin noise method (Pêcheux, 2021)(V, 2020). My Old script.

2.2.1. Development

The implementation of this noise wasn’t that difficult. I created a class Called NoiseSettings, which contains values like seed, strength & persistence.

public class NoiseSettings
{
    public string seed = string.Empty;
    public float strength = 1;
    [Range(1, 8)]
    public int numLayers = 1;
    public float baseRoughness = 1;
    public float roughness = 2;
    public float persistence = .5f;
    public Vector3 centre;
    public float minValue = .1f;
}

The noise settings will be used by the noise filter, which contains the settings and noise itself. The noise filter has functions to generate a random alphanumeric seed and a method to evaluate a vertex on the noise.

public int GenerateRandomSeed(int length = 12)
{
    Random rnd;

    string seed = string.Empty;
    for (int i = 0; i < length; i++)
    {
        rnd = new Random(seed.GetHashCode());
        seed += ALPHANUMERIC[rnd.Next(0, ALPHANUMERIC.Length)];
    }

    return seed.GetHashCode();
}

public float Evaluate(Vector3 point)
{
    float noiseValue = 0;
    float frequency = settings.baseRoughness;
    float amplitude = 1;

    // Changing number of layers will change the detail of the noise
    for (int i = 0; i < settings.numLayers; i++)
    {
        float v = noise.Evaluate(point * frequency + settings.centre);
        noiseValue += (v + 1) * .5f * amplitude;
        frequency *= settings.roughness;
        amplitude *= settings.persistence;
    }

    return noiseValue * settings.strength;
}

2.2.2. Result

In the image below, you can see two planets. One of them uses the normalized cube method and the other one is the spherified cube. During this process, it also applied multiple layers of our noise.

3. The Shader

Besides having nicely shaped planets, it would be nice to give them some colour as well. To do so, we will use a shader which will automatically assign colours. I will go further into this in the “Research” chapter.

And as the finishing touch, I will also write an atmospheric scattering shader to give it that extra earth like feeling. This will both be visible on the planet and from a distance. The atmosphere will also tackle my last requirement, where the player has to feel like he’s standing on a planet and not a sphere.

3.1. Research

When looking at most earth-like planets, you can see a common situation where the soil is dependent on height. For example, below sea level (0 km), you will mostly find water. When you go up into the mountains, you will see most visible surfaces are made of rock and when you go even higher you will find snow. Based on this concept will also create the planet shader. To make the experience a bit smoother, I’m going to implement tessellation. For this, we will use unity’s documentation and mainly focus on Phong based tessellation; mainly since this will smooth the planet surface without losing too much performance.

Next to nice height based colours, most planets also have an atmosphere, which definitely helps improve the look of a planet. With the help of a research paper (O’Neil, 2005). And a video by Sebastian Lague, I gathered all the technical information needed to make the atmosphere. The technique we will use is called raymarching. This method will simulate atmospheric scattering, by looping over multiple scatter points and for each scatter point calculating the transmittance (or the more scientific term, optical depth).

3.2. Development

To sample the correct colours on the planet, we only need to keep track of the maximum and minimum height of the vertices. This will be done while generating the faces or chunks and afterwards assigning these values to the material.

Result of the gradient maps

In the planet shader, we will then check the distance of a vertex from the centre and calculate the height as a percentage to use with our gradient (texture).

The same method will be used for the smoothness and metallic. The gradient maps can be generated in the inspector and don’t have to be saved as an image/asset. As the designer, you are able to set the size of the gradient map and the number of steps used for processing the gradient.

To make this last step of processing the gradient easier, I have written an extension function for the Gradient data type.

public static Texture2D ToTexture2D(this Gradient g, float stepCount, Vector2Int size)
{
    Texture2D texture = new Texture2D(size.x, size.y);
    Color value;
    for (int i = 0; i  value).ToArray());
    }
    texture.Apply();
    return texture;
}

3.2.1 Atmosphere

To get a nice-looking atmosphere, we will use a method called Ray Marching. We will use an article by Nvidia about Atmospheric Scattering to get to the final result.

I could implement the atmosphere with different methods. For example, I could make an image effect (post-processing) or an unlit shader which could be applied to a cube with inverted face normals.

To become more experienced with image effects, I decided to go with that one. To get started, we first need the basics of ray marching. We need to loop to N scatter points for and through X Optical Depth Points. The scatter points will determine the final colour at a certain point. The optical depth points will mainly be used for the shading.

The only problem is that the current method is pretty expensive to render. This has to do with a double for loop. For each scatter point, the shader will also calculate all the depth points. So for example with 10 scatter points and 100 optical depth points (which runs fine) we make 1000 calculations for each vertex/pixel. A fix for this could be to bake the optical depth into a map and read the pixel’s value for the optical depth.

4. Result

In the images below, you can see the final result. While I’m very satisfied with my current progress/result. There are definitely points of improvement.

Project’s GitHub

The requirements I set were:

  • A “Solar” system with Earth-like planets
  • The user should be able to walk on the planets
  • When on a planet, it should look like you’re on earth and not just a random sphere.

The first and last requirements have definitely been accomplished. The atmosphere helps to make the sphere more like a planet, than just another boring sphere.
The second bullet point, on the other hand, is a completely different story. No, the player isn’t able to walk on the planet. But while flying through the galaxy, the user can fly across the surface of the planet and get the same visual when walking on the planet. Besides, this walking on the planet wouldn’t be that interesting at the moment since there isn’t any vegetation.

In the next chapter, I will go further into this and possible solutions to make it more interesting and how I could improve the performance. I would have loved to improve the performance for this project, but sadly I ran out of time and only implemented a base system for chunks; which actually broke my implementation of tessellation.

5. Future

If there was more time to work on this project. I would have loved to improve the performance. This could be done by moving the mesh generation into a compute shader and adding baking for the optical depth points of the atmosphere.

Besides improving the performance, add some more gameplay elements like challenges/missions (e.g. a certain planet you have to visit). One of the challenges could be, that the player has to travel from his home planet to other planets and make them more lively by growing/placing foliage from his home planet. This immediately fixes the problem of the boring planets.

Before I can implement the last example, I will have to implement on planet movement, which shouldn’t be that hard and add a lot of possibilities for new challenges.

References

Cajaraville, O. S. (2019, May 19), Medium – Four Ways to Create a Mesh for a Sphere

Flick, J., Cat Like Coding

Pêcheux, M. (2021, July 29). Making a seamless Perlin noise in C# – Nerd For Tech. Medium.

V., & V. (2020, July 24). Perlin noise. Code 2D.

Gustavson, S (2005, March 22). Simplex noise demystified

Chapter 16. Accurate Atmospheric Scattering. (2005). NVIDIA Developer.

Why is the sky not purple? (2012, 24 mei). Physics Stack Exchange.

Related Posts