Introduction
My goal for R&D was to create a planet generation system which the user can access from the Unity Editor.
- Planets can be created from a spherical mesh (which can be changed dynamically from the Editor), allowing the changing of the terrain (through vertex manipulation) in order to form all kinds of different landscapes (such as mountains/plains).
- Each planet also supports different kinds of shaders/effects in order to allow different types of biomes with additional effects to make them look more life-like (such as an ocean with a water effect).
- In order to keep the scope limited, all planets are meant to be used for background/decorative purposes and as such, do each support a different ‘Level of Detail (LOD)‘, but cannot be walked/landed on (such as in ‘Kerbal Space Program’).
- Once the user has finished the planets he/she wants to make, the created mesh and shaders can be exported/saved for use in other projects.
Meshes
The first part of the planet generation system we need to get working is the mesh, which needs to be scalable and supporting different levels of detail whilst still allowing for terrain manipulation.
For the creation of the initial mesh, I made use of a tutorial made by Sebastian Lague1. Because we’ll later need to take UV coordinates into account when we start wrapping the various shaders, it’s better to have a mesh consisting of seperate faces rather than one whole. Because of this, we make use of two different scripts: ‘TerrainFace’ (which handles the creation of each seperate face of the mesh) and ‘Planet’ (which is used to combine the various faces). Implementing these scripts gives us a mesh consisting of several seperate faces:

using UnityEngine;
public class TerrainFace
{
readonly Mesh mesh; // Mesh for terrain face
readonly int resolution; // Resolution ('Level of Detail') of terrain face
Vector3 localUp, axisA, axisB; // Local 'Up'-vector and axes
public TerrainFace(Mesh mesh, int resolution, Vector3 localUp)
{
this.mesh = mesh;
this.resolution = resolution;
this.localUp = localUp;
axisA = new Vector3(localUp.y, localUp.z, localUp.x);
axisB = Vector3.Cross(localUp, axisA);
}
// Constructs mesh of terrain face
public void ConstructMesh()
{
// Set up arrays for vertices and triangles
Vector3[] vertices = new Vector3[resolution * resolution],
int[] triangles = new int[(resolution - 1) * (resolution - 1) * 6];
int triIndex = 0;
// Set triangles based on resolution
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
int i = x + y * resolution;
Vector2 percent = new Vector2(x, y) / (resolution - 1);
Vector3 pointOnUnitCube = localUp + (percent.x - .5f) * 2 * axisA + (percent.y - .5f) * 2 * axisB;
Vector3 pointOnUnitSphere = pointOnUnitCube.normalized;
vertices[i] = pointOnUnitSphere;
normals[i] = vertices[i].normalized;
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;
}
}
}
// Clear previous mesh, assign vertices + triangles and set new normals
mesh.Clear();
mesh.vertices = vertices;
mesh.triangles = triangles;
mesh.RecalculateNormals();
}
}
using UnityEngine;
public class Planet : MonoBehaviour
{
// Resolution of planet
[Range(3,256)]
public int resolution = 10;
[SerializeField, HideInInspector]
MeshFilter[] meshFilters;
TerrainFace[] terrainFaces;
void OnValidate()
{
Initialize();
GenerateMesh();
}
// Initialize terrain faces and mesh filters
void Initialize()
{
if (meshFilters == null || meshFilters.Length == 0)
{
meshFilters = new MeshFilter[6];
}
terrainFaces = new TerrainFace[6];
Vector3[] directions =
{
Vector3.up,
Vector3.down,
Vector3.left,
Vector3.right,
Vector3.forward,
Vector3.back
};
for (int i = 0; i < 6; i++)
{
if (meshFilters[i] == null)
{
GameObject meshObj = new("mesh");
meshObj.transform.parent = transform;
meshObj.AddComponent<MeshRenderer>().sharedMaterial = new Material(Shader.Find("Standard"));
meshFilters[i] = meshObj.AddComponent<MeshFilter>();
meshFilters[i].sharedMesh = new Mesh();
}
terrainFaces[i] = new TerrainFace(meshFilters[i].sharedMesh, resolution, directions[i]);
}
}
// Construct mesh for each terrain face
void GenerateMesh()
{
foreach (TerrainFace face in terrainFaces)
{
face.ConstructMesh();
}
}
}
One minor change we still need to make is that the normals of the various faces do not match up along the seams, so these become noticeable once we add a light-source (as can be seen in the picture above). We can solve this problem by making a few simple changes to the code:

// Constructs mesh of terrain face
public void ConstructMesh()
{
// Set up arrays for vertices, normals and triangles
Vector3[] vertices = new Vector3[resolution * resolution],
normals = new Vector3[resolution * resolution];
int[] triangles = new int[(resolution - 1) * (resolution - 1) * 6];
int triIndex = 0;
// Set triangles based on resolution
for (int y = 0; y < resolution; y++)
{
for (int x = 0; x < resolution; x++)
{
int i = x + y * resolution;
Vector2 percent = new Vector2(x, y) / (resolution - 1);
Vector3 pointOnUnitCube = localUp + (percent.x - .5f) * 2 * axisA + (percent.y - .5f) * 2 * axisB;
Vector3 pointOnUnitSphere = pointOnUnitCube.normalized;
vertices[i] = pointOnUnitSphere;
normals[i] = vertices[i].normalized; // Assign normals by normalizing vertices
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;
}
}
}
// Clear previous mesh
mesh.Clear();
mesh.vertices = vertices;
mesh.normals = normals; // Assign new normals instead of recalculating
mesh.triangles = triangles;
}
}
Scaling
For scaling the planets we are creating, we need to make a few tiny changes used for the code constructing the mesh:
// Added variable for controlling the size of the planet
public float Radius;
public void ConstructMesh()
{
// Set triangles based on resolution
for (int y = 0; y < Resolution; y++)
{
for (int x = 0; x < Resolution; x++)
{
int i = x + y * Resolution;
Vector2 percent = new Vector2(x, y) / (Resolution - 1);
Vector3 pointOnUnitCube = localUp + (percent.x - .5f) * 2 * axisA + (percent.y - .5f) * 2 * axisB;
Vector3 pointOnUnitSphere = pointOnUnitCube.normalized;
vertices[i] = pointOnUnitSphere * Radius; // Multiply vertices by radius
normals[i] = vertices[i].normalized; // Assign normals by normalizing vertices
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;
}
}
}
//...
}
Level of Detail (LOD)
For changing the ‘Level of Detail’ (LOD), we make use of a variable named ‘resolution’, which controls the number of triangles present on each terrain face. To understand this better and see its effect more directly, we can enable the ‘Shaded Wireframe’-view in the Editor, giving us the following screenshot:

The user can change this variable by adjusting the ‘Resolution’-slider in the Inspector:

Decreasing the resolution of the planet to an extremely low value, such as ‘5’, gives us the following screenshot:

Allowing the user to change their planet’s LOD gives them additional planet customization, either for performance reasons or stylistic reasons (such as making the planet for a low-poly game).
Terrain Manipulation
For the editing of the terrain, we can make use of two different techniques: quad-trees or noise filtering. Aftering researching both ways of working, I found that noise filtering would be best suited for my own generation system, as I had no intention of creating planets with caves and/or destructable terrain (something which quad-trees would have been highly useful for).
First off, we make use of an open-source script for the noise. Since the planet contains multiple different layers of noise each with different settings, we combine all of these specific settings into a single class:
using UnityEngine;
[System.Serializable]
public class NoiseSettings
{
// Two types of filtering for noise: 'Simple' & 'Rigid'
public enum FilterType
{
Simple,
Rigid
};
public FilterType filterType;
[ConditionalHide("filterType", 0)]
public SimpleNoiseSettings simpleNoiseSettings;
[ConditionalHide("filterType", 1)]
public RidgidNoiseSettings ridgidNoiseSettings;
[System.Serializable]
public class SimpleNoiseSettings
{
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;
}
[System.Serializable]
public class RidgidNoiseSettings : SimpleNoiseSettings
{
public float weightMultiplier = .8f;
}
}
As can be seen, we make use of two types of noise filtering: ‘simple‘ & ‘rigid‘. Broadly speaking, ‘simple’ noise filtering is meant for softer landscapes which involve similar terrain (such as hills/plains), whilst ‘rigid’ noise filtering is meant for landscapes which much rougher topography (such as mountainous areas).
For both types of noise filtering, we create seperate ‘NoiseFilter’-classes, named accordingly to their filter type. Both of these are virtually identical, with the one major difference being the ways they evaluate their noise:
// Noise evaluation function for 'SimpleNoiseFilter'
public float Evaluate(Vector3 point)
{
float noiseValue = 0;
float frequency = settings.baseRoughness;
float amplitude = 1;
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;
}
noiseValue = Mathf.Max(0, noiseValue - settings.minValue);
return noiseValue * settings.strength;
}
// Noise evaluation function for 'RigidNoiseFilter'
public float Evaluate(Vector3 point)
{
float noiseValue = 0;
float frequency = settings.baseRoughness;
float amplitude = 1;
float weight = 1; // Additional value to control weight of noise
for (int i = 0; i < settings.numLayers; i++)
{
// Instead of using more rounded noise, we get the absolute value
float v = 1-Mathf.Abs(noise.Evaluate(point * frequency + settings.centre));
v *= v;
v *= weight;
// Unlike simple noise, we make use of extra weight multipliers for the weight of our noise
weight = Mathf.Clamp01(v * settings.weightMultiplier);
noiseValue += v * amplitude;
frequency *= settings.roughness;
amplitude *= settings.persistence;
}
noiseValue = Mathf.Max(0, noiseValue - settings.minValue);
return noiseValue * settings.strength;
}
Next up, we set up the noise filters our planet will use based on the values we’ve set in the Inspector:
using UnityEngine;
public class ShapeGenerator
{
ShapeSettings settings; // Settings for planet mesh
INoiseFilter[] noiseFilters; // List of noise filters
public MinMax elevationMinMax;
public void UpdateSettings(ShapeSettings settings)
{
this.settings = settings;
noiseFilters = new INoiseFilter[settings.noiseLayers.Length];
for (int i = 0; i < noiseFilters.Length; i++)
{
// Noise filters are initialized based on Inspector settings and 'Factory'-pattern
noiseFilters[i] = NoiseFilterFactory.CreateNoiseFilter(settings.noiseLayers[i].noiseSettings);
}
elevationMinMax = new MinMax();
}
//...
}

Adding various noise layers with different types and settings to our planet gives us the following result:

Shading
In order to properly shade our planet so we can add all kinds of different biomes/environments, we need to take several importants steps to make sure that we can not only add shaders to our planet with relative ease, but that these shaders are also easily supported on a variety of different devices. We can take care of the latter problem by making use of Unity’s ‘Universal RP’ (Render Pipeline)-package2, which would allow the user to export his/her planet(s) to devices with highly varied performance levels, from movile to high-end PCs.

In order to more easily let us change any graphical shader settings we wish to tweak in our project, it’s best to create a ‘Pipeline Asset’, a special kind of object which stores all of our pipeline settings. We then need to assign this object in the ‘Graphics’-tab of our project settings to make sure any changes we make have an effect on our project:

Next up, we need to create our planet shader and the material which will store this shader so we can actually apply it to our planet. What this package also provides us with is ‘Shader Graph’, a GUI-like editor for creating shaders (we would otherwise have to do this manually by writing the shader code ourselves using HLSL, which can become difficult for more complicated shaders). We start by creating a ‘Shader Graph Importer’-object and developing a shader which fits all the specific requirements needed for our planet generation system:

Once our shader has been created, we can then create our material which makes use of our ‘Planet’-shader:

The final step is to change our planets ‘Colour’-settings and provide some additional features that will allow us to customize the planet’s shader:


Now, we are almost ready to have the planet be shaded with all sorts of different colors. Despite this, we still need to make one final important change due to the nature of our planet generation system.
Texturing
Unlike a regularly shaded object in Unity, we need to able to have the planet’s shader change dynamically. This means that anytime the user makes a change in the Inspector, the planet’s entire texture needs to be changed instantly. This take can be accomplished by using the following ‘ColorGenerator’-script:
using UnityEngine;
public class ColorGenerator
{
// Attributes for our color settings, planet texture and resolution
ColourSettings settings;
Texture2D texture;
const int textureResolution = 50;
public void UpdateSettings(ColourSettings settings)
{
this.settings = settings;
// If no material has been assigned to our planet, simply assign a default texture with the specified resolution
if (texture == null)
{
texture = new Texture2D(textureResolution, 1);
}
}
// We update the planet's elevation if any changed is made
public void UpdateElevation(MinMax elevationMinMax)
{
settings.planetMaterial.SetVector("_elevationMinMax", new Vector4(elevationMinMax.Min, elevationMinMax.Max));
}
// Update the planet's colors if any settings are changed
public void UpdateColours()
{
// Create an array for the colors of all pixels with our specified resolution
Color[] colours = new Color[textureResolution];
// Fill this array of colors based on the values of our gradient
for (int i = 0; i < textureResolution; i++)
{
colours[i] = settings.gradient.Evaluate(i / (textureResolution - 1f));
}
// Set the pixels with the color values and apply the new texture
texture.SetPixels(colours);
texture.Apply();
settings.planetMaterial.SetTexture("_texture", texture);
}
}
Once we’ve added this code, we can start tweaking around with the new features. After some time spent changing all the different settings, we get the following result:

Lighting
Write about research and development of lighting shaders.
Effects
Write about research and development of environmental effects.
Unity Editor
GUI Editor Tools
With most of the work pertaining to the planet itself done, it seems like a good time to explain the way the Editor GUI for modifying the planet’s different values and attributes has been set up. We can do this by taking a look at the ‘PlanetEditor’-script, which handles these functions within the Inspector:
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(Planet))] // We specify here that this will be a custom editor of type 'Planet'
public class PlanetEditor : Editor
{
// We create variables for our planet mesh as well as our 'Shape' and 'Color'-settings objects
Planet planet;
Editor shapeEditor;
Editor colourEditor;
// This functions handly and user interactions with our custom GUI
public override void OnInspectorGUI()
{
using (var check = new EditorGUI.ChangeCheckScope())
{
base.OnInspectorGUI();
// If any value in our custom GUI has been altered, generate a new and/or changed planet
if (check.changed)
{
planet.GeneratePlanet();
}
}
// If the user presses the 'Generate Planet'-button, generator a new and/or changed planet
if (GUILayout.Button("Generate Planet"))
{
planet.GeneratePlanet();
}
// Draws sub-items in our custom GUI for our 'Shape' and 'Color'-settings objects
DrawSettingsEditor(planet.shapeSettings, planet.OnShapeSettingsUpdated, ref planet.shapeSettingsFoldout, ref shapeEditor);
DrawSettingsEditor(planet.colourSettings, planet.OnColourSettingsUpdated, ref planet.colourSettingsFoldout, ref colourEditor);
}
void DrawSettingsEditor(Object settings, System.Action onSettingsUpdated, ref bool foldout, ref Editor editor)
{
// Provides a null-check to make sure that the 'settings'-object has been assigned in the Inspector
if (settings != null)
{
foldout = EditorGUILayout.InspectorTitlebar(foldout, settings);
using var check = new EditorGUI.ChangeCheckScope();
// Checks to see if our created title-bar is folded out or not
if (foldout)
{
CreateCachedEditor(settings, null, ref editor);
editor.OnInspectorGUI();
// Update settings if any value has changed
if (check.changed)
{
onSettingsUpdated?.Invoke();
}
}
}
}
private void OnEnable()
{
planet = (Planet)target; // When this custom GUI-script is enabled, it will set our planet as its target
}
}

The reason sub-items are used for the planet settings is because this is more convenient for the user: instead of having all these variables stored in a single script (which would be much harder to change if the user wanted to quickly edit the planet’s shape or colour), we store these values seperately in so-called ‘settings objects’, which the user can quickly swap in and out of the ‘Planet’-script.
Exporting
In order to properly export our planet and make it sure can be used as any other model the user wants/needs for their game, we need to make sure that the planet is exported in a standardized format. Luckily, we can make use of the FBX-format (generally speaking the most common export format for models) for this. To access this format in Unity, we can make use of the ‘FBX Exporter’-package3 from the Unity Registry:

Once we have this package installed, we can export our planet (provided we first turn it into a prefab) as a .fbx-file:

This creates a .fbx-file of our planet in the project’s ‘Assets’-folder. We can grab our special material (needed for the shading and the effects) and apply it to our exported .fbx-model to see if it is applied properly:

The planet can now be used by the user in other projects. All that is required is the .fbx-model and the material needed for the planet (the material needed for the shader cannot be applied to the model directly, and must be imported with the .fbx-model).
Testing
Meshes
Testing the planet’s mesh (either via scaling or terrain manipulation) went pretty smoothly. Implementing the scaling of the planet was pretty easy and no real issues emerged during the project. Adding the terrain manipulation was a bit trickier, but also eventually succeeded. What I did notice was that it took a lot of serious practice in tweaking the noise settings in order to get a decent-looking landscape for the planet, which I found a little unexpected.
Shading
Shading the planet ended up being a pretty tricky experience, since my experience with ‘Shader Graph’ was very limited compared to other Unity tools. Despite this, I feel that I learned a lot about how to implement shaders in Unity and what kind of methods are necessary to get these working without issue.
Unity Editor
I picked up the implementation of the GUI Editor Tools largely from the tutorials I was using, but despite this it still took some time fully understanding all the different attributes and their function. Adding the exporting feature was much easier (largely because most of the functionality from the built-in package from Unity), but unfortunately I wasn’t able to get it to work exactly as I’d hoped (initally, my plan was to have the planet be exported with the material built-in, but because of the way the shader functioned, this turned out to be impossible).
Conclusions
Write down conclusions about R&D research here.
References
- Sebastian Lague, ‘Procedural Planet Generation’ (https://www.youtube.com/playlist?list=PLFt_AvWsXl0cONs3T0By4puYy6GM22ko8) ↩︎
- Unity Documentation, ‘Universal Render Pipeline’ (https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@17.0/manual/index.html) ↩︎
- Unity Documentation, ‘FBX Exporter’ (https://docs.unity3d.com/Packages/com.unity.formats.fbx@2.0/manual/index.html) ↩︎
