Introduction: Self-Contained 7x7x7 LED Cube

About: Currently working on https://hedronvision.com. Stay tuned...

LED cubes are true 3D displays that work by lighting up points in a 3D lattice of LEDs.

On the 3D display you can produce some truly mesmerizing animations. This Instructable will walk you through creating an LED cube for yourself that is completely self-contained and powered by an Arduino Mega. Once you have it programmed, all you have to do is plug it into the wall and it will display whatever you tell it to! This cube avoids the complication of multiplexers and instead uses an Arduino Mega to directly control transistor circuits.

Without further ado, here's a video of the cube in action:
(Although there are limitations to taking a 2D video of a 3D display)


We'll start by making the physical cube and then turn to programming it.


-----------------------------------------------------------------------------------------------------------------

Besides being for fun, this project is an entry for the Make-to-Learn, Lighting, and Epilog Challenge V Contests. I would really appreciate your vote!

**Please click on the orange vote ribbon in the upper right-hand corner of this page if you enjoy this Instructable.**

-----------------------------------------------------------------------------------------------------------------
What would you do if you were to win the Epilog Zing Laser?

My high school got an Epilog Laser this year, and on it I had my first experience using a laser cutter. I was immediately struck by how effective it was at turning designs into reality.

I first used it last December to cut ornate snowflake Christmas ornaments out of acrylic and wood, some of which are pictured in photo 2 on step 12. Not only did it inspire me to teach myself Illustrator, but the results were delightful, and I hope to share the process in a future Instructable. I went on to use the laser cutter to slice Stanford's logo out of sticky felt to decorate the top of my graduation cap, as well as to make acrylic coasters, place mats and the LED layer template the box for this project.

Unfortunately, since I have now graduated, I have lost access to the laser cutter. Laser cutters have the precision to make things that would be essentially impossible to fabricate otherwise, such as the radially symmetric lines on the snowflake ornaments or the pinpoint holes that allow the legs of the LED cube to rest through the lid of the box. If I were to win the Epilog Laser, I would continue to use it to fabricate neat items too delicate and too complex to be made otherwise. In short, I'd love to win the laser to continue my adventure of learning how to create objects with computer controlled machinery.

-----------------------------------------------------------------------------------------------------------------
Below are my answers to the contest Make-to-Learn Youth Contest questions:

What did you make?

I built and programmed a 7x7x7 LED cube from scratch. The description above and the rest of the Instructable tell the story much better than is possible in the short answer to a question. Therefore, please refer to the rest of the Instructable for a more complete answer to this question.

How did you make it?

I was originally inspired by chr's Instructable (here), which first introduced me to LED cubes and how neat they are. I wanted to make a LED cube that was self-contained--that you could just plug into the wall and have run--rather than one that requires input from a computer.

I co-opted the idea of using the legs of the LEDs to form the framework of the lattice that most other LED cubes use, but I came up with the rest of the physical and circuitry design on my own. I built and assembled the whole cube from scratch. Several of the routines that the cube runs were inspired by chr's, but I wrote the code myself or in conjunction with teaching my cousin to program.

The only major change of plans I had while building the cube was to use an Arduino Mega instead of a Due. The Due has the advantage of a higher clock speed, but I realized that I needed 5V for the digital out pins to be able to fully switch the transistors in my circuit. Otherwise, the voltage drop across the LEDs would have been capped at 2.6V rather than the 3.3V they were rated to.

Where did you make it?

I did the majority of the work on this project in the lab and shop at my high school, as this was my second semester project for my Applied Science Research Class. We have a laser cutter in our school lab, which I used to cut out the acrylic box.

I did some of the soldering at home as well.

What did you learn?

A lot! I learned how to used Adobe Illustrator to create things with the laser cutter. In terms of electronics, I came to really understand how transistors work while working on this project, and it was by far the largest and most complex circuit I have ever designed. On the programming side, I learned how to use pointers and memory management to write the C++ code that controls the cube. It was neat to see a real-world application of polymorphism and to learn about the virtual keyword in C++.

More generally, this project taught me the value of building a smaller-scale prototype and the power of digital circuits coupled with a microprocessor.

My cousin was with me while I was programming the cube. He had no programming experience, but I taught him enough that with some help he was able to write two of the eight routines that are displayed on the cube. More on that in the programming section!

Step 1: Skills Required

The skills required for this project are actually pretty minimal; most can be learned along the way, especially given that you have plans to work off of.

That being said, you should really have some experience with basic electronics and also with soldering, since this project requires a lot of that.

My programs should work for you out of the box, but if you want to write your own display routines, some experience with object-oriented programming in C++ (Arduino) would be helpful.

Step 2: How It Works!

In its simplest form, an LED cube is a three dimensional array of lights in space, with each light either on or off at any given moment in time.  To achieve complete control over those lights, each LED needs to appear to be controlled individually.

Doing this directly, however, is not feasible. If x is the number of LEDs on each side of the LED cube, we'd need x3 digital output ports to control each LED individually.  This might work for smaller cubes, but for larger values of x, this quickly exceeds the number of ports directly available on any standard microcontroller.  For this 7x7x7 cube, 343 ports would be needed.

This problem is commonly resolved by flickering through the layers of the LED cube quickly enough to create the illusion of being able to control every LED independently.  Let's look at how this would reduce the number of ports needed:

In each colored layer in the photo above, the cathode (to ground) leg of each LED is connected.  The anode (voltage in) legs of all the LEDs in each column are also connected.  Without going into details yet, assume that the microcontroller can connect and disconnect each column from power and each layer from ground.

To display something on the cube, we would first ground the red layer and apply voltage to the columns we want to turn on.  Then we would disconnect everything and repeat the process for the orange layer, this time applying voltage only to the columns of the LEDs we want to light up in that layer.

By quickly flickering through the layers, every layer will appear to be lit at once.  The flicker fusion threshold, the rate above which flickering lights appear constant, in humans is about 60Hz, so the cube needs to cycle through every layer at least 60 times per second.

Switching the LEDs in layers and columns makes larger cubes feasible.  A total of x2 + x digital outputs are needed to control such a cube: x2 for the columns and x for the layers.  Therefore, to build a 7x7x7 cube, 56 ports are needed, rather than 343.  The Arduino Mega has 70 pins that can be used as digital I/O ports, leaving 14 ports open for external push button switches to switch the pattern being displayed on the cube.

Step 3: Tools

In order to do the electronics you will need:
  • Good soldering iron
  • Patience
  • Wire stripper
  • 2 Pairs of needle-nose pliers
  • Eye protection, etc.

Alligator clips and a power supply are handy, as is an ohmmeter for checking connections.

I used a laser cutter to build my acrylic box and to make the template I used for soldering.  I'm sure an inventive person could get by without, but the laser cutter was tremendously helpful.

Calipers are also helpful but not necessary.

As always, make sure you know how to safely use a tool before using it.

Step 4: Materials

You'll need the following for this project:

1x              Arduino Mega
                  --The brains of our project

1000x       3mm diffused blue LEDs
                  **Buy these off of eBay and make sure to get the diffused variety.  They'll be
                     much cheaper that way (~$25), and diffused LEDs have a much better
                     view angle than normal LEDs.

1x              5V Wall Wart power supply
                  --This will supply power to the project.
                  **eBay is a great place to get these.  I got a 2A one, but 1A should work just
                      fine.

1x              DC female power jack
                  --Get power from the wall wart
                  **Make sure the jack size matches the size of the jack on the power supply
                      you bought.  Like here.

8x              Momentary pushbutton switches
                  --User input to switch programs
                  **I got mine at RadioShack here.

1x              Switch rated to at least 1A that mounts through a hole
                  --On/Off Switch
                  **I used a DPDT switch, but other kinds would work just fine.

2x              Prototype Board
                  **Get the ones that have some holes already connected.  It will save you a
                      ton of time.  I got mine here.

~               Header pins
                  --For connecting PCBs and for connecting ribbon cable to the Arduino.
                  **Get a bunch of the male ones and some smaller sockets like those on
                      the Arduino.

6ft             7+ conductor ribbon cable

3ft             35+ conductor ribbon cable

1pkg        Adhesive velcro
                 --Secure boards down inside box

53x          100Ω resistor

53x          560Ω resistor

9x            4.7kΩ resistor

8x            10kΩ resistor

9x            TIP31C NPN power transistor
                --For switching layers

53x          2N3904 NPN transistor
                --For switching columns

~3ft         24 and 22 gauge insulated wire

1x           Acrylic Sheet
                --For case

4x           3" by 1/8" Stainless steel screws with acorn caps
                --For case

1x           1/8" Particle board
                --For soldering template

1 ton       Solder

1x            Tube of epoxy

2x            Male jumper wires
                **Could substitute wire and header pins

The total cost should be somewhere around $150. 

Step 5: Make a Prototype

It is really worth making a miniature prototype to make sure everything will work as expected.

I built a 2x2x2 cube out of 8 green LEDs that were lying around at school and got it to light up in a spiral pattern.  I'm not going to go into detail here, since the methods used to build the prototype are the same as for the main cube.

At the very least, build the prototype to see what you are getting yourself into.

Step 6: Wiring Diagrams

The electrical circuit needs to accomplish three tasks:  Providing a stable DC supply, allowing the current-sensitive, digital output pins on the Arduino Mega to switch the relatively high currents needed for the LEDs, and allowing the Arduino to detect when the routine switching buttons are pressed.  The circuit diagrams below are broken up to reflect these three different tasks.

For the DC supply itself, I made use of a 5V, 2A wall wart power supply to convert the AC from the wall outlet.  When plugged into the wall, it actually provides 5.4V, but the Arduino’s internal voltage regulator is able to bring that voltage down to the 5V it requires.  Tests with a variable voltage power supply found that the Arduino Mega continued to function even when only 3.9V was supplied.

The wall wart’s jack plugs into a power port through a hole in the acrylic enclosure.  As shown in the second diagram, the leads on the port run to a double pole, double throw switch which allows power to be easy switched off from the outside of the cube without needing to remove the power jack.  The power and ground lines then go to power the circuits controlling the LEDs and to the unregulated voltage input and ground pins on the Arduino.

Before discussing the second circuit, it is necessary to explain the choice of LEDs for the cube.  I chose 3mm diffused blue LEDs  because they are bright, give the appearance of a point light source and have better view angles than non-diffused LEDs.  The variety I purchased are designed to drop 3.3V and have a current of 20mA, giving them an effective resistance of 165 Ω.

The transistor circuit that allows the Arduino pins to switch the columns and layers of the cube is significantly more complex.  An abbreviated diagram is shown in the first image above. The 5V line from the power circuit provides runs into this circuit.  The following circuit is repeated for each column.  The 5V first goes through a 100Ω resistor to drop the voltage enough for the LED and to bring it to a low enough level that it can be controlled by the 5V Arduino pin.  The current then flows into an NPN transistor, the base of which is controlled by an Arduino output pin protected by a 560Ω resistor.

Current then flows from the emitter of the transistor out to the anodes of the LEDs in that column.

Another NPN transistor controls the grounding of each of the layers.  The current flows out of the collective cathode for that layer into the collector of a power NPN transistor.  The current through this transistor could equal 20mA*49 = 0.98A, so a power transistor is needed.  A 4.7kΩ resistor protects the Arduino pin that controls this transistor.  The emitter of the transistor is connected straight to ground.

The final circuit is used for detecting when the pushbutton switches on the exterior of the acrylic case are pressed.  The circuit is diagrammed in the last image.  When the push button is open, the digital input port is grounded through the resistor.  If the pushbutton is pressed, however, the digital input will be at the 5V coming from the Arduino’s voltage regulator.  This allows the microcontroller to detect the button being pushed so that it knows when to switch to a different display mode.

Step 7: Laser Cut the Soldering Template

Let's start with the LED lattice.

To solder each layer of the LED lattice, we'll need a 2D template.  Measure the shorter leg of your LEDs with a pair of calipers, or failing that, a ruler.

You then want to make a grid of holes spaced exactly that distance from center to center. The holes should hold the LEDs snugly.  I used Illustrator and a laser cutter to create the template out of 1/8" particle board, but you could probably do it with a drill press.  

Make sure that the holes go all the way through the material to make removing the LEDs easier.

Step 8: Solder Layers

For each layer of the cube, you want to make a perfect 7x7 array of LEDs with their cathodes connected.

I began by inserting LEDs into the template in an L with the cathode leg bent flat against the neighboring LED. The cathodes were then soldered together, and more rows of LEDs were added to begin to form a comb, as starts to be shown in the second picture.

I then added more wires to strengthen the "comb" as shown in the second picture.  I used 24 gauge wire to best match the legs of the LEDs.  To straighten the wire, simply pull from both ends with pliers and smooth out any imperfections with your fingers.  The extra supporting wires were added at the end and center of the comb (not pictured).  Be sure not to have any shorts to the anodes of the LEDs.

Finally, carefully poke each LED out from the template by pushing from below with a blunt object.

Repeat this process for all 7 layers.

After you have soldered a layer, it is worth checking it because once it is soldered into the cube, there will be no way of fixing it.  Just set your power supply to 5 volts, connect the cathodes to ground through a 100Ω resistor and connect each anode to 5V in turn.

Step 9: Assemble the Cube

The first step in putting the layers together is bending the tips of the anode legs of each LED in the lower layers to hook around the LED above them.  Then, wooden  spacers were used to prop the next layer in place as the bent anodes were soldered to the base of the anodes on the LEDs above them.  The layers were then slid slightly to ensure that each layer was perfectly above the one below it. Repeating this process seven times yielded the 7x7x7 LED lattice.

Trim any protrusions beside the bottom legs of the cube and congratulate yourself on a job well soldered.

Step 10: Solder Ribbon Cable to the Layers

Each layer will eventually need to be wired to the PCB, and ribbon cable is the neatest way to do that.  Solder a strip of ribbon cable to the back left corner of the cube with one wire going to each layer, as shown.

The picture was taken from the back of the cube.

Step 11: Solder Circuit Boards

More soldering!

The transistor circuits switching the columns and layers and the resistors for the pushbutton-detection circuits need to be soldered to the circuit boards.  

I put 35 of the column circuits on the lower board and 14 on the upper board.  7 transistor circuits for the layers are on the upper board, as are the resistors for the pushbuttons.

Then, solder header pins and sockets to connect ground and power between the boards.

Step 12: Acrylic Box

Now to laser cut the acrylic box!

If you don't have access to a laser cutter, that's okay.  It's possible there is one locally, at a TechShop, for example, and online services to order laser cut parts exist.  Otherwise, you can likely modify a pre-built enclosure to fit your needs.

Either way, the enclosure will need to have:
--49 Holes for the legs of the LEDs
--Holes to mount the switch
--A hole for the Arduino USB port
--A hole for the power port
--A slot to admit the ribbon cable that grounds the layers
--Holes to bolt the whole thing together

I've attached the Illustrator files I used when I cut the box from 1/8" acrylic.  The bottom of the box was identical to the top, but without any of the holes in the center.  The laser beam was fine enough that the kerf does not have to be corrected for.  You may have to adjust hole sizes if your components were different sizes from mine.

Once the box is ready, carefully coax the legs of the cube into the top of the box.  It helps to start with one side and use a pencil to poke the legs into place.

Carefully epoxy the power port where shown in the image.

Step 13: Wire Everything Together

All the circuits now need to be connected with ribbon cable.

I started by connecting all of the transistor circuits to the Arduino, making sure that the transistors were connected to the Arduino pins in a consistent way.  I chose to do it top to bottom, left to right.  Then I connected the circuits for the pushbuttons on the circuit board to the Arduino.

The hardest part is soldering the ribbon cable to the header pins going into the Arduino.  By far the easiest way to do this is to part the strands of each wire around the top of the header pin and then twist them on the other side, clamping the pin between them.

Then, screw all the switches through the acrylic and solder one side of all the pushbutton switches together. Solder one half of a male jumper wire to that.  This wire will plug into a 5V pin on the Arduino to supply power to the pushbutton circuits.  Solder each other lead of the pushbuttons to a wire in a strip of ribbon cable.

Next, solder ribbon cable to each leg of the LED lattice underneath the top of the acrylic box.  This holds the lattice in place.  I soldered across in rows left to right, front to back, for reasons that will become clear later.  You can see those cables in image #3.

Things get messy soldering the cube to the circuit boards.  Just do it carefully, matching the first transistor circuit you soldered to the first column you soldered, and it will turn out all right.  Then, solder the ribbon cable coming from the pushbuttons into the pushbutton circuits on the circuit board.

You're getting close!  Finally, solder the leads from the power port to the power switch.  Run wire from the switch to power the circuit board and solder half jumper wires so the power can go into the Arduino.

Then, Velcro down the boards inside the box, push all the header pins and jumper wires into their sockets, and screw the box closed.  The hardware is done!

Step 14: Software Intro

Now that the hardware is done, let's make the cube come alive!

I've attached a zip file of my whole program.  For those who just want to run it, drag the contents of the libraries folder into your Arduino libraries folder, and drag the LED_Cube folder into your Arduino folder.  Compile it and run.

You need the latest version of Arduino (1.0.4) to run my program.  Otherwise you will get errors that the new[] and delete[] operators are not defined.

When you press a button the whole cube will light up, and when you release, a display routine will begin.  This might be a firework simulation, or a bouncing ball.  

If one or more lights doesn't work on the cube, don't panic.  For me, a solder join on the lattice had cracked.  Just find the break with an ohmmeter and repair it with the soldering iron.

Step 15: Program Overview

If you're interested in how the program works or in how to write your own display routines for the cube, keep reading.

For this program, I tried to loosely follow the Model-View-Controller paradigm.  All that means is that I keep the information about the model--information about what the state should be and updating it--separate from the view, which displays that information on the cube.  The main Arduino sketch acts as the controller, passing information from the model to the view to be displayed.

We'll be writing a lot of classes to make a clean object-oriented program. Arduino deals with these classes as libraries. Before we get to the main program, I'll talk about a couple of those libraries.

PushButton:

A PushButton deals with getting input from the pushbutton switches on the front of the cube.  You tell it what pin the pushbutton circuit is connected to, and then you can ask it whether it has been pressed or released since you last checked it.

Routine:

A routine object is responsible for calculating what the cube should be displaying each turn.  It is repeatedly told to update its state based on the amount of time that has passed since it was last told to update.  We can extend Routine and override the update method to make new routines for the cube.  Routines also hold a 3D array of booleans that represents the current state of the cube.  The routine does not do the actual displaying of the array on the cube.  That falls to the CubeView.

CubeView:

A CubeView handles displaying a 3D array of booleans (from the Routine) on the cube.  It does this one layer at a time, shifting to the next layer every time you tell it to display.


Now for a high level view of the main program:

The main program keeps around a pointer to the Routine we are currently running.  In the loop() we repeatedly tell the current routine to update, copy the new frame the routine has generated into a local variable, and then check to see if there has been any input on the buttons.  If any buttons have been pressed, we delete the old routine and replace it with a new one that lights up the whole cube.  If a button is released, we delete the old routine and replace it with a new routine that corresponds to that button.

This code is repeatedly interrupted (420 times per second with the current settings) to display the next layer on the cube.  Here, we simply pass the last frame we've fully calculated to the CubeView to be displayed.  More information about interrupts can be found here.  Having the display method interrupt the other code keeps the refresh rate independent of calculation time and prevents flickering.

If you want to take videos of a cube you've built, simply change the #define REFRESH_RATE to something like 240.  That will make the cube flash so fast that the camera doesn't pick it up.

More on routines in the next step:

/***    LED_Cube*    *    Code for an Arduino Mega to control an LED cube.  Ports 0 to 48 should *    switch the columns, with (0,0 to 6) going to ports 0 to 6,*    (1,0 to 6) going to ports 7 to 13, etc.*    *    Ports A0 through A6 control the layers with A0 controlling z = 0 and A6*    controlling z = 6.  **    Ports A8 to A15 serve as inputs for the pushbutton switches.**    Program written by Lopuz3*    Spring 2013 **/#include <PushButton.h>#include <CubeView.h>#include <Routine.h>#include <AllOn.h>#include <CornerToCorner.h>#include <CubeFrame.h>#include <Fade.h>#include <Ripple.h>#include <Wave.h>#include <PouringRain.h>#include <BouncingBall.h>#include <Particle.h>#include <Firework.h>#define CUBE_SIZE 7#define NUM_BUTTONS 8#define REFRESH_RATE 60

PushButton buttons[NUM_BUTTONS];
CubeView cube = CubeView();
Routine *currentRoutine =new Routine();

boolean lastCompleteFrame[CUBE_SIZE][CUBE_SIZE][CUBE_SIZE];

void setup() 
{
  setUpButtons();
  setUpInterrupts();
}

void setUpInterrupts()
{
  cli();//stop interrupts while we set them up//set up an interrupt with timer1
  TCCR1A =0;
  TCCR1B =0;
  TCNT1  =0;
  //make the interrupt ocurr at the correct frequency.  The frequency is REFRESH_RATE*CUBE_SIZE
  OCR1A = (16000000/REFRESH_RATE/1024/CUBE_SIZE -1);
  TCCR1B |= (1<< WGM12);
  // Set to CS10 and CS12 so we have the 1024
  TCCR1B |= (1<< CS12) | (1<< CS10);  
  TIMSK1 |= (1<< OCIE1A);
  sei();//reallow interrupts 
}

void setUpButtons()
{
  for(byte i =0 ; i < NUM_BUTTONS ; i++)
  {
    buttons[i] = PushButton(i+A8);
  }
}

//Called by timer interrupt
ISR(TIMER1_COMPA_vect)
{
  cube.displayLayer(lastCompleteFrame);
}

void loop() 
{
  currentRoutine->update(getTimeSinceLastFrameInMicros());
  memcpy(&lastCompleteFrame, &currentRoutine->cubeModel, sizeof(boolean)*CUBE_SIZE*CUBE_SIZE*CUBE_SIZE);
  getButtonInput();
}

void getButtonInput()
{
  if(anyButtonWasPressed())
  {
    delete currentRoutine;
    currentRoutine =new AllOn();
  }
  if(buttons[0].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new CornerToCorner();
  }
  if(buttons[1].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new CubeFrame();
  }
  if(buttons[2].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new Fade();
  }
  if (buttons[3].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new Ripple();
  }
  if (buttons[4].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new Wave();
  }   
  if (buttons[5].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new PouringRain();
  }
  if (buttons[6].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new BouncingBall();
  }
  if (buttons[7].wasReleased())
  {
    delete currentRoutine;
    currentRoutine =new Firework();
  } 
}

boolean anyButtonWasPressed()
{
  for(byte i =0 ; i < NUM_BUTTONS ; i++)
  {
    if(buttons[i].wasPressed())
    {
      returntrue;
    }
  }
  returnfalse;
}

/*Get the time since the last call of this function in microseconds*/unsignedlong getTimeSinceLastFrameInMicros()
{
  staticunsignedlong lastTime =0;
  unsignedlong dt = micros()-lastTime;
  lastTime = micros();
  return dt;
}

/***    PushButton.h*    *    Class to control input from a pushbutton switch.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef PushButton_h#define PushButton_h#include "Arduino.h"classPushButton
{
  public:
    PushButton(int pin);
	PushButton();
    boolean isDown();
	boolean wasPressed();
    boolean wasReleased();
  private:int _pin;
    boolean lastState;
};

#endif

/***    Routine.h*    *    Base class to set up a model for the cube.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef Routine_h#define Routine_h#define CUBE_SIZE 7#include "Arduino.h"classRoutine
{
  public:
    Routine();
	virtual~Routine();
	virtualvoid update(unsignedlong dt);
    boolean cubeModel[CUBE_SIZE][CUBE_SIZE][CUBE_SIZE];
};

#endif

/***    CubeView.h*    *    Class to display a model on the cube.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef CubeView_h#define CubeView_h#define CUBE_SIZE 7#include "Arduino.h"classCubeView
{
  public:
    CubeView();
	void displayLayer(boolean model[CUBE_SIZE][CUBE_SIZE][CUBE_SIZE]);
  private:
	byte currentLayer;
};

#endif

Step 16: Coordinate System

Before we turn to making new extensions of Routine, we need to define a coordinate system.  I chose the one pictured above, because it kept the standard, 2D X-Y plane for the column switching, which made thinking about the grid of columns easier for me.

To access the boolean corresponding to the LED at (x,y,z) in a 3D array, we would use cubeModel[x][y][z].

Step 17: AllOn

Every time we want to define a new display pattern for the cube, we just have to write a class that extends routine, and make it correspond to a button in the main program.  If you know basic syntax for classes in any programming language, it shouldn't be hard to learn how to do this in C++, using my routines as a model.

Just put your .cpp and .h files in a folder in libraries and you can call on them from the main program.

The simplest extension of routine is to turn all the lights on rather than having them all off by default.  For this we just set all the coordinates in our cubeModel to true in the constructor.


/***    AllOn.h*    *    Extention of Routine to light the entire cube.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef AllOn_h#define AllOn_h#include "Arduino.h"#include "Routine.h"classAllOn:public Routine
{
  public:
	AllOn();
};

#endif


/***    AllOn.cpp*    *    Extention of Routine to light the entire cube.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "AllOn.h"

AllOn::AllOn()
{
  for(byte x =0 ; x < CUBE_SIZE ; x++)
  {
    for(byte y =0 ; y < CUBE_SIZE ; y++)
    {
      for(byte z =0 ; z < CUBE_SIZE ; z++)
      {
        cubeModel[x][y][z] =true;
      }
    }
  }
}

Step 18: CornerToCorner

In this extension of Routine I also overrode the update method, giving the cube behavior over time.

The program starts by randomly choosing a vertex of the cube.  Every step, the lights advance out from their starting vertex toward the opposing corner.  When the light has filled the whole cube, it retreats away from the corner where it started.  The routine then repeats with a new random corner.

A simple rule tells the program to light a pixel: If the sum of the x, y and z distances is below a certain threshold, the pixel is lit.  All other pixels remain off.  The threshold is upped by one each step, and then the process is repeated in reverse to darken the cube.



/***    CornerToCorner.h*    *    Extention of Routine to light up the cube starting at a corner.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef CornerToCorner_h#define CornerToCorner_h#define STEP_TIME 75000#include "Arduino.h"#include "Routine.h"classCornerToCorner:public Routine
{
	public:
		CornerToCorner();
		void update(unsignedlong dt);
	private:unsignedlong timeSinceLastStep;
		int step;
		boolean growing;
		boolean xHigh, yHigh, zHigh;
};

#endif

/***    CornerToCorner.cpp*    *    Extention of Routine to light up the cube starting at a corner.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "CornerToCorner.h"

CornerToCorner::CornerToCorner()
{
	timeSinceLastStep =0;
	step =0;
	growing =true;
	xHigh = random(2);
	yHigh = random(2);
	zHigh = random(2);
}

void CornerToCorner::update(unsignedlong dt)
{	
	timeSinceLastStep += dt;
	if (timeSinceLastStep > STEP_TIME)
	{
		if(growing)
		{
			for(byte x =0 ; x < CUBE_SIZE ; x++)
			{
				for(byte y =0 ; y < CUBE_SIZE ; y++)
				{
					for(byte z =0 ; z < CUBE_SIZE ; z++)
					{
						int xDist = x;
						if(xHigh)
						{
							xDist = CUBE_SIZE -x -1;
						}
						int yDist = y;
						if(yHigh)
						{
							yDist = CUBE_SIZE -y -1;
						}
						int zDist = z;
						if(zHigh)
						{
							zDist = CUBE_SIZE -z -1;
						}
						cubeModel[x][y][z] = (xDist + yDist + zDist <= step);
					}
				}
			}
			step++;
			if(step > (CUBE_SIZE-1)*3)
			{
				step =0;
				growing =false;
			}
		}
		else
		{
			for(byte x =0 ; x < CUBE_SIZE ; x++)
			{
				for(byte y =0 ; y < CUBE_SIZE ; y++)
				{
					for(byte z =0 ; z < CUBE_SIZE ; z++)
					{
						int xDist = x;
						if(xHigh)
						{
							xDist = CUBE_SIZE -x -1;
						}
						int yDist = y;
						if(yHigh)
						{
							yDist = CUBE_SIZE -y -1;
						}
						int zDist = z;
						if(zHigh)
						{
							zDist = CUBE_SIZE -z -1;
						}
						cubeModel[x][y][z] =!(xDist + yDist + zDist <= step);
					}
				}
			}
			step++;
			if(step > (CUBE_SIZE-1)*3)
			{
				step =0;
				growing =true;
				xHigh = random(2);
				yHigh = random(2);
				zHigh = random(2);
			}
		}
		timeSinceLastStep -= STEP_TIME;
	}
}

Step 19: CubeFrame

This routine also starts with choosing a random vertex.  This time the wire frame of a cube expands out of that corner  before contracting back down.  The process then repeats with a new corner.

Again, a relatively simple rule allows us to decide whether a point is on the wire frame cube or not.  At least two of the x, y, or z distances from the corner of  must either be the current size or the cube or zero.  The remaining distance must be less than the size of the cube.




/***    CubeFrame.h*    *    Extention of Routine to expand and contract a wireframe of a cube.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef CubeFrame_h#define CubeFrame_h#define STEP_TIME 100000#include "Arduino.h"#include "Routine.h"classCubeFrame:public Routine
{
	public:
		CubeFrame();
		void update (unsignedlong dt);
	private:unsignedlong timeSinceLastExpantion;
		int wireframeSize;
		boolean growing;
		boolean xHigh, yHigh, zHigh;
};

#endif


/***    CubeFrame.cpp*    *    Extention of Routine to expand and contract a wireframe of a cube.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "CubeFrame.h"

CubeFrame::CubeFrame()
{
	timeSinceLastExpantion =0;
	wireframeSize =0;
	growing =true;
	xHigh = random(2);
	yHigh = random(2);
	zHigh = random(2);
}	

void CubeFrame::update (unsignedlong dt)
{
	timeSinceLastExpantion += dt;
	if (timeSinceLastExpantion > STEP_TIME)
	{
		for(byte x=0 ; x < CUBE_SIZE ; x++)
		{
			for(byte y=0 ; y < CUBE_SIZE ; y++)
			{
				for(byte z=0 ; z < CUBE_SIZE ; z++)
				{
					int xDist = x;
					if(xHigh)
					{
						xDist = CUBE_SIZE -x -1;
					}
					int yDist = y;
					if(yHigh)
					{
						yDist = CUBE_SIZE -y -1;
					}
					int zDist = z;
					if(zHigh)
					{
						zDist = CUBE_SIZE -z -1;
					}
					if( xDist < wireframeSize && yDist < wireframeSize && zDist < wireframeSize)
					{
						int count =0;
						if(xDist ==0|| xDist == wireframeSize -1)
						{
							count++;
						}
						if(yDist ==0|| yDist == wireframeSize -1)
						{
							count++;
						}
						if(zDist ==0|| zDist == wireframeSize -1)
						{
							count++;
						}
						
						if(count >=2)
						{
							cubeModel[x][y][z] =true;
						}
						else
						{
							cubeModel[x][y][z] =false;
						}
					}
					else
					{
						cubeModel[x][y][z] =false;
					}
				}
			}
		}
		
		if(wireframeSize == CUBE_SIZE)
		{
			growing =false;
		}
		if(wireframeSize ==0)
		{
			growing =true;
			xHigh = random(2);
			yHigh = random(2);
			zHigh = random(2);
		}
		if(growing)
		{
			wireframeSize++;
		}
		else
		{
			wireframeSize--;
		}
		timeSinceLastExpantion-= STEP_TIME;
	}
}

Step 20: Fade

Here we randomly light up an unlit voxel (volumetric pixel) until the whole cube is lit.  When that ocurrs, the voxels are then randomly turned back off, one by one.



/***    Fade.h*    *    Extention of Routine randomly fade the cube in and out.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef Fade_h#define Fade_h#define STEP_TIME 20000#include "Arduino.h"#include "Routine.h"classFade:public Routine
{
	public:
		Fade();
		void update(unsignedlong dt);
	private:int numLeft;
		unsignedlong timeSinceLastStep;
		boolean lighting;
};

#endif

/***    Fade.cpp*    *    Extention of Routine randomly fade the cube in and out.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "Fade.h"

Fade::Fade()
{
	numLeft = CUBE_SIZE*CUBE_SIZE*CUBE_SIZE;
	timeSinceLastStep =0;
	lighting =true;
}

void Fade::update(unsignedlong dt)
{
	timeSinceLastStep += dt;
	if (timeSinceLastStep > STEP_TIME)
	{
		int toToggle = random(numLeft);
		int unchangedCounter =0;
		for(byte x =0 ; x < CUBE_SIZE ; x++)
		{
			for(byte y =0 ; y < CUBE_SIZE ; y++)
			{
				for(byte z =0 ; z < CUBE_SIZE ; z++)
				{
					if(lighting != cubeModel[x][y][z])
					{
						if(unchangedCounter == toToggle)
						{
							cubeModel[x][y][z] = lighting;
							numLeft--;
						}
						unchangedCounter++;
					}
				}
			}
		}
		if(numLeft ==0)
		{
			numLeft = CUBE_SIZE*CUBE_SIZE*CUBE_SIZE;
			lighting =!lighting;
		}
		timeSinceLastStep -= STEP_TIME;
	}
}

Attachments

Step 21: Ripple

This routine shows a ripple calculated with trig functions.  The height at each x-y coordinate is a function of the sine of the distance from the center plus time.



/***    Ripple.h*    *    Extention of Routine to display a ripple effect using the sin function.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef Ripple_h#define Ripple_h#include "Arduino.h"#include "Routine.h"classRipple:public Routine
{
	public:
		Ripple();
		void update(unsignedlong dt);
	private:float runTime;
};
#endif

/***    Ripple.cpp*    *    Extention of Routine to display a ripple effect using the sin function.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "Ripple.h"

Ripple::Ripple()
{
	runTime =0;
}

void Ripple::update(unsignedlong dt)
{
	runTime +=1.0*dt/1000000;

	for(byte x =0 ; x < CUBE_SIZE ; x++)
	{
		for(byte y =0 ; y < CUBE_SIZE ; y++)
		{
			for(byte z =0 ; z < CUBE_SIZE ; z++)
			{
				cubeModel[x][y][z] =false;	
			}
		}
	}	
	for(byte x =0 ; x < CUBE_SIZE ; x++)
	{
		for(byte y =0 ; y < CUBE_SIZE ; y++)
		{
			float cDist = sqrt(((x-3)*(x-3))+((y-3)*(y-3)));
			
			float zfloat =3*sin(.75*cDist +5*runTime) +3;
		
			int z =int(zfloat +.5);
		
			cubeModel[x][y][z] =true;
		}		
	}		
}

Step 22: Wave

The routine again uses trig functions to trace out a wave through time.  Again, disregarding constants, the height of each x-y coordinate is a function of the sum of sin(x+time) + cos(y + time).  Interestingly, there are lines of points through the cube that remain constant while the rest of the wave fluctuates.



/***    Wave.h*    *    Extention of Routine to display a Wave effect using the sin function.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef Wave_h#define Wave_h#include "Arduino.h"#include "Routine.h"classWave:public Routine
{
	public:
		Wave();
		void update(unsignedlong dt);
	private:float runTime;
		float fastSin(double x);
		float fastCos(double x);
};
#endif

/***    Wave.cpp*    *    Extention of Routine to display a Wave effect using the sin function.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "Wave.h"

Wave::Wave()
{
	runTime =0;
}

void Wave::update(unsignedlong dt)
{
	runTime +=1.0*dt/1000000;

	for(byte x =0 ; x < CUBE_SIZE ; x++)
	{
		for(byte y =0 ; y < CUBE_SIZE ; y++)
		{
			for(byte z =0 ; z < CUBE_SIZE ; z++)
			{
				cubeModel[x][y][z] =false;	
			}
		}
	}	
	for(byte x =0 ; x < CUBE_SIZE ; x++)
	{
		for(byte y =0 ; y < CUBE_SIZE ; y++)
		{			
			float zfloat =1.5*sin(.75*(x-3) +5*runTime) +1.5*cos(.75*(y-3) +5*runTime) +3;
			int z =int(zfloat +.5);
		
			cubeModel[x][y][z] =true;
		}		
	}		
}

/**float Wave::fastSin(double x){	x = (int(100*x) % 628)*1.0/100;	if(x > 4.71)	{		x-=6.28;	}	else if(x > 1.57)	{		x = 3.14-x;	}	return (x - x*x*x/6);}float Wave::fastCos(double x){	return fastSin(x + 1.57);}**/

Attachments

Step 23: PouringRain

Rain randomly pours down into the cube, piling up on the bottom.

An unlit LED at the top of the cube is randomly chosen to become a drop.  This drop then falls until it either hits the bottom of the cube or piles on top of a previously fallen drop.  This process is repeated until the cube is full, at which point the routine repeats.




/***    PouringRain.h*    *    Extention of Routine to display the cube filling up with rain.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef PouringRain_h#define PouringRain_h#define STEP_TIME 40000#include "Arduino.h"#include "Routine.h"classPouringRain:public Routine
{
	public:
		PouringRain();
		void update(unsignedlong dt);
	private:unsignedlong timeSinceLastStep;
		boolean spawnDrop;
		byte xCurrent;
		byte yCurrent;
		byte zCurrent;
};
#endif

/***    PouringRain.cpp*    *    Extention of Routine to display the cube filling up with rain.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "PouringRain.h"

PouringRain::PouringRain()
{
	timeSinceLastStep =0;
	spawnDrop =true;
}

void PouringRain::update(unsignedlong dt)
{
	timeSinceLastStep += dt;
	
	if (timeSinceLastStep > STEP_TIME)
	{
		if (spawnDrop)
		{
			byte lit =0;
			
			for ( byte x =0; x < CUBE_SIZE; x++)
			{
				for ( byte y =0; y < CUBE_SIZE; y++)
				{	
					if (cubeModel[x][y][CUBE_SIZE -1])
					{
						lit++;
						
					}
				}
			}
			if(lit == CUBE_SIZE*CUBE_SIZE)
			{
				for(byte x =0 ; x < CUBE_SIZE ; x++)
				{
					for(byte y =0 ; y < CUBE_SIZE ; y++)
					{
						for(byte z =0 ; z < CUBE_SIZE ; z++)
						{
							cubeModel[x][y][z] =false;
						}
					}
				}
				return;
			}
			byte randomIndex = random(CUBE_SIZE*CUBE_SIZE - lit);
			
			byte unlit =0;
			
			for ( byte x =0; x < CUBE_SIZE; x++)
			{
				for ( byte y =0; y < CUBE_SIZE; y++)
				{
					if(!cubeModel[x][y][CUBE_SIZE -1])
					{
						if(randomIndex == unlit)
						{
							cubeModel[x][y][CUBE_SIZE -1] =true;
							xCurrent = x;
							yCurrent = y;
							zCurrent = CUBE_SIZE -1;
							spawnDrop =false;
						}
						unlit ++;
					}
				}
			}
		}
		else
		{
			if(cubeModel[xCurrent][yCurrent][zCurrent -1] || zCurrent ==0)
			{
				spawnDrop =true;
			}
			else
			{
				cubeModel[xCurrent][yCurrent][zCurrent] =false;
				zCurrent--;
				cubeModel[xCurrent][yCurrent][zCurrent] =true;
			}
		}	
		timeSinceLastStep -= STEP_TIME;
	}
}	

Step 24: BouncingBall

This is a physics simulation of a ball bouncing inside the cube.

To make this easier, I defined a particle class that simulates physics within some bounds.  You initialize the particle with some radius, position, and velocity and tell it whether or not it should bounce.  You can also apply acceleration and air resistance to the particle.

This routine just creates a particle with radius 2 at a random position with random velocity, and takes snapshots of its position as it bounces about.



/***    BouncingBall.h*    *    Extention of Routine to display a bouncing ball.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef BouncingBall_h#define BouncingBall_h#include "Arduino.h"#include "Routine.h"#include "Particle.h"classBouncingBall:public Routine
{
	public:
		BouncingBall();		
		~BouncingBall();
		void update(unsignedlong dt);
	private:
		Particle *ball;
};
#endif

/***    BouncingBall.cpp*    *    Extention of Routine to display a bouncing ball.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "BouncingBall.h"

BouncingBall::BouncingBall()
{
	ball =new Particle(random(CUBE_SIZE), random(CUBE_SIZE), CUBE_SIZE -1-1, random(3,9), random(3,9), 0, true, 2, CUBE_SIZE);
}

void BouncingBall::update(unsignedlong dt)
{
	float timeChange =1.0*dt/1000000;
	ball->accelerateZ(-15, timeChange);
	ball->move(timeChange);
	for(byte x =0 ; x < CUBE_SIZE ; x++)
	{
		for(byte y =0 ; y < CUBE_SIZE ; y++)
		{
			for(byte z =0 ; z < CUBE_SIZE ; z++)
			{
				cubeModel[x][y][z] =false;
			}
		}
	}
	
	for(byte x =0 ; x < CUBE_SIZE ; x++)
	{
		for(byte y =0 ; y < CUBE_SIZE ; y++)
		{
			for(byte z =0 ; z < CUBE_SIZE ; z++)
			{
				cubeModel[x][y][z] = ball->pointIsInSphere(x, y, z);
			}
		}
	}
}	

BouncingBall::~BouncingBall()
{
	delete ball;
}

/***    Particle.h*    *    Class to calculate the motion of a particle moving through space.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef Particle_h#define Particle_h#include "Arduino.h"classParticle
{
	public:
		Particle(float x, float y, float z, float vX, float vY, float vZ, boolean bounces, float radius, int cubeSize);
		Particle();
		void move(float dt);
		void accelerateZ(float aZ, float dt);
		void accelerate(float aX, float aY, float aZ, float dt);
		void drag(float coef, float dt);
		int getRoundedX();
		int getRoundedY();
		int getRoundedZ();
		boolean pointIsInSphere(byte x, byte y, byte z);
	private:float _x, _y, _z, _vX, _vY, _vZ;
		float _radius;
		boolean _bounces;
		int _cubeSize;
};
#endif

Step 25: Firework

This time we use the flexibility of the particle class to simulate a firework!

A particle is sent up in the center of the cube.  When it gets to z=4, the particle is replaced with a random number of other particles.  The particles are sent out with large, random velocities but are subject to a large amount of drag, just like in a real firework.

These particles slide down the sides of the cube, disappearing just before they reach the ground.

Then the process repeats with another firework.

It's neat that you can run something this complicated just off an Arduino.



/***    Firework.h*    *    Extention of Routine to simulate a firework.*    *    Program written by Lopuz3*    Spring 2013 **/#ifndef Firework_h#define Firework_h#include "Arduino.h"#include "Routine.h"#include "Particle.h"classFirework:public Routine
{
	public:
		Firework();		
		~Firework();
		void update(unsignedlong dt);
	private:
		boolean exploded;
		int numDebris;
		Particle *rocket;
		Particle *debris;
};
#endif

/***    Firework.cpp*    *    Extention of Routine to simulate a firework.*    *    Program written by Lopuz3*    Spring 2013 **/#include "Arduino.h"#include "Firework.h"

Firework::Firework()
{
	exploded =false;
	rocket =new Particle(CUBE_SIZE/2, CUBE_SIZE/2, 0, 0, 0, 10, false, 0, CUBE_SIZE);
	debris =new Particle[0];
}

void Firework::update(unsignedlong dt)
{
	for(byte x =0 ; x < CUBE_SIZE ; x++)
	{
		for(byte y =0 ; y < CUBE_SIZE ; y++)
		{
			for(byte z =0 ; z < CUBE_SIZE ; z++)
			{
				cubeModel[x][y][z] =false;
			}
		}
	}
	float timeChange =1.0*dt/1000000;
	if(!exploded)
	{
		rocket->move(timeChange);
		if(rocket->getRoundedZ() >=4)
		{
			delete[] debris;
			exploded =true;
			numDebris = random(30, 40);
			debris =new Particle[numDebris];
			for(byte i =0 ; i < numDebris ; i++)
			{
				debris[i] = Particle(CUBE_SIZE/2, CUBE_SIZE/2, 4, random(-10,11), random(-10,11), random(0,21), false, 0, CUBE_SIZE);
			}
		}
		cubeModel[rocket->getRoundedX()][rocket->getRoundedY()][rocket->getRoundedZ()] =true;
	}
	else
	{
		byte visableCount =0;
		for(byte i =0 ; i < numDebris ; i++)
		{
			debris[i].accelerateZ(-15, timeChange);
			debris[i].drag(0.05, timeChange);
			debris[i].move(timeChange);
			if(debris[i].getRoundedZ() >0)
			{
				visableCount++;
				cubeModel[debris[i].getRoundedX()][debris[i].getRoundedY()][debris[i].getRoundedZ()] =true;
			}
		}
		if (!visableCount)
		{
			exploded =false;
			delete rocket;
			rocket =new Particle(CUBE_SIZE/2, CUBE_SIZE/2, 0, 0, 0, 10, false, 0, CUBE_SIZE);
		}
	}
}	

Firework::~Firework()
{
	delete rocket;
	delete[] debris;
}

Step 26: Emission Spectrum

That's it for the actual construction and programming of the cube.  You now have a fully functioning and self-contained LED cube that can be powered from a wall outlet!

Out of interest, I performed one test on the LEDs in the cube.  Using an emission spectrometer, I measured the intensity of the light being emitted by the blue LEDs at different wavelengths in an otherwise darkened room.  The above is the graph of that data.  The peak of 468 nm is a function of the bandgap of the LED and is consistant with zinc selenide, indium gallium nitride, and other common blue LED semiconductor materials.

We've confirmed that the LEDs are indeed blue!

Seriously, though, I hope you enjoyed reading about my cube and might be inspired to build one of your own.  The 3D visual effects are even more amazing when not confined to to a 2D video.

Again, I would really appreciate your vote for the contests I've entered.  If you enjoyed reading, please click on the orange ribbon in the upper right-hand corner.

Make-to-Learn Youth Contest

Grand Prize in the
Make-to-Learn Youth Contest

Epilog Challenge V

Runner Up in the
Epilog Challenge V

Lamps & Lighting Contest

Participated in the
Lamps & Lighting Contest