The Startup
Published in

The Startup

Fractal Fun: The Cantor Set

When I was eight, my mom brought home a math book from the library. This wasn’t any old math book; it told of the weird side of math— the side that contained fractals and mobius strips. Mobius strips were easy to make: take a strip of paper, twist it once, and tape it together. Cut it down the middle, and boom, you have a longer loop. Fractals, however, were a lot harder to replicate, and drawing infinite triangles soon became boring.

This is where the power of Houdini comes in. You don’t have to draw the triangles; Houdini does it for you. This is going to be a multi-part exploration of various fractals in 2d and 3d forms. It is also an exercise in for-each loops, since it is the best way to recreate the recursive nature of fractals. The Cantor Set is relatively straightforward, so let’s start here. Its 2D and 3D counterparts, Cantor dust, aren’t quite as straightforward, but we’re tackling them here anyways. Anyhow, let’s dive in.

(Note: I’m using Houdini 17.5.258, and I assume you have at least passing familiarity with the software. If not, there’s plenty of resource’s on SideFX’s website to help you get started.)

The First Dimension

Start with an L-system.

Angle: 90
Premise: F
Rule 1: F = FfF
Rule 2: f = fff

If you scrub through the Generations parameter, you can see all the dimensions. But what if you want to see them all at once? This is where the for loop comes in handy. Put down a For-Each Number loop, and stick the L-system in there. Also put in a Transform node after the L-system.

Take the metadata node of the for-each loop and plug it into the second input of the L-system. In the Generations parameter, set it to equal the current iteration number:

detail(1,”iteration”,0)

Then set the metadata node to be a spare input of the transform node. To make sure that all generations are equal in size and spread apart from each other, change these values:

Transform X: detail(-1,”iteration”,0)*0.05
//0.05 is the distance between each generation, can be any number
Uniform Scale: 1/bbox(0,D_YSIZE)

Now you should be able to scrub through the Iterations parameter and see it increase and decrease.

The Second Dimension: Cantor Dust

Start off with a grid.

Rows: 2
Columns: 2

Plop down four clip nodes. This is to manually divide the grid into a 3x3 grid.

//Clip 1:
Keep: All Primitives
Origin X: bbox(0,D_XMAX)-bbox(0,D_XSIZE)/3
DirectionX: 1
DirectionY: 0
//Clip 2:
Keep: All Primitives
Origin X: bbox(0,D_XMIN)+bbox(0,D_XSIZE)/3
DirectionX: 1
DirectionY: 0
//Clip 3:
Keep: All Primitives
Origin Z: bbox(0,D_ZMAX)-bbox(0,D_ZSIZE)/3
DirectionY: 0
DirectionZ: 1
//Clip 4:
Keep: All Primitives
Origin Z: bbox(0,D_ZMIN)+bbox(0,D_ZSIZE)/3
DirectionY: 0
DirectionZ: 1

Now, in a point wrangle, find the corners. The fact that the corners have exactly two connected points makes this relatively straightforward:

if(neighbourcount(0,@ptnum)==2)
{
@group_corner=1;
}

Using a Group Promote, promote the “corner” group to primitives. Then use a Delete sop to delete primitives not in the “corner” primitive group. This gives us our first generation. Put down a Group Delete at the end of all this to keep things nice and clean for the next generation.

Now put the entire chain of nodes minus the grid in a for-each primitive loop. This for-each loop iterates over each primitive to create Cantor dust. Then put this for-each loop in a for-each count loop, which will control the number of generations of Cantor dust. This second for-each loop needs a bit of tweaking:

//Block Begin
Method: Fetch Feedback
//Block End
Gather Method: Feedback Iteration

The Third Dimension: Cantor Dust

Forewarning: this works with the same logic as the second dimension, but a lot of work is done to cover what Polyfill misses.

Start with a cube. Drop down a primitive wrangle and set @count to 0. Count will be our iterator so that it will be acting on cubes rather than individual primitives.

@count=0;

Now, put down six clip nodes:

//Clip 1:
Keep: All Primitives
Origin X: bbox(0,D_XMAX)-bbox(0,D_XSIZE)/3
DirectionX: 1
DirectionY: 0
//Clip 2:
Keep: All Primitives
Origin X: bbox(0,D_XMIN)+bbox(0,D_XSIZE)/3
DirectionX: 1
DirectionY: 0
//Clip 3:
Keep: All Primitives
Origin Z: bbox(0,D_YMAX)-bbox(0,D_YSIZE)/3
//Clip 4:
Keep: All Primitives
Origin Z: bbox(0,D_YMIN)+bbox(0,D_YSIZE)/3
//Clip 5:
Keep: All Primitives
Origin Z: bbox(0,D_ZMAX)-bbox(0,D_ZSIZE)/3
DirectionY: 0
DirectionZ: 1
//Clip 6:
Keep: All Primitives
Origin Z: bbox(0,D_ZMIN)+bbox(0,D_ZSIZE)/3
DirectionY: 0
DirectionZ: 1

Use an attribute delete node to delete @count. Don’t worry, we’ll add it back later. Put down a Clean sop, making sure that “Consolidate Points” is on. Since we only need the corners of the cube to create the next generation, use a point wrangle to find the corner points and place them in a group:

if(neighbourcount(0,@ptnum)==3)
{
@group_corner=1;
}

Using a Group Promote, promote the “corner” group to primitives. Then use a Delete sop to delete primitives not in the “corner” primitive group. This gives us the outside of the cubes we want. Put down a Group Delete at the end of all this to keep things nice and clean.

Here’s the fun part: each of these cubes only has three faces. That’s not what we want. Let’s use two Polyfill sops to fix that. Uncheck “Smooth” for both Polyfills, and check “Reverse Patches” on one Polyfill. Merge the two together, and put down a Clean sop.

Consolidate Points: On
Orient Polygons: On
Fix Overlaps: On
Delete Overlap Pairs: Off

Now comes the part where we add @count back and fix any holes Polyfill may have left behind. In a point wrangle:

//this gets our corners right
if(@ptnum<8)
{
@count=@ptnum;
}

Then, put this in a detail wrangle:

//this gets all the points connected to that particular point
for(int i=0; i<8; i++)
{
int points[], group2[], group3[];
int group1[] = neighbours(0,i);
append(points,group1);

foreach(int j; int value; group1)
{
int foo[] = neighbours(0,value);
append(group2,foo);
}
group2 = sort(group2);
foreach(int j; int value; group2)
{
if(value==group2[j+1])
{
group2[j]=-1;
}
}
foreach(int j; group2)
{
removevalue(group2,-1);
}
append(points,group2);

foreach(int j; int value; group2)
{
int foo[] = neighbours(0,value);
append(group3,foo);
}
group3 = sort(group3);
foreach(int j; int value; group3)
{
if(value==group3[j+1])
{
group3[j]=-1;
}
}
foreach(int j; group3)
{
removevalue(group3,-1);
}
append(points,group3);
points = sort(points);
foreach(int j; int value; points)
{
if(value==points[j+1])
{
points[j]=-1;
}
}
foreach(int j; points)
{
removevalue(points,-1);
}
foreach(int j; int value; points)
{
setpointattrib(0,"count",value,i,"set");
}
}

Now, promote @count from a point attribute to a primitive attribute. Put down yet another detail wrangle. This time, we’re taking any primitives that p̶o̶l̶y̶f̶a̶i̶l̶e̶d̶ Polyfill managed to miss and replacing them with cubes. This detail wrangle figures out which cubes weren’t created properly.

//figure out how many times each count value appears
int many[];
for(int i=0; i<38; i++)
{
int foo = prim(0,"count",i);
append(many,foo);
}
many = sort(many);
//if ==3, add prims to make a cube
i[]@bad;
for(int i=0; i<8; i++)
{
int appear[] = find(many,i);
int length = len(appear);
if(length==3)
{
setpointgroup(0,"bad",i,1,"set");
}
}

And now, in a point wrangle with the Group set to “bad,” plop this code in to make the cubes whole:

int connected[] = neighbours(0,@ptnum);
// get x y z min/max
vector p0 = point(0,"P",connected[0]);
vector p1 = point(0,"P",connected[1]);
vector p2 = point(0,"P",connected[2]);
float x[],y[],z[];
append(x,p0.x);
append(x,p1.x);
append(x,p2.x);
append(y,p0.y);
append(y,p1.y);
append(y,p2.y);
append(z,p0.z);
append(z,p1.z);
append(z,p2.z);
x=sort(x);
y=sort(y);
z=sort(z);
foreach(int i; float value; x)
{
if(value>x[i+1]-0.001&&value<x[i+1]+0.001)
{
removeindex(x,i);
}
}
foreach(int i; float value; y)
{
if(value>y[i+1]-0.001&&value<y[i+1]+0.001)
{
removeindex(y,i);
}
}
foreach(int i; float value; z)
{
if(value>z[i+1]-0.001&&value<z[i+1]+0.001)
{
removeindex(z,i);
}
}
//make points to make the cube
int pt0, pt1, pt2, pt3, pt4, pt5, pt6, pt7;
pt0 = addpoint(0,set(x[0],y[0],z[0]));
pt1 = addpoint(0,set(x[1],y[0],z[0]));
pt2 = addpoint(0,set(x[0],y[0],z[1]));
pt3 = addpoint(0,set(x[1],y[0],z[1]));
pt4 = addpoint(0,set(x[0],y[1],z[0]));
pt5 = addpoint(0,set(x[1],y[1],z[0]));
pt6 = addpoint(0,set(x[0],y[1],z[1]));
pt7 = addpoint(0,set(x[1],y[1],z[1]));
//make the cube
int pr0, pr1, pr2, pr3, pr4, pr5;
pr0 = addprim(0,"poly",pt1,pt0,pt2,pt3);
pr1 = addprim(0,"poly",pt0,pt1,pt5,pt4);
pr2 = addprim(0,"poly",pt2,pt0,pt4,pt6);
pr3 = addprim(0,"poly",pt3,pt2,pt6,pt7);
pr4 = addprim(0,"poly",pt1,pt3,pt7,pt5);
pr5 = addprim(0,"poly",pt4,pt5,pt7,pt6);
setprimattrib(0,"count",pr0,@ptnum,"set");
setprimattrib(0,"count",pr1,@ptnum,"set");
setprimattrib(0,"count",pr2,@ptnum,"set");
setprimattrib(0,"count",pr3,@ptnum,"set");
setprimattrib(0,"count",pr4,@ptnum,"set");
setprimattrib(0,"count",pr5,@ptnum,"set");

Next, use a clean sop to make the cubes nice and clean.

Consolidate Points: On
Orient Polygons: On
Fix Overlaps: On
Delete Overlap Pairs: Off

In a primitive wrangle, increment @count so that you aren’t multiplying by zero. It will error at you, but that’s because you haven’t set up the for-each loops yet.

@count+=1;
int mult=detail(1,"iteration",0)*8;
@count+=mult;

Now let’s set up the for-each loops. Put down a for-each named primitive loop. Connect the Block Begin to the first clip node, and connect the Block End to the last point wrangle that was just created. In the Block Begin node, click on “Create Meta Import Node.” This will create detail attributes that are associated with the for each loop, which is exactly what is needed in the last point wrangle. Connect this metadata node to the second input of the last point wrangle that was just created. In the Block End, change the “Piece Attribute” parameter from “name” to “count.”

The first for-loop ran over primitives to create Cantor dust. Now, let’s make a second for-loop. This one will control the number of generations this Cantor dust has. Put the first for-loop in a for-each count loop. This second for-each loop needs a bit of tweaking:

//Block Begin
Method: Fetch Feedback
//Block End
Gather Method: Feedback Iteration

Here you have it, three generations of the Cantor set.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Jessica Beckenbach

Jessica Beckenbach

41 Followers

Houdini / Technical Artist. SCAD Visual Effects ‘20. Lives and breathes Houdini, Nuke, Unreal, and (sometimes) Maya.