Graphs
visualizing data

Introduction
- 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
StartandUpdateUnity 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.
This tutorial is an updated version of the original Graphs tutorial. It uses the Shuriken particle system, introduced in Unity 3.5.
Preparations
Now we create a cube via GameObject / Create Other / Cube 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 GameObject / Align With View (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.
Creating the first graph
Rename the particle system object to Graph 1,
create a C# script named Grapher1 as a minimal GameObject class, and
drag it to our object so it gets added as a component.
using UnityEngine;
public class Grapher1 : MonoBehaviour {}
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];
}
}
Besides the position, we can also use color to provide the same information. We'll make the red color component of the points equals 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.
using UnityEngine;
public class Grapher1 : MonoBehaviour {
public int resolution = 10;
private ParticleSystem.Particle[] points;
void Start () {
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;
}
}
}
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 do this in the Update method.using UnityEngine;
public class Grapher1 : MonoBehaviour {
public int resolution = 10;
private ParticleSystem.Particle[] points;
void Start () { … }
void Update () {
particleSystem.SetParticles(points, points.Length);
}
}
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 () {
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);
}
}
using UnityEngine;
public class Grapher1 : MonoBehaviour {
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
void Start () { … }
private void CreatePoints () {
if(resolution < 2){
resolution = 2;
}
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 () { … }
}
for loop, which
we'll execute each update. (The code snippet below only contains the Update method,
everything else stays the same.) 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);
}
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);
}
Showing multiple graphs
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){
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;
}
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);
}
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.
using UnityEngine;
public class Grapher1 : MonoBehaviour {
public enum FunctionOption {
Linear,
Exponential,
Parabola,
Sine
}
public FunctionOption function;
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
…
}
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.
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;
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
void Start () { … }
private void CreatePoints () { … }
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);
}
…
}
Although we need to 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;
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
void Start () {
CreatePoints();
}
private void CreatePoints () {
if(resolution < 2){
resolution = 2;
}
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();
}
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
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.
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 () {
if(resolution < 2){
resolution = 2;
}
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;
}
}
}
Update method currently only loops over resolution points, while it should
loop over all of them. void Update () {
if(currentResolution != resolution){
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);
}
private void CreatePoints () {
if(resolution < 2){
resolution = 2;
}
else if(resolution > 100){
resolution = 100;
}
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;
}
}
}

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);
delegate is called.
void Update () {
if(currentResolution != resolution){
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);
}
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);
}
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;
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
void Start () {
CreatePoints();
}
private void CreatePoints () {
if(resolution < 2){
resolution = 2;
}
else if(resolution > 100){
resolution = 100;
}
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){
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){
float squareRadius = (p.x - 0.5f) * (p.x - 0.5f) + (p.z - 0.5f) * (p.z - 0.5f);
return 0.5f + Mathf.Sin(15 * Mathf.PI * squareRadius - 2f * t) / (2f + 100f * squareRadius);
}
}
Full-blown 3D
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.
Grapher3. First, we'll limit resolution
to 40, which translates to 27000 points. We also need to initialize the Y position of the points and
the green color component.
private void CreatePoints () {
if(resolution < 2){
resolution = 2;
}
else if(resolution > 30){
resolution = 30;
}
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;
}
}
}
}
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){
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);
}
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);
}
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 = 2f * p.x - 1f;
p.z = 2f * 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){
float squareRadius =
(p.x - 0.5f) * (p.x - 0.5f) +
(p.y - 0.5f) * (p.y - 0.5f) +
(p.z - 0.5f) * (p.z - 0.5f);
return Mathf.Sin(4 * Mathf.PI * squareRadius - 2f * t);
}
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;
}
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 bool absolute;
public float threshold = 0.5f;
public FunctionOption function;
public int resolution = 10;
private int currentResolution;
private ParticleSystem.Particle[] points;
void Start () {
CreatePoints();
}
private void CreatePoints () {
if(resolution < 2){
resolution = 2;
}
else if(resolution > 30){
resolution = 30;
}
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){
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 = 2f * p.x - 1f;
p.z = 2f * 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){
float squareRadius =
(p.x - 0.5f) * (p.x - 0.5f) +
(p.y - 0.5f) * (p.y - 0.5f) +
(p.z - 0.5f) * (p.z - 0.5f);
return Mathf.Sin(4 * Mathf.PI * squareRadius - 2f * t);
}
}
Downloads
- graphs.unitypackage
- The finished project.