Blender Mandelbulb

A few weeks ago, I started experimenting with using Python in Blender to create fractal meshes. My goal was to create a Mandelbulb – similar to what you can get from the app Mandelbulb 3D. While I had some success (I was able to generate a Mandelbrot mesh), I was not able to generate a very detailed Mandelbulb. Part was a lack of understanding with regard to the math involved. Let’s be honest: I didn’t know what the hell I was doing. Part was the large number of vertices required to get the kind of detail I desired. Fractals can be quite detailed. When I started hitting millions of vertices (and, again, hitting the limits of what I remembered in math), I began to wonder if I had taken on a project that was well beyond my experience.

Then, with help/feedback from Henrik Ohlin and Vít Procházka (who actually provided me with the core Python code I needed), I began to see that I may have been going in the wrong direction, trying to create a mesh. I do not know why, or what put me on this track, but I started playing with OSL (Open Shading Language) to create custom shaders in Blender. With more trial and error, and a lot of Googling, I was finally able to create a Mandelbulb in Blender. The solution was definitely not to create a mesh. Instead, the answer was in using the math to control the density of a volume. It can certainly be done with Python; but, I am stubborn and wanted to create a shader. Why hit your thumb with a hammer, when you can bang your head against a wall? Hair was pulled out. Foul words were uttered. But I got there. With some schooling from Henrik on the Volume Settings (Step Size and Max Steps), I finally achieved my goal. Here are some renders.

Colors were still a challenge. It seems that one does not apply colors to a volume scatter exactly the same way as with other surface shaders. Still, with more trial and error, I finally got it. Or, close enough that I gave myself a pat on the back.

From there, I could tweak the math to get some variations.

Downsides… Volumes can take a lot longer to render. This uses an OSL script, so GPU rendering is not an option… which means the render takes even longer. However, were GPU an option, I suspect that the detail might just crash it anyway.

Are you ready to play?

You can download the zip, containing both the blend file and the OSL script.

Creative Commons License


This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
However, donations are always welcome! (but not required)

 Does anyone actually ever donate?? Probably not. Sigh. I guess it will be another night of cat & dog hair soup.

Download the file: OSL_MandelbulbTest.zip (124 KB)

At the risk of sounding like a broken record, scan the file before you extract the contents. Yes, I try to keep my site secure. However, there is a limit to how much I can, or am willing to, do. Always assume that any downloads, even from trusted sites, might be compromised. Scan them. Or don’t, and gamble with your computer.

The Blend Scene

This is the scene. Bottom left is the OSL script and bottom right is the node setup. Unless you are up for playing with the math, you do not need to mess with the script. The node setup does need a little explanation.

Here is a basic explanation of the settings. This is all still rough, so bear with me.

Inputs

  • upperBound/lowerBound:  These values are used to help define the inner and outer transparency boundaries. Generally, you wouldn’t want to mess with those. However, you can make some big changes to the volume’s appearance if you do.
  • stepSize: This value helps in the detail level of the volume. Here again, you probably do not need to mess with this. However, if you do, you will likely have to alter the upper/lowerBound values. Otherwise, the volume may disappear.
  • n: This determines the number of iterations in the underlying math. Originally, my thought was that changing this value would improve the detail level. It might play a role. However, I found that increasing it also increases the render time. You should leave it at 2.
  • power: This is the main setting. It plays the biggest role in changing the shape of the volume. Higher values yield more detail. Go high enough and the volume becomes more and more spherical, because the detail becomes finer. Go below 1 and you also get a sphere-like volume. You’ll get the most interesting results with values between 2 and 20.
  • Color1, Color2, lowerColorBound, and upperColorBound: These are used to generate the colors of the volume. It is still a WIP, so the settings are not very user friendly. You may need to adjust the lowerColorBound and upperColorBound values, depending on the other settings you change.
  • scatterDensity and absorptionDensity: these are used to control the… wait for it… density of the volume. Mind-boggling, I know. The values are rather high, here. They may not need to be quite this high. Do remember that lowering the density will have an impact on the detail you see and how the color is rendered.

Outputs

  • VolumeColor: this is equivalent to a Volume Scatter node.
  • AbsorptionColor: this is equivalent to a Volume Absorption node.
  • ColorOut: this will be the calculated color used for the VolumeColor output, without the scatter.
  • val: is the raw output from the mandelbulb function. You can use that to play with other nodes, like Emission or your own Scatter & Absorption nodes.
  • distanceOut: Not sure if this is useful, but it is the value used to control the color fade for ColorOut and VolumeColor.

You can use just the scatter (VolumeColor) or absorption (AbsorptionColor) outputs, or you can mix them as I have here. Mixing them does seem to provide a more solid looking volume. Another setting that will help with the apparent solidity is the Volume Sampling Step Size (screen shot to the right) – thanks, Henrik, for that rather important tip. Smaller Step Size values (the left text input) will render more solid volumes – at the cost of more CPU and higher render times. Higher Step Sizes will render faster, but the volume will become more translucent and fuzzy.

There you have it. Use it. Or don’t. Play with it, modify it, expand on it. I would love to see what others can build. Show me what you do. The script is offered up as is, no warranties or guarantees. It may have bugs. LOL, of course it will have bugs! It is still a rough draft. The script only tries to generate one type of fractal. As such, it cannot begin to compare with software like Mandelbub 3D. Consider this more of a personal experiment. I wanted to see if I could do it.

While it is not the mesh result for which I had originally hoped, it does fulfill the overall intent. I did not know the OSL language when I started. I cannot say with any certainty that I have more than a novice grasp of it now. I am bad about providing meaningful comments. In the script, you will see some comments that I used to modify the Python code that Vit provided, when I converted it to OSL. Those commented bits of code were obtained from the Hypercomplex Fractals page.

The OSL Script

I really should explain the script. There are a number of interesting things happening. However, I do not think I understand OSL, or the math involved, well enough to offer any meaningful explanation. Yet. I will try to explain what I *think* I understand. The script took me a few days to build. With some practice, I may be able to make improvements to the functionality and performance. I can say that online documentation for OSL, Blender OSL in particular, is lacking. It is not too difficult to find some places that list a lot of the available functions. You can even find where people have shared other scripts. Explanations of those scripts and how to best use the functions are what seem to be missing. I had trouble finding thorough documentation, anyway.

OSL is not Python. It is similar to C++, but it is not that either. It is a custom language.

There are several parts to the script. The main function is shader MandelbulbVolume.

shader MandelbulbVolume(
    vector Vector = 0,
    float upperBound = 100,
    float lowerBound = 5,
    float stepSize = 0.125,
    int n = 2,
    float power = 8,
    color Color1 = 0,
    color Color2 = 1,
    int scatterDensity = 1000000,
    int absorptionDensity = 1000000,
    float lowerColorBound = -3,
    float upperColorBound = 1,
    output closure color VolumeColor = holdout(),
    output closure color AbsorptionColor = holdout(),
    output color ColorOut = 0,
    output float val = 0,
    output float distanceOut = 0
) {
    float i = 0;
    vector p = Vector;

    i = mandelbulb(p,power,n,stepSize);
    if ((i > lowerBound*stepSize) && (i < upperBound*stepSize)) {
        closure color scatter = henyey_greenstein(0)*scatterDensity;
        closure color absorp = absorption() * absorptionDensity;
        float d = distance(p,vector(0,0,0));
        float dAdjusted = (d*(upperColorBound-lowerColorBound))+lowerColorBound;
        distanceOut = dAdjusted;

        color mixColor = mix(Color1,Color2,dAdjusted);
        VolumeColor = mixColor * scatter;
        color hsl = transformc("hsl",mixColor);
        if (hsl[0]<0.5) {
            hsl[0] +=0.5;
        } else {
            hsl[0] -=0.5;
        }
        color rgb = transformc("hsl","rgb",hsl);
        AbsorptionColor = rgb * absorp;
        ColorOut = mixColor;
        val = i;
    } else {
        VolumeColor = color(0,0,0) * henyey_greenstein(0);
        val = 0;
    }
}

This function defines the resulting material node inputs and outputs. The sockets are defined within the parenthesis. What is done with the inputs and how the outputs are calculated is defined by what is within the brackets.

The shader outputs are created using the keywords ‘output closure color’. Anything that has the keyword ‘output’ will be a value that is output from the resulting node – i.e. the sockets on the right of a material node. The other items in the parenthesis are input nodes – the sockets on the left of the material node. The Mandelbulb math is called with:

i = mandelbulb(p,power,n,stepSize);

The Mandelbulb function returns a general value that is used to determine the difference between the volume and transparency. If ‘i’ doesn’t fall within the user-defined lower and upper bounds, then the volume is transparent, as defined by the final else block. I should have used VolumeColor = transparent(); It would be one less calculation, anyway.

The ‘closure color scatter’ and ‘closure color absorp’ lines define the uncolored scatter and absorption, which are later combined with colors and assigned to the corresponding output nodes, ‘AbsorptionColor = rgb * absorp;’ and ‘VolumeColor = mixColor * scatter;’

In between those lines is a bunch of messy code to try and apply the two input colors to the volume. My thought was that one color should be more dominant closer to the origin and would fade out to the second color with distance from the origin. The line

'float dAdjusted = (d*(upperColorBound-lowerColorBound))+lowerColorBound;'

is how I tried to provide the user a way to exercise some control over that color fade. Yes, it is rough and clumsy.

The math functions are more difficult to explain. In short, I can’t. I have a rudimentary understanding, at best, of what is happening. For that reason, I left the comments in the code to show how I tried to translate the  code from the Hypercomplex Fractals page. It is worth mentioning again that I would not have come up with this – or would, at the very least, still be working at it – were it not for Vit. He provided the Python code. I just ported it to OSL and made some minor adjustments and bug fixes. The main math functions would be the mandelbulb function and the triplexPow function. You can really alter/mess-up/destroy the results by making changes to the calculations in either of those. For example, try changing line 34:

float phi = n * asin(inputVec[2] / r);

from asin to sin or sinh.

Or, change line 36:

out = pow(r,n) * vector((cos(theta) * cos(phi)),(sin(theta) * cos(phi)),sin(phi));

to

out = pow(r,n) * vector((acos(theta) * acos(phi)),(sinh(theta) * acos(phi)),sin(phi));

Some alterations and you get something like this (with power set to 8.0):

changes to lines 34 & 36:

float phi = n * sin(inputVec[2] / r);

and

out = pow(r,n) * vector((acos(theta) * acos(phi)),(sinh(theta) * acos(phi)),sin(phi));

There you have it. Probably not very useful beyond people who love fractals and are in to Blender. If I make any significant improvements, I’ll let you know, either in an update to this post or as a new post.