Graphs, visualizing data

In this tutorial we'll write some C# scripts to display increasingly complex graphs in Unity 4. You'll learn to
• create graphs, from a single line to animated volumes;
• control a particle system;
• write various mathematical functions;
• change behavior while in play mode;
• use the `Start` and `Update` Unity event methods;
• write loops, both single and nested;
• use arrays, enumerations, and delegates;

You're assumed to know your way around Unity's editor and know the basics of creating C# scripts. If you've completed the Clock tutorial you're good to go.

Note that I will often omit chunks of code that have remained the same. The context of the new code should be clear.

This tutorial has been superseded by Building a Graph.

Preparations

We start by opening a new project without any packages. We'll be creating our graphs inside a unit cube, placed between (0, 0, 0) and (1, 1, 1). Let's set up the editor so we get a good view of this area. The is a handy predefined view configuration so let's select that. It's in or in the dropdown list at the top right of the screen. Set the view mode of all of them to . Also rotate the perspective view so that all three axes are pointing towards you.

Now we create a cube via and set its position to (0.5, 0.5, 0.5). This gives us a reference point for calibrating our views. Now zoom and pan the views so they're focused on the unit cube.

Finally, select the Main Camera and make it match the perpective view via (we did the opposite in the Clock tutorial). If that doesn't work right, make sure the correct view is active by clicking in it and then try again.

The cube is no longer needed, so we remove it. We then create a particle system via and reset its transform. Right now it's producing random particles, which we don't want. So we deactivate everything except its renderer.

Uncheck Looping, Play On Awake, Emission, and Shape. This leaves us with an inert particle system that we can use to visualize graph data.

Creating the first graph

We begin with a simple line graph where the value of Y depends on the value of X. We'll use the positions of the particles to visualize this.

Rename the particle system object to Graph 1, create a C# script named Grapher1 as a minimal `GameObject` class, and add it as a component to our object.

```using UnityEngine;

public class Grapher1 : MonoBehaviour {}```
The first thing we need to do is create some particles that will serve as the points of our graph. We'll create them inside the special `Start` method, which is a Unity event method that is called once before updates start happening.

How much particles should we use? The more particles, the higher the sample resolution of our graph. Let's make it customizable, with a default resolution of 10.

```using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private ParticleSystem.Particle[] points;

void Start () {
points = new ParticleSystem.Particle[resolution];
}
}```
Now we can set resolution to any number we want, which is a bit too generous. Technically it must be at least zero, and a very high resolution will result in very bad performance.

We can make sure that the variable is within bounds when we initialize the array. In case the resolution is out of bounds, we'll reset it to the minimum and log a warning message. Let's use a sane range of 10–100.

```	void Start () {
if (resolution < 10 || resolution > 100) {
Debug.LogWarning("Grapher resolution out of bounds, resetting to minimum.", this);
resolution = 10;
}
points = new ParticleSystem.Particle[resolution];
}```
Now that we have the points it's time to position them along the X axis. The first point should be placed at 0 and the last should be placed at 1. All other points should be placed in between. So the distance – or X increment – between two points is 1 / (resolution - 1).

Besides the position, we can also use color to provide the same information. We'll make the red color component of the points equal to their position along the X axis.

We'll use a `for` loop to iterate over all points and set both their position and color, which are `struct` values of type `Vector3` and `Color`. We also need to set the size of the particles, otherwise they won't show up. A size of 0.1 is fine.

```	void Start () {
if (resolution < 10 || resolution > 100) {
Debug.LogWarning("Grapher resolution out of bounds, resetting to minimum.", this);
resolution = 10;
}
points = new ParticleSystem.Particle[resolution];
float increment = 1f / (resolution - 1);
for (int i = 0; i < resolution; i++) {
float x = i * increment;
points[i].position = new Vector3(x, 0f, 0f);
points[i].color = new Color(x, 0f, 0f);
points[i].size = 0.1f;
}
}```
So far, it doesn't work. When playing, nothing shows up. That's because we need to feed our particles to the particle system. Conveniently, every component has a `particleSystem` property that we can use to access its particle system, if it has one. All we need to do is call its `SetParticles` methods, providing our array of particles and the amount of particles we wish it to use. Because we want the system to use all particles, we simply provide the array's length. We will add an `Update` method to do this every frame.
```	void Update () {
particleSystem.SetParticles(points, points.Length);
}```
That's it! We now get a nice black to red line of points along the X axis. How many points are shown depends on what we put in the resolution field before entering play mode.
Right now, resolution is only taken into account when the graph is initialized. Updating its value while in play mode doesn't do anything. Let's change that.

A simple way to detect a change of resolution is by storing it twice and then constantly checking whether both values are still the same. If at some point they're different, we need to rebuild the graph. We'll create a private variable currentResolution for this purpose.

Because rebuilding the points is the same as their first initialization, let's move that code into a new private method which we name `CreatePoints`. That way we can reuse the code.

```using UnityEngine;

public class Grapher1 : MonoBehaviour {

public int resolution = 10;

private int currentResolution;
private ParticleSystem.Particle[] points;

void Start () {
CreatePoints();
}

private void CreatePoints () {
if (resolution < 10 || resolution > 100) {
Debug.LogWarning("Grapher resolution out of bounds, resetting to minimum.", this);
resolution = 10;
}
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution];
float increment = 1f / (resolution - 1);
for(int i = 0; i < resolution; i++){
float x = i * increment;
points[i].position = new Vector3(x, 0f, 0f);
points[i].color = new Color(x, 0f, 0f);
points[i].size = 0.1f;
}
}

void Update () {
if (currentResolution != resolution) {
CreatePoints();
}
particleSystem.SetParticles(points, points.Length);
}
}```
Now the graph is recreated as soon as we change the value of resolution. However, you'll notice that the console will spit out warnings whenever resolution goes out of bounds, even while typing. We can make this experience better by using the `Range` attribute to tell the Unity editor to use a slider instead of a number box.

As we only care about valid editor input and won't change our resolution via code, we can now remove our own resolution check, though you may decide to keep it.

```	[Range(10, 100)]
public int resolution = 10;```
Now it's time to set the Y position of the points. Let's start simple and make Y equal to X. In other words, we're visualizing the mathematical equation y = x or the function f(x) = x. To do this, we need to loop over all points, get their position, use the X value to compute the Y value, then set their new position. Once again we use a `for` loop, which we'll execute each update.
```	void Update () {
if (currentResolution != resolution) {
CreatePoints();
}
for (int i = 0; i < resolution; i++) {
Vector3 p = points[i].position;
p.y = p.x;
points[i].position = p;
}
particleSystem.SetParticles(points, points.Length);
}```
The next step is to make the point's green color component the same as its Y position. As red plus green becomes yellow, this will cause the line to go from black to yellow.
```	void Update () {
if (currentResolution != resolution) {
CreatePoints();
}
for (int i = 0; i < resolution; i++) {
Vector3 p = points[i].position;
p.y = p.x;
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}```
You might have noticed that when you change the code and go back to Unity while you're still in play mode, you will be presented with `NullReferenceException` error messages. This is because our private points variable was not remembered by Unity when everything got reloaded.

We could solve this problem by checking whether points is `null` besides checking for a resolution change. This will allow us to stay in play mode all the time while editing our code, which is quite convenient. Note that this check also removes the need for the `Start` method, so we can delete it.

```using UnityEngine;

public class Grapher1 : MonoBehaviour {

[Range(10, 100)]
public int resolution = 10;

private int currentResolution;
private ParticleSystem.Particle[] points;

private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution];
float increment = 1f / (resolution - 1);
for(int i = 0; i < resolution; i++){
float x = i * increment;
points[i].position = new Vector3(x, 0f, 0f);
points[i].color = new Color(x, 0f, 0f);
points[i].size = 0.1f;
}
}

void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
for (int i = 0; i < resolution; i++) {
Vector3 p = points[i].position;
p.y = p.x;
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
}```

Showing multiple graphs

Just one graph is a little boring. It would be nice if we had multiple graphs to show. All that's needed is different ways to compute `p.y`, the rest of the code can stay the same. Let's make this explicit by extracting the code that computes `p.y` and put it in its own method, which we'll call `Linear`. All this method does is mimic the mathematical function f(x) = x. We're making this method `static` because it doesn't require an object to function. All it needs is an input value.
```	void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
for (int i = 0; i < resolution; i++) {
Vector3 p = points[i].position;
p.y = Linear(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}

private static float Linear (float x) {
return x;
}```
It's easy to add other mathematical functions by creating more methods and calling them instead of `Linear`. Let's add three new method. The first is `Exponential`, which calculates f(x) = x2. The second is `Parabola`, which calculates f(x) = (2x - 1)2. The third is `Sine`, which calculates f(x) = (sin(2πx) + 1) / 2.
```	private static float Exponential (float x) {
return x * x;
}

private static float Parabola (float x){
x = 2f * x - 1f;
return x * x;
}

private static float Sine (float x){
return 0.5f + 0.5f * Mathf.Sin(2 * Mathf.PI * x);
}```
Having to change the code each time we want to switch between these three options isn't very handy, even though we can remain in play mode. Let's create an enumeration type which contains an entry for each function we want to show. We call it `FunctionOption`, but because we define it inside our class it's officially known as `Grapher1.FunctionOption`.

Add a `public` variable named `function` of the new type. This gives us a nice field in the inspector for selecting functions.

```	public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine
}

public FunctionOption function;```
Selecting a function in the inspector is nice, but it doesn't do anything yet. Each update, we need to decide which method to call based on the value of function. There are various ways to do this and we'll use an array of delegates.

We first define a `delegate` type for methods that have a single `float` as both input and output, which corresponds to our function methods. We call it FunctionDelegate. Then we add a `static` array named functionDelegates and fill it with delegates to our methods, in the same order that we named them in our enumeration.

Now we can select the desired `delegate` from the array based on our function variable, by casting it to an integer. We store this `delegate` in a temporary variable and use it to calculate the value of Y.

```	private delegate float FunctionDelegate (float x);
private static FunctionDelegate[] functionDelegates = {
Linear,
Exponential,
Parabola,
Sine
};

void Update () {
if(currentResolution != resolution){
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
for(int i = 0; i < resolution; i++){
Vector3 p = points[i].position;
p.y = f(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}```
Finally, we can change which function is graphed while in play mode!

Although we need to recreate the graph each time we select another function, the rest of the time nothing really changes. So it's not required to compute the points each update. However, things change if we add time to the functions. As an example, let's change the `Sine` method so it calculates f(x) = (sin(2πx + Δ) + 1) / 2, where Δ is equal to the play time. This results in a slowly animating sine wave. Here's the entire script.

```using UnityEngine;

public class Grapher1 : MonoBehaviour {

public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine
}

private delegate float FunctionDelegate (float x);
private static FunctionDelegate[] functionDelegates = {
Linear,
Exponential,
Parabola,
Sine
};

public FunctionOption function;

[Range(10, 100)]
public int resolution = 10;

private int currentResolution;
private ParticleSystem.Particle[] points;

private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution];
float increment = 1f / (resolution - 1);
for (int i = 0; i < resolution; i++) {
float x = i * increment;
points[i].position = new Vector3(x, 0f, 0f);
points[i].color = new Color(x, 0f, 0f);
points[i].size = 0.1f;
}
}

void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
for (int i = 0; i < resolution; i++) {
Vector3 p = points[i].position;
p.y = f(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}

private static float Linear (float x) {
return x;
}

private static float Exponential (float x) {
return x * x;
}

private static float Parabola (float x){
x = 2f * x - 1f;
return x * x;
}

private static float Sine (float x){
return 0.5f + 0.5f * Mathf.Sin(2 * Mathf.PI * x + Time.timeSinceLevelLoad);
}
}```

Adding an extra dimension

Up to this points we're only using the X axis for input, and in one case time as well. Now we will make a new graph object that uses the Z axis too, thus producing a grid instead of a line.

Make sure that you're not in play mode. Create a new Unity object just like Graph 1 along with a new grapher script, calling them Graph 2 and Grapher2 instead. You can speed this up by duplicating them and then making the necessary changes. Disable Graph 1 by toggling the checkbox in front of its name field, because we're not using it anymore. Copy the code from `Grapher1` to `Grapher2`, only changing the `class` name to `Grapher2`. We'll modify the rest of the code in a moment.

The fastest way to do the above is to duplicate the script and edit its class name, then duplicate the object, rename it, and drag the new script on top of the old one.

To change the line into a square grid, we change the `CreatePoints` method of `Grapher2`. We need to create a lot more points and use a nested `for` loop to initialize them. We now set the Z position and the blue color component too.
```	private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution * resolution];
float increment = 1f / (resolution - 1);
int i = 0;
for (int x = 0; x < resolution; x++) {
for (int z = 0; z < resolution; z++) {
Vector3 p = new Vector3(x * increment, 0f, z * increment);
points[i].position = p;
points[i].color = new Color(p.x, 0f, p.z);
points[i++].size = 0.1f;
}
}
}```
Now we have a nice flat grid! But shouldn't it show the Linear function? It does, but currently only for the first row of points along the Z axis. If you select a different function, only these points will change while the rest remain as they are. This is because in the `Update` method currently only loops over `resolution` points, while it should loop over all of them.
```	void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
for (int i = 0; i < points.Length; i++) {
Vector3 p = points[i].position;
p.y = f(p.x);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}```
Now we can see our functions again, extended along to Z axis. However, there's something going on that's pretty weird. Try rotating the perspective view while displaying the parabola. From some angles, the graph is drawn wrong. This is because the particles are drawn in the order that we've created them, they don't take view direction into account. You can fix this by setting the Sort Mode of the particle system's Renderer module to By Distance instead of None. While this makes sure that the graph is shown correctly from all view angles, it results in a performance hit as well. So don't use it when you're displaying a huge amount of points. Fortunately, if we only look at the graphs from the correct direction, we can get away with not sorting at all.
Let's update our function code so we can take advantage of the new dimension. First change the input paramaters of `FunctionDelegate` to a vector and a float instead of just a single float. While we could specify the X and Z position separately, we'll simply give it the entire position vector. We'll also include the current time, instead of having to look it up inside the functions themselves.
`	private delegate float FunctionDelegate (Vector3 p, float t);`
Now we need to update the function methods accordingly and change how the `delegate` is called.
```	void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
float t = Time.timeSinceLevelLoad;
for (int i = 0; i < points.Length; i++) {
Vector3 p = points[i].position;
p.y = f(p, t);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}

private static float Linear (Vector3 p, float t) {
return p.x;
}

private static float Exponential (Vector3 p, float t) {
return p.x * p.x;
}

private static float Parabola (Vector3 p, float t){
p.x = 2f * p.x - 1f;
return p.x * p.x;
}

private static float Sine (Vector3 p, float t){
return 0.5f + 0.5f * Mathf.Sin(2 * Mathf.PI * p.x + t);
}```
We're ready to include Z in our mathematical functions! For example, change the Parabola function to f(x,z) = 1 - (2x - 1)2 × (2z - 1)2. We can also go wild with the Sine function, layering multiple sines to get a complex oscillating effect.
```	private static float Parabola (Vector3 p, float t){
p.x += p.x - 1f;
p.z += p.z - 1f;
return 1f - p.x * p.x * p.z * p.z;
}

private static float Sine (Vector3 p, float t){
return 0.50f +
0.25f * Mathf.Sin(4f * Mathf.PI * p.x + 4f * t) * Mathf.Sin(2f * Mathf.PI * p.z + t) +
0.10f * Mathf.Cos(3f * Mathf.PI * p.x + 5f * t) * Mathf.Cos(5f * Mathf.PI * p.z + 3f * t) +
0.15f * Mathf.Sin(Mathf.PI * p.x + 0.6f * t);
}```
Let's finish `Grapher2` by adding a Ripple function, which is a single sine wave emanating from the center of the grid. Here's the entire script.
```using UnityEngine;

public class Grapher2 : MonoBehaviour {

public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine,
Ripple
}

private delegate float FunctionDelegate (Vector3 p, float t);
private static FunctionDelegate[] functionDelegates = {
Linear,
Exponential,
Parabola,
Sine,
Ripple
};

public FunctionOption function;

[Range(10, 100)]
public int resolution = 10;

private int currentResolution;
private ParticleSystem.Particle[] points;

private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution * resolution];
float increment = 1f / (resolution - 1);
int i = 0;
for (int x = 0; x < resolution; x++) {
for (int z = 0; z < resolution; z++) {
Vector3 p = new Vector3(x * increment, 0f, z * increment);
points[i].position = p;
points[i].color = new Color(p.x, 0f, p.z);
points[i++].size = 0.1f;
}
}
}

void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
float t = Time.timeSinceLevelLoad;
for (int i = 0; i < points.Length; i++) {
Vector3 p = points[i].position;
p.y = f(p, t);
points[i].position = p;
Color c = points[i].color;
c.g = p.y;
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}

private static float Linear (Vector3 p, float t) {
return p.x;
}

private static float Exponential (Vector3 p, float t) {
return p.x * p.x;
}

private static float Parabola (Vector3 p, float t){
p.x = 2f * p.x - 1f;
p.z = 2f * p.z - 1f;
return 1f - p.x * p.x * p.z * p.z;
}

private static float Sine (Vector3 p, float t){
return 0.50f +
0.25f * Mathf.Sin(4 * Mathf.PI * p.x + 4 * t) * Mathf.Sin(2 * Mathf.PI * p.z + t) +
0.10f * Mathf.Cos(3 * Mathf.PI * p.x + 5 * t) * Mathf.Cos(5 * Mathf.PI * p.z + 3 * t) +
0.15f * Mathf.Sin(Mathf.PI * p.x + 0.6f * t);
}

private static float Ripple (Vector3 p, float t){
p.x -= 0.5f;
p.z -= 0.5f;
float squareRadius = p.x * p.x + p.z * p.z;
return 0.5f + Mathf.Sin(15f * Mathf.PI * squareRadius - 2f * t) / (2f + 100f * squareRadius);
}
}```

Full-blown 3D

It's time to add the third dimension! This will turn our graph from a grid into a cube, which we can use for volumetric representations. In other words, we'll create a tiny voxel system.

Duplicate Graph 2 and Grapher2 and change them into Graph 3 and Grapher3, just like we did for the second graph. Don't forget to disable Graph 2 and make sure that you're not in play mode.

We'll make a few changes to `Grapher3`. First, we'll limit `resolution` to 30, which translates to 27,000 points. Make sure you adjust the resolution slider so it's in range. If you created it from a duplicate graph set to a higher resolution, it will still have this value.

We also need to initialize the Y position of the points and the green color component.

```	[Range(10, 30)]
public int resolution = 10;

private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution * resolution * resolution];
float increment = 1f / (resolution - 1);
int i = 0;
for (int x = 0; x < resolution; x++) {
for (int z = 0; z < resolution; z++) {
for (int y = 0; y < resolution; y++) {
Vector3 p = new Vector3(x, y, z) * increment;
points[i].position = p;
points[i].color = new Color(p.x, p.y, p.z);
points[i++].size = 0.1f;
}
}
}
}```
Right now Graph 3 looks the same as Graph 2, except that it might seem a little more solid. This is because we still set the Y positions in the `Update` method. So all points with the same X and Z position will collapse to the same Y position. We must no longer set the Y position, but the color's alpha component instead. That way our functions will define the volume's density.
```	void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
float t = Time.timeSinceLevelLoad;
for (int i = 0; i < points.Length; i++) {
Color c = points[i].color;
c.a = f(points[i].position, t);
points[i].color = c;
}
particleSystem.SetParticles(points, points.Length);
}
```
Now our graph looks like a mostly solid cube. The functions aren't very visible, because they do not vary along the Y axis. Only the two animated functions, `Sine` and `Ripple`, produce somewhat interesting results.

Let's change the function computed by `Linear` into f(x,y,z) = 1 - x - y - z. That way it starts solid at (0, 0, 0) and fades to transparent along a straight line. We can do a similar thing with `Exponential` as well. Even better, let's animate them a bit so it's more interesting to look at.

```	private static float Linear (Vector3 p, float t) {
return 1f - p.x - p.y - p.z + 0.5f * Mathf.Sin(t);
}

private static float Exponential (Vector3 p, float t) {
return 1f - p.x * p.x - p.y * p.y - p.z * p.z + 0.5f * Mathf.Sin(t);
}```
Next, we update `Parabola` so it will produce a cylinder, once again with a little pulsating animation. We also add the third dimension to `Ripple`, turning it into a sphere-spawning animation.
```	private static float Parabola (Vector3 p, float t){
p.x += p.x - 1f;
p.z += p.z - 1f;
return 1f - p.x * p.x - p.z * p.z + 0.5f * Mathf.Sin(t);
}

private static float Ripple (Vector3 p, float t){
p.x -= 0.5f;
p.y -= 0.5f;
p.z -= 0.5f;
float squareRadius = p.x * p.x + p.y * p.y + p.z * p.z;
return Mathf.Sin(4f * Mathf.PI * squareRadius - 2f * t);
}
```
Finally, we'll update `Sine` too. We transform it into eight blobs by multiplying the square sines of X, Y, and Z together. We only animate the Z-based sine, but we make a distinction here. The top and bottom half of the graph will move in opposite directions.
```	private static float Sine (Vector3 p, float t){
float x = Mathf.Sin(2 * Mathf.PI * p.x);
float y = Mathf.Sin(2 * Mathf.PI * p.y);
float z = Mathf.Sin(2 * Mathf.PI * p.z + (p.y > 0.5f ? t : -t));
return x * x * y * y * z * z;
}```
A nice variant of our current graph would be one where all voxels are either fully visible or fully transparent. It would result in a solid but pixelated appearance. Let's add an `absolute` field to toggle such behaviour, along with a `threshold` field that determines how solid a voxel must be before it becomes visible. Each update, we check whether `asbolute` is on and use that to decide how to set the alpha of the points. Here's the complete script.
```using UnityEngine;

public class Grapher3 : MonoBehaviour {

public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine,
Ripple
}

private delegate float FunctionDelegate (Vector3 p, float t);
private static FunctionDelegate[] functionDelegates = {
Linear,
Exponential,
Parabola,
Sine,
Ripple
};

public FunctionOption function;
public bool absolute;
public float threshold = 0.5f;

[Range(10, 30)]
public int resolution = 10;

private int currentResolution;
private ParticleSystem.Particle[] points;

private void CreatePoints () {
currentResolution = resolution;
points = new ParticleSystem.Particle[resolution * resolution * resolution];
float increment = 1f / (resolution - 1);
int i = 0;
for (int x = 0; x < resolution; x++) {
for (int z = 0; z < resolution; z++) {
for (int y = 0; y < resolution; y++) {
Vector3 p = new Vector3(x, y, z) * increment;
points[i].position = p;
points[i].color = new Color(p.x, p.y, p.z);
points[i++].size = 0.1f;
}
}
}
}

void Update () {
if (currentResolution != resolution || points == null) {
CreatePoints();
}
FunctionDelegate f = functionDelegates[(int)function];
float t = Time.timeSinceLevelLoad;
if (absolute) {
for (int i = 0; i < points.Length; i++) {
Color c = points[i].color;
c.a = f(points[i].position, t) >= threshold ? 1f : 0f;
points[i].color = c;
}
}
else {
for (int i = 0; i < points.Length; i++) {
Color c = points[i].color;
c.a = f(points[i].position, t);
points[i].color = c;
}
}
particleSystem.SetParticles(points, points.Length);
}

private static float Linear (Vector3 p, float t) {
return 1f - p.x - p.y - p.z + 0.5f * Mathf.Sin(t);
}

private static float Exponential (Vector3 p, float t) {
return 1f - p.x * p.x - p.y * p.y - p.z * p.z + 0.5f * Mathf.Sin(t);
}

private static float Parabola (Vector3 p, float t){
p.x += p.x - 1f;
p.z += p.z - 1f;
return 1f - p.x * p.x - p.z * p.z + 0.5f * Mathf.Sin(t);
}

private static float Sine (Vector3 p, float t){
float x = Mathf.Sin(2 * Mathf.PI * p.x);
float y = Mathf.Sin(2 * Mathf.PI * p.y);
float z = Mathf.Sin(2 * Mathf.PI * p.z + (p.y > 0.5f ? t : -t));
return x * x * y * y * z * z;
}

private static float Ripple (Vector3 p, float t){
p.x -= 0.5f;
p.y -= 0.5f;
p.z -= 0.5f;
float squareRadius = p.x * p.x + p.y * p.y + p.z * p.z;
return Mathf.Sin(4f * Mathf.PI * squareRadius - 2f * t);
}
}```
We can now produce nice graphs from data with up to three dimensions! It's possible to create very intricate volumetric stuff, only your imagination and mathematical knowledge is the limit.

Enjoyed the tutorial? Help me make more by becoming a patron!

graphs.unitypackage
The finished project.

When is `Start` called exactly?
The `Start` method is called after the component is created, once it's active, and before the first time its `Update` method is called. It's only called once.
How do arrays work?
Arrays are objects of fixed length that contain a linear sequence of variables. When declaring a variable, putting square brackets behind its type indicates that you want an array of that type. So `int myVariable;` gets you an integer, while `int[] myVariable;` get you an array of integers.

Accessing one of the entries inside an array is done by putting its array index – not its position – between square brackets behind the variable. So `myVariable[0]` gets you the first entry in the array, `myVariable[1]` gets you the second, and so on.

Actually creating an array and assigning it to the variable is done with `myVariable = new int[10];` which in this case creates a new array with room for 10 entries. Alternatively, you can create one implicitly by listing its initial values between curly brackets, like `myVariable = {1, 2, 3};` does.

What's a ParticleSystem.Particle?
`ParticleSystem.Particle` is a struct type that is used to hold the data of a particle. There's a dot in the type name because it's actually a nested type. The `Particle` type has been defined inside the `ParticleSystem` type. Hence, to the outside world it is known as `ParticleSystem.Particle`.

Note that there is also a legacy `Particle` type outside of `ParticleSystem`, but this type is not used by Unity's Shuriken particle system.

What does `new` do?
The `new` keyword is used to construct a new instance of an object or a `struct` value. It's followed by calling a special constructor method, which has the same name as the `class` or `struct` it belongs to.
What does `Debug.LogWarning` do?
It is a static method from Unity's `Debug` class that allows you to write text entries to the console. You can use `Log`, `LogWarning`, or `LogError` to indicate the type of message. The first argument of these methods is the text to log. The optional second argument allows you to link the message to an object, which will be highlighted in the editor when you click the message.
What's `this`?
The `this` keyword refers to the current object or `struct` who's method is being called. It's being used implicitly all the time when referring to stuff from the same class. For example, whenever we've written `resolution` we could've also written `this.resolution`. You typically only use `this` when passing a reference to the object itself, like we did with `Debug.LogWarning`.
How does a `for` loop work?
A `for` loop is a compact way of writing a loop that iterates over something. In this case we use an integer named `i` as the iterator. The first part declares the iterator, the second part checks the loop's condition, and the third part increments the iterator.

You can use a `while` loop to get the exact same result, but it doesn't conveniently group the iterator code.

`for(int i = 0; i < 10; i++) { DoStuff(i); }`

is the same as

`int i = 0; while(i < 10) { DoStuff(i); i++; }`

What does `i++` do?
The `++` operation increments a variable by one. It can also be used inside an expression, in which case it counts as the old value if it's written behind the variable, or the new value if it's written in front of it. For example,

`int x = 0; int y = 10 + x++;`

results in an `x` of 1 and a `y` of 10, while

`int x = 0; int y = 10 + ++x;`

results in an `x` of 1 and a `y` of 11.

You can also use `--` in the same way for decrementing.

What's an attribute?
An attribute is a means to attach metadata to variables, methods, and classes. For example, you can tell Unity how to display a variable in the editor, whether to save or not save a variable, indicate that a component requires other components, and lots of other stuff.

Attributes are added between brackets in front of whatever they're attached to and can have arguments, like `[Nice] int number` and `[Nice(42)] int number`. Multiple attributes are separated by commas, like `[Nice, Sweet] int number`.

What's `null`?
The default value of a variable that's not a simple value is `null`. This means that the variable doesn't reference anything. Trying to invoke or access anything from a variable that is `null` results in an error. You can test for this value to make sure that doesn't happen. You can also set such a variable to `null` yourself, in case you no longer need whatever it was referencing.

Note that objects don't automatically cease to exist when setting a reference to them to `null`. Only when there's nobody left with a reference to them will they be become candidates for removal by the garbage collector.

Why not directly set `position.y`?
It would work if position were an object reference, but it is a `struct` value.

A unique object instance exists only once, but can be referenced from multiple variables. Assigning it to a variable doesn't copy it. It's like multiple people shouting someone's name and giving orders. If someone else learns this person's name as well, that won't suddenly duplicate this person.

Simple values like integers and `struct` instances aren't like this. If you ask for one then you get a duplicate that you have to remember yourself and can change independent of all the others. It's like the person's name, but shouting it won't get you any response.

So when we ask for the point's position, we get a duplicate. Changing that duplicate won't have any effect unless we copy it back to the point.

What does `return` do?
You use the `return` keyword to incidate that a method is finished and what its result is. What you return must match the type of the method. If it's a `void` method then you simply return nothing.

It's not needed to have a `return` statement at the end of a `void` or a special constructor method, for all other methods it's required.

It is possible to have multiple `return` statements inside a method. In that case there are multiple possible exit points. You'd typically use `if` statements to determine which `return` gets used.

Does it need to be `static`?
No, in this case the methods don't need to be. However, later on these methods will be used in a `static` array, which does require them to be `static` as well.

While in a lot of cases you can do without `static`, I consistently use it whenever the variable or method involved can exist on its own, indepenent of any object instances.

What's `Mathf`?
`Mathf` is a `struct` that contains a collection of `static` methods and values useful for mathematical operations with floats. It's a function library.
What's an `enum`?
You use `enum` to define an enumeration type, which is an ordered list of names. A variable of this type can have one of these names as its value. Each of these names corresponds to a number, by default starting at zero.

Enumerations are useful whenever you need a limited list of options.

What's a `delegate`?
If you can store values and object references in a variable, is it possible to store method references as well? Yes, it's known as a delegate.

You define a delegate type as if you're creating a method, except there's no code body. After that, you can use this type to create a delegate variable, to which you can assign any method that matches the type. You can then use this variable as if it were a method.

Delegates are actually more complex than this. They behave like lists and can be used for complex event handling, but we don't need that in this tutorial.

Why list the method names twice?
It's important to realize that we're writing the exact same words in completely different contexts.

Inside the `FunctionOption` enumeration, it's just a list of words. That they're the same as our method names is by design, but in no way required.

Inside the declaration of the `functionDelegates` array, the words are actual references to our methods. Here the names must match or it doesn't work.

What does `+=` do?
The code `x += y;` adds x and y together and assigns the result back to x. You can consider it a short alternative for the code `x = x + y;`

There are other operators that behave in a similar fashion, like `-=`, `*=`, and `/=`.

What does the question mark do?
The question mark, combined with a colon, is an in-line short version of an `if` statement. For example,

`int y; if(x > 1) { y = 10; } else { y = 20; }`

can be condensed into

`int y = x > 1 ? 10 : 20;`