The Rhombic Dodecahedron and its Strange Twin

Space-Filling Shapes

During a recent game jam, I tried to imagine a world of repeating shapes that were not cubes. Cubic building blocks are perhaps one of the most common and well-worn tropes in game design, and for good reason. As it turns out, only five regular polyhedrons in the entire universe are space-filling, and only one of those five is a platonic solid: the cube.

A space-filling polyhedron is one that can be used to generate a tessellation in space. That means that by duplicating and translating (not rotating) the shape, we can create a three-dimensional tiling that leaves no gaps between its constituent shapes. This is of course easy to visualize with a cube: eight stacked cubes form a larger, ‘2×2’ supercube, and 27 stacked cubes will form a larger still ‘3×3’ supercube.

Things begin to get both messy and interesting when you explore tessellations with other non-platonic space-filling shapes. And so began my brief but exciting journey into the lands of the Rhombic Dodecahedron, where I made an alarming discovery about the two non-prismatic and non-platonic space filling shapes.

The five Space-Fillers

The Call of the Tetrahedron

Before I settled on the Rhombic Dodecahedron for this game jam, I started out playing with the Tetrahedron. The Tetrahedron, at first glance, seems like it really should be a space-filling shape. It would make intuitive sense to stack one right-side-up tetrahedron with three adjacent right-side-down tetrahedrons, and to be able to continue in this way indefinitely.

If this seems like a naive assumption, I do not feel too guilty about it: Aristotle himself claims in his work “On the Heavens” that the Tetrahedron is indeed a space-filler. In the millennium hence, mathematicians have not been able to prove him correct; to this day, tetrahedron-packing algorithms have at best been able to fill about 78% of space. In 2009, Haji-Akbari went as far as to use Monte Carlo simulations to show that an equilibrium fluid of hard tetrahedra will spontaneously transform into dodecagonal quasicrystal, which fills 83% of space.

How in my naivete I imagined a tessellation of tetrahedrons. In actuality, this is a retessellation consisting of alternating octahedrons (white) and tetrahedrons (red).
A form of the truncated tetrahedron (the Triakis truncated tetrahedron) can form an alternating honeycomb.

In any case, the shape is not space-filling (even Aristotle can be wrong!), and I abandoned it for the Rhombic Dodecahedron.

The Rhombic Dodecahedron

Rhombic Dodecahedron, Perspective

The Rhombic Dodecahedron is an extremely interesting shape. To start, the Rhombic Dodecahedron is a semi-regular polyhedron (each of its faces is identical, and every edge has the same length), but of course not a platonic solid. Each face of the Rhombic Dodecahedron is a parallelogram (a rhombus) with two distinct angles between its edges (a platonic solid must be comprised of regular polygonal faces; a regular polygon must have sides of equal length (which a Rhombus does) and be equi-angular (which a Rhombus is not)).

The Rhombic Dodecahedron’s vertices can actually be generated by overlaying the vertices of a cube and an octahedron. In its way, the non-platonic Rhombic Dodecahedron is the child of these two platonic solids. The octahedron will have to be larger than the cube, meaning that the vertices of the Rhombic Dodecahedron must be circumscribed by two separate spheres of varying radius.

The Rhombic Dodecahedron is also a headahe-inducing shape to look at. From one perspective, the Rhombic Dodecahedron appears to be an isometric cube – indeed, without any lighting, perspective or gradient, it would be impossible to discriminate the dodecahedron from the cube. From its other angle, the dodecahedron looks like a quadrilateral kite.

Reportedly, the Louvre holds in its Egyptian collection an ancient rhombic dodecahedral die from the Ptolemaic kingdom. I can’t find images of the original die, but I was able to discover this replica. It’s strangely comforting to know that over 2,300 years ago, somebody else thought Rhombic Dodecahedrons were fascinating.

Garnet also has the crystalline habit of the Rhombic Dodecahedron.

Naturally occurring Garnet

Data Representation of the Rhombic Dodecahedron

For those unmoved by or uninterested in the code I used for this project, skip to the “results” section.

Perhaps as a result of my PISES work, I like to keep my code totally airgapped and separated from Unity gaming engine. For this reason, I created an abstract representation of the Rhombic Dodecahedron, which can be passed to a unity-specific “Rhombic Generator” that actually renders it in Unity.

Let us start with the vertices of the foundational cube and octahedron. The eight vertices of the cube are (+-1, +-1, +-1). These are the vertices where three faces join at an obtuse angle. The six vertices of the octahedron, where four faces join at an acute angle, are (+-2, 0, 0), (0, +-2, 0) and (0, 0, +-2).

Cube in blue; Octahedron in green; Union in light green

When constructing our dodecahedron, we’ll specify a scale and a centerpoint.

    // Primary Constructor ////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////
    public RhombicDodecahedron(float scale, Vector3 center)
    {
        // Set center and scale.
        this.center = center;
        this.scale = scale;

        vertices = new Dictionary<int, Vector3>();
        faces = new Dictionary<int, Rhombus>();

        // Generate the 14 vertices.  
        // First, the eight vertices where three faces meet at obtuse angles.
        Vector3 v1 = new Vector3( 1,  1,  1); 
        Vector3 v2 = new Vector3( 1,  1, -1);
        Vector3 v3 = new Vector3( 1, -1,  1);
        Vector3 v4 = new Vector3( 1, -1, -1);
        Vector3 v5 = new Vector3(-1,  1,  1);
        Vector3 v6 = new Vector3(-1,  1, -1);
        Vector3 v7 = new Vector3(-1, -1,  1);
        Vector3 v8 = new Vector3(-1, -1, -1);

        // Second, the six vertices where four faces meet at acute angles.
        Vector3 v9  = new Vector3( 2,  0,  0);
        Vector3 v10 = new Vector3(-2,  0,  0);
        Vector3 v11 = new Vector3( 0,  2,  0);
        Vector3 v12 = new Vector3( 0, -2,  0);
        Vector3 v13 = new Vector3( 0,  0,  2);
        Vector3 v14 = new Vector3( 0,  0, -2);

        // If using a non default scale, update the vertices.
        if (scale != DEFAULT_SCALE)
        {
            Vector3.Multiply(v1, scale);
            Vector3.Multiply(v2, scale);
            Vector3.Multiply(v3, scale);
            Vector3.Multiply(v4, scale);
            Vector3.Multiply(v5, scale);
            Vector3.Multiply(v6, scale);
            Vector3.Multiply(v7, scale);
            Vector3.Multiply(v8, scale);
            Vector3.Multiply(v9, scale);
            Vector3.Multiply(v10, scale);
            Vector3.Multiply(v11, scale);
            Vector3.Multiply(v12, scale);
            Vector3.Multiply(v13, scale);
            Vector3.Multiply(v14, scale);
        }

        // Index these vertices.
        vertices.Add(1,  v1);
        vertices.Add(2,  v2);
        vertices.Add(3,  v3);
        vertices.Add(4,  v4);
        vertices.Add(5,  v5);
        vertices.Add(6,  v6);
        vertices.Add(7,  v7);
        vertices.Add(8,  v8);
        vertices.Add(9,  v9);
        vertices.Add(10, v10);
        vertices.Add(11, v11);
        vertices.Add(12, v12);
        vertices.Add(13, v13);
        vertices.Add(14, v14);

        // ...

We will need to store information about the faces of the rhombic dodecahedron as well (foreshadowed by the collection of type Rhombus, initialized at the top of the constructor).

    // Rhombus ////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////
    // Represents one face of the Rhombic Dodecahedron.
    public class Rhombus
    {
        // Identifier
        public int id;

        // Opposing face
        public int opposingFace;

        // The four vertices
        public Vector3 vA;
        public Vector3 vB;
        public Vector3 vC;
        public Vector3 vD;

        // The midpoint
        public Vector3 midpoint;

        // The Rhombic Dodecahedron that this is a face of
        public RhombicDodecahedron parent;

        // Default Constructor
        public Rhombus(int id, Vector3 a, Vector3 b, Vector3 c, Vector3 d, RhombicDodecahedron p)
        {
            // ID
            id = id; 

            // Vertices
            vA = a;
            vB = b;
            vC = c;
            vD = d;
            
            parent = p;

            // Midpoint
            midpoint = new Vector3((vA.X + vC.X) / 2, (vA.Y + vC.Y) / 2, (vA.Z + vC.Z) / 2) + parent.center;
        }
    }

There are three important notes about this class.

  1. A midpoint must be stored – this is how we will know where to “snap” future tessellations of the dodecahedron. The midpoint is found simply by finding the midpoint of the line connecting two oppositional vertices of the rhombus.
  2. The vertices must be wound anticlockwise. This is the responsibility of the caller. Were this an industrial setting, with a team of developers on hand, this would be enforced in the Rhombus’ constructor. The reason for the anticlockwise winding is for the eventual generation of a mesh.
  3. A reference to the parent RhombicDodecahedron is stored.

With Rhombus defined, we can finish the primary constructor of Rhombic Dodecahedron.

        // ...

        // Create rhombuses.  // Double check these windings.

        // anticlockwise windings, supposedly
        faces.Add(0, new Rhombus(0, v3, v9, v1, v13, this));
        faces.Add(1, new Rhombus(1, v7, v12, v3, v13, this));
        faces.Add(2, new Rhombus(2, v5, v10, v7, v13, this));
        faces.Add(3, new Rhombus(3, v1, v11, v5, v13, this));
        faces.Add(4, new Rhombus(4, v2, v11, v1, v9, this));
        faces.Add(5, new Rhombus(5, v3, v12, v4, v9, this));
        faces.Add(6, new Rhombus(6, v8, v12, v7, v10, this));
        faces.Add(7, new Rhombus(7, v5, v11, v6, v10, this));
        faces.Add(8, new Rhombus(8, v2, v9, v4, v14, this));
        faces.Add(9, new Rhombus(9, v4, v12, v8, v14, this));
        faces.Add(10, new Rhombus(10, v8, v10, v6, v14, this));
        faces.Add(11, new Rhombus(11, v6, v11, v2, v14, this));
    }

As you can see from my commentary, I was skeptical about the integrity of my windings (I worked this out by eyeballing a pencil sketch I made). They turned out to be correct, however.

The frenzied fever-dream sketch in my notebook, where I worked out the vertex windings. Ignore the cellular ring stuff – that’s going to be a whole series of future posts!

With this, we have our abstract Rhombic Dodecahedron represented. Now we need to render it.

Rendering the Rhombic Dodecahedron: Setup

Rendering the Rhombic Dodecahedron requires some setup. First, some constants:

public class RhombicGenerator : MonoBehaviour
{
    // Constants //////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////

    // Distance from creator at which to render a rhomblock
    float DEFAULT_RHOMBLOCK_DISTANCE = 10f;

    // Distance at which a block under observation should have new blocks snapped to it
    float MAX_SNAPPING_DISTANCE = 100f;

    // Default rhomblock scale
    float DEFAULT_RHOMBLOCK_SIZE = 5f;

    // The three types of Rhomblocks
    public enum BlockType 
    {
        GREYBLOCK,
        DARKBLOCK,
        GLOWBLOCK
    }

    // Mapping of numerals to block types 
    private static Dictionary<int, BlockType> selectionToBlockType = new Dictionary<int, BlockType>()
    {
        {1, BlockType.GREYBLOCK},
        {2, BlockType.DARKBLOCK},
        {3, BlockType.GLOWBLOCK}
    };

If these constants are not already self-explanatory, their uses will be made clear shortly.

For this jam, I decided to create three different types of “rhomblock:” a grey-block, a dark-block, and a glow-block (hence the enumeration in the code above). I created the following texture in GIMP, and then mapped a set of Vector2 throughout it to represent the vertices of each rhombus.

Ignore the hairy edges on the dark block – because of how the UV vertices are mapped, the fuzz won’t show up.
    // Materials, Colors //////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////

    // Greyblock
    Vector2 greyblock_uva = new Vector2(0.0f, 3152f / 4000f);
    Vector2 greyblock_uvb = new Vector2(489f / 4000f, 1.0f);
    Vector2 greyblock_uvc = new Vector2(979f / 4000f, 3152f / 4000f);
    Vector2 greyblock_uvd = new Vector2(489f / 4000f, 2304f / 4000f);

    // Darkblock
    Vector2 darkblock_uva = new Vector2(981f / 4000f, 3152f / 4000f);
    Vector2 darkblock_uvb = new Vector2(1470f / 4000f, 1.0f);
    Vector2 darkblock_uvc = new Vector2(1960f / 4000f, 3152f / 4000f);
    Vector2 darkblock_uvd = new Vector2(1470f / 4000f, 2304f / 4000f);

    // Glowblock - Same as greyblock - will be emissive.    
    Vector2 glowblock_uva = greyblock_uva;
    Vector2 glowblock_uvb = greyblock_uvb;
    Vector2 glowblock_uvc = greyblock_uvc;
    Vector2 glowblock_uvd = greyblock_uvd;

    // Panel Coloration
    private Color selectedPanel = new Color(1f, 1f, 1f, 1f);
    private Color deSelectedPanel = new Color(1f, 1f, 1f, 0.2f);

Notice we have specified some panel colors – these will be for the very small UI that will display which type of block is currently being used. More on this later.

We will need some references to other Unity objects: namely, the camera in use, the GameObject representing the player / user, the GameObjects used for the UI, and the material that we are using to hold the texture image above.

    // References /////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////

    // The user camera
    public Camera camera;

    // The user
    public GameObject creator;

    // The UI block selection panels
    public GameObject panel1;
    public GameObject panel2;
    public GameObject panel3;

    // The rhomblock material
    public Material rhomboidAtlas; 

Last, we need to set up some variables. Some of these variables are self-explanatory; those that aren’t will be explained shortly.

    // Variables //////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////

    // Mapping of Unity game objects to abstract dodecahedrons
    private Dictionary<GameObject, RhombicDodecahedron> gameObjectToRhombicDodecahedron;

    // Is the mouse button depressed?
    bool mousedown;

    // Forward Ray
    Vector3 snippedRay;

    // Ray Hit Data
    RaycastHit hit;

    // Absract block under observation
    RhombicDodecahedron blockUnderObservation;

    // Game object associated with block under observation
    GameObject objectUnderObservation;

    // The index of the next rhomblock to be created
    int index;

    // The current type of block being created
    public BlockType currentBlockType;

Rendering the Rhombic Dodecahedron: Execution

Program Flow

The general program flow will be as follows:

  • The user will specify a type of rhomblock with the 1, 2, or 3 key.
  • When LMB is depressed, a rhomblock will be instantiated.
  • If the user is trained on a pre-existing rhomblock within range, snap the new block to the pre-existing block.
  • If the user is not trained on a pre-existing romblock within range, instantiate the new block a fixed distance in front of the user.
  • When RMB is depressed, if the user is trained on a pre-existing rhomblock within range, delete that block.
Start

The start method is straightforward. We set our block selection to “1,” greyblock. We update the UI panel colors to show that greyblock is selected. We initialize our mapping of GameObjects to RhombicDodecahedrons, we set mouseDown to false, and we begin our rhomblock indexing at 0.

    // Start //////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////
    // Select block type 1; initialize variables
    void Start()
    {
        // Default Block Selection
        currentBlockType = selectionToBlockType[1];
        panel1.GetComponent<Image>().color = selectedPanel;
        panel2.GetComponent<Image>().color = deSelectedPanel;
        panel3.GetComponent<Image>().color = deSelectedPanel;

        // Initialize Variables
        gameObjectToRhombicDodecahedron = new Dictionary<GameObject, RhombicDodecahedron>();    
        mousedown = false;
        index = 0;
    }
Update

In our update method, we will begin by checking to see if there is a block under observation by using a raycast. If there is a block under observation, we will save a reference to it for later, in the case that we need to create a block attached to it.

We will also check for Keycode inputs for 1, 2 or 3, in which case we need to change our block type.

    // Update /////////////////////////////////////////////////////////////////
    //////////////////////////////////////////////////////////////////////////
    void Update()
    {
        // Focus Check ////////////////////////////////////////////////////// 
        //////////////////////////////////////////////////////////////////// 
        blockUnderObservation = null;
        if (Physics.Raycast(creator.transform.position, creator.transform.forward, out hit, MAX_SNAPPING_DISTANCE)) 
        {
            objectUnderObservation = hit.transform.gameObject;
            blockUnderObservation = gameObjectToRhombicDodecahedron[objectUnderObservation];
        }

        // Block Selection ////////////////////////////////////////////////// 
        //////////////////////////////////////////////////////////////////// 
        if (Input.GetKey(KeyCode.Alpha1))
        { 
            currentBlockType = selectionToBlockType[1]; 
            panel1.GetComponent<Image>().color = selectedPanel;
            panel2.GetComponent<Image>().color = deSelectedPanel;
            panel3.GetComponent<Image>().color = deSelectedPanel;
        }

        if (Input.GetKey(KeyCode.Alpha2))
        { 
            currentBlockType = selectionToBlockType[2]; 
            panel1.GetComponent<Image>().color = deSelectedPanel;
            panel2.GetComponent<Image>().color = selectedPanel;
            panel3.GetComponent<Image>().color = deSelectedPanel;
        }

        if (Input.GetKey(KeyCode.Alpha3))
        { 
            currentBlockType = selectionToBlockType[3]; 
            panel1.GetComponent<Image>().color = deSelectedPanel;
            panel2.GetComponent<Image>().color = deSelectedPanel;
            panel3.GetComponent<Image>().color = selectedPanel;
        }

// ...

Things get interesting when the left mouse button is clicked. There are two possibilities: First, that a pre-existing rhomblock is under observation, and second, that no rhomblock is under observation. The first case is the more challenging.

In the case of a pre-existing block, we need to determine which face we want to snap a new block to. We can do this by taking the distance of each face’s centroid (Rhombus.midpoint) to the raycast hit point. Whichever face is the closest to the hit point is the face that is under focus.

Once we know the correct face, we can determine the distance from the pre-existing rhomblock’s centerpoint to its facial centroid. We can then duplicate that vector from the face’s centroid out to the centerpoint of our new rhomblock.

The pre-existing rhombus, in black; its centerpoint to centroid in red; the new rhombus, in grey; its centerpoint to centroid in blue. The two vectors are of equal magnitude and orientation, arranged tip-to-tail.

It looks something like this:

        // ...
        // Block Creation /////////////////////////////////////////////////// 
        //////////////////////////////////////////////////////////////////// 
        if (Input.GetMouseButtonDown(0)) 
        {
            mousedown = true;

            // Check to see if a rhombic dodecahedron is under observation.
            if (blockUnderObservation != null) 
            {
                // Determine what face we are viewing. 
                float closest = 100;
                RhombicDodecahedron.Rhombus closestRhombus = null;
                foreach (RhombicDodecahedron.Rhombus rhombus in blockUnderObservation.faces.Values) 
                {
                    float distanceToHit = UnityEngine.Vector3.Distance(hit.point, new UnityEngine.Vector3(rhombus.midpoint.X, rhombus.midpoint.Y, rhombus.midpoint.Z));
                    if(distanceToHit < closest) 
                    {
                        closest = distanceToHit;
                        closestRhombus = rhombus;
                    }
                }

                // Determine the midpoint-to-center offset.
                // This distance could be determined in the start method, if we wanted the scale of all blocks to be fixed for the entire duration of the program.
                UnityEngine.Vector3 midpoint = new Vector3(closestRhombus.midpoint.X, closestRhombus.midpoint.Y, closestRhombus.midpoint.Z);
                UnityEngine.Vector3 centerToMidpoint = midpoint - new UnityEngine.Vector3(blockUnderObservation.center.X, blockUnderObservation.center.Y, blockUnderObservation.center.Z);
                
                // Determine the new center
                Vector3 newBlockCenter = midpoint + centerToMidpoint;

                // Create the new block.
                RhombicDodecahedron rhomBlock = new RhombicDodecahedron(DEFAULT_RHOMBLOCK_SIZE, new System.Numerics.Vector3(newBlockCenter.x, newBlockCenter.y, newBlockCenter.z));
                GameObject rhomBlockObject = InstantiateRhomBlock(rhomBlock);
                gameObjectToRhombicDodecahedron.Add(rhomBlockObject, rhomBlock);
            }
            else 
            {
                snippedRay = (creator.transform.position) + ((creator.transform.forward) * DEFAULT_RHOMBLOCK_DISTANCE); 
                RhombicDodecahedron rhomBlock = new RhombicDodecahedron(DEFAULT_RHOMBLOCK_SIZE, new System.Numerics.Vector3(snippedRay.x, snippedRay.y, snippedRay.z));

                GameObject rhomBlockObject = InstantiateRhomBlock(rhomBlock);
                gameObjectToRhombicDodecahedron.Add(rhomBlockObject, rhomBlock);
                index++;
            }
        }

// ...

As you can see, if there is no Rhomblock under observation, the handling is simple: simply instantiate a rhomblock at a pre-fixed distance in front of the observer.

Finally, we handle mouse release and right-clicks. We need to track mouse releases because if we don’t we’ll create one rhomblock per frame while the mouse is depressed.

// ...

        // The mouse has been released.  
        else if (Input.GetMouseButtonUp(0))
        {
            mousedown = false;
        }

        else if (Input.GetMouseButtonDown(1))
        {
            // Destroy the block under observation.
            gameObjectToRhombicDodecahedron.Remove(objectUnderObservation);
            Destroy(objectUnderObservation);
        }
    }

Now that we have handled the logic surrounding the rhomblock’s placement, and determined its position in space – we need to actually create the game object and mesh. This is handled in InstantiateRhomBlock().

First, we create a new game object and its associated meshes. We initialize collections of vertices, triangles and UV. We then gather the vertices from the abstract rhombic dodecahedcron.

    // Block Instantiation ////////////////////////////////////////////// 
    //////////////////////////////////////////////////////////////////// 
    GameObject InstantiateRhomBlock(RhombicDodecahedron rhomBlock)
    {

        // Create a game object and add a mesh renderer.
        GameObject rhomBlockObject = new GameObject("Rhomblock " + index);
        rhomBlockObject.AddComponent<MeshRenderer>();
        rhomBlockObject.AddComponent<MeshFilter>();

        // Obtain Mesh reference.
        Mesh mesh = rhomBlockObject.GetComponent<MeshFilter>().mesh;

        // Initialize collections. 
        List<Vector3> vertices = new List<Vector3>();
        List<int> triangles = new List<int>();
        List<Vector2> UV = new List<Vector2>();

        int currentVertexIndex = 0;

        // Gather vertices from the abstract rhombic dodecahedron. 
        foreach (KeyValuePair<int, System.Numerics.Vector3> entry in rhomBlock.vertices)
        {vertices.Add(new Vector3(entry.Value.X, entry.Value.Y, entry.Value.Z));}

        // Set UV, depending on what type of block is selected
        Vector2 uva = new Vector2();
        Vector2 uvb = new Vector2();
        Vector2 uvc = new Vector2();
        Vector2 uvd = new Vector2();

        if (currentBlockType == BlockType.GREYBLOCK || currentBlockType == BlockType.GLOWBLOCK) 
        {
            uva = greyblock_uva;
            uvb = greyblock_uvb;
            uvc = greyblock_uvc;
            uvd = greyblock_uvd;
        }

        if (currentBlockType == BlockType.DARKBLOCK) 
        {
            uva = darkblock_uva;
            uvb = darkblock_uvb;
            uvc = darkblock_uvc;
            uvd = darkblock_uvd;
        }

The next part of this method involves establishing the triangles of the Rhombic Dodecahedron. Each rhombic face can be decomposed into two triangles. The vertices of these triangles must be wound anti-clockwise. I am sorry to report that I established these triangles by staring at my fever-dream pencil sketch, pictured above, and working it out in my head. The UV array simply needs a UV coordinate for each of the 14 vertices of the dodecahedron. Once again, I came up with these entries by eyeballing my sketch. If you really want to decompose the triangles for yourself, you could compare my array entries to the vertices in my sketch.

If you are interested in a detailed breakdown of how UV and Triangle arrays work for mesh generation in unity, refer to my post, “Rendering the Static Icosphere in Unity Gaming Engine,” where I dive more deeply into these topics. This post is not really about the gritty details of mesh generation.


        triangles.AddRange(new List<int> {
            // Top Faces //////////
            12,  2,  0,  8,  0,  2,
            12,  6,  2, 11,  2,  6, 
            12,  4,  6,  9,  6,  4, 
            12,  0,  4, 10,  4,  0,
            // Side faces /////////
            11,  3,  2,  8,  2,  3,
             8,  1,  0, 10,  0,  1,
            10,  5,  4,  9,  4,  5, 
             9,  7,  6, 11,  6,  7,
            // Bottom Faces ///////
            13,  3,  7, 11,  7,  3,
            13,  1,  3,  8,  3,  1, 
            13,  5,  1, 10,  1,  5, 
            13,  7,  5,  9,  5,  7
        });

        UV.AddRange(new List<Vector2> {
            uvb, uvd, uvd, uvb, uvd, uvb, uvb, uvd, uvc, uvc, uva, uva, uva, uvc
        });

Last, we need to set a few properties of the mesh, map our game object to our Rhombic Dodecahedron, and position it in space.

        // Set the mesh properties; recalculate normals.
        mesh.vertices = vertices.ToArray();
        mesh.triangles = triangles.ToArray();
        mesh.uv = UV.ToArray();
        rhomBlockObject.AddComponent<MeshCollider>();
        mesh.RecalculateNormals();

        // Set material
        rhomBlockObject.GetComponent<Renderer>().material = rhomboidAtlas;

        // Check to see if this is a glowblock.
        if (currentBlockType == BlockType.GLOWBLOCK) 
        {
            rhomBlockObject.GetComponent<Renderer>().material.EnableKeyword("_EMISSION");   
            rhomBlockObject.GetComponent<Renderer>().material.SetColor("_EmissionColor", Color.white);   
            Light light = rhomBlockObject.AddComponent<Light>();
            light.color = Color.white;
            light.intensity = 3;
            light.range = 20;
        }

        // Set position
        rhomBlockObject.transform.position = new Vector3(rhomBlock.center.X, rhomBlock.center.Y, rhomBlock.center.Z);

        return rhomBlockObject;
    }

Results

Finally, we can play with our rhomblocks in 3D space and see what we’re able to create.

The first thing that I quickly learned is that the Rhomblock has essentially two polarities when building structures; its “hexagonal” alignment and its “kite” alignment.

It very quickly became clear that it is impossible to create a flat surface with the rhomblock.

Creating tunnels involved creating a series of stacked, canted hex-rings.

Things start to get interesting when we attempt to create walls. Despite the ‘polarity’ of your floor structure, walls extending from a rhombic ‘plane’ will always have alternating polarities.

Isometric projection

An Alarming Discovery

I continued working on this pattern, until suddenly… a Truncated Octahedron emerged! What?!

You can see a slight error in my patterning in that some of the square faces turned out rectangular.
Isometric projection

The Truncated Octahedron is also a rare, space-filling shape. In fact, the truncated octahedron is the only Archimedean solid that fills space. I began searching for tessellations of the truncated octahedron, and to my astonishment, tessellating a truncated octahedron creates – you guessed it – a Rhombic Dodecahedron!

A rhombic dodecahedron, comprised of truncated octahedrons.
The Convex Hull of a truncated octahedral tessellation

It cannot be a coincidence that of the five semi-regular polyhedra that can fill space (cube, triangular prism, hexagonal prism, rhombic dodecahedron, cuboctahedron), the two polyhedrons that are non platonic and non prismatic seem to coupled in this strange but definite way. Is there a name for this relationship? Surely, it must be well explored in geometry – but after hours of googling, I was unable to discover a specific or named term for this relationship. If I’m lucky, somebody wiser than me will stumble upon this post and let me know what it is that I’ve discovered!

Bonus Content: Linda’s Art

I set my partner loose with the rhomblocks. Here are some of the strange, flower-like, alien motherships she created:

One thought on “The Rhombic Dodecahedron and its Strange Twin

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s