Example 9.5 – 3D Cantor Set


A recent source of inspiration has been some of the work done by the developer of a procedural world generator Miguel Cepero of Voxel Farm/Voxel Studio and documented at his blog Procedural World. I’ve recently experiment with a few different grasshopper scripts based on some of the concepts he discusses, and I wanted to show a couple of these here on this blog. The first is a script based on an extremely well-known fractal know as the Cantor Set and here on proc-world translated into 3D. a fractal known as “Cantor Dust”.

Step One – Setup a Basic Cantor Set Script

Setting up a 2D Cantor set is a very straightforward process if you’ve already tried setting up a few of your own recursive loops in Grasshopper using Anemone. If you haven’t done so, I would refer you to a few of the earlier examples in this blog under sections 8 and 9. Here I’m showing the entire script for a 2D Cantor set from which we will build our 3D script.


All we are doing here is Taking a single line segment, imported from Rhino, and then using the “Shatter” component breaking it into 3 equal segments. The middle segment is discarded, and the other two segments, retrieved through the “List Item” component are then Moved a small distance upwards. They are also looped back to be shattered again (and again). Like many recursive fractals, even this small script will crash your computer if you let it run for too long, but after 4 or 5 rounds the geometry gets so small as to almost disappear into “dust” anyway. I also have a second process looping through channel D1 to save all of my old geometry.” This step can be eliminated if you use the “record” function of Anemone, but I like to keep the geometry around in containers for future use.

Even at this early stage, you’ll notice that if we change where the line is shattered, the script will give different results. Below are tests showing different potential shatter patterns by changing the values in the panel and the results after 4 recursions.


Step Two – Adding Randomness to the Standard Cantor Script

Before going into the 3D version, we are going to make just a couple of more variations to show the principles we will be using going forward. Here two random number generators are introduced, one to randomize the division points, and one to randomize the vertical distance moved.


The first random number generator, pictured above, generates a value between .15 and .49 to determine the first division point, and then subtracts this value from 1 to determine the second division point. This will always lead to a symmetrical division. The generator is tied to the counter (to which I add a small value to avoid a constant “0” seed) and a number slider.


A second random number generator can be used to determine the amount of movement. Simple Enough.

Step 3 – Standard 3D Cantor Set

We will forget the random number generator for a minute and will just try and modify our script to do a standard, 3D Cantor set. The first modification is we will start by inputing a surface into our loop instead of a line. For now we will input a simple square surface. Next, instead of using the “Shatter” component to split the line, we will use Isotrim together with Divide Domain2, splitting our surface into 9 subsurfaces (3×3). Finally, we list the four corner surfaces (0, 2, 6, 8) for further subdivisions. When these surfaces are moved, we should also go ahead and change this to a “Z” vector instead of the “Y” we used in the previous script. By now the script should look something like what is shown below.


One further addition to our script will be to add an “Extrude” component to give us solid geometry, extruding our geometry an amount equal to the amount moved in the Vertical direction. but we still need to keep the un-extruded, moved surfaces, as these will be recursively looped and subdivided, not the extruded geometry.


Step Four – Irregular Surface Divisions

It was pretty easy in our 2D version to use our random number generator to produce values for shattering our line. It is much much MUCH more complicated in this 3D example, as there isn’t any kind of simple component for irregularly dividing surfaces. Furthermore, we want at each recursion to assign different random values to each surface at each round, so that they are each acting independently of each other. This will require careful structuring of data. In short, instead of our simple Surface => Divide Domain2 => Isotrim routine, we are replacing it with spaghetti salad. :0


This will not be easy, but don’t panic. I will try and explain. OK, maybe you can panic now and just download the completed script at the end of this post, but if you want to walk through it, I’ll do my best.

We’ll start by dividing our surface into 4 sections using the standard Isotrim before the looping starts. I am creating the surface in a bit of an awkward way, exploding the curve, then using the first and third segments of my rectangle to create my surface using the “Edge Surface” component.

f6_04_scriptinitialYou could use boundary surface at the beginning and it will work at first, but to increase the script’s flexibility for running the Cantor set on multiple irregular polygons, which I will do at the very end, you need to construct your surface in a way that will produce what is called an “untrimmed surface”. The boundary surface component creates a “trimmed surface” which can cause problems in some instances. I’m only telling you this because I was hitting my head against the desk for several hours trying to figure out why my script wasn’t working with multiple irregular shapes until I stumbled upon a solution to the problem.

OK, moving on. You can use your own rectangle for now, but I am using just one 10 x 12 unit rectangle for this example. Once the four initial subsurfaces pass into the loop, you need to make sure they are grafted into its own branch so that each subsurface can be treated independently, and get its own random number set.  Next, we use the Deconstruct Domain2  (Not Divide Domain2) to get the “U” and “V” values for each surface. U in this case corresponds to the Y axis and V to the X, but this has to do with how I created my surface, not at all to do with X/Y coordinates. Rotate the shape and you will see the U and V values remain the same regardless of the orientation of the rectangle.


The Deconstruct Domain2 component gives a U0 and a U1, as well as V0 and V1 value for each surface. This can be seen as the start and end value for the domains, relative to the surface. I then want to create some new U and V values, two to be precise, at random values between each U0/U1 and V0/V1 pairing. This will be similar to how we created the random values in the 2D cantor set. First, we find out the bounds of each pair by subtracting the start value from the end value. This value is then multiplied by one of a set of random numbers generated. You need as many random numbers as you have items, and then the random numbers need to be grafted to match the data structure of the surfaces. I used a lot of panels here to show what is going on.

In the next part of this step, we are going to collate our numbers and construct new domains corresponding to each of our individual subsurfaces

Below is the top half of this construct, just for the U values. We are using the “Merge” component to merge first the U start value(U0), then the location of the 1st cut (U0+Random Number), then the location of the 2nd cut (U1 – Random Number), and finally the U end value (U1). This will create a small sublist corresponding to each subsurface from the previous part of this step. While you won’t see the surface divisions yet, hopefully you can see how the values in the panel correspond to the U divisions shown in the image to the left, that we are looking for.


These sublists now just need to be converted to domains. To do this, Use Shift list, followed by Construct Domain to get a domain spanning between each value in our list, and then Cull the last item, using Cull Index, since this is “junk” that we don’t need (the domain between the last value and the first value). to get the right index, I used a formula, but it might be safe to just say cull Item 3.

Once this is set up, do the same for the V values, here shown without the panels.


Lastly, we need to do a bit more gymnastics to weave, so to speak, the two linear domain sets of domains together, into one squared domain. If we simply plug the values together with the Construct Domain2 Component, however, we will not get what we are looking for,  since you will notice from the last step, we had 3 domains for each subsurface (in this case 12 domains total). This is not enough, and will only split the surface into 3 subsurfaces, once for each domain. To solve this, we need to duplicate our list of domains 3 times using the “Duplicate Data” component(which will repeat each data component 3 times, but only in its own sublist, and then use”Partition List” to get the three duplicates into their own separate lists. Then we can construct our squared domain with “Construct Domain2”


Finally, although not altogether obvious, we need to use “Trim Tree” to get rid of the outermost branch without flattening our data all the way. In the end, we want just four sublists to correspond to our original four subsurfaces. Once this is done, plug into the Isotrim component to (hopefully!) get the surface division to work.

Step 5 – Test Looping and Make Additional Modifications as Desired

So now that the hard part is behind us, we can carefully increase our number of iterations, and if that is working we can modify the script and adjust parameters to get it to behave more like what we’ve envisioned.


This particular script doesn’t seem to bring much after about 4 loops…except system crashes. After looking at its behavior, I decided I didn’t like for the really tiny pieces to get as much vertical extrusion as the bigger pieces. I decided having a component of each shapes size added to the move and extrusion height equations might help.


So with this minor modification, the results are a bit different.

A few Variations

f6_optionIf all is working well, you can input multiple outlines at once and it will perform the algorithm faithfully. It *should* work with any four sided closed polygon, although you may need to “flip” the direction of the line in some cases if you are getting unexpected results. The image above is of a 4×4 starting grid.


An this is from an irregular field of polygons I drew. Each polygon is four sides.

Once rendered It looks a little like the death star surface…




OK, well, if you have trouble figuring this out, click here to download the GH file