The Reutersvärd Triangle

(0 comments)

The Reutersvärd Triangle is an example of an impossible object: a two-dimensional optical illusion perceived as a projection of a three-dimensional object that cannot exist. The Python code below creates an SVG image of Reutersvärd's impossible triangular arrangement of cubes, similar to the more famous Penrose Triangle.

Reutersvard triangle

The image is created by first defining the vertices and faces of the unit cube centered on the origin, rotating it intoa suitable orientation, and then projecting displaced copies of it onto the edges of a triangle in the SVG canvas. By redrawing some of the faces of a couple of the cubes it is possible to create the illusion in the figure above. Without redrawing these faces (set IMPOSSIBLE = False in the code), a "possible" triangle can be produced:

A possible triangle

import numpy as np

IMPOSSIBLE = True

# The vertices of the unit cube centered on the origin.
vertices = np.array(((-1,-1,-1), (-1,1,-1), (1,-1,-1), (1,1,-1),
                     (-1,-1,1), (-1,1,1), (1,-1,1), (1,1,1)
                    )) / 2
# An array of faces, each referred to as the indexes into the vertices array.
faces = np.array(((0,1,3,2), (2,3,7,6), (0,2,6,4),
                  (4,6,7,5), (0,4,5,1), (5,7,3,1),
                ))
# There are three distinct face colours; opposite faces have the same colour.
face_colours = ('#96ceb4', '#ffcc5c', '#ff6f69')*2

def rotate(arr, axis, th):
    """Rotate the array of vectors arr by th about axis='x','y' or 'z'."""

    c, s = np.cos(th), np.sin(th)
    if axis == 'x':
        R = np.array(((1,0,0), (0,c,-s), (0,s,c)))
    elif axis == 'y':
        R = np.array(((c,0,s), (0,1,0), (-s,0,c)))
    elif axis == 'z':
        R = np.array(((c,-s,0), (s,c,0), (0,0,1)))
    else:
        raise ValueError("axis must be one of 'x', 'y' or 'z'")
    return (R @ arr.T).T

vertices = rotate(vertices, 'y', np.pi/4)
vertices = rotate(vertices, 'x', np.pi/4)
vertices = rotate(vertices, 'z', np.pi/6)

def scale_and_translate(vertices, displacement, scale=1):
    """Scale and translate the vertices array."""

    return vertices*scale + displacement

def get_path(face_array, vertices):
    """Return the SVG path attribute string for a given face."""

    path = ' '.join(['L{},{}'.format(x,y) for (x,y,z) in vertices[face_array]])
    path = 'M' + path[1:] + 'Z'
    return path

def svg_path(path, face_colour):
    """Return the SVG path element for a given face."""

    return '<path d="{path}" fill="{face_colour}" stroke="none"/>'.format(
                path=path, face_colour=face_colour)

def face_visible(face_array):
    """Is the face described by face_array visible?"""

    # The unit vector in the positive z-direction.
    zhat = np.array((0,0,1))

    # Calculate a normal vector to the (convex) polygonal face: the face is
    # only visible if this vector points towards the viewer (ie in the
    # negative z-direction.
    v0, v1, v2 = vertices[face_array[:3]]
    normal = np.cross(v1-v0, v2-v1)
    if normal @ zhat > 0:
        return False
    return True

# The nine displacement vectors to the centres of each cube.
ncubes = 9
displacements = np.zeros((ncubes,3))
th = np.arange(0,2*np.pi,2*np.pi/3) + np.pi/6
# Every third displacement lies on a circle (the corners of the triangle).
displacements[::3] = np.array((np.cos(th), np.sin(th), np.zeros_like(th))).T
# Fill in the remaining displacements by linear interpolation between corners.
idx = np.array((1,4,7))
displacements[idx] = displacements[idx-1]*2/3 + displacements[(idx+2)%9]/3
displacements[idx+1] = displacements[idx-1]/3 + displacements[(idx+2)%9]*2/3
# Apply a bit of scaling to the displacements so that the object "looks right".
displacements *= 2

def redraw_faces_impossibly():
    """Redraw some faces to make the object 'impossible'."""

    displacement = displacements[0]
    displaced_cube = scale_and_translate(vertices, displacement)
    print(svg_path(get_path(faces[2],displaced_cube),face_colours[2]),file=fo)
    print(svg_path(get_path(faces[0],displaced_cube),face_colours[0]),file=fo)
    displacement = displacements[1]
    displaced_cube = scale_and_translate(vertices, displacement)
    print(svg_path(get_path(faces[2],displaced_cube),face_colours[2]),file=fo)
    print(svg_path(get_path(faces[1],displaced_cube),face_colours[1]),file=fo)


with open('reutersvard.svg', 'w') as fo:
    print('<svg width="500" height="500"'
      ' xmlns="http://www.w3.org/2000/svg"'
      ' xmlns:xlink= "http://www.w3.org/1999/xlink" viewBox="-4 -4 8 8">',
          file=fo)

    for j,displacement in enumerate(displacements):
        displaced_cube = scale_and_translate(vertices, displacement)

        for i, (face_array, face_colour) in enumerate(zip(faces,face_colours)):
            if not face_visible(face_array):
                continue
            print(svg_path(get_path(face_array, displaced_cube), face_colour),
                  file=fo)

    if IMPOSSIBLE:
        redraw_faces_impossibly()

    print('</svg>', file=fo)
Current rating: 5

Comments

Comments are pre-moderated. Please be patient and your comment will appear soon.

There are currently no comments

New Comment

required

required (not published)

optional

required