Catlike Coding

Catlike Coding

Unity C# Tutorials

Constructing a Fractal

Details Through Recursion

  • Instantiate game objects.
  • Work with recursion.
  • Use coroutines.
  • Add randomness.

Fractals are intriguing and often beautiful. In this tutorial we'll write a small C# script that manifests some fractal-like behavior.

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 then you're good to go.

This is an old tutorial and will be upgraded, but it's still fun to do. Mentions of diffuse and specular materials no longer apply to Unity 2017, so you can ignore that.

You'll create randomized 3D fractals.

How to Make a Fractal

We are going to create a 3D fractal. We use the concept that the details of a fractal can look exactly like the whole thing. We can apply this to an object hierarchy in Unity. Start with some root object, then add children to it that are smaller but otherwise identical. Doing this by hand would be cumbersome, so we'll create a script to do it for us.

Start with a new project and a new scene. I put a directional light in there and moved the camera to a more interesting angle, but you can set it up as you please. I also went ahead and created a material to use for the fractal later. It simply uses the specular shader with default settings, which is just a bit more pleasing to look at than the default diffuse one.

Create a new empty game object and place it at the origin. This will be the base of our fractal. Then create a new C# script named Fractal and add it to the object.

using UnityEngine;
using System.Collections;

public class Fractal : MonoBehaviour {

}
Project setup.
unitypackage

Showing Something

What will our fractal look like? Let's make it configurable by adding a public mesh and material variable to our Fractal component script. We then insert a Start method in which we add a new MeshFilter component and a new MeshRenderer component. At the same time, we directly assign our mesh and material to them.

using UnityEngine;
using System.Collections;

public class Fractal : MonoBehaviour {
	
	public Mesh mesh;
	public Material material;

	private void Start () {
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = material;
	}
}

Now we can assign our custom material to the fractal component. Also assign a mesh by clicking on the dot next to the property and selecting Unity's default cube from the popup. After doing so, a cube will show up when we enter play mode.

Of course we could have also added the components manually, but we're creating a fractal here.

inspector in edit mode inspector in play mode cube in play mode
Components appear in play mode.
unitypackage

Making Children

How do we create the children of this fractal? The easiest way is to just go ahead and make one in Start by creating a new game object and adding a Fractal component to it. Try it, hit play, and quickly exit play mode again.

	private void Start () {
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = material;
		new GameObject("Fractal Child").AddComponent<Fractal>();
	}

The problem is that every new fractal instance will create yet another one. This happens each frame, without end. Let it run for a while and your computer will get into trouble as it runs out of memory. Typically recursive algorithms that don't stop will consume your machine's resources almost instantly and result in either a stack overflow exception or a crash. In this case it is a rather benign explosion, because it happens slowly.

To prevent this from happening, we introduce the concept of a maximum depth. Our initial fractal instance will have a depth of zero. It's child will have a depth of one. The child of this child will have a depth of 2. And so on, until the maximum depth is reached.

Add a public maxDepth integer variable and set it to 4 in the inspector. Also add a private depth integer. Then only create a new child if we are below the maximum depth.

What will happen when we enter play mode now?

	public int maxDepth;

	private int depth;

	private void Start () {
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = material;
		if (depth < maxDepth) {
			new GameObject("Fractal Child").AddComponent<Fractal>();
		}
	}
Maximum depth.

Exactly one child got created. Why? Because we never gave depth a value, it's always zero. Because zero is less than 4, our root fractal object created a child. The child's depth value is also zero. However, we never set the child's maxDepth either, so it is also zero. Therefore the child did not create another one.

Besides that, the child also lacks a material and a mesh. We need to copy these references from its parent. Let's add a new method that takes care of all the necessary initializations.

	private void Start () {
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = material;
		if (depth < maxDepth) {
			new GameObject("Fractal Child").
				AddComponent<Fractal>().Initialize(this);
		}
	}
	
	private void Initialize (Fractal parent) {
		mesh = parent.mesh;
		material = parent.material;
		maxDepth = parent.maxDepth;
		depth = parent.depth + 1;
	}

When we enter play mode this time, four children will be created, as was expected. But they're not really children, as they all appear in the hierarchy root. The parent–child relationship between game objects is defined by their transformation hierarchy. So a child needs to make the parent of it's transform component equal to it's fractal parent's transform.

	private void Initialize (Fractal parent) {
		mesh = parent.mesh;
		material = parent.material;
		maxDepth = parent.maxDepth;
		depth = parent.depth + 1;
		transform.parent = parent.transform;
	}
children without nesting children with nesting
Children, without and with nesting.
unitypackage

Shaping Children

So far the children are superimposed on their parent, which means we still only see a single box. We need to move them inside their local space so that they become visible. And because they are supposed to be smaller than their parent, we have to scale them down too.

First the scaling. How much? Let's make it configurable with a new variable named childScale and assign it a value of 0.5 in the inspector. Don't forget to pass this value from parent to child as well. Then use it to set the child's local scale.

Next, where shall we move the children? Let's simply move them straight up, so that they touch their parent. We assume that the parent has a size of one in all directions, which is true for the cube that we're using. Moving up by a half puts us at the point where parent and child should touch. So we have to move an additional amount equal to half the size of the child.

	public float childScale;

	private void Initialize (Fractal parent) {
		mesh = parent.mesh;
		material = parent.material;
		maxDepth = parent.maxDepth;
		depth = parent.depth + 1;
		childScale = parent.childScale;
		transform.parent = parent.transform;
		transform.localScale = Vector3.one * childScale;
		transform.localPosition = Vector3.up * (0.5f + 0.5f * childScale);
	}
inspector child scale of 0.5 child scale comparison
Child scale of 0.5, and from 0.3 to 0.7 for comparison.
unitypackage

Making Multiple Children

What we are creating now looks like a tower, but not really a fractal. We need it to branch, by creating multiple children per parent. It is easy to just create a second object, but it also has to grow in a different direction. So let's add a direction parameter to our Initialize method and use that to position the second child to the right instead of up.

	private void Start () {
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = material;
		if (depth < maxDepth) {
			new GameObject("Fractal Child").AddComponent<Fractal>().
				Initialize(this, Vector3.up);
			new GameObject("Fractal Child").AddComponent<Fractal>().
				Initialize(this, Vector3.right);
		}
	}

	private void Initialize (Fractal parent, Vector3 direction) {
		…
		transform.localPosition = direction * (0.5f + 0.5f * childScale);
	}
Two children per parent.

This is starting to look more like it! Do you still understand in what order the cubes have been created? As they're all created in a few frames, it goes too fast for us to see it grow. I think it's fun to slow this process down so we can watch it happen. We can do this by using a coroutine to create the children.

Think of coroutines as methods in which you can insert pause statements. While the method invocation is paused, the rest of the program continues. Though this point of view is too simplistic, it is all we need to make use of it right now.

Move the two child creation lines to a new method named CreateChildren. This method needs to have IEnumerator as a return type, which exists in the System.Collections namespace. That's why Unity includes it in their default script template and why I included it in the beginning as well.

Instead of just calling this method in Start, we have to call it as an argument for Unity's StartCoroutine method.

Then add a pause directive before creating each child. Do this by creating a new WaitForSeconds object for half a second or so, then yielding it back to Unity.

	private void Start () {
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = material;
		if (depth < maxDepth) {
			StartCoroutine(CreateChildren());
		}
	}

	private IEnumerator CreateChildren () {
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").
			AddComponent<Fractal>().Initialize(this, Vector3.up);
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").
			AddComponent<Fractal>().Initialize(this, Vector3.right);
	}

Now we can watch it grow! Did you see a problem in the way it did? Let's add a third child per parent, this time on the left side.

	private IEnumerator CreateChildren () {
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").
			AddComponent<Fractal>().Initialize(this, Vector3.up);
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").
			AddComponent<Fractal>().Initialize(this, Vector3.right);
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").
			AddComponent<Fractal>().Initialize(this, Vector3.left);
	}
three children three children with overdraw
Three children per parent, normal and overdraw vision.

The problem is that the children have the same orientation as their parent. This means that a left child whose parent is itself a right child will find itself inside its grandparent, and vice versa. To solve this, we have to rotate the children so that their upward direction will point away from their parent.

We will solve this by adding an orientation parameter to Initialize. It will be a quaternion used to set the local rotation of the new child. The upward child needs no rotation, the right child needs to rotation 90 degrees clockwise, and the left child needs to rotate in the opposite direction.

	private IEnumerator CreateChildren () {
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").AddComponent<Fractal>().
			Initialize(this, Vector3.up, Quaternion.identity);
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").AddComponent<Fractal>().
			Initialize(this, Vector3.right, Quaternion.Euler(0f, 0f, -90f));
		yield return new WaitForSeconds(0.5f);
		new GameObject("Fractal Child").AddComponent<Fractal>().
			Initialize(this, Vector3.left, Quaternion.Euler(0f, 0f, 90f));
	}
	
	private void Initialize (Fractal parent,
	                         Vector3 direction,
	                         Quaternion orientation) {
		…
		transform.localRotation = orientation;
	}
three children rotated three children rotated with overdraw
Three children per parent, now rotated.

Now that the children are rotated, they no longer immediately grow back into the fractal. But you might have noticed that some of the smallest children still end up disappearing into the root cube. This happens because with a scaling factor of 0.5, this fractal will self-intersect in four steps. You can reduce the scaling to solve this problem, or use spheres instead of cubes.

spheres spheres with overdraw
Spheres don't self-intersect with a child scale of 0.5.
unitypackage

Better Code For More Children

Our code has become a bit unwieldy. Let us generalize by moving the direction and orientation data to static arrays. Then we can reduce CreateChildren to a short loop and use the child index as a parameter for Initialize.

	private static Vector3[] childDirections = {
		Vector3.up,
		Vector3.right,
		Vector3.left
	};

	private static Quaternion[] childOrientations = {
		Quaternion.identity,
		Quaternion.Euler(0f, 0f, -90f),
		Quaternion.Euler(0f, 0f, 90f)
	};

	private IEnumerator CreateChildren () {
		for (int i = 0; i < childDirections.Length; i++) {
			yield return new WaitForSeconds(0.5f);
			new GameObject("Fractal Child").AddComponent<Fractal>().
				Initialize(this, i);
		}
	}

	private void Initialize (Fractal parent, int childIndex) {
		…
		transform.localPosition =
			childDirections[childIndex] * (0.5f + 0.5f * childScale);
		transform.localRotation = childOrientations[childIndex];
	}

Now let's introduce two more children by simple adding their data to the arrays. One going forward, the other going backward.

	private static Vector3[] childDirections = {
		Vector3.up,
		Vector3.right,
		Vector3.left,
		Vector3.forward,
		Vector3.back
	};

	private static Quaternion[] childOrientations = {
		Quaternion.identity,
		Quaternion.Euler(0f, 0f, -90f),
		Quaternion.Euler(0f, 0f, 90f),
		Quaternion.Euler(90f, 0f, 0f),
		Quaternion.Euler(-90f, 0f, 0f)
	};
Five children per parent, the full fractal.

We now have the full fractal structure. But what about the underside of the root cube? The idea is that the fractal is growing out of something, like a plant. Although I won't, if you want to, you could add a special sixth child going downward that's only added to the root node. Adding it to all nodes would be a waste, because it would grow directly into its grandparent.

unitypackage

Explosive Growth

How many cubes are we actually creating? As we're always creating five children per parent, the total number of cubes when fully grown will depend on the maximum depth. A maximum depth of zero produces only one cube, the initial root. A maximum depth of one produces five additional children, for a total of six cubes. As it is fractal, this pattern repeats and we can write it as the function f(0) = 1, f(n) = 5 × f(n - 1) + 1.

The above function produces the sequence 1, 6, 31, 156, 781, 3906, 19531, 97656, and so on. You will see these numbers show up as the amount of draw calls in the game view statistics of Unity. If you have dynamic batching enabled, it will be the sum of Draw Calls and Saved by batching.

Unity will probably be able to handle a maximum depth up to four or five. Any higher and your frame rate will become horrible.

Besides quantity, duration is also an issue. Right now we're pausing half a second before creating a new child. This produces synchronized bursts of growth for a few seconds. We could distribute the growth more evenly by randomizing the delays. This also results in a more unpredictable and organic pattern that I find more fun to watch.

So let's replace the fixed delay with a random range between 0.1 and 0.5. I also increased the maximum depth to 5 to make the effect more noticeable.

	private IEnumerator CreateChildren () {
		for (int i = 0; i < childDirections.Length; i++) {
			yield return new WaitForSeconds(Random.Range(0.1f, 0.5f));
			new GameObject("Fractal Child").AddComponent<Fractal>().
				Initialize(this, i);
		}
	}
unitypackage

Adding Color

The fractal is a little dull. Let's liven it up by adding some color variation. We do this by interpolating from white at the root to yellow at the smallest children. The static Color.Lerp method is a handy way to do this. The interpolator goes from zero to one, which we do by dividing our current depth by the maximum depth. Because we don't want an integer division here, we convert depth to a float first.

	private void Start () {
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = material;
		GetComponent<MeshRenderer>().material.color =
			Color.Lerp(Color.white, Color.yellow, (float)depth / maxDepth);
		if (depth < maxDepth) {
			StartCoroutine(CreateChildren());
		}
	}
Colored, but no dynamic batching.

This already looks a lot more interesting! But another thing happened as well. Dynamic batching used to work, but now it doesn't. How do we fix this?

The problem is that by adjusting the color or a child's material we silently created a duplicate of that material. This is necessary because otherwise everything using that material would end up with the same color. However, batching only works when the exact same material is used for multiple items. Equivalence is not checked – that would be too expensive to do – it must really be the same material.

Let's create only one material duplicate per depth instead of per cube. Add a new array field to hold the materials. Then check whether an array exists in Start and if not call a new InitializeMaterials method. In this method we will explicitly duplicate our material and change its color per depth.

	private Material[] materials;

	private void InitializeMaterials () {
		materials = new Material[maxDepth + 1];
		for (int i = 0; i <= maxDepth; i++) {
			materials[i] = new Material(material);
			materials[i].color =
				Color.Lerp(Color.white, Color.yellow, (float)i / maxDepth);
		}
	}
	
	private void Start () {
		if (materials == null) {
			InitializeMaterials();
		}
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material = materials[depth];
		if (depth < maxDepth) {
			StartCoroutine(CreateChildren());
		}
	}

Now instead of passing the material reference from parent to child, pass the materials array reference instead. If we didn't, each child would be forced to create its own materials array and we wouldn't have solved the problem.

	private void Initialize (Fractal parent, int childIndex) {
		mesh = parent.mesh;
		materials = parent.materials;
		…
	}
Colored, now with dynamic batching.

We have our batching back, and it makes quite a difference in this case! The color is still not that interesting though. A nice tweak is to give the deepest level a radically different color. This can reveal patterns in the fractal that you might not have noticed before.

Simply change the last color to magenta afterwards. Also adjust the interpolator so that we still see the full transition to yellow. And while we're at it, squaring it results in a slightly nicer transition.

	private void InitializeMaterials () {
		materials = new Material[maxDepth + 1];
		for (int i = 0; i <= maxDepth; i++) {
			float t = i / (maxDepth - 1f);
			t *= t;
			materials[i] = new Material(material);
			materials[i].color = Color.Lerp(Color.white, Color.yellow, t);
		}
		materials[maxDepth].color = Color.magenta;
	}
Now with magenta tips.

Let's add a second color progression, say from white to cyan with red tips. We'll use a single two-dimensional array to hold them both, then select one at random when we need a material. This way our fractal will look different each time we enter play mode. Feel free to add a third progression if you like.

	private Material[,] materials;

	private void InitializeMaterials () {
		materials = new Material[maxDepth + 1, 2];
		for (int i = 0; i <= maxDepth; i++) {
			float t = i / (maxDepth - 1f);
			t *= t;
			materials[i, 0] = new Material(material);
			materials[i, 0].color = Color.Lerp(Color.white, Color.yellow, t);
			materials[i, 1] = new Material(material);
			materials[i, 1].color = Color.Lerp(Color.white, Color.cyan, t);
		}
		materials[maxDepth, 0].color = Color.magenta;
		materials[maxDepth, 1].color = Color.red;
	}
	
	private void Start () {
		if (materials == null) {
			InitializeMaterials();
		}
		gameObject.AddComponent<MeshFilter>().mesh = mesh;
		gameObject.AddComponent<MeshRenderer>().material =
			materials[depth, Random.Range(0, 2)];
		if (depth < maxDepth) {
			StartCoroutine(CreateChildren());
		}
	}
Randomized colors.
unitypackage

Randomizing Meshes

Besides colors, we also also randomly choose which mesh to use. Let's replace our public mesh variable with an array and pick one out of it at random in Start.

	public Mesh[] meshes;
	
	private void Start () {
		if (materials == null) {
			InitializeMaterials();
		}
		gameObject.AddComponent<MeshFilter>().mesh =
			meshes[Random.Range(0, meshes.Length)];
		gameObject.AddComponent<MeshRenderer>().material =
			materials[depth, Random.Range(0, 2)];
		if (depth < maxDepth) {
			StartCoroutine(CreateChildren());
		}
	}

	private void Initialize (Fractal parent, int childIndex) {
		meshes = parent.meshes;
		…
	}

If we were to put only a single cube in our new array property in the inspector, then the result would be the same as before. But add a sphere and you suddenly get a 50% chance of spawning either a cube or a sphere per element of the fractal.

Fill this array however you like. I put the sphere in there twice so it is twice as likely to be used as the cube. You can also add other meshes, though capsules and cylinders don't work so well because they're elongated.

inspector random meshes
Randomly choosing between a cube and a sphere.
unitypackage

Making the Fractal Irregular

Our fractal is nice and complete, but we can make it a bit more organic by cutting off some of its branches. We do that by introducing a new public spawnProbability variable. We pass it along and use it to randomly determine whether we spawn a child or skip it. A probability of 0 means that no children will spawn at all, while a probability of 1 means that all children will spawn. Even values a little below one could drastically alter the shape of our fractal.

The static Random.value property produces a random value between zero and one. Comparing it to spawnProbability tells us whether we should create a new child or not.

	public float spawnProbability;

	private IEnumerator CreateChildren () {
		for (int i = 0; i < childDirections.Length; i++) {
			if (Random.value < spawnProbability) {
				yield return new WaitForSeconds(Random.Range(0.1f, 0.5f));
				new GameObject("Fractal Child").AddComponent<Fractal>().
					Initialize(this, i);
			}
		}
	}

	private void Initialize (Fractal parent, int childIndex) {
		…
		spawnProbability = parent.spawnProbability;
		…
	}
inspector irregular growth
A 70% spawn chance produces irregular shapes.
unitypackage

Rotating the Fractal

All this time our fractal has been a good boy and stayed motionless. But a little movement would make it a lot more interesting to watch. Let's add a very simple Update method that rotates around the current Y axis at a speed of 30 degrees per second.

	private void Update () {
		transform.Rotate(0f, 30f * Time.deltaTime, 0f);
	}

With this simple addition all the fractal's parts are now merrily spinning. All at the same speed. Yes, let's randomize it! And make the maximum speed configurable too.

Note that we have to initialize our rotation speed in Start – not in Initialize – because the root element should rotate too.

	public float maxRotationSpeed;
	
	private float rotationSpeed;

	private void Start () {
		rotationSpeed = Random.Range(-maxRotationSpeed, maxRotationSpeed);
		…
	}

	private void Initialize (Fractal parent, int childIndex) {
		…
		maxRotationSpeed = parent.maxRotationSpeed;
		…
	}
	
	private void Update () {
		transform.Rotate(0f, rotationSpeed * Time.deltaTime, 0f);
	}
Configured for speed.
unitypackage

Adding More Chaos

Are there more adjustments that we could do to throw our fractal out of whack in subtle ways? There are many! One of them is to knock the fractal's elements out of alignment by adding a subtle rotation. Let's call it twist.

	public float maxTwist;

	private void Start () {
		rotationSpeed = Random.Range(-maxRotationSpeed, maxRotationSpeed);
		transform.Rotate(Random.Range(-maxTwist, maxTwist), 0f, 0f);
		…
	}

	private void Initialize (Fractal parent, int childIndex) {
		…
		maxTwist = parent.maxTwist;
		…
	}
inspector some twist
Some nice twist.

Another option would be to mess with the child scale. Or maybe skip a depth sometimes. Fiddle with positions? From this point I leave it up to you!

unitypackage PDF