Making (Procedural) Noise in Unity

This article was originally edited and published on the Log Rocket's Blog. This version is the original unedited author's version of the article. Access the Log Rocket's version here.

One of the most common ways of achieving visual variety is using procedural noise. Given their ability to add intricate details with low memory and manual costs, procedural noises are of great importance to the field of computer graphics, and their use is widespread throughout the industry.

In this article, we discuss two approaches for using procedural noises in Unity, as well as how they can achieve many results and outcomes. The first is a code approach to procedural noise based on the Unity Mathematics package. This method is ideal for controlling the noise parameters and their results in applications that are either not entirely visual or that will feed other algorithms, such as Prefab Spawners.

The second one is based on using Shader Graph Nodes to use procedural noise in shaders. This approach is more suitable for Unity users who are more familiar with visual coding and are looking for visual results using custom shaders. It is important to note that this approach requires the use of either the Universal Render Pipeline or the High-Definition Render Pipeline to enable the use of the Shader Graph tools.

Procedural Noise

Procedural Noise, or simply Noise, has been used in the industry for many processes, including clouds, waves, animation, and so on. In computer graphics, noise is a random, unstructured pattern without evident design. Procedural noise describes a process in which we can procedurally generate noise patterns given a set of inputs.

Notice that noise is described as an unstructured pattern and is usually sought for these characteristics. That is because the pattern-like feature allows us to convey detail and familiarity without presenting a straightforward design. Clouds, as mentioned before, have a clear visual identity but are not immediately clear about how their shapes are formed by just staring at them.

One of the most basic approaches to noise is White Noise: a random collection of values from a range in which every possible value has equal chances of appearing. 

In more accessible terms, generating White Noise is like filling in a list of values, in which we throw a die for each value. All values from a die have the same probability, implying that they have equal chances of appearing. The image below illustrates what a White Noise 2D image looks like:

White Noise example.

Its highly unpredictable behavior is both an advantage and a disadvantage of White Noise. It is advantageous because it can quickly add variation to our process with a very low clear structure (or no clear structure). White Noise is a good candidate if we want to portray dust or stars, for example.

But it is a disadvantage if we aim to model noise-like patterns like the ones cited previously. It is too chaotic to resemble clouds, for instance. For that, we resort to using other sources of noise.

In general, instead of referring to using noise directly, we use the term Noise Function, which is a process that results in a noise value. A White Noise function could be written as:

float whiteNoiseFunction(float minimalValue, float maximalValue) 
{
	return Random.Range(minimalValue, maximalValue);
}

Or, for a Unity-like noise function to generate a 2D point:

Vector2 white2DNoiseFunction(float minimalValue, float maximalValue) 
{
   return new Vector2(Random.Range(minimalValue, maximalValue), Random.Range(minimalValue, maximalValue));
}

Luckily, Unity already has many implementations for noise functions that we can use to generate our noise values.

Unity Default Noise Function

For starters, a Unity project has direct access to one of the more famous noise functions used in the industry: the Perlin Noise function. An example of what a Perlin Noise looks like can be seen below:

Perlin Noise example.

The Perlin Noise function does not generate random numbers for all its positions. Instead, it divides the space into cells, forming a grid. Random numbers are generated at the edges of the grid, and the values within the cells are interpolated according to their proximity to the closest edges. This process creates a wave-like pattern in which values increase and decrease across space.

The Perlin Noise function implementation can be easily expanded from 2D to 3D and so on (for whatever reason, you might need that).

We can access the Perlin Noise function in Unity directly from the Mathf library. It has the following signature:

public static float PerlinNoise(float x, float y);

Since the Perlin Noise function is based on a grid, its Unity implementation does not have a traditional implementation of one dimension (1D). However, that can be easily achieved by managing the inputs used. For example, the documentation suggests that simply using 0 as one of the arguments is an easy alternative to generating a 1D Perlin Noise value.

Due to being accessible by default, the Mathf implementation of the Perlin Noise function might be the most commonly seen noise function used in Unity tutorials. However, more noise function implementations can be accessed in Unity by importing the Mathematics package.

Unity Noise Functions with the Mathematics Package

Unity’s Mathematics package encompasses efficient vector types and math functions with a shader-like syntax. As of the writing of this article, the Mathematics library was still considered a work in progress and should be used with care.

The package can be downloaded and imported to your project using the Package Manager. Select the option for the packages under Unity Registry, then select the Mathematics package, as seen in the image below:

The Mathematics noise functions are available as methods of the base Noise class. The noise functions available can be divided into four main groups, which are:

  • Cellular Noise Functions: These are based on the Cellular/Worley Noise function. For example: float2 cellular2x2 (float2 p).
  • Perlin Noise Functions: These are based on the previously discussed Perlin noise function. For example: float cnoise (float2 p).
  • Simplex Noise Functions: These are similar approaches to Perlin Noise called Simplex Noise. For example: float snoise (float2 v).
  • Additional Simplex Noise Functions: These are extensions of the Simplex Noise functions presented at the library. For example: float3 psrdnoise (float2 pos, float2 per, float rot).

Except for the Perlin Noise function, which was already covered previously, let us take a better look at these other functions and their usage in code. But, before that, it is crucial to understand how we can convert and work with the different data types used by the Mathematics package and Unity’s default variable types.

Mathematic Data Types

The Mathematics package uses other data types than the regular ones used by Unity. For example, instead of using Vector2, it uses float2. The core difference between them is that the Mathematic datatypes are specially designed for better performance and, as stated before, to better match with shader-like programming languages.

Although it is unclear whether Unity will phase out the older data types completely, for now, using both types is valid (not necessarily recommended), and the conversion between types is straightforward due to their similar data structures.

For example, converting a float2 to a Vector2 works as simple as:

float2 f2 = new float2(2.0f, 3.0f);
Vector2 v2 = (Vector2) f2;

Cellular Noise Functions

As stated previously, these functions are based on the Worley Noise Function, which is based on the Perlin Noise Function. In simple terms, the Worley Noise Function generates random points across a surface. Then, it calculates the value of individual points on this surface by interpolating its value towards the closest random point. Due to this process, the resulting image resembles a cellular (hence the other name) structure. The image below provides an example of a resulting 2D image built by using the Worley Noise Function.

Worley Noise example.

There are many variations in the package’s cellular functions, which vary from dimensionality (either 2D or 3D) and efficiency (lesser precision for the sake of speed). The cellular function receives a float2 as a parameter for the x and y position and returns its corresponding noise value. The more performance-friendly version of this function is the cellular2x2 function that has the same signature and use.

From a basic test comparison, the cellular2x2 is about three times faster in execution compared to the cellular counterpart.

[The test was done by executing each noise function 100.000 using the same x and y parameters and measuring the total time spent.]

Simplex Noise Functions

Simplex Noise functions were initially developed by Ken Perlin, the same developer of the original Perlin Noise function. It is computationally more efficient than the Perlin Noise function and better suited for more dimensions (such as 4 and 5). Moreover, the Simplex Noise function has less noticeable visual artifacts when compared to the original function.

Simplex Noise example.

As for the others, many variations for the Simplex Noise function are available in the package, which varies in dimensionality (from 2D, 3D, and 4D). All the functions share the same name, snoise, and differ from the available parameters. The 2D call for a Simplex Noise function takes a float2 as a parameter for the x and y positions and returns the corresponding noise value.

Oddly enough, in a basic test comparing the Simplex Noise function (snoise) and the Mathematic package implementation of the Perlin Noise function (cnoise), the Perlin functions for 2D and 4D values were about 24% ad 42%, respectively, faster than the Simplex function, even though the Simplex method is supposed to be more efficient. Similar results were found for the other variations.

[The test was done by executing each noise function 100.000 using the same x and y parameters and measuring the total time spent.]

Extra Methods

The Mathematics library also contains other methods besides the ones we just discussed, which are variations by adding new parameters to control their results. For example, the library includes a psdrnoise function, which takes 3 parameters: a float2 for the position, another float2 for the tiling and a float value for the rotation. Due to their niche usage, this article will not focus further on them. If you want to read more about them, I suggest you check their source codes in the repository and read this thread at the Unity Forum.

Moreover, feel free to drop a comment if you want an article on the topic 😉

Common Noise Applications via Code

Accessing noise functions via code allows us to use their values directly in our functions instead of having them calculated into an intermediary structure (a texture, for example). Thus, these functions are also suitable substitutes for randomizers.

Let’s say, for example, that you want to distribute some prefabs in a scene (a Prefab Spawner); we could achieve it by using the following code:

public void SpawnPrefab(Vector3 position, float frequency, float radius)
{
   //Generates a random point within a given radius
   var randomPosition = Random.insideUnitSphere * radius;
   //Add the original position
   randomPosition.x += position.x;
   randomPosition.y += position.y;
   randomPosition.z += position.z;
   //Only instantiates the prefab if it is within a certain noise frequency. For this case, we are using the position itself as the noise function parameter.
   if (noise.snoise(position) <= frequency)
   {
       Instantiate(prefab, randomPosition, Quaternion.identity);
   }
}

Another helpful approach for noises via code is to procedurally generate textures through them. However, for that, we have a more accessible and flexible alternative.

Noise Through Shader Graphs

Shader Graph is a Unity tool that allows users to visually program shaders with a node-based system. It enables fast and visual iterations while designing shaders. Moreover, although it is node-based, Shader Graph has specific nodes that allow the user to write shader language as a text, string, or load shader HLSL code. 

To access the Shader Graph tool, your application needs to be in either the Universal Render Pipeline or the High Definition Render Pipeline. Shaders can be created by accessing the menus Asset > Create > Shader Graph.

Shaders can be edited in the Shader Graph window, which displays the parameters, previews, and outputs functionalities. This window is used to design the shader through connecting nodes. Due to its complexity, this article will not cover the Shader Graph tool itself, except for some of its functionalities related to working with the Noise nodes. For more information on how to get started with the Shader Graph tool, refer to this other article.

Noise Nodes

By default, the Shader Graph tool has three nodes for noises: the Gradient Noise, the Simple Noise, and the Voronoi Noise. The image below shows these nodes with their respective previews and default values.

Let us discuss them one by one and compare them to the noise functions discussed before:

  • Gradient Noise Node: According to the documentation, it is a Perlin Noise Function, although the implementation is more akin to a Simplex Noise Function.
  • Simple Noise Node: According to the documentation, the Simple Noise is a White Noisesque function, but its result is similar to a Simplex Noise Function due to its interpolation between nearby values.
  • Voronoi Noise Node: Voronoi is another name used to describe the Worley Noise Function or the Cellular Noise Function discussed before. Different from the others, this node has input values for the Angle Offset between its control points and for the Cell Density, which determines how many points are available.

Similarly to the Mathematic package, these noise functions receive coordinates as parameters. The main difference is that these nodes automatically assign the UV coordinate as input for the noise function coordinates, which can be easily changed to any other type of two-dimensional vector.

A common approach is combining these noises by performing simple mathematical operations, such as summing and multiplying their values. The image below shows the result of multiplying these three noises in sequence.

As stated at the end of the last section, we can use the Mathematics package to generate noise values via code and use them on our systems to control aspects. Alternatively, we use the Shader Graph tool to generate noise values via nodes and use them visually in our materials and other visual elements, such as particle systems and trail renderers

Indeed you can mix both of them and use them for each other’s purposes, but this approach might be counterproductive. I recommend you stick to the rule of thumb of “code for code, visuals for visuals.”

Typical Noise Applications via Shader Graph

Combining simple nodes is easy and fast to achieve many outcomes. For example, due to its interpolated values, both the Gradient Noise node and the Simple Noise node are good candidates to make a simple cloud shader:

Multiplying Simple Noises with varying scales is an excellent way to generate less unstructured patterns since low values create darker areas, forming shapes on the brighter areas. These shapes are still noisy enough to appear cloud-like. This result is input into a Step node which, given a value, returns 1 if it is higher than the input variable or 0 otherwise.

The following image shows an extension of this given graph for an animated version of the cloud pattern achieved by offsetting the UVs from both Simple Noise nodes in opposing directions:

The gif below shows the animated result:

Animating UVs

Although only partially related to this topic, a combination of nodes is often repeated for many Shader Graphs that use noises: animating the UVs by offsetting their values over time. 

We have seen its usage on the previous graph for a cloud-like pattern. Still, it deserves a brief explanation since it is commonly used in many other implementations of animated noise patterns.

The graph below shows the necessary nodes to achieve this:

We use the Offset parameter to move the UV by a given value in x and y. The offset value is calculated using the Time node, multiplied by an Intensity value which helps us control how fast/slow we want the movement.

The result is then used to multiply a Vector2 for which direction we want to offset the UV. If we want a horizontal movement, we use a Vector with x = 1 and y = 0, while a vertical movement would use a vector x = 0 and y = 1.

Other combinations are possible, but if the directional vector’s magnitude is not 1 (i.e., the directional vector is not unitary), it will affect the intensity and cause your movement to be either faster or slower than initially expected. To safeguard against this issue, you can use a Normalize node after the Multiply node and connect its output to the Tiling and Offset node.

For simplicity, the coming Shader Graphs will use this graph as a Sub Graph. This Sub Graph, named UVOffsetOverTime, takes two float parameters: Intensity and Direction. Its output is a Vector2 for the newly calculated UV. 

Furthermore, it is noticeable that this Sub Graph uses the default UV0 for the Tiling and Offset node. If that is not your (main) intention, you can also easily add a third parameter to pass the UV to the graph.

Water Shader Graph with Noise Nodes

Using these same ideas, we can quickly sketch other effects, such as a Water shader:

Water Shader in a plane.

In this case, we are also using the Time node to change Voronoi’s Angle Offset parameter. This way, the internal points for the Voronoi Noise function will also move, which creates a cellular-like motion characteristic of liquids such as water and lava surfaces.

On the other hand, to break the visual appearance of cells, the result of the Voronoi node is used in a Power node, which emphasizes higher values (closer to 1) and reduces lower values (closer to 0).

Lava Shader Graph with Noise Nodes

As an example of a shader that uses all three of these noise nodes for different functions, we can build a Lava shader such as this:

Lava Shader in a plane.

As you can see above, the Lava shader has considerably more nodes than our simple Water shader. Let us see it in parts to understand how the noise nodes were applied to achieve the final result.

We use the UVOffsetSubGraph twice, with opposing directional vectors, to control two Simple Noise nodes. The two Simple Noise nodes have different scales to create enough difference between them, as we will combine their results later, similar to what was done for the Clouds Shader Graph.

Moreover, to strengthen the noise values before multiplying them with a color, both results of the Noise Nodes are multiplied by 2. This process brightens up the result.

Both results are added together using the Add node. After combining them, we should already get a result resembling lava or a hot surface. To spicy it further, we will add a pulsating color to emphasize the lava movement and fiery intensity.

For a more varied noise texture, we multiply both actual noise results from the previous steps and use the result as the UV coordinates of a Voronoi Noise node. Mixing noise values and UV channels is a quick way of achieving a wide range of visuals.

Instead of using the resulting Voronoi directly, which would still be too similar to its inputs, we use the One Minus node to invert it, followed by a Saturate node that clamps the result between 0 and 1. The saturate node is often used to guarantee that the outcome of inversion operations and other functions do not yield negative numbers that might cause issues with the upcoming nodes in the graph.

To select only isles from the current noise texture, we use the Step node again to filter only the results higher than a given value. For the effect shown here, the value used was 0.33.

For the pulsating effect, we will oscillate the Angle Offset of the previously discussed Voronoi Node by combining the Time node and a Gradient Noise, as seen below.

Generally, a promising approach for pulsating values is to use the Sine or Cosine functions with the Time node. The Time node itself already contains outputs for both functions to save us some time. As we do not want a negative pulse, we plug the sine value into an Absolute node.

To break the monotony of a simple sine function, we use a 1D Gradient Noise approach by using the Time node output as a UV coordinate (thus, both x and y components are the same). 

Notice that even though we are using a 1D result for the Gradient Noise node, it is important to still consider the scale input parameter. A more significant value for the scale will cause more variation than a smaller one. In this scenario, we use a scale of 4 to achieve a smaller variation over time.

Moreover, the Gradient Noise node output is multiplied by a value to control its intensity in the final result. This example multiplies it by 0.3 to reduce its impact to only 30%. Indeed those values can be tweaked for a different desired outcome.

Finally, both the sine result and the noise are added up and used as the Angle Offset of the Voronoi Noise node.

We took two steps to combine the pulsating colors into our previous lava texture.

The first one is to remove the pulsating area from the previously generated texture. If we ignore this step, adding (or multiplying) the pulsating value on top of the current result might alter the colors and results too much, either tending the result to a bland white color or canceling opposing colors (for example, if we multiply a Green pulse with a Blue texture). 

An easy way to achieve the removal is to use again the One Minus node followed by a Saturate node, and multiply the result with the previous texture. That will make the pulsating area black and keep the non-pulsating area with its intended values. 

Then, for the second step, we multiply the pulsating texture with the desired color and, using the Add node, combine it with the last node, merging all of them.

As seen throughout this article, plenty of options enable noises and randomness in your Unity project. The standard math library and the Mathematics package allow various methods to use noise functions via code. At the same time, the Shader Graph tool provides three versatile noise function nodes for procedural generation.

Indeed, many other noise functions used by the industry are not entirely available through these methods, such as the Fractal Noise function and the Curl Noise Function. However, most of those can be programmed in a C# function to be used in code or implemented in HLSL language in the Shader Graph. In other words, although not all functions are presented, we have enough tools to implement them.

Finally, consider that another easy alternative to the lack of specific noise functions or patterns is the use of pre-made textures. The Lava Flowing Shader is a free asset in Unity’s asset store that achieves a good-looking lava material by combining multiple textures and shader programming. 

Consider that this discussion boils down to the usual conflict. At the same time, in developing applications, we can use more memory (textures) or more processing time (procedural noise functions) to achieve our results. It is up to us to decide what yields the best efficiency and workflow. Luckily enough, the noise functions presented have a minimal performance footprint.

Thanks for reading, and let me know if you would like more Unity strategies for fast development and prototyping.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: