For the design of a sculptural ceiling for Rise Nation’s first location in Los Angeles we developed a rock-like ceiling that reacted to the sound of the workouts below with light. Rise Nation is one of the first gyms to use climbing equipment similar to spin classes. The form of the ceiling was meant to evoke a rocky surface when the lights are on, while during a workout when the lights are off the lighting is meant to be much more atmospheric like lightning. It would be very difficult to line all of the seems of the piece with addressable LEDs so we came up with a system where we exposed the caps between the cladding panels and attached the LEDs to the back of those panels shining up at a white surface. Each module acts as a “pixel” so the lighting is much simpler while still giving the visual impression that each seam is lit. The above prototype was the first test of this idea and proved to be very promising!

Here is a grasshopper definition that creates the cell grid for the ceiling and offsets the cladding: cell_grid.zip The definition also retains the cell geometry for the framing that can be made by cutting flat material. The ceiling structure we installed below was generated using a version of the definition above.

We built the cells in modules of four “pixels.” The base element of each module is a flat foursquare frame made of aluminum. We added verticel stiffeners to keep this flat module from deflecting and then hung the four cells from the foursquare frame.

The above sketch shows how the larger “pixels” of light show through the seams of the panels. The structure was imported directly from the grasshopper definition using text files. The source code for the sketch is below or you can download the code and text files as an application: Rise_ceiling.zip

import controlP5.*;
import ddf.minim.*;

Minim minim;
AudioInput sound;

ControlP5 cp5;

float hvalue;

ArrayList <PVector> frame = new ArrayList<PVector>();
ArrayList <PVector> panels = new ArrayList<PVector>();
ArrayList <PVector> lights = new ArrayList<PVector>();

FloatList lts = new FloatList();

///camera nav
int oldx = mouseX;
int oldy = mouseY;
float rotx = 0;
float roty = 0;
float rotz = -PI/2.0;
float zcam = 100;

////controls
float amplitude;
float threshold_pulse;
float threshold_random;
boolean pulse = true;
float fade = 1.0;
boolean wave_on = true;
float wave_amplitude = 1.0;
float wave_speed = 1.0;
boolean random_on = true;
float random_num;
boolean panels_on;
float panel_alpha = 100;
boolean structure_on;

float wave_count = -11;


////pulse
float pulsex = 0;
float pulsey = 0;
float pulsez = 0;
float pulse_speed = 3;
float pulse_width = 10;
float decay = 0.9;
float dist = 99999;

///geom offset
int xo = -11;
int yo = -5; 
int zo = 0;
float scg = 1;

void setup() {
  size(1800,900,P3D);
  perspective(PI/3.0,(float)width/height,1,100000);
  
  minim = new Minim(this);
  sound = minim.getLineIn(Minim.STEREO, 1024);
  
  cp5 = new ControlP5(this);
  
  cp5.addSlider("amplitude")
    .setPosition(40,40)
    .setRange(0,200)
    .setSize(200,20)
    .setValue(200)
    .setColorForeground(color(20,200,200))
    .setColorLabel(color(255))
    .setColorBackground(color(70,70,70))
    .setColorValue(color(0,0,0))
    .setColorActive(color(0,255,255))
  ;
  cp5.addSlider("threshold_pulse")
    .setPosition(40,80)
    .setRange(0,200)
    .setSize(200,20)
    .setValue(100)
    .setColorForeground(color(20,200,200))
    .setColorLabel(color(255))
    .setColorBackground(color(70,70,70))
    .setColorValue(color(0,0,0))
    .setColorActive(color(0,255,255))
  ;
  
  cp5.addSlider("threshold_random")
    .setPosition(40,120)
    .setRange(0,200)
    .setSize(200,20)
    .setValue(100)
    .setColorForeground(color(20,200,200))
    .setColorLabel(color(255))
    .setColorBackground(color(70,70,70))
    .setColorValue(color(0,0,0))
    .setColorActive(color(0,255,255))
  ;
  
    cp5.addToggle("pulse")
     .setPosition(320,40)
     .setSize(20,20)
     .setValue(true)
     .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
     ;
   
   cp5.addSlider("pulse_speed")
    .setPosition(360,40)
    .setRange(0,1)
    .setSize(200,20)
    .setValue(0.8)
    .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
  ;
  
  cp5.addSlider("pulse_width")
    .setPosition(360,80)
    .setRange(0,5)
    .setSize(200,20)
    .setValue(2)
    .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
  ;
  
  cp5.addSlider("decay")
    .setPosition(width-280,80)
    .setRange(0.5,1)
    .setSize(200,20)
    .setValue(0.9)
    .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
  ;
  
   cp5.addToggle("random_on")
     .setPosition(640,40)
     .setSize(20,20)
     .setValue(true)
     .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
     ;
   
   cp5.addSlider("random_num")
    .setPosition(680,40)
    .setRange(0.9,1)
    .setSize(200,20)
    .setValue(0.95)
    .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
  ;
  
  cp5.addToggle("wave_on")
     .setPosition(960,40)
     .setSize(20,20)
     .setValue(true)
     .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
     ;
  
   cp5.addSlider("wave_amplitude")
    .setPosition(1000,40)
    .setRange(0,5)
    .setSize(200,20)
    .setValue(1.0)
    .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
  ;
  
  cp5.addSlider("wave_speed")
    .setPosition(1000,80)
    .setRange(0,0.5)
    .setSize(200,20)
    .setValue(0.4)
    .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
  ;
  
  cp5.addToggle("panels_on")
     .setPosition(width-320,40)
     .setSize(20,20)
     .setValue(true)
     .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
     ;
     
  cp5.addToggle("structure_on")
     .setPosition(width-320,80)
     .setSize(20,20)
     .setValue(true)
     .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
     ;
     
  cp5.addSlider("panel_alpha")
    .setPosition(width-280,40)
    .setRange(0,255)
    .setSize(200,20)
    .setValue(100)
    .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
  ;
     
  cp5.addButton("ResetView")
     .setValue(0)
     .setPosition(160,160)
     .setSize(80,20)
     .setColorForeground(color(20,200,200))
     .setColorLabel(color(255))
     .setColorBackground(color(70,70,70))
     .setColorValue(color(0,0,0))
     .setColorActive(color(0,255,255))
     ;
  
  String[] vec = loadStrings("frame.txt");
  for(int i = 0; i < (vec.length/3); ++i){
    float xx = (float(vec[i*3])+xo)*scg;
    float yy = -(float(vec[i*3+1])+yo)*scg;
    float zz = (float(vec[i*3+2])+zo)*scg;
    frame.add(new PVector(xx,yy,zz));
  }
  
  vec = loadStrings("panels.txt");
  for(int i = 0; i < (vec.length/3); ++i){
    float xx = (float(vec[i*3])+xo)*scg;
    float yy = -(float(vec[i*3+1])+yo)*scg;
    float zz = (float(vec[i*3+2])+zo)*scg;
    panels.add(new PVector(xx,yy,zz));
  }
  
  vec = loadStrings("lights.txt");
  for(int i = 0; i < (vec.length/3); ++i){
    float xx = (float(vec[i*3])+xo)*scg;
    float yy = -(float(vec[i*3+1])+yo)*scg;
    float zz = (float(vec[i*3+2])+zo)*scg;
    lights.add(new PVector(xx,yy,zz));
      lts.append(0.0);
  }
  //noLoop();
}

void draw() {
  background(0);
  lights();
 
  
  hvalue = 0;
  for(int i = 0; i < sound.mix.size() - 1; i++){
    //line(i, 700 + sound.mix.get(i)*amplitude, i+1, 700 + sound.mix.get(i+1)*amplitude);
    if(hvalue <  abs(sound.mix.get(i))){
      hvalue = abs(sound.mix.get(i));
    }
  }
  
  hvalue = hvalue * amplitude;
  
  if(hvalue > threshold_pulse){
     dist = 0;
     pulsez = random(-4,4);
     pulsey = random(-4,4);
     pulsex = random(-50,50);

  }

  ////sound bar////
  noStroke();
  rectMode(CORNER);
  fill(70);
  rect(40,160,50,200);
  fill(20,200,200);
  rect(40,360,50,(hvalue*-1));
  
  stroke(255);
  strokeWeight(1);
  line(100,(360-threshold_pulse),110,(360-threshold_pulse));
  line(115,(360-threshold_random),125,(360-threshold_random));
  
  
  if((pulse == true) && (hvalue > threshold_pulse)){
    pulsex = random(-10,10);
    pulsey = random(-5,5);
    pulsez = random(-1,1);
    dist = 0;
    
  }
  
  wave_count = wave_count + wave_speed;
  if(wave_count > 11){
    wave_count = -11;
  }
  
  pushMatrix();
  cam();
  
  strokeWeight(1);
  if(structure_on == true){
     for(int i = 0; i < frame.size()-1; ++i){
        PVector sp = frame.get(i);
        PVector ep = frame.get(i+1);
        stroke(255);
        strokeWeight(1);
        line(sp.x,sp.y,sp.z,ep.x,ep.y,ep.z);
        i++;
     }
  }
   
   if(panels_on == true){
     for(int i = 0; i < panels.size(); i++){
        fill(255,panel_alpha);
        noStroke();
        PVector sp = panels.get(i);
        if(sp.z == 9999){
          beginShape();
          float limit = sp.x-xo;
          for(int j=0; j < limit; j++){
            i++;
            PVector np = panels.get(i);
            vertex(np.x,np.y,np.z);
          }
          endShape();
        }
     }
   }
   
   for(int i = 0; i < lights.size();){
      float aa = lts.get(i);
      PVector np = lights.get(i);
      if(aa > 1){
        aa = aa *decay;
      }else{
        aa = 0;
      }
      lts.set(i,aa);
      float dd = dist(pulsex,pulsey,pulsez,np.x,np.y,np.z);
      if(abs(dist-dd) < pulse_width && pulse == true){
        lts.set(i,255);
      }
       dd = dist(wave_count,0,0,np.x,0,0);
      if(dd < wave_amplitude && wave_on == true){
        lts.set(i,255);
      }
      float rr = random(0,8.0);
      if(hvalue > threshold_random && random_on == true && rr < random_num){
        lts.set(i,255);
      }
      fill(255,aa); 
      noStroke();
      beginShape();
      for(int j=0; j < 4; j++){
        np = lights.get(i);
        vertex(np.x,np.y,np.z);
        i++;
      }
      endShape();
   }
  
   popMatrix();
   
   dist = dist + pulse_speed;
   
}

void cam() {
  int newx = mouseX;
  int newy = mouseY;
  translate(width/2, height/2,zcam);
  rotateY(rotx);
  rotateX(roty);
  rotateZ(rotz+PI/2);
  translate(0,0,0);
  //rotateZ(PI);
  if ((mousePressed == true) && (mouseY > 200)) {
    rotx = rotx + (oldx-newx)/50.0;
    roty = roty + (oldy-newy)/50.0;
  }
  oldx = newx;
  oldy = newy;
  
  
  //println("X  " + rotx);
  //println("Y  " + roty);
  //println("Z  " + zcam);
}

void mouseWheel(MouseEvent event) {
  float e = event.getCount();
  zcam = zcam - e*5;
}


void ResetView() {
  rotx = 0;
  roty = 0;
  rotz = -PI/2.0;
  zcam = 760;
}