Simulator and lighting code for Cosmic Praise, a fifty-two-foot-tall cosmic-ray detector covered in LEDs.
http://douglasruuska.com/cosmic-praise
- Install dependencies
sudo pip install colormath
- Run the simulator (pre-built for OSX, Ubuntu 14.04 32-bit, and Linux Mint 64-bit; for other platforms, you can get the source and build yourself from our OPC fork here: https://github.com/Dewb/openpixelcontrol)
simulator/osx-10.9/gl_server layout/cosmicpraise.json
- Run the client code to send pixels to the simulator:
python client/python/cosmicpraise.py -l layout/cosmicpraise.json -f 60 --sim
- The client will start running an effect, and you should see it running in the simulator. Go back to the client window and hit Enter to switch to the next effect. (More sophisticated show control is available via OSC, see below.)
The tower structure is covered with 150 meters of WS2812 LED strip and 49 Philips Color Kinetics RGB fixtures; over 4600 individual pixels. The strips are run by LEDscape running on Beaglebone Blacks receiving OpenPixelControl (TCP), and the CK fixtures are powered by Ethernet-enabled CK power suppies communicating over Kinet (UDP).
We've modified the OpenPixelControl Python client to speak Kinet as well as OPC, and extended the OpenPixelControl layout and simulator to model and simulate the features of the Cosmic Praise tower, including flat surfaces of illumination representing the color wash targets of the Color Kinetics fixtures.
section | lights |
---|---|
base | 24 ColorBurst fixtures, about 12' off the ground, pointing down at the vinyl base cover, which is painted with techniques that react well to color-changing light. |
middle | 24 roughly 6 meter WS2812 LED strips crisscrossing along with the steel beams of the tower. Each strip is split into a 4.77m lower section and a 1.33m upper section, just as the steel structural beams are. |
railing | 12 CK Fresco coves with two fixtures each, just below the handrail, illuminating the woodcut panels of the tower top railing. |
roofline | 6 2.33m WS2812 strips ringing the roof of the tower top |
spotlight | A 300W LED spotlight on a rotating bearing above the tower roof |
spire | 16 1m WS2812 strips making an 8' antenna atop the tower roof. |
- Fork the Cosmic Praise repo to your own account.
- Create a new file in client/python/effects by copying
_blank.py
to<your name>.py
. - Define your effect function (see next section) and put its name in the
__all__
list. - Test your code in the simulator and revise. Commit it, then make another one!
- When they all look beautiful, create a pull request on Github to contribute your changes back to the main repo.
You can see the existing effect library here: https://github.com/Dewb/CosmicPraise/tree/master/client/python/effects
The simplest possible effect would be to just color every pixel in the tower the same color (in this case, hue 0 in the default palette, or red.)
def simplestExampleEffect(tower, state):
for pixel in tower:
tower.set_pixel(pixel, 0)
Slightly more complicated is to color each pixel differently with some math based on its cylindrical coordinates, and the animation time:
def verySimpleExampleEffect(tower, state):
for pixel in tower:
tower.set_pixel(pixel, pixel['theta'] / twopi, state.time % 0.5)
An effect is just a function that takes two arguments, tower
and state
, and calls tower.set_pixel(pixel, chroma, luma)
on whatever parts of the structure it wants to light up. tower.set_pixel
expects a pixel item from an iterator, plus two values: a "chroma" and a "luma" value. These will be mapped to the current palette of the sculpture, so we can overlap or sequence multiple effects and still achieve the effect of a unified aesthetic object.
chroma
and luma
should both range from 0.0 to 1.0. You can think of chroma
as indexing through an imaginary watercolor paintbox of unknown size, with 0.0 the left side of the box and 1.0 the right side, and luma
as making it full strength at 1.0, or watering it down to transparent at 0.0.
There is also a tower.set_pixel_rgb(pixel, rgb)
, which expects a RGB tuple of values 0.0-1.0, for effects that must be a specific color, whether for debugging or for a specific aesthetic need. But we encourage you to use tower.set_pixel
unless absolutely necessary.
The tower object also provides iterators over the entire structure, or a certain part, like tower.railing
or tower.spire
. Iterating over these generators gives you pixel items, each of which is a dictionary with information about the pixel including its (x,y,z) coordinates in 3D space, its (theta, r, z) coordinates in cylindrical 3D space, its strip index and address, etc. So you can color different parts of the structure with different techniques:
def simpleExampleEffect(tower, state):
# make the base blue
for pixel in tower.base:
tower.set_pixel_rgb(pixel, (0, 0, 1))
# make the railing red
for pixel in tower.railing:
tower.set_pixel_rgb(pixel, (1, 0, 0))
# fade the tower middle from blue to red
tower_height = 15.0
for pixel in tower.middle:
s = pixel['z'] / tower_height
tower.set_pixel_rgb(pixel, (s, 0, (1 - s)))
# and spin a yellow line clockwise around the clockwise tower diagonals
n = int(state.time % 12)
for pixel in tower.clockwise_index(n):
tower.set_pixel_rgb(pixel, (1, 1, 0))
# make the roofline and spire flash green
for pixel in chain(tower.roofline, tower.spire):
tower.set_pixel_rgb(pixel, (0, state.time % 1, 0))
The tower object provides the following methods and generators at the moment:
method | use |
---|---|
tower.set_pixel(pixel, chroma, luma) |
Set the color of a pixel according to the current global palette, where chroma and luma range from 0.0 to 1.0. This is the preferred method, for unified color blending across multiple effects. |
tower.set_pixel_rgb(pixel, rgb) |
Set the color of a pixel to a RGB tuple, each from 0.0 to 1.0. Use only if strictly necessary. |
basic generators | iterates over |
---|---|
tower or tower.all |
every pixel, in arbitrary order |
tower.spire |
all the pixels in the spire strips, starting at the bottom of the spire and proceeding counterclockwise in each ring |
tower.spire_index(n) |
where n=0 through 15, all the pixels in one specific ring, starting at the bottom of the spire and proceeding counterclockwise in each ring |
tower.roofline |
all the pixels in the roofline strips in counterclockwise order |
tower.railing |
the 24 railing cove lights in counterclockwise order |
tower.base |
the 24 colorburst fixtures illuminating the base section vinyl mural, in counterclockwise order |
tower.middle , tower.diagonals |
the diagonally crisscrossing strips on the top two steel sections of the tower, one strip at a time, in counter-clockwise order, pixels ordered from top to bottom |
tower.diagonals_index(n) |
where n=0 through 23, a specific diagonal strip, pixels ordered from top to bottom |
tower.spotlight |
Iterates over just one pixel: a NINE THOUSAND lumen 300W LED spotlight, the kind they use on the Empire State Building and the Zakim Bridge. It's represented in the simulator as a single dot, but it will actually be spinning a tight beam across the playa. Unless we have enough time to put together a network control system, it will probably be spinning at roughly 60-70rpm. If conditions line up right, the beam should be visible like a laser in the dusty air. What crazy things can you come up with to do with it? |
The above generators cover the basic parts of the structure, but we have additional fancier generators just for the diagonal grid on the middle of the tower, which is sort of our main play surface. See addressOrderTest, diamondTest, and lightningTest for a demonstration of the fancier generators.
fancy generators | iterates over |
---|---|
tower.clockwise |
only the clockwise middle diagonal crossing strips |
tower.counter_clockwise |
only the counter-clockwise middle diagonal crossing strips |
tower.clockwise_index(n) , tower.counter_clockwise_index(n) |
where n=0 through 11, a specific diagonal strip of a certain direction, pixels ordered from top to bottom |
tower.diagonals_index_reversed(n) , tower.clockwise_index_reversed(n) , tower.counter_clockwise_index_reversed(n) |
Same as above, but the sequence and the pixel order starts at the bottom. This is not the same as calling reversed(tower.diagonals(n)), because the diagonals are interleaved in a different order at the top and bottom of the tower. |
tower.diagonal_segment(index, row) |
one segment of the diagonal grid, from the strip index 0-23, where row = 0 is the topmost segment, row = 5 is the bottom-most. |
tower.diagonal_segment(index, toprow, bottomrow) |
an arbitrary line segment on the diagonal grid, from the strip index 0-23, beginning at row toprow (0-5) and ending at row endrow (0-5), inclusive. |
tower.diamond(row, col) |
Four sections of diagonal strip in a diamond pattern. Row counts from 0 to 4 down from the top, column is from 0 to 23 counting counter-clockwise. |
tower.diamonds_even |
evenly spaced non-overlapping diamonds on rows 0, 2, 4 |
tower.diamonds_odd |
evenly spaced non-overlapping diamonds on rows 1 and 3 |
tower.diamonds_even_shifted , tower.diamonds_odd_shifted |
same as above, but rotated slightly |
tower.lightning(start, seed) |
a branching path down the tower middle, similar to a lightning bolt, where start=0 through 23, the starting location of the bolt, and seed is a value from 0.0-1.0 that determines the branching decisions. |
The state object provides:
property | purpose |
---|---|
state.time |
the current time, to drive animations |
state.events |
a list of recent spark chamber events. Each event is a tuple of (event time, power level). Power levels are currently always zero until we get the ADC-PIC-Arduino-MIDI pipeline sorted. You can simulate spark chamber events by creating a virtual MIDI source or plugging in a hardware MIDI device, restarting the client, and sending MIDI note on messages (note number not important.) This feature requires that the python-rtmidi be installed, see below. |
state.random_values |
a list of 10,000 pregenerated random numbers, consistent across frames |
state.accumulator |
an effect-defined accumulation value, useful for feedback effects |
Effects can define additional named arguments after the (tower, state) arguments. Any named arguments will be slurped up into the OSC server and exposed as endpoints for timeline or interactive control. This will allow us to get a lot more variation and interest out of simple effects.
def cortex(tower, state, sVert=0.0, sHorizon=0.0, spiralAltPeriod=4.0):
...
In order to use the OSC features, you'll need to install the pyOSC module.
pip install pyosc --pre
Pypy is a new version of the Python language tools that is substantially faster that the default implementation. Virtual environments provide a nice method for keeping python projects and their dependencies locally managed and seperate from the system. Running pypy inside a virtual environment is the recommended method of running the Cosmic Praise python client.
- Install virtualenv if it isn't already on your system:
$ sudo pip install virtualenv
-
Install pypy. You may be able to install it directly from your system's package manager (e.g.
sudo apt-get install pypy
orbrew install pypy
.) If not, you can download it from http://pypy.org/download.html and link it into /usr/local/bin. -
Create a new environment for Cosmic Praise and install the required libraries.
$ virtualenv -p /usr/local/bin/pypy $HOME/local/cosmic-praise
$ . $HOME/local/cosmic-praise/bin/activate
(cosmic-praise)$ pip install colormath
(cosmic-praise)$ pip install git+https://bitbucket.org/pypy/numpy.git
$ virtualenv -p /usr/bin/pypy $HOME/local/cosmic-praise
$ . $HOME/local/cosmic-praise/bin/activate
(cosmic-praise)$ pip install colormath
(cosmic-praise)$ sudo apt-get install build-essential pypy-dev git
(cosmic-praise)$ pip install git+https://bitbucket.org/pypy/numpy.git
- Now you can run the Cosmic Praise client in pypy to get much better performance:
pypy client/python/cosmicpraise.py -l layout/cosmicpraise.json -f 60 --sim
To receive MIDI events from the spark chamber, the python-rtmidi module is required. If you don't need to use or test this feature, you can ignore this section.
(cosmic-praise)$ sudo apt-get install libasound2-dev libjack-dev
(cosmic-praise)$ sudo pip install python-rtmidi --pre
Ubuntu users may need to build python-rtmidi from the source distro, see the troubleshooting section below.
Make sure you have typed the path to the layout .json
file correctly. If all else fails, try building your own copy of the simulator from https://github.com/Dewb/openpixelcontrol.
You need to reinstall python-rtmidi linked with libc++.
(cosmic-praise)$ pip uninstall python-rtmidi
- Download the python-rtmidi source from https://pypi.python.org/pypi/python-rtmidi#downloads
- Edit
setup.py
and change the line that readslibraries += ["pthread"]
tolibraries += ["pthread", "stdc++"]
- Run
pypy setup.py install
and try running the client again.