Fractals: Mandelbulb

Fractals: Mandelbulb

By Niels Duivenvoorden

This article is about the progress made researching fractals, in specific, the Mandelbulb.

Different approaches were made to achieve the final result, including Mandelbrot, Ray Marching and Mandelbulb.

The requirements for the final product are an explorable Mandelbulb, where the exponent, iterations, speed of the animation and the color are easy to adjust.

Table of Contents

Project information

The idea to research fractals was created after watching the animated movie Big Hero 6. Originally, the idea was to create the entire portal scene, where the player floats through the Mandelbulb environment.

If more time was available, to focus changed to a neural network or a flocking like system to create a simulation. The idea behind this was to have the agents start at a certain point, where they have to figure out how to get to the end point without hitting the flying debris.

The different points of focus depend on each others progress. The most important one were the fractals thus shaders. Only once a topic was completely finished, the move was made to work on a new topic.

Approach

As noted before, the different topics were created at the start of the project. With the use of the Double Diamond process model, I tried to achieve the wished end product for a certain topic.

Mandelbrot

It’s my first time diving deep into fractals and shaders, so understanding the Mandelbrot set is essential for this project. In short, the Mandelbrot set is formed by a simple equation:

{\displaystyle f_{c}(z)=z^{2}+c}

A key aspect to create the set is that for each pixel, the check whether it ends up inside or outside the area after the maximum steps are reached. Based on this rule, the pixels get their color (The Art of Code, 2018b). This equation does not provide enough information to get to the actual visualization of the Mandelbrot, where more explanation is needed.

We now know what value gets adjusted where in the equation, though unfortunately we can’t use this in code. To convert the equation to code, understanding complex numbers is a must. A complex number is similar to a vector 2, since they have an X and a Y component. The X is the real component and the Y is the imaginary component of the complex number.

Different rules apply to imaginary numbers compared to real numbers. In the table below, the differences are displayed. Let’s look at the first part of the equation, which is squaring a number.

multiplicationsolution
1 * 11
r * rr
real numbers
multiplicationsolution
1 * 1-1
i * i-r
imaginary numbers
complex numbers

We know that z is a complex number. Since the equation squares this number, let’s do the same. Take note that imaginary numbers are still present thus need to be calculated with its rules.

z = (x + yi)

z² = (x + yi)(x + yi) =

z² = x² + xyi + xyi – y² =

z² = x² – y² + 2xyi

We still have to add the start position to finish the equation. This is the part where the point stays bounded or not. Let’s convert our current state to the final equation which can be used in code.

z² + c= x² – y² + 2xyi + c

Let’s use this new equation in code. We want to adjust the color of every pixel in a certain area, so we need to work inside of the fragment part. Since computers can’t run infinite loops, we need a maximum iteration amount. I chose to create a property so it’s easy to adjust in Unity.

float2 c = i.uv - .5; //start position
float2 z; //current position
for (float iter = 0; iter < _Iterations; iter++)
{
	z = float2(z.x * z.x - z.y * z.y, 2 * z.x * z.y) + c;
	if (length(z) > 2 ) break;
}
return (iter / 255);

See how the equation created before is implemented into this code? It’s literally the same! Let’s check what the result of this small piece of code is. Implementing the ability to zoom and move makes the experience even more mesmerizing (The Art of Code, 2018).

The end result of this topic was a good contribution to my research, though far from what the 3D version of the fractal is.

Fractal objects

Since Big Hero 6 was an inspiration, the focus needed to change to a 3D approach. In the scene, the characters fly through parts of the shape, where this is impossible with the 2D Mandelbrot. Time to experiment with 3D fractals.

Since it’s my first time working with 3D space, I decided to start off with 3D objects provided by Unity. For this attempt, an empty object is placed inside the scene. We need to attach components to the object since children will inherit from this. Every object has its own material and mesh. I created arrays to store the different meshes, adjustable in the inspector.

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

The materials array is an empty array at start, where it will initialize and create different materials inside of the InitializeMaterials method. We do this before actually applying the material to the object. Inside of this function, a new material is created based on the depth of the child. Using linear interpolation, the final color is calculated. Note that every child at the maximum depth gets the color magenta assigned.

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

To actually create the children objects, the method CreateChildren is used. Note that only when the current depth of the child is less than the maxDepth this will occur. For each of the five defined directions, being up, left, right, forwards and backwards, there is a chance a child will spawn. The higher the probability, the bigger the chance of a child spawning on that surface.

    void CreateChildren()
    {
        for (int i = 0; i < childDir.Length; i++)
        {
            if (Random.value < spawnProb)
            {
                new GameObject("FC").AddComponent<Fractal>().Instantiate(this, i);
            }
        }
    }

Since child objects inherit from their parents, we have to instantiate them accordingly. Inside of the Instantiate method, the parent and index are needed to calculate the children properties, hence the parameters. Once the function is called, we parse a reference from the parent to the child. You might be able to tell that an addition to the parents’ depth is executed, along for calculation the scale.

    void Instantiate(Fractal _parent, int _index)
    {
        meshes = _parent.meshes;
        materials = _parent.materials;
        maxDepth = _parent.maxDepth;
        depth = _parent.depth + 1;
        childScale = _parent.childScale;
        transform.parent = _parent.transform;
        spawnProb = _parent.spawnProb;
        transform.localScale = Vector3.one * childScale;
        transform.localPosition = childDir[_index] * (.5f + .5f * childScale);
        transform.localRotation = childOri[_index];
    }

Note that the arrays childDir and childOri are used to determine the local position and rotation.

    static Vector3[] childDir =
    {
        Vector3.up,
        Vector3.right,
        Vector3.left,
        Vector3.forward,
        Vector3.back
    };
    static Quaternion[] childOri =
    {
        Quaternion.identity,
        Quaternion.Euler(0,0,-90),
        Quaternion.Euler(0,0,90),
        Quaternion.Euler(90, 0, 0),
        Quaternion.Euler(-90, 0, 0),
    };

We’re able to spawn in child objects where the meshes are randomly taken from an array and the color changes based on the depth. A few examples to demonstrate the current state.

Probability 1, scale .5
Probability .5, scale .5
Probability .3, scale .7

This looks quite nice, though misses some real time adjustments. Let’s focus on adding a rotation to each child. We first want to generate a rotation speed. Let’s pick a random between the maxima and minima of one value.

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

To actually add the rotation, we have to use the Update function. Unity provides the Rotate function to apply to a transform, where we only want to apply our rotation to the y angle.

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

Don’t forget to parse the value through the children!

    void Instantiate(Fractal _parent, int _index)
    {
        ...
        maxRotationSpeed = _parent.maxRotationSpeed;
    }

Solely looking at the rotation of the object creates quite a nice animation, though implementing a twist will make it even better. A twist basically is a rotation offset, which might cause the rotation of objects to be slightly shifted. The code is quite self-explanatory, especially regarding we just finished the rotation for the y angle. For a twist, we adjust the rotation value on the x angle. Note that we don’t apply a twist during runtime, it only gets adjusted at Start. Again, make sure the childs also get the twist values parsed through.

    private void Start()
    {
        transform.Rotate(Random.Range(-maxTwist, maxTwist), 0, 0);
    }
    void Instantiate(Fractal _parent, int _index)
    {
        ...
        maxTwist = _parent.maxTwist;
    }

Again, quite some nice progress, especially it being 3D, though far from the end product. I really had to focus on how to convert the Mandelbrot set into a 3D space, the so-called Mandelbulb.

Ray Marching

Whilst researching the equation for the Mandelbulb, I stumbled upon another problem. Previously we used objects generated by Unity to visualize a fractal form. That approach is limited in multiple ways. Since the depth generates exponentially, it’s performance heavy. Next to that, the meshes or shapes used limit us from generating a Mandelbulb figure. To solve this, I researched Ray Marching, which generated 3D visuals by using math.

Let’s first cover what a Ray Marcher actually does. My approach was by learning about Ray Marching by watching different videos about this topic, which I highly recommend.

This took me quite some time to understand, which is understandable. Let’s dive deep and give my explanation of how Ray Marching works.

Inside a scene, we always have a camera. The camera has a UV coord, a 3D space position, a look position. This gives us a ray as output, the so-called ViewRay, which is important. We need this ray to determine where it intersects with the object. After the distance to where the intersection happens is retrieved, we apply a material, distance and light to form the final color of the pixel.

For the camera part, we need a Ray Origin, or camera position and a Ray Direction, or look position. Using these essentials creates the ability to view along the Ray Direction.

To get where the intersection is, the exact distance is not gained immediately. We first “march” along the Ray Direction to where the object is closest to the scene. In the picture below, the Ray Origin is represented with the red dot, where the Ray Direction is the blue line. We follow the line until we hit the object with the dotted sphere. If the distance to the surface is too high, we redo this step. This is visualized by the yellow dots. Each dot is a step or march, where a new sphere is created checking whether the distance to the surface is low enough to call it an intersection. At the end of the blue line, a green dot is shown which indicates where the Ray Direction intersected with the shape since the surface of the shape was met. (The Art of Code, 2018c)

Even though this is the base of Ray Marching, some tweaks are needed for performance. To make sure the Ray Direction doesn’t keep on being checked, a hard cap is needed. Once the distance to a certain point is larger than the cap, nothing will be returned.

If however an intersection is found, for example the green point in the image above, the distance gets returned.

The last value used is the maximum amount of steps the Ray Marcher can do. If more steps than the maximum are needed, we simply skip.

So, I’ve been talking about returning the distance. The distance towards the shape you want to show can be calculated once you know the boundaries of the shape. One of the easiest shapes is a sphere. Let’s have a look at the formula.

distanceToSphere = length(SpherePos – CameraPos) – SphereRadius

This explains how Ray Marching works, though there is no end result yet. Lets convert this amazing method into code. Luckily we have loops, which is nice looking back at the gif where new spheres are created at the edges of previous ones.

The RayMarch method should only return the distance towards an intersection, so a float. As the essentials above describe, we need a Ray Origin, and a Ray Direction. Next to that we should look at the values which determine how detailed the Ray Marcher should be, the iterations, maximum distance reached and the distance to the surface to check for hits. I created properties for these values.

The different steps or iterations of the loop are visible in the gif above. The first iteration has a distanceOrigin of 0. Then the loop starts, which is where the magic happens. Inside of the loop, a point is created, named p. This is the direction Origin multiplied by the Ray Direction added to the Ray Origin. Note that an addition happens to the direction Origin, the found Distance will be added changing the value.

iterationequation
0Ray Origin + 0 * Ray Direction
1Ray Origin + (Ray Origin + 0 * Ray Direction) * Ray Direction
            //Returns the distance to the scene along depth of the viewing ray
            float Raymarch(float3 ro, float3 rd){
                float dO = 0;
                float dS;
                for(int i = 0; i < MAX_STEPS; i++){
                    float3 p = ro + dO * rd;
                    dS = GetDist(p);
                    dO += dS;
                    if(dS < SURF_DIST || dO > MAX_DIST) break;
                }
                return dO;
            }

Another essential part of a Ray Marcher is calculating the distance. This happens in the GetDist function. In this example code, the sphere and torus are located at (0, 0, 0).

            //Returns distance from point p to the scene
            float GetDist(float3 p){
                float d;
                //sphere
                d = length(p) - .5; 

                //torus
                d = length(float2(length(p.xz) - .5, p.y)) - .1;

                return d;
            }

Let’s first assign the Unity camera to the shader. The camera needs to tell its position, which is stored in a TEXCOORD. For the hitPosition we do the same thing.

            struct v2f
            {
                ...
                float3 ro : TEXCOORD1;
                float3 hitPos : TEXCOORD2;
            };

To actually assign the camera to the created variables, an adjustment to the vertex shader is needed. The option to switch to world space is commented out, but good for future reference.

            v2f vert (appdata v)
            {
                ...
                //world space
                // o.ro = _WorldSpaceCameraPos;
                // o.hitPos = mul(unity_ObjectToWorld, v.vertex);

                //object space with homogeneous coordinates
                o.ro = mul(unity_WorldToObject, float4(_WorldSpaceCameraPos, 1));
                o.hitPos = v.vertex;

                return o;
            }

Inside of the fragment shader, we’re able to set the Ray Origin and Ray Direction.

            fixed4 frag (v2f i) : SV_Target
            {
                //pivot
                float2 uv = i.uv - .5f;

                //origin
                float3 ro = i.ro;

                //direction
                float3 rd = normalize(i.hitPos - ro);


                fixed4 col = 0;
                
                return col;
            }

This will still not create a colored shape, so let’s start on using the created methods. We’re able to Raymarch to a certain point, where a torus will be displayed. Whenever the distance towards the point is lower than the maximum distance, the pixel should be colored. The fixed4 col represents the color, which is set to black at start.

            fixed4 frag (v2f i) : SV_Target
            {
                ...
                
                if(d < MAX_DIST){

                    //coloring of hits
                    }else{
                    //don't render pixel
                    discard;
                }

                return col;
            }

Since this will result in a black torus, without lighting, normals are needed.

            //Calculate normal
            float3 GetNormal(float3 p){
                //epsilon
                float2 e = float2(.02, 0);
                float3 n = GetDist(p) - float3(
                GetDist(p - e.xyy),
                GetDist(p - e.yxy),
                GetDist(p - e.yyx)
                );
                return normalize(n);
            }

Now the correct color calculation can happen.

            ...
            //coloring of hits
                if(d < MAX_DIST){
                    float3 p = ro + rd * d;
                    float3 n = GetNormal(p);
                    col.rgb = n;
                    }else{
                    //don't render pixel
                    discard;
                }
Black torus
Normalized torus

Let’s have some fun with the shader since it’s able to render any mathematical shape, including combinations.

Addition of two shapes

float GetDist(float3 p){
   float d1, d2;
   //sphere
   d1 = length(p) - .5; 

   //torus
   d2 = length(float2(length(p.xz) - .5, p.y)) - .1;

   return min(d1, d2);
}

Subtraction of two shapes

float GetDist(float3 p){
   float d1, d2;
   //sphere
   d1 = length(p) - .5; 

   //torus
   d2 = length(float2(length(p.xz) - .5, p.y)) - .1;

   return max(-d1, d2);
}

Very nice progress regarding the end product. Able to calculate shapes based on a distance function which is needed to get the Mandelbulb. Could’ve added smoothing to the objects for fluid like objects (Sebastian Lague, 2019; SimonDev, 2022) , but it’s not that relevant for this project.

Mandelbulb

Since Ray Marching is implemented into the project, the ability to create the Mandelbulb should be higher. Luckily, researchers found the equation to solve the distance function for the Mandelbulb (Mandelbulb: The Unravelling of the Real 3D Mandelbrot Fractal, 2009). For the Mandelbrot, the equation is known regarding the research.

{\displaystyle f_{c}(z)=z^{2}+c}

The problem with the equation of the Mandelbrot set is that z and c are ‘triplex’ numbers, or basically 3D coords (x, y, z) (The Unravelling of the Real 3D Mandelbrot Fractal, 2009).

Let’s focus on converting from Cartesian to Polar vice versa. This is still hard to understand, but it works (Rajabala, n.d.).

inline void cartesian_to_polar(float3 p, out float r, out float theta, out float psi)
			{
				r = length(p);
				float r1 = p.x * p.x + p.y *  p.y;
				theta = atan(p.z / r1);
				psi = atan(p.y / p.x);
			}

inline void polar_to_cartesian(float r, float theta, float psi, out float3 p)
			{
				p.x = r * cos(theta) * cos(psi);
				p.y = r * cos(theta) * sin(psi);
				p.z = r * sin(theta);
			}

To calculate the Mandelbulb set, all essentials are present. Much like the Mandelbrot, the Mandelbulb also has iterations. Inside of the Mandelbulb distance function, we create a loop, capped at the maximum iterations. The first step to the equation is converting the Cartesian point to a Polar one. The above function does the calculation for us. After the polar coords are retrieved, we determine the rate of change of the points in the set. Finding p ^.5 is the next step. The adjusted values now get converted back into Cartesian. This is where the Ray Origin and Direction are added to the found point. As like removing pixels that don’t stay bound in the Mandelbrot, the Mandelbulb does the same.

			float Mandelbulb(float3 c){				
				//radius of inside sphere of the mandelbulb 
				const float delta = 2;	
				
				float3 p = c;
				float dr = 2, r = 1;

				for(int i = 0; i < si; i++)
				{		
					//easing between exponents	
					se = lerp(_Exponent - .5, _Exponent + .5, sin(_Time.y * _Speed));

					// convert cart to polar
					float theta, psi;
					cartesian_to_polar(p, r, theta, psi);

					// rate change of points
					dr = se * pow(r, se - 1) * dr + 1.0;

					// find p ^ .5
					r = pow(r, se);
					theta *= se;
					psi *= se;

					// polar to cart
					polar_to_cartesian(r, theta, psi, p);
					
					// add position point
					p += c;

					// check if point is outside range of mandelbulb
					if (length(p) > delta) {
						break;
					}
				}
				// Greens method
				return log(r) * r / dr; 
			}

If the color is determined by getting the normal, just like the Ray Marching example, this result is shown.

This is what I was looking for from the start, though it feels quite limited. The color to start with, it’s not adjustable and currently shows the UV. To solve this, I added three parameters for colors, each one for a different axis. This little formula adds the different colors to each other and makes it easier to adjust from within Unity.

					col.rgb = _ColorX * n.x + _ColorY * n.y + _ColorZ * n.z;

Just like the experiment with Fractal object, the Mandelbulb should animate. Since different properties are exposed inside of Unity, it was easy to figure out what to adjust during runtime, the Exponent.

Inside of the Mandelbulb function, some additions were made. The s in front of the variable is for speed. For the Exponent, a simple lerp between the Exponent value – .5 and + .5 is achieved with the use of a sinus. The sinus returns a multiplication of Time and _Speed, making even the speed adjustable. Tried to make the iterations also change its value, which works, though the result is not that impressive.

			float Mandelbulb(float3 c){				
				...
				float se = _Exponent, si = _Iterations;

				//iteration increase/decrease
				//si = lerp(_Iterations - 1, _Iterations + 1, sin(_Time.y * .5 + .5) * _Speed);

				for(int i = 0; i < si; i++)
				{		
					//easing between exponents	
					se = lerp(_Exponent - .5, _Exponent + .5, sin(_Time.y * _Speed));
					...

With some simple camera movement, this is the result.

You might’ve seen that the inside of the Mandelbulb is filled, which is not optimal. In Big Hero 6, the characters are able to fly through the scene, where the inside of the Mandelbulb is visible. Luckily we dealt with subtracting shapes from other shapes with the Ray Marching experiment. Let’s add a sphere on the inside of the Mandelbulb which should be extracted from the distance.

			//Returns distance from point p to the scene
			float GetDist(float3 p){
				float cd; //combined distance

				float d1 = Mandelbulb(p);	
				float d2 = Sphere(p, .8);

				//add shapes
				// cd = min(d1, d2);

				//subtract shapes, negative is the 'hollow'
				cd = max(-d2, d1);

				return cd;
			}

For anyone curious, I experimented a bit with different shapes. The following to be exact.

			float Sphere(float3 p, float r){
				return length(p) - r;
			}

			float Box(float3 p, float w){
				float3 d = abs(p) - w;
				return min(max(d.x, max(d.y, d.z)), 0.0) + length(max(d, 0.0));
			}

			float Torus(float3 p, float ic, float r){
				return length(float2(length(p.xz) - ic, p.y)) - r;
			}

Let’s have a final look at the inside of the Mandelbulb!

Conclusion

My goal was to create the Mandelbulb fractal, hollowed out where agents or a player are able to fly through. The Mandelbulb fractal was achieved by creating a Ray Marching shader, where the distance to the surface of the Mandelbulb was calculated. Even though the end result is mesmerizing, it lacks the feel of input or simulation.

At the start of the research and development period, I should’ve defined my end goals very specifically, with an eye on creating a realistic scope. I wanted to explore fractals, neural networks and flocking, though this is anything but feasible for the given time in combination with zero to no experience regarding shaders.

Looking at the end product, I found some remarks worth noting. Let’s start with the biggest one, the inside of the set. Yes, the player is able to fly through the fractal, though the only eye candy available are the edges of the negative sphere with the Mandelbulb. This results in a sphere like inside, instead of the interesting outside of the set.

Another remark is technology, but most likely also the approach of coding. Currently I did not take performance completely into account, since the ‘simulation’ is playable. Though, the properties of the shader are limited. There is no such thing as an infinite amount of iterations.

Next to that is the set not mirrored, which results in some weird artifacts. If you look at the end result video, there is quite the seam between the left and the right of the set. Looking back at the scene from Big Hero 6 does calm me though, since the seam is also visible there.

Lastly, the current way of rendering the set is with the use of a plane mesh. This is a child of the camera which works just fine since the set is placed in world space. It feels like there should be a better solution to this problem, where Post Processing might be something for the future.

Though some remarks are found, I’m still very happy with the end result. It’s been 2 months since I first touched a shader, where this result blows my mind. In the end, I know mathematics is not my strongest subject, though I managed.

I still would like to implement flocking with an Octree into this project in the future. Flocking is quite clear to me, thus creating the Octree is the main focus. I’m not too sure about the Neural Network approach. It stumbled upon me during the PCG assignment, where I created a Natural Selection project. But for now, it’s time to explore the outside of the Mandelbulb first.

Source code

Feel free to mess around with the source code yourself! Don’t hesitate to contact me about any questions.

References

Alpha Beta Gamer. (2019, January 4). Marble Marcher – A Beautiful Blend of Evolving Fractals & Super Monkey Ball-esque Ball Rolling! [Video]. YouTube. https://www.youtube.com/watch?v=Ke0Bd52E7qg

ANGRY CARROT. (2020, March 7). Unity Tutorial Basic: Constructing a Fractal 1 [Video]. YouTube. https://www.youtube.com/watch?v=JLPiIOEEcD8

Braian Strak. (2022, January 23). I Made 3D FRACTALS In Unity! [Video]. YouTube. https://www.youtube.com/watch?v=AOIFUDFToFs

CodeParade. (2018, May 19). How to Make 3D Fractals [Video]. YouTube. https://www.youtube.com/watch?v=svLzmFuSBhk

Flick, J. (n.d.). Constructing a Fractal, a Unity C# Tutorial. https://catlikecoding.com/unity/tutorials/constructing-a-fractal/

Hutchins, D., Riley, O., Erickson, J., Stomakhin, A., Habel, R., Kaschalk, M., & Walt Disney Animation Studios. (n.d.). Big Hero 6 : Into the Portal. disneyanimation.com. https://media.disneyanimation.com/uploads/production/publication_asset/140/asset/BH6_Into_the_Portal.pdf

Jae Hyun Yoo – Disney – Big Hero 6. (n.d.). https://sites.google.com/site/semanticdatas/00-game-dev/disney-big-hero-6

Jelle Vermandere. (2020, March 14). Making a 3D Fractal Game: Fractal Collector [Video]. YouTube. https://www.youtube.com/watch?v=0jwkZKDOzfc

Logan. (2022, May 21). Fractals in Unity: This is the coolest thing i have made [Video]. YouTube. https://www.youtube.com/watch?v=piDZjiqtUBU

Mandelbulb: The Unravelling of the Real 3D Mandelbrot Fractal. (2009, November 8). https://www.skytopia.com/project/fractal/mandelbulb.html

Rajabala. (n.d.). GitHub – rajabala/Fractals: A Unity implementation of various fractals (for learning). GitHub. https://github.com/rajabala/Fractals

Sebastian Lague. (2019, April 2). Coding Adventure: Ray Marching [Video]. YouTube. https://www.youtube.com/watch?v=Cp5WWtMoeKg

SimonDev. (2022, August 29). Ray Marching, and making 3D Worlds with Math [Video]. YouTube. https://www.youtube.com/watch?v=BNZtUB7yhX4

The Art of Code. (2018a, January 27). Making a Mandelbrot fractal explorer in Unity [Video]. YouTube. https://www.youtube.com/watch?v=kY7liQVPQSc

The Art of Code. (2018b, March 31). The Mandelbrot Fractal Explained! [Video]. YouTube. https://www.youtube.com/watch?v=6IWXkV82oyY

The Art of Code. (2018c, December 7). Ray Marching for Dummies! [Video]. YouTube. https://www.youtube.com/watch?v=PGtv-dBi2wE

The Art of Code. (2019a, May 31). Making a Mandelbrot fractal explorer in Unity – Part 2 [Video]. YouTube. https://www.youtube.com/watch?v=zmWkhlocBRY

The Art of Code. (2019b, December 18). Writing a ray marcher in Unity [Video]. YouTube. https://www.youtube.com/watch?v=S8AWd66hoCo

The Coding Train. (2022, March 11). Coding Challenge 168: MandelBulb 3D Fractal [Video]. YouTube. https://www.youtube.com/watch?v=NJCiUVGiNyA

The Unravelling of the Real 3D Mandelbrot Fractal. (2009, November 8). https://www.skytopia.com/project/fractal/2mandelbulb.html

World of Zero. (2016, September 30). Fractal Shaders in Unity – Mandelbrot and Julia Set Surface Shaders [Video]. YouTube. https://www.youtube.com/watch?v=SVj0LWmQD-E

Zucconi, A. (2017a, May 12). Fractals 101. Alan Zucconi. https://www.alanzucconi.com/2016/08/17/fractals-101/

Zucconi, A. (2017b, May 12). Fractals 101: The Mandelbrot Set. Alan Zucconi. https://www.alanzucconi.com/2016/08/23/fractals-101-mandelbrot/

Leave a Reply

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