3D fractals

Artpiece made by Machina Infinitum1

Contents

  • Part 1 – Introduction and research
  • Part 2 – Getting started with fractals in Unity
    • 2.1 First fractal creation
    • 2.2 Improved fractal creation
  • Part 3 – Little creations
    • 3.1 Fern
    • 3.2 Tree
  • Part 4 – Mandelbrot
    • 4.1 Shader
    • 4.2 Perfecting the mandelbrot
  • Part 5 – Menger sponge
    • 5.1 Getting familiar with the fractal
    • 5.2 Menger sponge code
    • 5.3 Final sponge
  • Part 6 – Final results
    • 6.1 Recap & learning points
    • 6.2 Little expo
  • Sources

Part 1 – Introduction and research

During the first years of my Game Development studies I feel like I haven’t done enough with math and cool mathematical shapes, though I’ve always liked math. And with inspiration from students from previous years I found out they made fractals in Unity, so I wanted to do something with that too. Also I wanted to do more with maybe game designing or anything art related, because I like to make art, in many different forms. My first idea for this project was to make trippy visuals and with the help of an article Alexander showed me, I combined the ideas to make trippy 3D fractal art in Unity.

Artpiece made by Matthew Haggett6

The inspiration I got was from artists that make mandelbulb art in a program specifically designed for that4. These people make beautiful creations with mandelbulbs and other fractals. If I were to recreate something like that in Unity I would have to keep it way more simple, so I don’t even want to look into mandelbulbs yet. My scope is to start with simple fractal objects and make something out of those. Later on I might look into shaders to make them more appealing or I will look at mandelbulbs, but that is not my scope for now.

With the inspiration and goal in vision, I wanted to know if other people had done similar things in unity, and I did find tutorials on how to make fractals2 and one guy that has made pretty much every possible fractal 2D and 3D with the help of shaders5. So with these sources I can get a pretty good headstart on making fractal art in Unity.

To sum it up, my main goal is to get familiar with fractals in Unity and make little artpieces based on fractal structures.

Part 2 – Getting started with fractals in Unity

2.1 First fractal creation

Before I can get into beautiful creations, I need to get a basic understanding of how a fractal could be created in Unity. And just to get started I followed a catlikecoding tutorial for a Sierpiński triangle2. I ended up making a simple octahedron fractal.

I start with a sphere, but it is also possible with other shapes, and I add a script to it that instantiates smaller clones of the sphere in different directions. I use this method:

Fractal CreateChild(Vector3 direction, Quaternion rotation)
{
    //instantiate one row of units, size of which is depth
    Fractal child = Instantiate(this);
    child.depth = depth - 1;

    //place the units next to each other on the the mentioned axis and rotation
    child.transform.localPosition = 0.75f * direction;
    child.transform.localRotation = rotation;
    child.transform.localScale = 0.5f * Vector3.one;
    return child;
}

This instantiates a clone of the sphere in the given direction and angle, right next to its parent, and halves its size compared to the parent. The depth represents the amount of times this cloning happens consecutively and can be changed in the inspector, it can’t be infinite because of processing issues (6 is actually the max for my laptop). I use this method for 6 directions, on the three axes in both ways. The line below is an example for the units created on the x-axis to the left of its parent unit.

    Fractal childC = CreateChild(Vector3.left, Quaternion.Euler(0f, 0f, 90f)); //X-axis, to the left

That is basically it for the simple octahedron fractal, the one in the picture has a depth of 5.

2.2 Improved fractal creation

The octahedron is a great start, but I want to be able to procedurally generate fractals. Catlikecoding has a great tutorial for this too, but it’s not available to look at for free anymore, so for this part I take inspiration from a project that has used this tutorial3. I still use something similar to the CreateChild method, but now instead of having to call the method in every direction and rotation, I store the directions and rotations in arrays. I initialize new units in every direction that there is with a for loop and let the new units inherit certain properties from their parent. So the initializing looks like this now:

private void Initialize(PCG_Fractal parent, int childIndex)
{
    mesh = parent.mesh;
    materials = parent.materials;
    maxDepth = parent.maxDepth;
    depth = parent.depth + 1;
    childScale = parent.childScale;
    transform.parent = parent.transform;
    transform.localPosition = (childScale / 2 + 0.5f) * direction[childIndex];
    transform.localRotation = rotation[childIndex];
    transform.localScale = Vector3.one * childScale;
}

And it’s called for every direction in the array:

    private IEnumerator CreateChildren()
    {
        for (int i = 0; i < direction.Length; i++)
        {
            yield return new WaitForSeconds(0.1f);
            new GameObject().AddComponent<PCG_Fractal>().Initialize(this, i);
        }
    }

To also visualize the depth of the units in color, I change the color of the material for each depth there is.

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.black, Color.white, t);
    }
    materials[maxDepth].color = Color.red;
}

In the start I give each unit a material and mesh that I assigned in the inspector, I initialize units for every depth until it reaches the maximum I put in the inspector, and I give them a name based on the depth.

    private void Start()
    {
        if (materials == null)
        {
            InitializeMaterials();
        }

        gameObject.AddComponent<MeshFilter>().mesh = mesh;
        gameObject.AddComponent<MeshRenderer>().material = materials[depth];

        if (depth < maxDepth)
        {
            StartCoroutine(CreateChildren());
        }

        name = ("Fractal " + depth);
    }

It doesn’t look any different from what I made first yet, except for the material of course. But at least it has the basics of a fractal.

Part 3 – Little creations

3.1 Fern

With a basis for fractals I can take a step closer to fractal art. And to start with something simple I want to recreate one of the most commonly known fractals found in nature, a fern. This shape in particular is intriguing to me because there’s also a fibonacci sequence within the shape.

With the script from the previous part I should only need to make some adjustments to get to the shape that I want to create. To start off I changed the material colors and removed a bunch of directions to keep the shape simple and similar to that of the fern. It was a bunch of trying out different settings and numbers, which resulted in a lot of different looks so here are some screenshots of the progress and some iterations:

It was actually pretty hard to get a shape that is similar to that of a fern, especially with a fibonacci spiral in it. I ended up with a regular fern shape and left out the fibonacci part for now, I might come back and add that later. The fern for now looks like this:

3.2 Tree

When I was trying to make the fern work, I somehow stumbled upon a shape that looked a lot more like a tree than a fern if I add a rotation to it. So I changed the colors a bit and ended up with this.

Part 4 – Mandelbrot

After my first few weeks I wasn’t sure where to go, so this week I took a few steps back and made a fractal that is simple but still cool to look at. Though the mandelbrot isn’t 3D, I still think it’s a nice addition to my project. For this fractal I used a couple of projects to inspire me, combined a bit of them all and added my own needs to it7, 8, 9.

4.1 Shader

The mandelbrot in Unity starts of as a 2D image with a shader on top. In the shader we implement the formula for a mandelbrot, which is f(z) = z^2 + c, where c is the starting position. This is done in the fragment shader part of the shader and it looks like this:

            for (iteration = 0; iteration < _maxIter; iteration++)
            {
                // f(z) = z^2 + c [waarbij c= startpositie]
                zNext.x = z.x * z.x - z.y * z.y + startPos.x;
                zNext.y = 2 * z.x * z.y + startPos.y;
                z = zNext;

                if (length(z) > 4) break;
            }

            if (iteration > _maxIter) {
                return 0;
            }

Also in the vertex shader I set the position to be adjustable to the screensize, which is the area.

            o.uv = _Area.xy + (v.uv - 0.5) * _Area.zw;

Ignore the funky colors, but this results in the familiar mandelbrot shape:

The colors that the mandelbrot shows are based on an rgb gradient and are animated based on time, as shown below.

            // Introduce a time-based factor to animate color
            float timeFactor = sin(_Time.y);
            float4 color;

            float normalizedIter = iteration / _maxIter;

            if (iteration < _maxIter)
            {
                // Apply the time factor to color animation
                color = tex2D(_MainTex, float2(normalizedIter *_Repeat + timeFactor, 0));
            }

            return color;

Then to look at all the beautiful parts of the fractal and to see how infinitely deep it really goes, I added a script to move around the picture and zoom in. I also added a rotation to the zooming for a trippy effect. I won’t bore you with the details of the script, but moving is done by using WASD and zooming in and out can be done by the keypad + and – buttons. The rotation is mostly done in the shader script though, so I will share that piece of code over here:

        float2 rotation(float2 start, float2 pivot, float angle) {
            start -= pivot;
            start = float2(start.x * cos(angle) - start.y * sin(angle),
                start.x * sin(angle) + start.y * cos(angle));
            start += pivot;

            return start;
        }

4.2 Perfecting the mandelbrot

After recieving feedback from the guild and reviewing the results myself, it did seem pretty nice, but still needed some improvements. One of the feedback points was that the colors are too flashy, so I thought they should change more gradually. I tried to implement a way to smoothly change the colors, and the guy from the tutorial showed a way to apply a gradient so I also added that. In short, the way it works is that it seperates the colors from each other and creates blocks that show different colors in the order of the gradient colors. You need the radius of the new color blocks, which is r, and then you need to change the colors based on how far away the block of color is from the boundaries of the fractal. You do that by modifying the iteration count according to the distance:

                float r = 20;
                float r2 = r * r;

                //calculate the mandelbrot
                float2 z = 0;
                float2 zNext;
                int iteration;
                for (iteration = 0; iteration < _maxIter; iteration++)
                {
                    z = float2(z.x * z.x - z.y * z.y, 2 * z.x * z.y) + startPos;
                    if (dot(z, z) > r) break;
                }

                if (iteration > _maxIter) {
                    return 0;
                }
                float dist = length(z);
                float fracIter = (dist - r) / (r2 - r);
                fracIter = log2(log(length(z) / log(r)));
                iteration += fracIter;

Then to smoothly animate the colors i added a new way to animate it, still keeping it time based. I also noticed that the colors displayed in the fractal don’t seem to take all the colors from the png, even though I use the _Color variable. So I changed that way it lerps for that calculation and made that change smoothly too.

                // Define animation duration
                float animationDuration = 7.0;
                float colorAnimationDuration = 7.0;

                // Calculate the time factor for the animations so animations are based on time
                float timeFactor = (_Time.y / animationDuration) - floor(_Time.y / animationDuration);
                float colorTimeFactor = smoothstep(0.0, 1.0, smoothstep(0.0, 1.0, frac(_Time.y / colorAnimationDuration)));
                float4 color;

                float _Color = lerp(0, 1, colorTimeFactor);

                float normalizedIter = (iteration / _maxIter);

                //apply color and time-based animation
                if (iteration < _maxIter)
                {
                    color = tex2D(_MainTex, float2(normalizedIter * _Repeat + timeFactor, _Color));
                }

                return color;

This resulted into this:

Also after looking at the code again, it seemed messy and after cleaning it up it also created something even smoother:

            float animationDuration = 7.0;
            float colorAnimationDuration = 7.0;

            float timeFactor = (_Time.y / animationDuration) - floor(_Time.y / animationDuration);
            float colorTimeFactor = smoothstep(0.0, 1.0, smoothstep(0.0, 1.0, frac(_Time.y / colorAnimationDuration)));
            float _Color = lerp(0, 1, colorTimeFactor);

            float4 color = sin(float4(0.2, 0.2, 0.8, 1) * normalizedIter * 20) * 0.5 + 0.5;
            color = tex2D(_MainTex, float2(normalizedIter * _Repeat + timeFactor, _Color));

            return color;

Part 5 – Menger sponge

5.1 Getting familiar with the fractal

The menger sponge is a fractal shaped in such a way that I can not recreate it with the code from the other 3D fractals. So I had to do some more research for this specific one. I started looking at a source that I took inspiration from before10, and thought of different options to use such as raymarching11. I also kind of thought I could do it all on my own, I knew the fundamentals of the menger sponge, because it’s the same regardless of the technique you use. The fundamentals are:

  • Begin with a cube.
  • Divide every face of the cube into 9 squares, creating 27 smaller cubes.
  • Remove the center cube from each face and the middle, leaving 20 cubes.
  • Repeat the two previous steps for each iteration

I could just not reshape the fractal script I had into one that implements these steps, so I had to take a diffent approach. I found a slightly outdated, but in-depth, tutorial on how to create a menger sponge12. Which I didn’t really agree with from the start, but for the research it was a great start to get to know the fractal. While I was following the tutorial I immediately changed parts of the code to fit my style, so I don’t have a lot of progress screenshots or bits of code. But I’ll explain the biggest hurdles I faced as good as possible.

One of the things in the tutorial that I did not want at all or agree with, was having two scripts to make one fractal work. The idea was to have one overlapping script for the whole fractal, creating the initial cube and all iterations, and one script to keep track of the size of the iterations. Just like my first fractal script I want it to be done in a single script, so I had to change some things to do that. It didn’t seem that hard, if the size thing was the only thing I had to do different, but after that adjustment I didn’t get the same result. The result I got was that every cube did get subdivided into 20 smaller ones, and were placed nicely in formation, but the space between those cubes was too large, as shown in the picture below. Note that every picture about the menger sponge has had three iterations.

For days I was hyperfocused on the size of the iterations, because I thought that was the only difference between my script and the tutorials. I even somehow created a fractal that did iterate three times, but appeared as if it only iterated twice, as shown in the picture on the right:

I was so confused as to why the size didn’t display right even though the numbers were correct, and then I figured out that there was another very important difference between my code and the tutorials’: my iterations were created as children of the iteration before, and were placed and scaled based on the position and size of the parent, while the tutorial created the iterations in global space. I revisited my first fractal script and figured out that I had to change the code to be more fitting to that, than to the code of the tutorial, because the fundamentals are simply different. This took me another few days, but finally I achieved a real menger sponge shape that works for every iteration:

5.2 Menger sponge code

So what ís the way I created the menger sponge? Well, firstly, the start of the script is exactly the same, I start with an empty object that I attach a mesh and material to, and run a funtion for every depth of the fractal. The creation of the iterations looks like this:

    private IEnumerator CreateChildren()
    {
        yield return new WaitForSeconds(1f);

        for (int x = -1; x < 2; x++)
        {
            for (int y = -1; y < 2; y++)
            {
                for (int z = -1; z < 2; z++)
                {
                    Vector3 cubePos = new Vector3(x, y, z);

                    float sum = Mathf.Abs(x) + Mathf.Abs(y) + Mathf.Abs(z);

                    if (sum > 1)
                    {

                        new GameObject().AddComponent<MengerSponge>().Initialise(this, cubePos);
                    }
                }
            }
        }

        GetComponent<MeshRenderer>().enabled = false;
    }

It took me a while to fully understand this well, but the for loops for every direction is actually very similar to the list of directions I had in the previous script. They create vectors in every direction of the initial cube, to the right, the middle, and the left, and that in the three axis. It’s actually a way better way to get all the directions I need! This part is where we divide the cube into 27 smaller cubes.

But I don’t need all 27, I only need the 20, leaving the center cubes out. So all the cubes with at least two zeroes in the coördinates are center cubes, and do not need to be created. This can easily be checked by seeing if the sum of the coördinates is larger than 1, because (-1, 0 , 0), (0, 0, 0), and (1, 0, 0) in any order are center cubes and all have a sum of 1 or smaller. So for all except those, I create a smaller cube.

The last line is where I “delete” the previous iteration so that it’s not visible anymore and doesn’t overlap with the newer iteration.

The “Initialise” function is exactly the same as the previous script:

private void Initialise(MengerSponge parent, Vector3 newPos)
{
    mesh = parent.mesh;
    materials = parent.materials;
    maxDepth = parent.maxDepth;
    depth = parent.depth + 1;
    transform.parent = parent.transform;
    transform.localPosition = newPos * (scale / 2 + .15f);
    transform.localScale = Vector3.one * scale;
}

It does all the same things, except for the rotation, because that’s not necessary. The new position is like I explained above similar to the direction. It’s multiplied by scale / 2 to position the cubes adjacent to each other, then the + 0.15 is to make sure the center of the child cubes allign with the center of the parent cube.

It took me a lot of time to figure out and I honestly made it so much harder for myself by being stubborn and wanting to change the perfectly fine tutorial code to fit my style from the original fractal script. But I’m happy that after actually weeks I managed to get the result that I actually wanted! 🙂

5.3 Final sponge

So far I’ve made little creations that represent or look like something, and to keep that going I also wanted to do something with this menger sponge. Though I didn’t want to do something that changes the shape of it too much, because then it wouldn’t be a menger sponge anymore. I settled on something simple, adding a material to the sponge and a little change in the code, so it kind of looks like a gelatinous cube.

The movement is made by putting the code below in the update, which changes the scale of the individual cubes based on time.

    if (depth >= maxDepth)
    {
        currentTime += Time.fixedDeltaTime;
        float t = Mathf.PingPong(currentTime / duration, 1f);
        float currentscale = Mathf.Lerp(scale, scale * 1.75f, t);
        transform.localScale = Vector3.one * currentscale;
    }

Part 6 – Final result

6.1 Recap & learning points

My goal at the start was to create artpieces in unity using 3D fractals, and maybe even apply shaders in the end, but this was maybe a little ambitious! I had to understand how fractals work in Unity and see how to work with that to create things of my own. I thought I could immediately jump into creating things, but most of the time was spent learning. Even though the first goal was not achieved, and the product may not be what was expected, I learnt something new every week and created something somehow every week.

With the new information learnt, it should be noted that it might not even be possible to create fractal art with procedurally generated fractals, so I may have been on the wrong track from the start. None of the fractals can iterate very far, let alone infinitely. For example the fractal from 2.2 can’t iterate further than the fifth one (shown in the video below), or the framerate will drop, and the menger sponge can’t even get past 3.

So it might not be possible to create something in unity as big as procedurally generated fractal art. Better solutions might be to create the fractals with shaders or to stick to programs made for that purpose, and not Unity.

6.2 Little expo

Because it’s not what I was going for, I’m not really proud of what I made, but I am glad I have some smaller things to show, so these are some of the final things I made that looked the best.

The fern:

This image has an empty alt attribute; its file name is image-153-edited.png

The mandelbrot:

And the menger sponge:

Part 7 –

7.1 Performance fixes

During this project I found out that the way these fractals are made, they result in performance issues. I kept thinking I could maybe fix it with my code and by changing some variables, in which I didn’t succeed obviously, but I completely forgot there are other ways to fix performance issues. So like we did in the workshop I want to apply batching to my code. I think it can be done with static batching at runtime13, and there are a few ways I have tried to implement it into the code, I’m using the Fern fractal script to test it all.

7.1.1

First I tried to use this line in the start, because I thought this would combine every gameobject with that line of code.

But the method should work on an object that is a parent and combines that and all of its children. So instead I tried this so the line of code only applies to the first object that is created, which is the parent of all the fractal iterations.

It doesn’t work, but I forgot that for my code I create a new material with every iteration, so I changed it so that it only uses one material, just to see if it works at all. And just to proof to you that every iteration has the same material, below is the Fern in white. Tried it with both ways above again, but doesn’t work.

Everything above was unnecessary because I was looking at the wrong thing all the time, because what I was supposed to use was dynamic batching. Which is also way easier, because it was just a button in the project settings. Turning it on increases the number saved by batches by a lot. This is with 3 fractals running at the same time.

7.1.2

Next thing I thought of to reduce was the vertex count, and what I had mostly learned from the workshop was that it was easiest to reduce this for specific meshes in blender, but for my project I only use unity meshes, so I can’t do that. I looked up other ways online and most things point out to reduce the poly count in blender or to use some kind of add on15. I also found another solution from the unity manual, to use vertex or mesh compression14. But mesh compression is not possible because I only use the simple unity meshes and don’t have any models or anything, and vertex compression is not possible when dynamic batching is turned on. And I should have read the whole thing in the beginning, because I was trying to find out why vertex compression didn’t change anything for way too long, before I read that!

I came to the conclusion that it would be either using dynamic batching or vertex compression, because they could not work at the same time. The changes vertex compression provide with dynamic batching turned off are so small that it’s not worth it, so I will just keep dynamic batching on and leave the reduction of the vertex count for now.

Just to cancel it out, the only lighting I’m using is the directional lighting, there’s no difference when using the default sky box, and I’m only using simple materials. The only effect that could use improvement is the mandelbrot shader, so I will not go into detail about the other things.

7.1.3

To optimize the shader script, I wasn’t sure where to start. While making the script I combined multiple tutorials and examples, so I checked with those if I strayed off the path too far in some way which makes the performance of mine way worse than theirs. But most of it seems fine, I changed a couple things and tested them but changed them back again, because there is basically no difference.

Based on another unity manual thing16 I changed a couple of floats into halfs, but only the ones that I was sure would be fine. I also checked if I do the other things listed correctly, if they apply, and it’s all good the way it is right now. I actually don’t even think the shader is so heavy on performance, it’s only 2D and literally only an image, so to test if it even makes a difference I just turn that fractal off. Turns out it does not make a difference, everything is the same with it turned off, seen in the picture below.

Sources

  1. https://www.mandelbulb.com/2021/machina-infinitum-3d-fractal-art-and-animation/
  2. https://catlikecoding.com/unity/tutorials/basics/jobs/
  3. https://github.com/Yifatshaik/Fractals-in-Unity
  4. https://www.mandelbulb.com/artist-profiles/
  5. https://github.com/pedrotrschneider/shader-fractals
  6. https://www.mandelbulb.com/2014/artist-profile-matthew-haggett/
  7. https://github.com/MatJab94/mandelbrot/tree/master
  8. https://www.youtube.com/watch?v=kY7liQVPQSc
  9. https://www.youtube.com/watch?v=zmWkhlocBRY
  10. https://github.com/pedrotrschneider/shader-fractals#menger-sponge
  11. https://www.youtube.com/watch?v=nrm-ZjWIfQw
  12. https://www.youtube.com/watch?v=IaW_tDoKDPI
  13. https://docs.unity3d.com/Manual/static-batching.html
  14. https://docs.unity3d.com/Manual/mesh-compression.html
  15. No specific source, just look it up! But this reddit post sums it up good enough: https://www.reddit.com/r/Unity3D/comments/7rzpm0/is_there_an_easy_way_to_reduce_the_poly_count_of/
  16. https://docs.unity3d.com/Manual/SL-ShaderPerformance.html

Leave a Reply

Your email address will not be published. Required fields are marked *