Customizing the Look

Changing the Python Code

The installer script places the code and data in the directory /boot/Pi_Eyes. If you’re not familiar with Linux text editors and so forth, you can move the SD card over to a regular Windows or Mac system, where the “boot” partition appears as a drive and you can edit these files with your text editor of choice.

If using TFT or OLED screens on a Snake Eyes Bonnet board, the Python script of interest is eyes.py. If using HDMI output (with or without the bonnet), look for cyclops.py (so named because it only renders one eye).

After making changes to the code, rather than tracking down and killing processes, it’s often easier just to reboot the Pi. After a minute or so, the revised code will run on startup.

This section near the top of the code (previously mentioned on the Hardware page) includes a couple of items that are relevant to the appearance of the eyes:

# INPUT CONFIG for eye motion ----------------------------------------------

JOYSTICK_X_IN   = -1    # Analog input for eye horiz pos (-1 = auto)
JOYSTICK_Y_IN   = -1    # Analog input for eye vert position (")
PUPIL_IN        = -1    # Analog input for pupil control (-1 = auto)
JOYSTICK_X_FLIP = False # If True, reverse stick X axis
JOYSTICK_Y_FLIP = False # If True, reverse stick Y axis
PUPIL_IN_FLIP   = False # If True, reverse reading from PUPIL_IN
TRACKING        = True  # If True, eyelid tracks pupil
PUPIL_SMOOTH    = 16    # If > 0, filter input from PUPIL_IN
PUPIL_MIN       = 0.0   # Lower analog range from PUPIL_IN
PUPIL_MAX       = 1.0   # Upper "
WINK_L_PIN      = 22    # GPIO pin for LEFT eye wink button
BLINK_PIN       = 23    # GPIO pin for blink button (BOTH eyes)
WINK_R_PIN      = 24    # GPIO pin for RIGHT eye wink button
AUTOBLINK       = True  # If True, eyes blink autonomously

Most relevant here are AUTOBLINK and TRACKING. If AUTOBLINK is changed to False, the eyes will stop their automatic blinking (responding only to button presses, if you have something like that wired up). If TRACKING is changed to False, the eyelids will no longer follow the pupils as they move around (this is an interesting thing that real eyes actually do, but sometimes you want a continuous wide-eyed stare).

Sometimes you may want no eyelids at all…just a full unblinking hemisphere. You can achieve this by simply commenting out the two lines that render the eyelids (put a “#” character at the start of each line). This is much later in the code, near line 430:

        upperEyelid.draw()
        lowerEyelid.draw()

The other settings above are mostly related to hardware stuff.

Changing Graphics

Certain aspects (such as iris color) can be changed by substituting different graphics files. These are in the directory /boot/Pi_Eyes/graphics. One could just overwrite the files that are there, but I prefer to keep the originals around for reference and assign new names to the changed files. To make the code load these changed files, look for this section starting around line 122:

# Load texture maps --------------------------------------------------------

irisMap   = pi3d.Texture("graphics/iris.jpg"  , mipmap=False,
              filter=pi3d.GL_LINEAR)
scleraMap = pi3d.Texture("graphics/sclera.png", mipmap=False,
              filter=pi3d.GL_LINEAR, blend=True)

Most common graphics formats (JPG, GIF, PNG — the latter with or without transparency) are supported. The images are square to make the OpenGL library happy.

The graphics are stored flat and unrolled, like a map projection. The horizontal (X) axis works like the longitude, or angle around the eye, while the vertical (Y) axis is the latitude.

The iris (file iris.jpg) is what we think of as the “color” of the eye and is most often what you’ll want to edit. Sometimes you just need to edit the hue & saturation in a program like Photoshop, or you can make something totally custom if you’re after a particular look.

The sclera (file sclera.png) is the “white” of the eye…which really isn’t that white at all. There’s veins and blotches and gross stuff!

This file is a transparent PNG to simulate the transition where the sclera meets the lens…it’s not an abrupt transition, there’s a slight “fuzziness” to it. When editing, try to preserve that transparency (and note the couple of transparent rows at the bottom, necessary because of the way OpenGL interpolates these images).

(The third file, lidMap, probably shouldn’t be changed. It’s complicated.)

Around line 79 in the code is this:

# Load SVG file, extract paths & convert to point lists --------------------

dom               = parse("graphics/eye.svg")

eye.svg (or cyclops-eye.svg for the single-eye code) is a Scalable Vector Graphics (SVG) file that determines the size and shape of various elements. Suppose you want Krampus eyes, with that freaky horizontal goat pupil? Or dragon eyes with a vertical slit pupil? (In fact there’s an example file there for that — dragon-eye.svg.)

InkScape and Adobe Illustrator (among others) can both load and save SVG files. But…editing / substituting this file is fairly tricky, as the names of individual paths are used in the Python code. If your graphics editor of choice does not maintain these element names exactly when changing or saving the file, the Python code will fail to run.

In Illustrator, you can see the path names by toggling “Layer 1” open:

The largest blue circle there, which extends to the edges of the document, can mostly be ignored…it’s there for reference and represents the outer bounds of the eye ball (which can’t be changed).

The iris path (which needs to remain a circle, though you can change its size) determines the size of the outer edge of the iris relative to the whole eye.

pupilMin and pupilMax are the size and shape of the pupil in its most contracted and most dilated positions, as it responds to light. This doesn’t need to be a circle! Have a look at dragon-eye.svg for example.

Two additional blue circles — scleraFront and scleraBack — are used in determining the “sweep” of the white of the eye. Notice it overlaps the outer edge of the iris slightly, and is open at the back (we have no need for the back of the eye, so it’s not modeled or rendered).

Red paths are used for animating the eyelids. upperLidOpen and upperLidClosed are the shape of the upper eyelid in its fully-open and fully-closed positions. lowerLidOpen and lowerLidClosed are the same for the lower eyelid. upperLidEdge and lowerLidEdge are used by the software to generate the other edge of the eyelid mesh geometry, which is really a 2D polygon that occludes the eye behind it.

You’ll notice the default eye is not symmetrical. Eyes are interesting things and have a unique shape left and right! Looking at the SVG graphics, the other eye (and nose, if we had one) would be to the left. cyclops-eye.svg is left/right symmetrical…it’s designed for the Pi’s single HDMI output, which might be split to two identical displays…the asymmetrical eye would look weird and lopsided in that case, so we go for the naïve “football shape.”

(At some point I hope to add a translucent nictitating membrane to the dragon eye, but this hasn’t happened yet.)

Replacing Everything

Well, almost everything.

Our Python code generates OpenGL animation that goes to the Pi’s normal HDMI video framebuffer, whether or not there’s actually an HDMI display attached. A separate program — fbx2 — continuously copies two sections of the framebuffer to the TFT or OLED displays attached to the Snake Eyes Bonnet.

This means, if you really want, the entire eye-animating program could be replaced with code of your own design in whatever language you like. As long as your code positions graphics in the right places, fbx2 will present that on the TFT/OLED screens. (I’ve even tried installing fbx2 over RetroPie — yes, you can play Doom on these eyes!)

The pi-eyes.sh installer script configures the Pi’s video output resolution to 640x480 pixels. It could actually be any supported resolution, but 640x480 was chosen for a reason, explained in a moment…

Picture two squares, side-by-side, that span the full width of the screen. On a 640x480 screen, that would be two 320x320 squares, with some unused space above and below. Within each square, picture another square inset, 80% the size. Or at this resolution, 256x256 pixels, with upper-left corners at…well, you can do the math. These two squares are what fbx2 copies to the TFT/OLED screens.

  • The 20% inset allows some “slop” off the edges of each eye, rather than butting them up right against each other. We use that extra space when generating the eyelid geometry. There’s stuff out there!
  • 256x256 pixels is exactly twice the resolution of the TFT/OLED displays, and the GPU provides us a fast and high-quality 2:1 downsampling in fbx2, so the images look super extra smooth. Any more or less and things wouldn’t look quite as good.
Last updated on 2017-09-26 at 12.30.30 AM Published on 2017-01-11 at 05.41.14 PM