Make yMin correct with unlit bassBox on model
[SugarCubes.git] / _Internals.pde
1 /**
2 * DOUBLE BLACK DIAMOND DOUBLE BLACK DIAMOND
3 *
4 * //\\ //\\ //\\ //\\
5 * ///\\\ ///\\\ ///\\\ ///\\\
6 * \\\/// \\\/// \\\/// \\\///
7 * \\// \\// \\// \\//
8 *
9 * EXPERTS ONLY!! EXPERTS ONLY!!
10 *
11 * If you are an artist, you may ignore this file! It just sets
12 * up the framework to run the patterns. Should not need modification
13 * for general animation work.
14 */
15
16 import glucose.*;
17 import glucose.control.*;
18 import glucose.effect.*;
19 import glucose.model.*;
20 import glucose.pattern.*;
21 import glucose.transform.*;
22 import glucose.transition.*;
23 import heronarts.lx.*;
24 import heronarts.lx.control.*;
25 import heronarts.lx.effect.*;
26 import heronarts.lx.modulator.*;
27 import heronarts.lx.pattern.*;
28 import heronarts.lx.transition.*;
29 import ddf.minim.*;
30 import ddf.minim.analysis.*;
31 import processing.opengl.*;
32 import rwmidi.*;
33
34 final int VIEWPORT_WIDTH = 900;
35 final int VIEWPORT_HEIGHT = 700;
36
37 // The trailer is measured from the outside of the black metal (but not including the higher welded part on the front)
38 final float TRAILER_WIDTH = 240;
39 final float TRAILER_DEPTH = 97;
40 final float TRAILER_HEIGHT = 33;
41
42 int targetFramerate = 60;
43 int startMillis, lastMillis;
44
45 // Core engine variables
46 GLucose glucose;
47 HeronLX lx;
48 LXPattern[] patterns;
49 MappingTool mappingTool;
50 PandaDriver[] pandaBoards;
51 MidiEngine midiEngine;
52
53 // Display configuration mode
54 boolean mappingMode = false;
55 boolean debugMode = false;
56 DebugUI debugUI;
57 boolean uiOn = true;
58 LXPattern restoreToPattern = null;
59 PImage logo;
60
61 // Handles to UI objects
62 UIContext[] overlays;
63 UIPatternDeck uiPatternA;
64 UICrossfader uiCrossfader;
65 UIMidi uiMidi;
66 UIMapping uiMapping;
67 UIDebugText uiDebugText;
68
69 // Camera variables
70 float eyeR, eyeA, eyeX, eyeY, eyeZ, midX, midY, midZ;
71
72 /**
73 * Engine construction and initialization.
74 */
75 LXPattern[] _patterns(GLucose glucose) {
76 LXPattern[] patterns = patterns(glucose);
77 for (LXPattern p : patterns) {
78 p.setTransition(new DissolveTransition(glucose.lx).setDuration(1000));
79 }
80 return patterns;
81 }
82
83 void logTime(String evt) {
84 int now = millis();
85 println(evt + ": " + (now - lastMillis) + "ms");
86 lastMillis = now;
87 }
88
89 void setup() {
90 startMillis = lastMillis = millis();
91
92 // Initialize the Processing graphics environment
93 size(VIEWPORT_WIDTH, VIEWPORT_HEIGHT, OPENGL);
94 frameRate(targetFramerate);
95 noSmooth();
96 // hint(ENABLE_OPENGL_4X_SMOOTH); // no discernable improvement?
97 logTime("Created viewport");
98
99 // Create the GLucose engine to run the cubes
100 glucose = new GLucose(this, buildModel());
101 lx = glucose.lx;
102 lx.enableKeyboardTempo();
103 logTime("Built GLucose engine");
104
105 // MIDI devices
106 midiEngine = new MidiEngine();
107 logTime("Setup MIDI devices");
108
109 // Set the patterns
110 Engine engine = lx.engine;
111 engine.setPatterns(patterns = _patterns(glucose));
112 engine.addDeck(_patterns(glucose));
113 logTime("Built patterns");
114 glucose.setTransitions(transitions(glucose));
115 logTime("Built transitions");
116 glucose.lx.addEffects(effects(glucose));
117 logTime("Built effects");
118
119 // Build output driver
120 PandaMapping[] pandaMappings = buildPandaList();
121 pandaBoards = new PandaDriver[pandaMappings.length];
122 int pbi = 0;
123 for (PandaMapping pm : pandaMappings) {
124 pandaBoards[pbi++] = new PandaDriver(pm.ip, glucose.model, pm);
125 }
126 mappingTool = new MappingTool(glucose, pandaMappings);
127 logTime("Built PandaDriver");
128
129 // Build overlay UI
130 debugUI = new DebugUI(pandaMappings);
131 overlays = new UIContext[] {
132 uiPatternA = new UIPatternDeck(lx.engine.getDeck(0), "PATTERN A", 4, 4, 140, 324),
133 new UIBlendMode(4, 332, 140, 86),
134 new UIEffects(4, 422, 140, 144),
135 new UITempo(4, 570, 140, 50),
136 new UISpeed(4, 624, 140, 50),
137
138 new UIPatternDeck(lx.engine.getDeck(1), "PATTERN B", width-144, 4, 140, 324),
139 uiMidi = new UIMidi(midiEngine, width-144, 332, 140, 158),
140 new UIOutput(width-144, 494, 140, 106),
141
142 uiCrossfader = new UICrossfader(width/2-90, height-90, 180, 86),
143
144 uiDebugText = new UIDebugText(148, height-138, width-304, 44),
145 uiMapping = new UIMapping(mappingTool, 4, 4, 140, 324),
146 };
147 uiMapping.setVisible(false);
148 logTime("Built overlay UI");
149
150 // Load logo image
151 logo = loadImage("data/logo.png");
152
153 // Setup camera
154 midX = TRAILER_WIDTH/2.;
155 midY = glucose.model.yMax/2;
156 midZ = TRAILER_DEPTH/2.;
157 eyeR = -290;
158 eyeA = .15;
159 eyeY = midY + 70;
160 eyeX = midX + eyeR*sin(eyeA);
161 eyeZ = midZ + eyeR*cos(eyeA);
162
163 // Add mouse scrolling event support
164 addMouseWheelListener(new java.awt.event.MouseWheelListener() {
165 public void mouseWheelMoved(java.awt.event.MouseWheelEvent mwe) {
166 mouseWheel(mwe.getWheelRotation());
167 }});
168
169 println("Total setup: " + (millis() - startMillis) + "ms");
170 println("Hit the 'p' key to toggle Panda Board output");
171 }
172
173 /**
174 * Core render loop and drawing functionality.
175 */
176 void draw() {
177 // Draws the simulation and the 2D UI overlay
178 background(40);
179 color[] colors = glucose.getColors();
180
181 String displayMode = uiCrossfader.getDisplayMode();
182 if (displayMode == "A") {
183 colors = lx.engine.getDeck(0).getColors();
184 } else if (displayMode == "B") {
185 colors = lx.engine.getDeck(1).getColors();
186 }
187 if (debugMode) {
188 debugUI.maskColors(colors);
189 }
190
191 camera(
192 eyeX, eyeY, eyeZ,
193 midX, midY, midZ,
194 0, -1, 0
195 );
196
197 translate(0, 40, 0);
198
199 noStroke();
200 fill(#141414);
201 drawBox(0, -TRAILER_HEIGHT, 0, 0, 0, 0, TRAILER_WIDTH, TRAILER_HEIGHT, TRAILER_DEPTH, TRAILER_HEIGHT/2.);
202 fill(#070707);
203 stroke(#222222);
204 beginShape();
205 vertex(0, 0, 0);
206 vertex(TRAILER_WIDTH, 0, 0);
207 vertex(TRAILER_WIDTH, 0, TRAILER_DEPTH);
208 vertex(0, 0, TRAILER_DEPTH);
209 endShape();
210
211 // Draw the logo on the front of platform
212 pushMatrix();
213 translate(0, 0, -1);
214 float s = .07;
215 scale(s, -s, s);
216 image(logo, TRAILER_WIDTH/2/s-logo.width/2, TRAILER_HEIGHT/2/s-logo.height/2-2/s);
217 popMatrix();
218
219 noStroke();
220 if (glucose.model.bassBox.exists) {
221 drawBassBox(glucose.model.bassBox, false);
222 }
223 for (Speaker speaker : glucose.model.speakers) {
224 drawSpeaker(speaker);
225 }
226 for (Cube c : glucose.model.cubes) {
227 drawCube(c);
228 }
229
230 noFill();
231 strokeWeight(2);
232 beginShape(POINTS);
233 for (Point p : glucose.model.points) {
234 stroke(colors[p.index]);
235 vertex(p.fx, p.fy, p.fz);
236 }
237 endShape();
238
239 // 2D Overlay UI
240 drawUI();
241
242 // Send output colors
243 color[] sendColors = glucose.getColors();
244 if (debugMode) {
245 debugUI.maskColors(colors);
246 }
247
248 // Gamma correction here. Apply a cubic to the brightness
249 // for better representation of dynamic range
250 for (int i = 0; i < colors.length; ++i) {
251 float b = brightness(colors[i]) / 100.f;
252 colors[i] = color(
253 hue(colors[i]),
254 saturation(colors[i]),
255 (b*b*b) * 100.
256 );
257 }
258
259 // TODO(mcslee): move into GLucose engine
260 for (PandaDriver p : pandaBoards) {
261 p.send(colors);
262 }
263 }
264
265 void drawBassBox(BassBox b, boolean hasSub) {
266
267 float in = .15;
268
269 if (hasSub) {
270 noStroke();
271 fill(#191919);
272 pushMatrix();
273 translate(b.x + BassBox.EDGE_WIDTH/2., b.y + BassBox.EDGE_HEIGHT/2, b.z + BassBox.EDGE_DEPTH/2.);
274 box(BassBox.EDGE_WIDTH-20*in, BassBox.EDGE_HEIGHT-20*in, BassBox.EDGE_DEPTH-20*in);
275 popMatrix();
276 }
277
278 noStroke();
279 fill(#393939);
280 drawBox(b.x+in, b.y+in, b.z+in, 0, 0, 0, BassBox.EDGE_WIDTH-in*2, BassBox.EDGE_HEIGHT-in*2, BassBox.EDGE_DEPTH-in*2, Cube.CHANNEL_WIDTH-in);
281
282 pushMatrix();
283 translate(b.x+(Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT-in, b.z + BassBox.EDGE_DEPTH/2.);
284 float lastOffset = 0;
285 for (float offset : BoothFloor.STRIP_OFFSETS) {
286 translate(offset - lastOffset, 0, 0);
287 box(Cube.CHANNEL_WIDTH-in, 0, BassBox.EDGE_DEPTH - 2*in);
288 lastOffset = offset;
289 }
290 popMatrix();
291
292 pushMatrix();
293 translate(b.x + (Cube.CHANNEL_WIDTH-in)/2., b.y + BassBox.EDGE_HEIGHT/2., b.z + in);
294 for (int j = 0; j < 2; ++j) {
295 pushMatrix();
296 for (int i = 0; i < BassBox.NUM_FRONT_STRUTS; ++i) {
297 translate(BassBox.FRONT_STRUT_SPACING, 0, 0);
298 box(Cube.CHANNEL_WIDTH-in, BassBox.EDGE_HEIGHT - in*2, 0);
299 }
300 popMatrix();
301 translate(0, 0, BassBox.EDGE_DEPTH - 2*in);
302 }
303 popMatrix();
304
305 pushMatrix();
306 translate(b.x + in, b.y + BassBox.EDGE_HEIGHT/2., b.z + BassBox.SIDE_STRUT_SPACING + (Cube.CHANNEL_WIDTH-in)/2.);
307 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
308 translate(BassBox.EDGE_WIDTH-2*in, 0, 0);
309 box(0, BassBox.EDGE_HEIGHT - in*2, Cube.CHANNEL_WIDTH-in);
310 popMatrix();
311
312 }
313
314 void drawCube(Cube c) {
315 float in = .15;
316 noStroke();
317 fill(#393939);
318 drawBox(c.x+in, c.y+in, c.z+in, c.rx, c.ry, c.rz, Cube.EDGE_WIDTH-in*2, Cube.EDGE_HEIGHT-in*2, Cube.EDGE_WIDTH-in*2, Cube.CHANNEL_WIDTH-in);
319 }
320
321 void drawSpeaker(Speaker s) {
322 float in = .15;
323
324 noStroke();
325 fill(#191919);
326 pushMatrix();
327 translate(s.x, s.y, s.z);
328 rotate(s.ry / 180. * PI, 0, -1, 0);
329 translate(Speaker.EDGE_WIDTH/2., Speaker.EDGE_HEIGHT/2., Speaker.EDGE_DEPTH/2.);
330 box(Speaker.EDGE_WIDTH-20*in, Speaker.EDGE_HEIGHT-20*in, Speaker.EDGE_DEPTH-20*in);
331 translate(0, Speaker.EDGE_HEIGHT/2. + Speaker.EDGE_HEIGHT*.8/2, 0);
332
333 fill(#222222);
334 box(Speaker.EDGE_WIDTH*.6, Speaker.EDGE_HEIGHT*.8, Speaker.EDGE_DEPTH*.75);
335 popMatrix();
336
337 noStroke();
338 fill(#393939);
339 drawBox(s.x+in, s.y+in, s.z+in, 0, s.ry, 0, Speaker.EDGE_WIDTH-in*2, Speaker.EDGE_HEIGHT-in*2, Speaker.EDGE_DEPTH-in*2, Cube.CHANNEL_WIDTH-in);
340 }
341
342 void drawBox(float x, float y, float z, float rx, float ry, float rz, float xd, float yd, float zd, float sw) {
343 pushMatrix();
344 translate(x, y, z);
345 rotate(rx / 180. * PI, -1, 0, 0);
346 rotate(ry / 180. * PI, 0, -1, 0);
347 rotate(rz / 180. * PI, 0, 0, -1);
348 for (int i = 0; i < 4; ++i) {
349 float wid = (i % 2 == 0) ? xd : zd;
350
351 beginShape();
352 vertex(0, 0);
353 vertex(wid, 0);
354 vertex(wid, yd);
355 vertex(wid - sw, yd);
356 vertex(wid - sw, sw);
357 vertex(0, sw);
358 endShape();
359 beginShape();
360 vertex(0, sw);
361 vertex(0, yd);
362 vertex(wid - sw, yd);
363 vertex(wid - sw, yd - sw);
364 vertex(sw, yd - sw);
365 vertex(sw, sw);
366 endShape();
367
368 translate(wid, 0, 0);
369 rotate(HALF_PI, 0, -1, 0);
370 }
371 popMatrix();
372 }
373
374 void drawUI() {
375 camera();
376 javax.media.opengl.GL gl = ((PGraphicsOpenGL)g).beginGL();
377 gl.glClear(javax.media.opengl.GL.GL_DEPTH_BUFFER_BIT);
378 ((PGraphicsOpenGL)g).endGL();
379 strokeWeight(1);
380
381 if (uiOn) {
382 for (UIContext context : overlays) {
383 context.draw();
384 }
385 }
386
387 // Always draw FPS meter
388 fill(#555555);
389 textSize(9);
390 textAlign(LEFT, BASELINE);
391 text("FPS: " + ((int) (frameRate*10)) / 10. + " / " + targetFramerate + " (-/+)", 4, height-4);
392
393 if (debugMode) {
394 debugUI.draw();
395 }
396 }
397
398
399 /**
400 * Top-level keyboard event handling
401 */
402 void keyPressed() {
403 if (mappingMode) {
404 mappingTool.keyPressed(uiMapping);
405 }
406 switch (key) {
407 case '-':
408 case '_':
409 frameRate(--targetFramerate);
410 break;
411 case '=':
412 case '+':
413 frameRate(++targetFramerate);
414 break;
415 case 'b':
416 EFF_boom.trigger();
417 break;
418 case 'd':
419 if (!midiEngine.isQwertyEnabled()) {
420 debugMode = !debugMode;
421 println("Debug output: " + (debugMode ? "ON" : "OFF"));
422 }
423 break;
424 case 'm':
425 if (!midiEngine.isQwertyEnabled()) {
426 mappingMode = !mappingMode;
427 uiPatternA.setVisible(!mappingMode);
428 uiMapping.setVisible(mappingMode);
429 if (mappingMode) {
430 restoreToPattern = lx.getPattern();
431 lx.setPatterns(new LXPattern[] { mappingTool });
432 } else {
433 lx.setPatterns(patterns);
434 LXTransition pop = restoreToPattern.getTransition();
435 restoreToPattern.setTransition(null);
436 lx.goPattern(restoreToPattern);
437 restoreToPattern.setTransition(pop);
438 }
439 }
440 break;
441 case 'p':
442 for (PandaDriver p : pandaBoards) {
443 p.toggle();
444 }
445 break;
446 case 'u':
447 if (!midiEngine.isQwertyEnabled()) {
448 uiOn = !uiOn;
449 }
450 break;
451 }
452 }
453
454 /**
455 * Top-level mouse event handling
456 */
457 int mx, my;
458 void mousePressed() {
459 boolean debugged = false;
460 if (debugMode) {
461 debugged = debugUI.mousePressed();
462 }
463 if (!debugged) {
464 for (UIContext context : overlays) {
465 context.mousePressed(mouseX, mouseY);
466 }
467 }
468 mx = mouseX;
469 my = mouseY;
470 }
471
472 void mouseDragged() {
473 boolean dragged = false;
474 for (UIContext context : overlays) {
475 dragged |= context.mouseDragged(mouseX, mouseY);
476 }
477 if (!dragged) {
478 int dx = mouseX - mx;
479 int dy = mouseY - my;
480 mx = mouseX;
481 my = mouseY;
482 eyeA += dx*.003;
483 eyeX = midX + eyeR*sin(eyeA);
484 eyeZ = midZ + eyeR*cos(eyeA);
485 eyeY += dy;
486 }
487 }
488
489 void mouseReleased() {
490 for (UIContext context : overlays) {
491 context.mouseReleased(mouseX, mouseY);
492 }
493 }
494
495 void mouseWheel(int delta) {
496 boolean wheeled = false;
497 for (UIContext context : overlays) {
498 wheeled |= context.mouseWheel(mouseX, mouseY, delta);
499 }
500
501 if (!wheeled) {
502 eyeR = constrain(eyeR - delta, -500, -80);
503 eyeX = midX + eyeR*sin(eyeA);
504 eyeZ = midZ + eyeR*cos(eyeA);
505 }
506 }