rhombic dodecahedral honeycombs

this is a rhombic dodecahedron:

it's a polyhedra with 12 identical faces and 14 vertices in 2 types -- 6 that have 4 edges, and 8 that have 3 edges. its most important feature, for our purposes, is that like cubes, it has a space-filling packing, so it's possible to stack a whole bunch of them together and fill space with no gaps and no overlaps. in a lot of ways, they're like the hexagons of 3d: if you take a bunch of circles and squeeze them together as tightly as possible, you can flatten their edges to get a bunch of hexagons on a hexagonal tiling, and likewise if you take a bunch of spheres and squeeze them together as tightly as possible, you can flatten their surfaces and turn them into rhombic dodecahedra in a rhombic-dodecahedral honeycomb.

that being said, much like how a hexagonal tiling requires a bit more math to construct than a square one, a rhombic-dodecahedral honeycomb requires a bit more math to construct than a cubic honeycomb. this is about all of that math.

(for the record, 'honeycomb' is the math word for a 3d tiling. i find that a really weird choice, but given that literal honeycombs also involve rhombic dodecahedrons, i'm giving that a pass here. but i'll still sometimes talk about cubic honeycombs or tetrahedral-octahedral honeycombs.)


the coordinate system

first, i'm going to be using a somewhat complicated coordinate system for this. when constructing grids, i'd like to keep track of a few things: the positions of the centers of each rhombic-dodecahedral cell, for one, but also the positions of all the vertices needed to render a rhombic-dodecahedral cell. and i'd like for all of these to be precise grid units: no floating-point grid values here. (unlike some polyhedra, the rhombic dodecahedron actually has all its vertices at integer values to begin with: they're all the permutations of ±2 0 0 or ±1 ±1 ±1.)

we're immediately running into a problem that you simply don't see in cubic honeycombs. if we only have the cell centers, we can't connect them together to get rhombic-dodecahedral cells. if we tried to do that, we'd actually be constructing the dual of the rhombic-dodecahedral honeycomb; the tetrahedral-octahedral honeycomb. a cubic honeycomb is self-dual, which means you can hook together cell centers to get a cubic honeycomb, but that's not how a rhombic-dodecahedral honeycomb is put together; you need additional points. the thing is, we can use that dual property: if we construct a tetrahedral-octahedral honeycomb, and figure out where all the cell centers are for that, we can hook those together to get all the vertices and edges we need to render a rhombic-dodecahedral honeycomb! these two honeycombs being the dual of each other means that if we want to draw one, we need to have some information about the other, because cell centers in one are cell boundaries in the other.

this leads to a kind of hybrid grid, that stores not just cell centers, but also coordinates for vertices. or, equivalently, this coordinate system stores both rhombic-dodecahedral cells and tetrahedral-octahedral cells together. i'm using 3d points for this, or at least three values, 0 0 0, but you shouldn't think of these values as having an immediate relationship to 3d cartesian space; i'll get to that later. think of hexagonal grid units, where you can have offset lines, or non-orthogonal axes, and you need some transform to position them on the plane.

data CoordType
  = RhombicDodecahedron
  | Octahedron | Tetrahedronα | Tetrahedronβ
  | InvalidPoint
  deriving (Eq, Ord, Enum, Show, Read)

c :: V3 Integer -> CoordType
c (V3 x y z)
  | (y `mod` 4) == 1 && even x &&  odd z = Tetrahedronα
  | (y `mod` 4) == 1 &&  odd x && even z = Tetrahedronβ
  | (y `mod` 4) == 3 && even x &&  odd z = Tetrahedronβ
  | (y `mod` 4) == 3 &&  odd x && even z = Tetrahedronα
  | (y `mod` 4) == 0 && even x && even z = Octahedron
  | (y `mod` 4) == 0 &&  odd x &&  odd z = RhombicDodecahedron
  | (y `mod` 4) == 2 && even x && even z = RhombicDodecahedron
  | (y `mod` 4) == 2 &&  odd x &&  odd z = Octahedron
  | otherwise = InvalidPoint

(there are two tetrahedron values because there are two different rotations of the tetrahedron that appear in the tetrahedral-octahedral honeycomb)

(also, for the record, i did not put this together by myself. twocubes from tumblr was the person who assembled the basic coordinate interactions that make this whole thing possible; i definitely couldn't have done this by myself.)

to chart out some values, this is what the grid looks like laid out (O for octahedron, Tα/Tβ for tetrahedrons, and RD for rhombic dodecahedrons. blank cells are points unused in the coordinate system):

y:0z
0123
x0 OO
1 RDRD
2 OO
3 RDRD
y:1z
0123
x0
1
2
3
y:2z
0123
x0 RDRD
1 OO
2 RDRD
3 OO
y:3z
0123
x0
1
2
3

so half of the grid units are used; the other positions represent weird midway, half-aligned values that aren't useful. even y-layers are all octahedrons and rhombic dodecahedrons, and odd y-layers are all tetrahedrons, and on top of that, every other layer swaps the positions of the octahedrons & rhombic dodecahedrons, or tetrahedron-a & tetrahedron-b.

(this incidentally means that 0 0 0 isn't the center of a rhombic dodecahedral cell, which might be weird. tbh this is just an arbitrary artifact of the way this coordinate system is constructed; there's absolutely no reason why you couldn't shift the math two layers down, so that 0 0 0 is a rhombic-dodecahedral cell and 0 2 0 is an octahedral cell. i just haven't done that and don't feel like restructuring the equations.)

(you could 'flatten' this system so that instead of being on separate layers, all the tetrahedrons are pushed down to fit in the open gaps of the octahedron/rhombic dodecahedron layers. then you'd get a nice coordinate system with no gaps, but it would mean you'd have to do a little more conversion to get 3d values out of the coordinates.)

the important thing is that this codifies up the vertices of the rhombic dodecahedron: each RD is the center of a cell, and its fourteen vertices are the four / cells diagonally above it, the four / cells diagonally below it, and then the four O cells diagonally to the side on the same level plus the two O cells two layers directly above and below it.

-- | given a coordinate point, return all the vertices used to
-- | construct that polyhedra
vertices :: V3 Integer -> [V3 Integer]
vertices x =
  let offsets = case c x of
    Tetrahedronα ->
      -- 4 rhombic dodecahedra centers
      [ V3 0 1 1, V3 0 1 (-1), V3 (-1) (-1) 0, V3 1 (-1) 0
      ]
    Tetrahedronβ ->
      -- 4 rhombic dodecahedra centers
      [ V3 (-1) 1 0, V3 1 1 0, V3 0 (-1) 1, V3 0 (-1) (-1)
      ]
    Octahedron ->
      -- 6 rhombic dodecahedra centers
      [ V3 0 2 0, V3 0 (-2) 0, V3 1 0 1
      , V3 1 0 (-1), V3 (-1) 0 1, V3 (-1) 0 (-1)
      ]
    RhombicDodecahedron ->
      -- 6 octahedra centers
      [ V3 0 2 0, V3 0 (-2) 0, V3 1 0 1
      , V3 1 0 (-1), V3 (-1) 0 1, V3 (-1) 0 (-1)
      -- 4 tetra-a centers
      , V3 1 1 0, V3 (-1) 1 0, V3 0 (-1) 1, V3 0 (-1) (-1)
      -- 4 tetra-b centers
      , V3 1 (-1) 0, V3 (-1) (-1) 0, V3 0 1 1, V3 0 1 (-1)
      ]
    InvalidPoint -> error $ "invalid point: " ++ show x
  in (x +) <$> offsets

note that this displays the dual structure: the rhombic dodecahedron is rendered by drawing to the centers of various octahedra and tetrahedra, and the tetrahedra and octahedron are rendered by drawing to the centers of various rhombic dodecahedra.

there's also the face-adjacent cell calculation, here:

-- | give coordinates of all face-adjacent cells
adj :: V3 Integer -> [V3 Integer]
adj x = let offsets = rawAdj x in (x +) <$> offsets

-- | the raw coordinate offsets between face-adjacent cells.
-- | this is useful both for calculating adjacent coordinates
-- | and for generating shared faces
rawAdj :: V3 Integer -> [V3 Integer]
rawAdj x = case c x of
  Tetrahedronα ->
    [ V3 (-1) 1 0, V3 1 1 0, V3 0 (-1) 1, V3 0 (-1) (-1)
    ]
  Tetrahedronβ ->
    [ V3 1 (-1) 0, V3 (-1) (-1) 0, V3 0 1 (-1), V3 0 1 1
    ]
  Octahedron ->
    [ V3   0  1 1, V3 0 1 (-1), V3 (-1) (-1) 0, V3 1 (-1)   0
    , V3 (-1) 1 0, V3 1 1   0 , V3   0  (-1) 1, V3 0 (-1) (-1)
    ]
  RhombicDodecahedron ->
    [ V3   2    0    0 , V3   0    0    2 , V3   1    2    1
    , V3   1    2  (-1), V3 (-1)   2    1 , V3 (-1)   2  (-1)
    , V3   1  (-2)   1 , V3   1  (-2) (-1), V3 (-1) (-2)   1
    , V3 (-1) (-2) (-1), V3   0    0  (-2), V3 (-2)   0    0
    ]
  InvalidPoint -> error $ "invalid point: " ++ show x

where now all the rhombic dodecahedron cells are only ever face-adjacent to other rhombic dodecahedron cells, and, due to the construction of the tetrahedral-octahedral honeycomb, all tetrahedrons are face-adjacent to only octahedrons, and octahedrons are face-adjacent to only tetrahedrons.


further detail about dual honeycombs

if we take a closer look at the coordinate grid above, we can discern some patterns:

mouseover the coordinates to hilight adjacent values -- either 'used vertices', which will always show cells in the dual honeycomb (which dual values are used as vertices), or 'adjacent faces', which will always show cells in the same honeycomb (which cells share a face with the selected cell). note how you can see the rotational differences between the two tetrahedron types here.

so, some general information about the connectivity here:

a rhombic dodecahedron with its vertex polyhedra drawn half-size. note that if they were drawn full-size, they'd form a space-filling packing: that's the tetrahedral-octahedral honeycomb


conversion to 3d space

that's all well and good, but so far this has all just been topological connections. all these coordinates don't really correspond to real 3d space. there's a little bit of a transform needed to get them aligned correctly.

as mentioned before, the rhombic dodecahedron has integer-aligned vertices: six vertices at ±2 0 0, 0 ±2 0, and 0 0 ±2, and eight vertices at ±1 ±1 ±1 (the vertices of a unit cube). (the six 2-length vertices are the 'octahedral' ones; the eight others are the 'tetrahedral' ones, incidentally.)

the transform to get 3d-space coordinates from the coordinate system i've been using is actually incredibly simple:

lattice :: V3 Integer -> V3 Float
lattice (V3 x y z) = V3 x' y' z'
  where
    x' = fromIntegral $ x + z
    y' = fromIntegral $ y
    z' = fromIntegral $ x - z

that generates cells that are 'pointy-side up'. if you want cells that are 'flat-side up' you need a different function:

lattice :: V3 Integer -> V3 Float
lattice (V3 x y z) = r $ V3 x' y' z'
  where
    r = rotate $ axisAngle (V3 0 0 1) (pi/4)
    x' = fromIntegral $ x + z
    y' = fromIntegral $ y
    z' = fromIntegral $ x - z

(this one is a little more complicated, and involves the same basic transform as the above, followed by rotating everything 45° around the z axis. this means that instead of values being neatly aligned at grid units, basically everything has position values that're around the square root of 2. if you don't know how to implement an axis-angle rotation, just check the javascript for this page, or refer to the matrix and quaternions FAQ.)

'pointy' cell in red, 'flat' cell in blue.

in addition to those ways, you can also align them to be 'slightly-pointy' side up, where one of their three-edge vertices is aligned up. this has the interesting property of aligning the honeycomb to a hexagonal grid, where six of its faces are in the 'same plane', three are aligned to the half-offset grid a layer 'up', and the other three are aligned to the half-offset grid a layer 'down':

(this is the one i'm the least aware of how to align upwards, so this axis is a little bit off. whoops.)

regardless of how you render them, the coordinate system is the same, all that changes is how it's rendered in real 3d space.

to actually render rhombic dodecahedral cells, there is one more thing you need: the correct faces and windings. for drawing in opengl, i have an additional function:

data RDFace = RDFace Int

rFace :: RDFace -> [V3 Integer]
rFace (RDFace i) = case i of
  0  -> [ V3   1   0   1 , V3   1   1    0 , V3   1   0 (-1), V3   1  (-1)   0 ]
  1  -> [ V3 (-1)  0   1 , V3   0   1    1 , V3   1   0   1 , V3   0  (-1)   1 ]
  2  -> [ V3   0   2   0 , V3   1   1    0 , V3   1   0   1 , V3   0    1    1 ]
  3  -> [ V3   1   0 (-1), V3   1   1    0 , V3   0   2   0 , V3   0    1  (-1)]
  4  -> [ V3   0   2   0 , V3   0   1    1 , V3 (-1)  0   1 , V3 (-1)   1    0 ]
  5  -> [ V3 (-1)  0 (-1), V3   0   1  (-1), V3   0   2   0 , V3 (-1)   1    0 ]
  6  -> [ V3   1   0   1 , V3   1 (-1)   0 , V3   0 (-2)  0 , V3   0  (-1)   1 ]
  7  -> [ V3   0 (-2)  0 , V3   1 (-1)   0 , V3   1   0 (-1), V3   0  (-1) (-1)]
  8  -> [ V3 (-1)  0   1 , V3   0 (-1)   1 , V3   0 (-2)  0 , V3 (-1) (-1)   0 ]
  9  -> [ V3   0 (-2)  0 , V3   0 (-1) (-1), V3 (-1)  0 (-1), V3 (-1) (-1)   0 ]
  10 -> [ V3   1   0 (-1), V3   0   1  (-1), V3 (-1)  0 (-1), V3   0  (-1) (-1)]
  11 -> [ V3 (-1)  0 (-1), V3 (-1)  1    0 , V3 (-1)  0   1 , V3 (-1) (-1)   0 ]
  _  -> error "rFace: invalid RDFace"

which gives you the offset from a given rhombic dodecahedron cell to get each of the rhombus faces in draw-order. here are eight 'flat' rhombic dodecahedrons rendered together:


rendering other honeycombs

...

(these functions are never used for actual rendering, so i threw them together for this demo, and so they might not all have the correct windings for real rendering)

taFace :: TAFace -> [V3 Integer]
taFace (TAFace i) = case i of
  0 -> [V3   0    1   1 , V3   0    1 (-1), V3 (-1) (-1)  0 ]
  1 -> [V3   0    1 (-1), V3 (-1) (-1)  0 , V3   1  (-1)  0 ]
  2 -> [V3 (-1) (-1)  0 , V3   1  (-1)  0 , V3   0    1   1 ]
  3 -> [V3   1  (-1)  0 , V3   0    1   1 , V3   0    1 (-1)]

tbFace :: TBFace -> [V3 Integer]
tbFace (TBFace i) = case i of
  0 -> [V3 (-1)  1    0 , V3   1   1    0 , V3   0 (-1)   1 ]
  1 -> [V3   1   1    0 , V3   0 (-1)   1 , V3   0 (-1) (-1)]
  2 -> [V3   0 (-1)   1 , V3   0 (-1) (-1), V3 (-1)  1    0 ]
  3 -> [V3   0 (-1) (-1), V3 (-1)  1    0 , V3   1   1    0 ]

oFace :: OFace -> [V3 Integer]
oFace (OFace i) = case i of
  0 -> [V3 0   2  0, V3   1  0   1 , V3   1  0 (-1)]
  1 -> [V3 0   2  0, V3   1  0 (-1), V3 (-1) 0 (-1)]
  2 -> [V3 0   2  0, V3 (-1) 0 (-1), V3 (-1) 0   1 ]
  3 -> [V3 0   2  0, V3 (-1) 0   1 , V3   1  0   1 ]
  4 -> [V3 0 (-2) 0, V3   1  0   1 , V3 (-1) 0   1 ]
  5 -> [V3 0 (-2) 0, V3 (-1) 0   1 , V3 (-1) 0 (-1)]
  6 -> [V3 0 (-2) 0, V3 (-1) 0 (-1), V3   1  0 (-1)]
  7 -> [V3 0 (-2) 0, V3   1  0 (-1), V3   1      1 ]

using those, you can draw tetrahedrons and octahedrons; that's how i drew the tetrahedra-octahedral honeycomb in the demo above. using all of them together, you can also draw the rectified honeycomb:

which contains rhombic prisms as well as rhombic dodececahedrons, tetrahedrons, and octahedrons. probably don't, though, this is all complicated enough as it stands.


(and that's the end of this so far. stuff i should probably add:

)