TECH 312: Menger Sponge



Like Sierpinski's gasket, the Menger Sponge pattern is a pattern that will repeat infintiely in 3 Dimensions. However, the Menger pattern differs in it's code as it uses recursion to create rectangles that fit infinitely into a predefined rectangular shape.



This simulation, inspired by LEGO, was created in Houdini by generating instances of the geometry based on time.



2D Menger Pattern

The menger pattern used objects to define seperate instances of the shape. The creation of the Class was created in a seperate .py file, and then called with another.

The method that the Menger code uses is called 'Recursion', which refers to a procedure calling itself. Recursion allows for an infinite repition, so must also be given limits as to the number of times the program is allowed to run. Otherwise the program will loop indefinitely and crash the computer.

A class object creates and stores the positions and size of the planes. That way, we can work with multiple instances of the class without compromising the unique identity of another menger object. The code then creates a mel file which creates planes and moves them to their pre-determined position.


The script used to define the Class Menger with it's variables and procedures:



class Menger2D:
    def __init__(self, rect, depth):
        self.bbox = rect
        self.data = []
        self.divide(rect,depth)
    #_______________________________________________________
    # A recursive proc that subdivides a rectangle into
    # 9 sub-rects. Each time the proc is called the arg
    # "depth" is decremented. Recursion terminates when its
    # value becomes zero.
    def divide(self, rect, depth):
        if depth == 0:
            self.data.append(rect)
            return
        x0,y0,z0, x1,y1,z1 = rect
        w = float(x1 - x0)/3
        h = float(y1 - y0)/3
        d = float(z1 - z0)/3
        x,y,z = x0,y0,z0
        rects = []
  
        x = x0
        for rows in range(3):
            rects.extend(self.column(x,y,z,w,h,d))
            x = x + w
        rects = self.delete(rects)
        # Recursion________________
        for rect in rects:
            self.divide(rect, depth - 1)
        return    
    #_______________________________________________________
    def delete(self, rects):
        hole = rects.pop(4)
        return rects
    #_______________________________________________________
    # Given the minimum x,z and maximum x,z coordinates
    # of a rectangle this proc returns the coordinates 
    # of a "row" of three sub-rectangles.
    def column(self, x,y,z, w,h,d):
        #x,y,z = x0,y0,z0
        X,Y,Z = x + w, y + h, z + d
        rects = []
        for n in range(3):
            rect = [x,y,z, X,Y,Z]
            rects.append(rect)
            z,Z = z + d, Z + d
        return rects
    
  
if __name__ == '__main__':    
    bounds = [-1,0,-1,    1,0,1]
    menger = Menger2D(bounds,3)
  

The script used to create the class into a .mel file for implementation in Maya:


#Menger2D_mel.py
import math
  
import random
  
from menger_2b import Menger2D
  
class Menger2DMel(Menger2D):
    #constructor 
    def __init__(self, box, depth, path):
        #base class constructor
        Menger2D.__init__(self, box, depth)
        self.path = path
        self.write()
        
    def write(self):
        """This method writes the data generatec by the base class
        as a mel script containing polyPlane mel commands. """
        
        f = open(self.path, 'w')    
        for box in self.data:
        
            rotx = 0
            roty = 0
            rotz = 0
        
            x, y, z, X, Y, Z = box
            w = X - x
            h = Z - z
            d = Y- y
  
            f.write('polyCube -h %1.3f -d %1.3f -w %1.3f;\n' % (h,d,w))
            f.write('rotate %1.3f %1.3f %1.3f;\n' % (rotx, roty, rotz))
            f.write('move -x %1.3f -y %1.3f -z %1.3f;\n' % (x, y, z))
        f.close
  
if __name__ == '__main__':    
    bounds = [-1, -1, -1, 1, 1, 1]
    menger = Menger2DMel(bounds, 1, '/home/eoverf20/mount/stuhome/tech312/python/Menger/menger.mel')
    
  
  




As the planes are created, I added a transform to the mel script that randomly rotates the individual planes up to 45 degrees.



3D Menger Pattern

By adding another for loop, we can create a third dimension to the Menger pattern. This becomes the 'layer' which in turn have 'rows'. The other difference from a 2D to a 3D object is deleting the correct sequence of cubes. To create the Menger, items 22, 16, 14, 13, 12, 10, and 4 in the sequence are deleted. For the 3D menger, the code creates a rib archive that can be called upon by Renderman at rendertime.

boxes = []
        for layer in range(3):
            x = x0
            for rows in range(3):
                boxes.extend(self.row(x,y,z,w,h,d))
                x = x + w
            y = y + h
        boxes = self.delete(boxes)
        # Recursion________________
        for box in boxes:
            self.divide(box, depth - 1)
        return boxes
   #_______________________________________________________
    # Uses the indices in the holeLUT to remove specific cubes
    # from the list of 27 cubes in the "boxes" arg.
    
    def delete(self, boxes):
        for n in 
range(len(self.holes)):
            hole = boxes.pop(self.holes[n])
            self.deleted.append(hole)
        return boxes



By adding shaders and lights, we can get some cool effects with the .rib files. However, adding lights in the rib nodes did give a little bit of noise to the render.





3D Menger Pattern - Variations

#=======================================================
if __name__=="__main__":
    bounds = [-1,0,-1, 1,2,1]
    
    holeList= []
    numHoles = random.randrange(25)
  
    for n in range(numHoles):
        newRandom = random.randrange(25)
        holeList.append(newRandom)
    holeList.sort(reverse=True)
    defaultHoles = [22,16,14,13,12,10,4]
        
    menger = Menger3DRib(holeList, bounds, 3, '/home/eoverf20/mount/stuhome/tech312/python/Menger/code_given/test3.rib')
    menger.write()

By changing the list of deleted boxes, we can create a large variety of different shapes. We can manually change the list to create a new shape, but I used the random function within python to determine the new sequence of deleted boxes. Before creating the rib file, the code randomly chooses how many out of a set of 25 boxes to delete and which ones. The list must be sorted from highest to lowest so that the value of the list will still exist as the preceding boxes are deleted.






Implementation in Houdini

Python can also be implemented in Houdini by creating a custom node. In order to do this, we must first create a .otl file within a Houdini scene.

Rather than use a language specific to Houdini, I used python. 'Python type' should be selected for Operator Style. Since the purpose of this node is to create geometry, the option 'geometry' must be selected for Network Type.

The parameter inputs I used for the Menger .otl were width, height, and depth. I also included 'iterations', which determines the number of recursions used to create the Menger Shape.


I implemented the Menger 3D code into Houdini fairly easily. There were only a few variations from the original python-to-mel workflow from cutter. The first is that a class system is unnecessary in Houdini as the digital asset interprets each node as a seperate object. So any variables given to that object would be inherent only there.

The second way the code varied was the lines that called the creation of the Menger itself. Rather than limit myself by defining the points of a polycube, I instead interpreted the bounding box data as center points. This way, I could put any sort of geometery onto these points to create the shape, using a copy node. The procedure for this is called 'center'.

I also created a procedure that would use the width input to control the size of the points - and therefore the size of the geometery on those points. Unfortuneately, this only works well if the user defined bounding box is completely square. Otherwise the 'pscale' attribute of the point can't accomodate for the shifting z and y values of the copied geometry. Perhaps later I will add a procedure that can correct this, or create an option to enable automatic resizing.





The code is then used to create and place the points.



Using a copy node and any geometry node, we can create instances on each point. The 'pscale' attribute will automatically resize the geometery.



Using 'box' geos


Using 'sphere' geos




Rollover image to see the result of using points to place the geometery


Conclusion

Using python for the first time, I realize that even though the syntax of this programming language seems more simplified than mel, it's potentially much more powerful. It also has the capability to interact with other languages and a much larger selections of programs than mel. Although using this code with a socket connection into Maya wasn't complicated, I think I prefer to use python with Houdini because it is a more flexible software with more immediate results. While Maya has to reinterpret python commands into mel (at least somewhat), Houdini is a much more direct way of executing commands.