Building a High-Performance Voxel Engine in Unity: A Step-by-Step Guide Part 3: Noise

CodeComplex
6 min readNov 22, 2023

--

Noise is the algorithmic backbone that allows us to simulate the randomness and complexity found in natural terrains. From the jagged peaks of mountain ranges to the rolling dunes of desert landscapes, noise is the unsung hero of procedural generation.

In this instalment, we unpack the mysteries of noise and how it can be harnessed to sculpt worlds within Unity’s robust engine. We’ll provide a step-by-step guide on integrating noise algorithms to elevate your voxel terrain from flat, monotonous blocks to dynamic, organic formations. So, prepare your coding tools and let your creativity flow — we’re about to take your voxel landscapes to new heights.

Instead of rolling our own noise library we will just use an existing script download it here add it to your script folder. Let’s see what noise can do…

Remove the color property in the voxel struct and we will add the voxel type property…

public VoxelType type; // Using the VoxelType enum
public enum VoxelType
{
Air, // Represents empty space
Grass, // Represents grass block
Stone, // Represents stone block
// Add more types as needed
}

Imagine expanding on this when we get into biomes later, Sand blocks, Snow blocks or Water blocks. Also change the voxel function to reflect the changes…

public Voxel(Vector3 position, VoxelType type, bool isActive = true)
{
this.position = position;
this.type = type;
this.isActive = isActive;
}

In our iteration over the voxels we need some additional info…

private void InitializeVoxels()
{
for (int x = 0; x < chunkSize; x++)
{
for (int y = 0; y < chunkSize; y++)
{
for (int z = 0; z < chunkSize; z++)
{
// Use world coordinates for noise sampling
Vector3 worldPos = transform.position + new Vector3(x, y, z);
Voxel.VoxelType type = DetermineVoxelType(worldPos.x, worldPos.y, worldPos.z);
voxels[x, y, z] = new Voxel(worldPos, type, type != Voxel.VoxelType.Air);
}
}
}
}

At each point (x, y, z), we transform the local coordinates to world coordinates, then determines the type of voxel that should be at that position using the DetermineVoxelType() method we will look a next.

private Voxel.VoxelType DetermineVoxelType(float x, float y, float z)
{
float noiseValue = Noise.CalcPixel3D((int)x, (int)y, (int)z, 0.1f);

float threshold = 125f; // The threshold for determining solid/air

//Debug.Log(noiseValue);

if (noiseValue > threshold)
return Voxel.VoxelType.Grass; // Solid voxel
else
return Voxel.VoxelType.Air; // Air voxel
}
// Add this to top of the chunk script
using SimplexNoise;

So just to showcase the power of noise we use Noise.CalcPixel3D function which generates a noise value for the given 3D point. The noise function likely produces a pseudo-random value that has some spatial coherence — nearby points will have similar noise values. This value is dependent on the position of the voxel.

The threshold variable sets a cutoff value to decide if a voxel is solid or air. If the noise value is greater than this threshold, the voxel is considered solid (in this case, Grass). If the noise value is below the threshold, the voxel is considered air (empty space). Press Play…

If you like the fly through cam here just attach the script to the main camera, press ctrl to toggle the look feature, arrow keys or ASWD to move, mouse to look around…

using UnityEngine;

public class CameraFlyThrough : MonoBehaviour
{
public float moveSpeed = 5.0f;
public float turnSpeed = 60.0f;
public float pitchSensitivity = 2.0f;
public float yawSensitivity = 2.0f;

private float yaw = 0.0f;
private float pitch = 0.0f;
private bool isCursorLocked = true;

private void Start()
{
ToggleCursorState(); // Initial cursor state setup
}

void Update()
{
// Toggle cursor visibility and lock state when Control is pressed
if (Input.GetKeyDown(KeyCode.LeftControl) || Input.GetKeyDown(KeyCode.RightControl))
{
isCursorLocked = !isCursorLocked;
ToggleCursorState();
}

if (isCursorLocked)
{
// Get the mouse input only when the cursor is locked
yaw += yawSensitivity * Input.GetAxis("Mouse X");
pitch -= pitchSensitivity * Input.GetAxis("Mouse Y");

// Clamp the pitch rotation to prevent flipping
pitch = Mathf.Clamp(pitch, -89f, 89f);

// Apply the rotation to the camera
transform.eulerAngles = new Vector3(pitch, yaw, 0.0f);
}

// Move the camera based on arrow keys/WASD input
float x = Input.GetAxis("Horizontal") * moveSpeed * Time.deltaTime; // A/D or Left Arrow/Right Arrow
float z = Input.GetAxis("Vertical") * moveSpeed * Time.deltaTime; // W/S or Up Arrow/Down Arrow

// Apply the movement
transform.Translate(x, 0, z);
}

private void ToggleCursorState()
{
Cursor.lockState = isCursorLocked ? CursorLockMode.Locked : CursorLockMode.None;
Cursor.visible = !isCursorLocked;
}
}

Using 3D noise we can carve an elaborate cave system out of our voxels. Bringing this around now ultimately we need some smooth terrain to work with.

Add new vars in the World class…

public int noiseSeed = 1234;
public float maxHeight = 0.2f;
public float noiseScale = 0.015f;
public float[,] noiseArray;

We will use seeded noise so as to change or replicate terrain formations, the maxHeight will stop our noise going too high up, the scale will deal with the resolution of the noise and the noiseArray will house the map.

Change the protection level for World class chunkSize…

public int chunkSize = 16; // Assuming chunk size is 16x16x16

Add a new script, call it GlobalNoise, remove the MonoBehaviour, add the “using SimplexNoise” at the top of the script and make it a static class. Add the GetNoise() function we will use it to pull back 2d noise, as we will be using the noise as heightmap…

using SimplexNoise;

public static class GlobalNoise {

public static float[,] GetNoise() {
Noise.Seed = World.Instance.noiseSeed;
// The number of points to generate in the 1st and 2nd dimension
int width = World.Instance.chunkSize * World.Instance.worldSize;
int height = World.Instance.chunkSize * World.Instance.worldSize;
// The scale of the noise. The greater the scale, the denser the noise gets
float scale = World.Instance.noiseScale;
float[,] noise = Noise.Calc2D(width, height, scale); // Returns an array containing 2D Simplex noise

return noise;
}
}

Call GetNoise() in the World class in the awake method…

noiseArray = GlobalNoise.GetNoise();

We also need a function to get the global noise value for a given coordinate, add this to the GlobalNoise class…

public static float GetGlobalNoiseValue(float globalX, float globalZ, float[,] globalNoiseMap)
{
// Convert global coordinates to noise map coordinates
int noiseMapX = (int)globalX % globalNoiseMap.GetLength(0);
int noiseMapZ = (int)globalZ % globalNoiseMap.GetLength(1);

// Ensure that the coordinates are within the bounds of the noise map
if (
noiseMapX >= 0 && noiseMapX < globalNoiseMap.GetLength(0) &&
noiseMapZ >= 0 && noiseMapZ < globalNoiseMap.GetLength(1))
{
return globalNoiseMap[noiseMapX, noiseMapZ];
}
else
{
return 0; // Default value if out of bounds
}
}

We pass the it the noise array and the global coordinates for the voxel and pull back the y value for that location to then say, anything above that y value is an air voxel and do not render.

Also have to change the DetermineVoxelType() method…

private Voxel.VoxelType DetermineVoxelType(float x, float y, float z)
{
float noiseValue = GlobalNoise.GetGlobalNoiseValue(x, z, World.Instance.noiseArray);

// Normalize noise value to [0, 1]
float normalizedNoiseValue = (noiseValue + 1) / 2;

// Calculate maxHeight
float maxHeight = normalizedNoiseValue * World.Instance.maxHeight;


if (y <= maxHeight)
return Voxel.VoxelType.Grass; // Solid voxel
else
return Voxel.VoxelType.Air; // Air voxel
}

So we get the noiseValue from the GetGlobalNoiseValue() we just added, normalize the noise to between 0 and 1, then multiply by the height to get our maxHeight we use to check against the y value and decide to render a grass block or air block, press play…

This gives us nice rolling hills and stepped terrain, already a good starting point for any voxel gamedev project.

If you want the shader graph recipe for the snowy mountain peaks…

We’ve seen how noise, a concept often associated with randomness and disorder, can be harnessed to sculpt terrains. Remember that the principles of noise generation are a gateway to endless possibilities. Whether it’s for gaming, simulation, or artistic expression, the techniques explored here lay down a foundation that invites innovation and creativity.

--

--

CodeComplex

Exploring the digital depths of coding and tech. Innovator at heart, sharing insights on voxel engine development and Unity. #CodeComplex 🌐💻🚀