3D-printable hexagonal mirror array capable of reflecting sunlight into arbitrary patterns

Overview

3D printed mirror array

I recently got engaged to the wonderful person holding the hexagonal mirror thingy in the photo below. We're both huge nerds and I wanted to do something unique for a proposal, so I designed and 3D printed a mirror array to ask the question for me. The mirrors are angled so that just before sunset on our 8th anniversary, it reflects the light from the setting sun onto the ground to spell out "MARRY ME?"

Since I had a lot of fun with this project and because I love open-source code, I figured I'd publish all the source code I wrote to generate the 3D printed model and make a writeup of how I designed this. It is straightforward to modify the code to produce a 3D-printable mirror array to project any arbitrary image onto any focal plane.

Supplies

  • Python 3 and Jupyter with the following libraries installed: numpy, matplotlib, numpy-stl, hexy, vpython
    • vpython is optional if you don't care about rendering the ray tracing visualizations
  • A 3D printer
    • I bought a Creality Ender 3 v2 for this project, which I am reasonably happy with, but any FDM printer with a reasonable build volume and ~.1mm accuracy should work fine.
    • Optional: some hair spray or 3D printing adhesive spray (to keep large prints from warping)
  • Any decent PLA filament: [amazon.com/dp/B01EKEMFQS]
  • 1-inch hexagonal mirror tiles: [amazon.com/dp/B08CSCNT86] or [amazon.com/dp/B07DF5W7L6]
  • Cyanoacrylate glue: [amazon.com/dp/B01LYO4R4I]
    • Most "super glue" is this type. You can use any glue that can bind plastic to glass, but it is important that you use glue that doesn't expand when it cures or it will mess up the very sensitive mirror angles! A very thin coating of cyanoacrylate glue is sufficient to hold the mirrors in place - too much glue can also distort the angles.
  • The Sun

Computing the mirror angles

The basic idea is that we have an array of mirrors forming a hexagonal grid. Each mirror is located with its centroid at some point in space, and we want for it to reflect a ray of sunlight to a pixel at some location on the ground (or any other focal plane). Since we know where each mirror is, where each corresponding target is, and where the sun is, we can solve for how each mirror needs to be oriented in 3D space in order to reflect the sunlight onto its corresponding target.

Consider just a single mirror and a single target pixel. The center of the mirror has coordinates located at some vector mirror_pos, and the center of the target pixel is at target_pos. Let's define vectors such that mirror_pos - target_pos is the vector pointing from the target to the mirror center, and is the vector pointing in the direction of the sun (a point source placed at infinity). Here we're assuming the sun is at an inclination of from the horizon and that the azimuthal angle relative to the mirror is .

Since the reflected angles are the same as the incident angles for a mirror, the normal vector of the mirror just needs to bisect these two vectors and , so we have that :

Now that we know how a mirror with a given location and target needs to be angled, let's move on to generating the 3D models for the mirror frame which will hold the mirror at this point in space with this correct orientation.

Generating a 3D model of a hexagonal prism

A 3D model is just a list of vertices and faces. The vertices are the corners of the polygon (a list of 3D coordinates) and the faces are a list of triangular facets, where each face is specified by tuple containing the indices of three vertices. So to generate the 3D model of the hexagonal prism, I just needed to compute the coordinates for the corners of the prism and define the triangular facets that form the outer faces of the prism.

The 3D printed plastic frame of the mirror array is made up of a grid of hexagonal prisms. Each hexagonal prism has its base on the plane and has a position specified by the location of the centroid of the top face (coordinate 7 in the left image), which is normal to the vector . To compute the coordinates for the top corners, I computed the intersection of the plane defined by and the vector pointing vertically from each corner of the base (right image). I then just used a hard-coded list of vertex indices to specify the faces (so, for example, one face is (0,5,6), while another is (5,12,13) when referring to the left image).

Another thing I included in this function was the ability to add a mirror aligner structure to the prism. This mirror aligner is a small protrusion on two sides of the hex prism which fits on a corner of the hexagonal mirrors, which makes it much less tedious to glue hundreds of mirrors to the final structure (right image below). A subtle detail here is that the coordinates of the mirror aligner are not collinear with the coordinates of the top corners of the hex prism: the mirror aligner always needs to make a angle, but if the hex prism has a top surface with a steep slope, the corner angles get distorted. So to compute the coordinates of the mirror aligner, I computed the projection of the x axis onto the plane of the the top of the prism as . Then I rotated this vector about the normal vector by increments of to get the corners of the aligner.

Generating a hexagonal grid of prisms

Once I had the code to generate the hex prim and the mirror aligner, it is straightforward to generate a grid of these structures, since fusing 3D models can be as simple as concatenating the lists of vertices and faces. This approach does result in some unwanted internal geometry, but most slicer software is smart enough to figure this out and ignore internal geometry when printing.

I generated the hexagonal grid coordinates that define the center of each pillar, then I generated the list of hex prisms with the appropriate orientations to reflect onto a list of target coordinates, and finally I fused the list of prisms together into a single .stl file. For aesthetic reasons, I chose to separate the prism by about a 2mm gap and have them joined at the bottom by a slightly wider prism. An example 3D model of a small grid of prisms which all have a common focal point is shown below. The aligner structures are highlighted in red. (Note the difference in angles between the aligner corners and the prism top corners.)

Optimally matching targets to mirrors

One of the more interesting problems to solve in this project was how to optimally match target light spots to corresponding mirrors to maximize the focal distance tolerance. (So that you can hold the structure at a slightly incorrect distance or orientation from the focal plane and it will still reflect a legible message.)

The mirror array will project a set of rays onto the focal plane to form an image, and when the structure is placed at exactly the right angle and distance from the focal plane, the image will (in theory) be exactly what you want it to be. But if you deviate from this height or angle, the image will slightly deform. However, you can mitigate this by making the rays as parallel as possible, so as to minimize any times where rays will cross each other before the focal plane.

Consider the following scenario in the figure below. There are four mirrors projecting light rays onto four targets which form a diamond formation when placed at the proper focal distance. On the left side, two of the rays will cross each other, while on the right side, the rays are closer to parallel and do not cross. If you move the focal plane relative to the mirror array, the left image will distort, becoming vertically squished, while the right image does not significantly distort (the points remain in the same relative configuration).

So we want to match the target points with corresponding mirrors so that the rays are as parallel as possible and minimal crossings occur. How should we do this? An early approach I took was to iterate over the mirrors and greedily assign each target to the mirror which minimizes the angle of incidence (so, minimizing the inner product of ). However, this approach didn't work as well as I had hoped because it only considered the magnitude of this inner product rather than the direction of the reflected light beam. You can see some of the distortion in the early test prints of the heart mirrors in the next section, which used this initial matching algorithm. (Although some of that distortion was also due to poor adhesion to the print bed.)

The algorithm I ended up settling on is inspired by the structure of the hexagonal grid itself. A hexagonal grid with radius of hexagons has a number of mirrors. So there's one mirror at the center (), and after that, 6 mirrors in the innermost ring (), 12 mirrors in the next ring, then 18, and so on. To minimize ray crossings, I computed the center of mass of the target pixel coordinates, then grouped the coordinates into bins based on their distance from the center of mass, where each bin has a size equal to the number of mirrors in that ring. So the centermost target pixel gets assigned to the center mirror, then the 6 other pixels closest to the center gets assigned to the first ring, and so on. Within each bin, I ordered the target coordinates clockwise starting from the bottom coordinate, and I assigned the target to the mirrors iterated in the same order. So the outermost ring of mirrors corresponds to the outermost ring of targets, and within this set of targets, the bottom target gets assigned to the bottom mirror, the leftmost target gets assigned to the leftmost mirror, etc., as shown in the figure below.

This matching algorithm ended up working quite well. (And I do love matching algorithms...) Here is a side by side comparison of the rays which form the "MARRY ME?" message when assigned to mirrors randomly (left) versus when assigned using this matching algorithm (right). You can see that the rays are much more parallel. This means that the mirror array can be held within a much wider tolerance of the correct distance from the focal plane while still projecting the message legibly.

(Also, I discovered the biggest near-failure of this project around the time I was writing the visualization code that I used to generate the images above. At that point, I was about 20 hours into printing the final mirror array frame, and I realized that what I would have printed would actually project the mirror image of what I wanted! "?EM YRRAM" I hadn't caught this issue previously because the test image of the heart was symmetric, so I didn't realize that I was printing the mirror of the correct image. Fortunately I found this mistake in time, since by this point the proposal date was just a week out, so if I had caught this any later I wouldn't have had time to print another frame. The takeaway from this near-miss is that writing visualization code to help debug what is going on is very important... 🙃 )

Printing the mirror array frame

So by this point I had developed the end-to-end process where you can input a set of coordinates that form your desired image, the location of the focal plane, and the location of the sun, and you get out a .stl model of the mirror array frame that you can 3D print. So now we just need to print this out and glue on the mirrors -- simple, right?

I printed four different attempts at a smaller-scale mirror array to test for the big thing. Each small frame had 37 mirrors and would project the design of a heart. (37 mirrors corresponds to a radius hex grid, which was the largest I could print out in a single piece given my Ender3's 220mm × 220mm print area.)

There were two main problems I encountered when printing these test arrays. The first was warping due to the print not fully adhering to the print bed. This would cause the corners at the bottom to slightly peel up due to tension from the cooling layers, which would affect the angles of the hexagonal pillars above the warped base region and distort the projected image. I solved this problem by lowering the bed temperature and applying some hairspray to the print bed (some 3D printing tribal knowledge...), which worked like a charm.

The second problem was how to print out a hex grid which was too big to fit within the print volume. Some slicer programs include built in tools to slice large prints into pieces, but I couldn't use these because the planar cuts would slice through the hexagonal pillars, which would create a ridge on the top face and distort the angles of the mirrors, which need to be accurate to about (which is a few tenths of a degree). So I created a few custom functions to input a set of pillar models and partition them into smaller printable sub-volumes which could be glue together. This way, the only seams would be at the base of the model, which wouldn't affect the normal vectors of the mirrors.

I played around with a couple of different partitioning schemes, but I settled on using this flower-like pattern shown below, both because I liked the way it looked, and because it provides more structural strength since there is no continuous seam across the entire structure along which the assembled object could snap in half.

The entire model took about a week to print (I used adaptive layer heights to speed things up a bit when printing the bottom and middle layers, but the top layers needed to be very fine to allow the mirrors to be mounted at exactly the right angles). I assembled the frame and used cyanoacrylate glue to bond each section of the frame at the base. (I had to slightly sand the edges of the final section to get it to fit.) The fully assembled frame is shown below.

The last step was carefully glueing all 196 mirrors to the completed frame. I found out on some of my earlier test structures that cyanoacrylate glue will produce a lot of vapors when it is curing which can deposit on the mirrors and cause them to look foggy. (They still reflect enough light to work but it looks uglier...) To prevent this, I had a fan running blowing air from right to left while I glued the mirrors, which I attached in a right-to-left, top-to-bottom ordering to minimize vapor exposure.

By the time I finished mounting all of the mirrors, it was the night before I was planning to propose, so in a nail-biting twist of fate, I didn't actually get to test the final structure at sunset in advance. Fortunately, it worked exactly as I wanted it to! (And even more fortunately, the temperamental fog of the SF Bay Area decided to take the day off.)

This was the first project I've done with 3D printing (and in fact I got into 3D printing specifically to do this idea), and it felt like magic having a fabricator sitting in my bathroom that could just bring this idea into existence from nothing but a bit of code and some basic principles of physics.

VIDEO_mirror_array_on_wall.mp4
Owner
Ben Bartlett
PhD candidate in applied physics at Stanford @fancompute. Currently working @Google. I like photonics, Python, and cats.
Ben Bartlett
Micropython-wifimanager-esp8266 - Simple Wifi Manager for ESP8266 using MicroPython

micropython-wifimanager-esp8266 Simple Wifi Manager for ESP8266 using MicroPytho

Abhinuv Nitin Pitale 1 Jan 04, 2022
Smart Tech Automation Remote via Kinematics Gesture control for IoT devices

STARK Smart Tech Automation Remote via Kinematics Gesture control for IoT devices View Demo · Report Bug · Request Feature Table of Contents About The

Juseong (Joe) Kim 1 Jan 29, 2022
Aqara Camera G3 integration for Home Assistant

Aqara Camera G3 integration for Home Assistant ATTENTION: The component only works after enabled telnet. Only supportd stream. Not support still image

14 Dec 18, 2022
Keystroke logging, often referred to as keylogging or keyboard capturing

Keystroke logging, often referred to as keylogging or keyboard capturing, is the action of recording the keys struck on a keyboard, typically covertly, so that a person using the keyboard is unaware

Bhumika R 2 Jan 11, 2022
MPY tool - manage files on devices running MicroPython

mpytool MPY tool - manage files on devices running MicroPython It is an alternative to ampy Target of this project is to make more clean code, faster,

Pavel Revak 5 Aug 17, 2022
This is an incredible led matrix simulation using the ultimate mosaik co-simulation framework.

This project uses the mosaik co-simulation framework, developed by the brilliant developers at the high-ranked Offis institue for computer science, Oldenburg, Germany, to simulate multidimensional LE

Felix 1 Jan 28, 2022
Activate Numpad inside the touchpad with top right corner switch or F8 key

This is a python service which enables switching between numpad and touchpad for the Asus UX433. It may work for other models.

Mohamed Badaoui 230 Jan 08, 2023
Huawei Solar sensors for Home Assistant

Huawei Solar Sensors This integration splits out the various values that are fetched from your Huawei Solar inverter into separate HomeAssistant senso

Thijs Walcarius 151 Dec 31, 2022
PlatformIO development platform for GSM modules

PlatformIO development platform for GSM modules Supported Modules Quectel M66 OpenCPU Arduino - TODO other - in progress... Supported Boards Comet M66

Georgi Angelov 5 Aug 06, 2022
USB Rubber Ducky with the Rasberry Pi pico microcontroller

pico-ducky Install Install and have your USB Rubber Ducky working in less than 5 minutes. Download CircuitPython for the Raspberry Pi Pico. Plug the d

AnOnYmOus001100 3 Oct 08, 2022
HomeAssistant - Polyaire AirTouch 4 Integration

HomeAssistant - Polyaire AirTouch 4 Integration Custom integration to add an AirTouch 4 AC Controller Installation: Copy contents of custom_components

7 Aug 05, 2022
[unmaintained] WiFi tools for linux

Note: This project is unmaintained. While I would love to keep up the development on this project, it is difficult for me for several reasons: I don't

Rocky Meza 288 Dec 13, 2022
Micro Displays for Raspberry Pi

micro-displays Micro Displays for Raspberry Pi Why? I'm super bored in lockdown. Add a Raspberry Pi 400 and a few tiny displays... The top half of the

ig 291 Jul 06, 2022
New armachat based on Raspberry Pi PICO an Circuitpython code

Armachat-circuitpython New Armachat based on Raspberry Pi PICO an Circuitpython code Software working features: send message with header and store to

Peter Misenko 44 Dec 24, 2022
Adafruit IO connected smart thermostat based on CircuitPython.

Adafruit IO Thermostat Adafruit IO connected smart thermostat based on CircuitPython. Background and Motivation I have a 24 V Heat-only system with a

Shubham Chaudhary 1 Jan 18, 2022
Monitor an EnvisaLink alarm module running Honeywell firmware, and set a Nest device to Home/Away depending on whether the alarm is Disarmed/Away.

Nestalarm Monitor an EnvisaLink alarm module running Honeywell firmware, and set a Nest device to Home/Away depending on whether the alarm is Disarmed

1 Dec 30, 2021
Various programs in Atari BASIC for #FujiNet for Atari 8-bit

FujiNet Various programs in Atari BASIC for #FujiNet for Atari 8-bit FujiNet-3D Tic Tac Toe In 1978, Scott Adams wrote a 3-D Tic Tac Toe game, for pla

Kay Savetz 2 Jan 01, 2022
Alarm Control Panel component for Zigbee Keypads using action_transaction field

hass_transaction_alarm_panel Alarm Control Panel component for Zigbee Keypads using action_transaction field. Works together with zigbee2mqtt Supporte

Konstantin 4 Jun 09, 2022
A Python script to monitor the latest block on an LCD.

PiHole-Monitoring A Python script to monitor the latest block on a lcd display. The first number represents the dns queries from the last 24h, the sec

Maxi 4 Dec 05, 2022
Create (templateable) cameras that display qr codes in homeassistant

QRCam This custom component creates cameras displaying qrcodes. The QRCodes can be static or generated from templates. If you use a template as conten

Jannes Müller 5 Oct 06, 2022