feat(oscope-of): 3D Tunnel + Mesh visualizers
Context: the 8 modes added in the previous commit are all 2D. For
visual variety on stage we want true depth — a tunnel that flies past
the audience and a deformable 3D object that you can read as a body.
Both should react to the same OSC bus the rest of the system uses.
Approach: Tunnel is a closed-form pseudo-3D fragment shader (1/r depth
mapping, no real raymarch needed for an infinite straight tube), so
it's GPU-cheap and works under PostFx like every other mode. Mesh uses
real geometry — ofMesh::icosphere subdivided 4x, vertices displaced
each frame from a 32-band envelope of the channel waveform. ofCamera
gives a proper perspective; we draw inside its viewport so the rest
of the app (HUD, GUI panels, post-fx) keeps its 2D pipeline.
Changes:
- visualizers/TunnelVis.{h,cpp} + shaders/tunnel.{vert,frag} :
travel speed = bpm * 0.012 + kick, twist = depth * lead amplitude,
brick pattern (ang × slice), bright lamps every 4 slices that pulse
on kick, IQ palette cycles along depth, distance fog modulated by
pad
- visualizers/MeshVis.{h,cpp} : icosphere with 32-band RMS displacement
per vertex (stable hash from vertex position picks the band),
pulse-on-beat envelope (0.18 lerp back), self-rotation modulated by
bpm, draws three layers in additive blend mode (dim wireframe back,
solid HSB sphere, bright inner wireframe at 0.85x), camera viewport
isolated via ofRectangle so it doesn't break the 2D pipeline,
synthetic band envelope when scope buffer is empty
- ofApp : two new Mode enum entries (Tunnel, Mesh), keys '0' and '-'
to select them, factory-reload includes tunnel shader, default_mode
config supports 'tunnel' and 'mesh', Hybrid mode now uses the 3D
tunnel as background and the 3D mesh as one of its quadrants
(replacing the 2D reactive duplicate)
Impact: 11 modes total (was 9), of which 2 are real 3D. Both compose
with the post-fx chain so chromatic / kaleido / feedback / glitch
apply to them too. Build clean on Intel MBP / Big Sur with the same
make OF_ROOT=$HOME/of Release flow.
This commit is contained in:
+18
-3
@@ -44,6 +44,8 @@ const char* modeName(ofApp::Mode m) {
|
||||
case ofApp::Mode::Plasma: return "Plasma";
|
||||
case ofApp::Mode::Particles: return "Particles";
|
||||
case ofApp::Mode::Kaleido: return "Kaleido";
|
||||
case ofApp::Mode::Tunnel: return "Tunnel 3D";
|
||||
case ofApp::Mode::Mesh: return "Mesh 3D";
|
||||
case ofApp::Mode::Hybrid: return "Hybrid";
|
||||
}
|
||||
return "?";
|
||||
@@ -75,6 +77,8 @@ void ofApp::setup() {
|
||||
plasma_ = std::make_unique<oscope::PlasmaVis>();
|
||||
particles_ = std::make_unique<oscope::ParticleVis>();
|
||||
kaleido_ = std::make_unique<oscope::KaleidoVis>();
|
||||
tunnel_ = std::make_unique<oscope::TunnelVis>();
|
||||
mesh_ = std::make_unique<oscope::MeshVis>();
|
||||
lissajous_->setup(W, H);
|
||||
spectro_->setup(W, H / 4);
|
||||
reactive_->setup(W, H);
|
||||
@@ -83,6 +87,8 @@ void ofApp::setup() {
|
||||
plasma_->setup(W, H);
|
||||
particles_->setup(W, H);
|
||||
kaleido_->setup(W, H);
|
||||
tunnel_->setup(W, H);
|
||||
mesh_->setup(W, H);
|
||||
|
||||
postfx_.setup(W, H);
|
||||
|
||||
@@ -137,6 +143,8 @@ void ofApp::loadSettings() {
|
||||
else if (m == "plasma") mode_ = Mode::Plasma;
|
||||
else if (m == "particles") mode_ = Mode::Particles;
|
||||
else if (m == "kaleido") mode_ = Mode::Kaleido;
|
||||
else if (m == "tunnel") mode_ = Mode::Tunnel;
|
||||
else if (m == "mesh") mode_ = Mode::Mesh;
|
||||
else mode_ = Mode::Hybrid;
|
||||
}
|
||||
|
||||
@@ -186,6 +194,8 @@ void ofApp::update() {
|
||||
plasma_->update(frame);
|
||||
particles_->update(frame);
|
||||
kaleido_->update(frame);
|
||||
tunnel_->update(frame);
|
||||
mesh_->update(frame);
|
||||
|
||||
applyOscFx();
|
||||
|
||||
@@ -229,13 +239,15 @@ void ofApp::drawMode(Mode m, int x, int y, int w, int h) {
|
||||
particles_->draw(x, y, w, h);
|
||||
break;
|
||||
case Mode::Kaleido: kaleido_->draw(x, y, w, h); break;
|
||||
case Mode::Tunnel: tunnel_->draw(x, y, w, h); break;
|
||||
case Mode::Mesh: mesh_->draw(x, y, w, h); break;
|
||||
case Mode::Hybrid: drawHybrid(w, h); break;
|
||||
}
|
||||
}
|
||||
|
||||
void ofApp::drawHybrid(int W, int H) {
|
||||
// Background plasma stretches across the full canvas
|
||||
plasma_->draw(0, 0, W, H);
|
||||
// Background : the 3D tunnel fills the canvas
|
||||
tunnel_->draw(0, 0, W, H);
|
||||
|
||||
// 2x2 quadrant overlay with additive blending
|
||||
ofEnableBlendMode(OF_BLENDMODE_ADD);
|
||||
@@ -244,7 +256,7 @@ void ofApp::drawHybrid(int W, int H) {
|
||||
lissajous_->draw(0, 0, hw, hh);
|
||||
polar_->draw (hw, 0, hw, hh);
|
||||
kaleido_->draw (0, hh, hw, hh);
|
||||
reactive_->draw(hw, hh, hw, hh);
|
||||
mesh_->draw (hw, hh, hw, hh);
|
||||
ofDisableBlendMode();
|
||||
|
||||
// Particle field on top, fullscreen
|
||||
@@ -307,6 +319,8 @@ void ofApp::keyPressed(int key) {
|
||||
case '7': mode_ = Mode::Plasma; break;
|
||||
case '8': mode_ = Mode::Particles; break;
|
||||
case '9': mode_ = Mode::Kaleido; break;
|
||||
case '0': mode_ = Mode::Tunnel; break;
|
||||
case '-': mode_ = Mode::Mesh; break;
|
||||
case 'f':
|
||||
fullscreen_ = !fullscreen_;
|
||||
ofSetFullscreen(fullscreen_);
|
||||
@@ -319,6 +333,7 @@ void ofApp::keyPressed(int key) {
|
||||
lissajous_->reloadShaders();
|
||||
reactive_->reloadShaders();
|
||||
plasma_->reloadShaders();
|
||||
tunnel_->reloadShaders();
|
||||
postfx_.reloadShader();
|
||||
ofLogNotice("ofApp") << "Shaders rechargés";
|
||||
break;
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#include "visualizers/PlasmaVis.h"
|
||||
#include "visualizers/ParticleVis.h"
|
||||
#include "visualizers/KaleidoVis.h"
|
||||
#include "visualizers/TunnelVis.h"
|
||||
#include "visualizers/MeshVis.h"
|
||||
|
||||
#include <memory>
|
||||
|
||||
@@ -29,6 +31,7 @@ public:
|
||||
enum class Mode {
|
||||
Lissajous, Spectrogram, Reactive, Waveform,
|
||||
Polar, Plasma, Particles, Kaleido,
|
||||
Tunnel, Mesh,
|
||||
Hybrid
|
||||
};
|
||||
|
||||
@@ -58,6 +61,8 @@ private:
|
||||
std::unique_ptr<oscope::PlasmaVis> plasma_;
|
||||
std::unique_ptr<oscope::ParticleVis> particles_;
|
||||
std::unique_ptr<oscope::KaleidoVis> kaleido_;
|
||||
std::unique_ptr<oscope::TunnelVis> tunnel_;
|
||||
std::unique_ptr<oscope::MeshVis> mesh_;
|
||||
|
||||
std::vector<float> ch1_, ch2_;
|
||||
Mode mode_ = Mode::Hybrid;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "MeshVis.h"
|
||||
|
||||
namespace oscope {
|
||||
|
||||
void MeshVis::setup(int w, int h) {
|
||||
w_ = w; h_ = h;
|
||||
sphereBase_ = ofMesh::icosphere(1.0f, 4);
|
||||
sphereDeformed_ = sphereBase_;
|
||||
cam_.setPosition(0.0f, 0.0f, 3.5f);
|
||||
cam_.lookAt(glm::vec3(0.0f));
|
||||
cam_.setNearClip(0.05f);
|
||||
cam_.setFarClip(100.0f);
|
||||
bandEnergy_.assign(32, 0.0f);
|
||||
}
|
||||
|
||||
void MeshVis::update(const VisFrame& frame) {
|
||||
bpm_ = frame.osc.bpm();
|
||||
kick_ = frame.osc.amp("kick");
|
||||
bass_ = frame.osc.amp("bass");
|
||||
lead_ = frame.osc.amp("lead");
|
||||
pad_ = frame.osc.amp("pad");
|
||||
|
||||
yaw_ += static_cast<float>(ofGetLastFrameTime()) * (0.18f + bpm_ * 0.0024f);
|
||||
pitch_ += static_cast<float>(ofGetLastFrameTime()) * 0.07f;
|
||||
|
||||
if (frame.osc.beatPulse()) pulseScale_ = 1.0f + 0.45f * (0.4f + kick_);
|
||||
pulseScale_ += (1.0f - pulseScale_) * 0.18f;
|
||||
|
||||
// Band energy from ch1 (32 buckets, RMS within each)
|
||||
const auto& s = frame.ch1;
|
||||
if (!s.empty()) {
|
||||
const std::size_t N = s.size();
|
||||
const std::size_t band = std::max<std::size_t>(1, N / bandEnergy_.size());
|
||||
for (std::size_t b = 0; b < bandEnergy_.size(); ++b) {
|
||||
float acc = 0.0f;
|
||||
for (std::size_t i = 0; i < band && b * band + i < N; ++i) {
|
||||
float v = s[b * band + i];
|
||||
acc += v * v;
|
||||
}
|
||||
float rms = std::sqrt(acc / std::max<std::size_t>(1, band));
|
||||
bandEnergy_[b] = bandEnergy_[b] * 0.78f + rms * 0.22f;
|
||||
}
|
||||
} else {
|
||||
// Synthetic when scope is silent : drive bands by voice amps
|
||||
const float t = ofGetElapsedTimef();
|
||||
const float bands[] = {kick_, bass_, lead_, pad_};
|
||||
for (std::size_t b = 0; b < bandEnergy_.size(); ++b) {
|
||||
float a = bands[b % 4];
|
||||
float syn = a * (0.5f + 0.5f * std::sin(t * (2.0f + b * 0.3f) + b));
|
||||
bandEnergy_[b] = bandEnergy_[b] * 0.85f + syn * 0.15f;
|
||||
}
|
||||
}
|
||||
|
||||
// Build deformed sphere
|
||||
const auto& base = sphereBase_.getVertices();
|
||||
auto& def = sphereDeformed_.getVertices();
|
||||
if (def.size() != base.size()) sphereDeformed_ = sphereBase_;
|
||||
for (std::size_t i = 0; i < base.size(); ++i) {
|
||||
const glm::vec3& v = base[i];
|
||||
// Use the vertex position to pick a band (stable hash)
|
||||
float pick = std::abs(v.x * 11.7f + v.y * 5.3f + v.z * 2.1f);
|
||||
std::size_t b = static_cast<std::size_t>(std::floor(pick * bandEnergy_.size()))
|
||||
% bandEnergy_.size();
|
||||
float displace = 1.0f
|
||||
+ bandEnergy_[b] * 1.6f
|
||||
+ (frame.osc.beatPulse() ? 0.0f : 0.0f);
|
||||
def[i] = v * displace * pulseScale_;
|
||||
}
|
||||
}
|
||||
|
||||
void MeshVis::draw(int x, int y, int w, int h) {
|
||||
ofPushStyle();
|
||||
ofPushMatrix();
|
||||
|
||||
// Restrict camera to this rectangle by translating the whole scene
|
||||
// so its center is at (x+w/2, y+h/2). ofCamera uses the global
|
||||
// window for its perspective matrix; we draw inside a viewport.
|
||||
glEnable(GL_DEPTH_TEST);
|
||||
glClear(GL_DEPTH_BUFFER_BIT);
|
||||
|
||||
cam_.begin(ofRectangle(x, y, w, h));
|
||||
|
||||
// Subtle background gradient
|
||||
ofDisableDepthTest();
|
||||
ofPushStyle();
|
||||
ofSetColor(8, 12, 30);
|
||||
ofDrawRectangle(-50, -50, 100, 100);
|
||||
ofPopStyle();
|
||||
ofEnableDepthTest();
|
||||
|
||||
ofPushMatrix();
|
||||
ofRotateRad(yaw_, 0, 1, 0);
|
||||
ofRotateRad(pitch_, 1, 0, 0);
|
||||
|
||||
// Wireframe back-pass — slightly larger, dim
|
||||
ofSetLineWidth(1);
|
||||
ofSetColor(60, 90, 160, 200);
|
||||
sphereDeformed_.drawWireframe();
|
||||
|
||||
// Solid pass (additive over background)
|
||||
ofEnableBlendMode(OF_BLENDMODE_ADD);
|
||||
const float baseHue = std::fmod(yaw_ * 30.0f + bass_ * 80.0f, 360.0f);
|
||||
ofSetColor(ofColor::fromHsb(baseHue, 200, 200, 130));
|
||||
sphereDeformed_.draw();
|
||||
|
||||
// Inner glow by drawing the wireframe again at smaller scale, brighter
|
||||
ofPushMatrix();
|
||||
ofScale(0.85f);
|
||||
ofSetColor(255, 220, 180, 180);
|
||||
sphereBase_.drawWireframe();
|
||||
ofPopMatrix();
|
||||
|
||||
ofDisableBlendMode();
|
||||
ofPopMatrix();
|
||||
|
||||
cam_.end();
|
||||
|
||||
glDisable(GL_DEPTH_TEST);
|
||||
ofPopMatrix();
|
||||
ofPopStyle();
|
||||
}
|
||||
|
||||
} // namespace oscope
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
// 3D mesh visualizer : icosphere whose vertices are pushed in/out by a
|
||||
// 32-band approximation of the channel waveform (cheap RMS per region,
|
||||
// not a real FFT — we want look, not measurement). Rotates on its own
|
||||
// and gets a kick on each beat. Uses ofCamera for proper perspective.
|
||||
|
||||
#include "Visualizer.h"
|
||||
#include "ofMain.h"
|
||||
|
||||
namespace oscope {
|
||||
|
||||
class MeshVis : public Visualizer {
|
||||
public:
|
||||
void setup(int w, int h) override;
|
||||
void update(const VisFrame& frame) override;
|
||||
void draw(int x, int y, int w, int h) override;
|
||||
|
||||
private:
|
||||
int w_ = 0, h_ = 0;
|
||||
ofMesh sphereBase_;
|
||||
ofMesh sphereDeformed_;
|
||||
ofCamera cam_;
|
||||
float yaw_ = 0.0f, pitch_ = 0.0f;
|
||||
float pulseScale_ = 1.0f;
|
||||
float bass_ = 0.0f, lead_ = 0.0f, kick_ = 0.0f, pad_ = 0.0f;
|
||||
float bpm_ = 120.0f;
|
||||
std::vector<float> bandEnergy_;
|
||||
};
|
||||
|
||||
} // namespace oscope
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "TunnelVis.h"
|
||||
|
||||
namespace oscope {
|
||||
|
||||
void TunnelVis::setup(int w, int h) { w_ = w; h_ = h; reloadShaders(); }
|
||||
|
||||
void TunnelVis::reloadShaders() { shader_.load("shaders/tunnel"); }
|
||||
|
||||
void TunnelVis::update(const VisFrame& frame) {
|
||||
bpm_ = frame.osc.bpm();
|
||||
kick_ = frame.osc.amp("kick");
|
||||
bass_ = frame.osc.amp("bass");
|
||||
lead_ = frame.osc.amp("lead");
|
||||
pad_ = frame.osc.amp("pad");
|
||||
travel_ += static_cast<float>(ofGetLastFrameTime()) * (0.6f + bpm_ * 0.012f + kick_ * 1.5f);
|
||||
}
|
||||
|
||||
void TunnelVis::draw(int x, int y, int w, int h) {
|
||||
if (!shader_.isLoaded()) {
|
||||
ofPushStyle();
|
||||
ofSetColor(40);
|
||||
ofDrawRectangle(x, y, w, h);
|
||||
ofSetColor(255, 80, 80);
|
||||
ofDrawBitmapString("tunnel shader missing", x + 12, y + 24);
|
||||
ofPopStyle();
|
||||
return;
|
||||
}
|
||||
shader_.begin();
|
||||
shader_.setUniform2f("uRes", static_cast<float>(w), static_cast<float>(h));
|
||||
shader_.setUniform1f("uTime", ofGetElapsedTimef());
|
||||
shader_.setUniform1f("uTravel", travel_);
|
||||
shader_.setUniform1f("uBpm", bpm_);
|
||||
shader_.setUniform1f("uKick", kick_);
|
||||
shader_.setUniform1f("uBass", bass_);
|
||||
shader_.setUniform1f("uLead", lead_);
|
||||
shader_.setUniform1f("uPad", pad_);
|
||||
ofDrawRectangle(x, y, w, h);
|
||||
shader_.end();
|
||||
}
|
||||
|
||||
} // namespace oscope
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
// Cylindrical raymarched tunnel — fullscreen fragment shader. Speed
|
||||
// follows BPM, twist follows lead amplitude, segment lighting pulses
|
||||
// on kick and bass.
|
||||
|
||||
#include "Visualizer.h"
|
||||
#include "ofMain.h"
|
||||
|
||||
namespace oscope {
|
||||
|
||||
class TunnelVis : public Visualizer {
|
||||
public:
|
||||
void setup(int w, int h) override;
|
||||
void update(const VisFrame& frame) override;
|
||||
void draw(int x, int y, int w, int h) override;
|
||||
void reloadShaders() override;
|
||||
|
||||
private:
|
||||
int w_ = 0, h_ = 0;
|
||||
float travel_ = 0.0f;
|
||||
float bpm_ = 120.0f, kick_ = 0.0f, bass_ = 0.0f, lead_ = 0.0f, pad_ = 0.0f;
|
||||
ofShader shader_;
|
||||
};
|
||||
|
||||
} // namespace oscope
|
||||
Reference in New Issue
Block a user