feat: 3D relief, lighting and reactive motion

The spectrogram now extrudes the geometry alongside the waveform, the
skin is lit with a screen-space normal, and the sphere tumbles and
pulses with the kick and signal energy.
This commit is contained in:
L'électron rare
2026-05-18 18:39:14 +02:00
parent e33375f7ac
commit 7fc4f41f4e
6 changed files with 60 additions and 14 deletions
+20 -3
View File
@@ -5,7 +5,9 @@ uniform float scrollOffset;
uniform int colormapId;
uniform int renderMode; // 0 = skin, 1 = points
in vec2 vSphereUV;
in vec2 vSphereUV;
in vec3 vViewPos;
in float vWave;
out vec4 fragColor;
// Polynomial colormap fits (public domain, Matt Zucker).
@@ -34,9 +36,24 @@ void main() {
float u = fract(vSphereUV.x - scrollOffset);
float mag = clamp(texture(spectroTex, vec2(u, vSphereUV.y)).r, 0.0, 1.0);
vec3 col = (colormapId == 0) ? magma(mag) : viridis(mag);
if (renderMode == 1) {
// round point sprites + a touch of waveform sheen
vec2 d = gl_PointCoord - vec2(0.5);
if (dot(d, d) > 0.25) discard; // round the points
if (dot(d, d) > 0.25) discard;
col += 0.20 * abs(vWave);
fragColor = vec4(col, 1.0);
return;
}
fragColor = vec4(col, 1.0);
// skin: light the displaced relief with a screen-space face normal
vec3 N = normalize(cross(dFdx(vViewPos), dFdy(vViewPos)));
vec3 V = normalize(-vViewPos);
if (dot(N, V) < 0.0) N = -N;
vec3 L = normalize(vec3(0.45, 0.65, 0.75));
float diff = max(dot(N, L), 0.0);
float rim = pow(1.0 - max(dot(N, V), 0.0), 2.5);
vec3 lit = col * (0.35 + 0.85 * diff) + rim * vec3(0.35, 0.45, 0.65);
fragColor = vec4(lit, 1.0);
}
+20 -6
View File
@@ -1,14 +1,20 @@
#version 150
uniform mat4 modelViewProjectionMatrix;
uniform sampler2D waveformTex; // width = samples, height = 2 (row0 CH1, row1 CH2)
uniform mat4 modelViewMatrix;
uniform sampler2D waveformTex;
uniform sampler2D spectroTex;
uniform float displaceAmount;
uniform float spectroAmount;
uniform float scrollOffset;
uniform float baseRadius;
uniform int renderMode; // 0 = skin, 1 = points
in vec4 position;
out vec2 vSphereUV; // x = longitude [0,1], y = latitude [0,1]
out vec2 vSphereUV;
out vec3 vViewPos;
out float vWave;
const float PI = 3.14159265359;
@@ -17,14 +23,22 @@ void main() {
float lon = atan(dir.z, dir.x) / (2.0 * PI) + 0.5;
float lat = asin(clamp(dir.y, -1.0, 1.0)) / PI + 0.5;
// waveform ripple (per hemisphere) + scrolling spectrogram relief
float row = (dir.y >= 0.0) ? 0.25 : 0.75; // CH1 north, CH2 south
float wave = texture(waveformTex, vec2(lon, row)).r; // [-1,1]
float r = baseRadius * (1.0 + displaceAmount * wave);
float wave = texture(waveformTex, vec2(lon, row)).r; // [-1,1]
float spec = texture(spectroTex,
vec2(fract(lon - scrollOffset), lat)).r; // [0,1]
float r = baseRadius * (1.0 + displaceAmount * wave
+ spectroAmount * spec);
if (renderMode == 1) {
r *= 1.05; // float the point cloud just outside the skin
r *= 1.06; // float the point cloud outside the skin
gl_PointSize = 6.0;
}
gl_Position = modelViewProjectionMatrix * vec4(dir * r, 1.0);
vec4 viewPos = modelViewMatrix * vec4(dir * r, 1.0);
gl_Position = modelViewProjectionMatrix * vec4(dir * r, 1.0);
vViewPos = viewPos.xyz;
vSphereUV = vec2(lon, lat);
vWave = wave;
}
+1
View File
@@ -78,6 +78,7 @@ void SphereViz::bindUniforms() {
shader_.setUniformTexture("waveformTex", waveTex_, 1);
shader_.setUniform1f("scrollOffset", scrollOffset_);
shader_.setUniform1f("displaceAmount", displace_);
shader_.setUniform1f("spectroAmount", spectroAmount_);
shader_.setUniform1f("baseRadius", baseRadius_);
shader_.setUniform1i("colormapId", colormapId_);
}
+2 -1
View File
@@ -35,6 +35,7 @@ private:
int waveformLen_ = 0;
float baseRadius_ = 200.0f;
float scrollOffset_ = 0.0f;
float displace_ = 0.18f;
float displace_ = 0.28f;
float spectroAmount_ = 0.40f;
int colormapId_ = 0;
};
+16 -4
View File
@@ -1,4 +1,6 @@
#include "ofApp.h"
#include <algorithm>
#include <cmath>
void ofApp::setup() {
ofSetFrameRate(60);
@@ -30,10 +32,6 @@ void ofApp::setup() {
void ofApp::update() {
if (frozen_) return;
// Auto-rotation advances only when the mouse is not orbiting the camera.
if (!ofGetMousePressed())
spin_ += 6.0f * static_cast<float>(ofGetLastFrameTime());
if (demoMode_) {
demo_.next(buf1_, buf2_, 8192);
} else {
@@ -48,6 +46,17 @@ void ofApp::update() {
analyzerCh1_.update(buf1_, buf1_, sr);
analyzerCh2_.update(buf2_, buf2_, sr);
// Audio-reactive motion: rotation speed tracks signal energy, the whole
// sphere pulses with the kick transient.
const oscope::AudioBands& b1 = analyzerCh1_.bands();
const oscope::AudioBands& b2 = analyzerCh2_.bands();
const float energy = 0.5f * (b1.full + b2.full);
const float kick = std::max(b1.kick, b2.kick);
const float dt = static_cast<float>(ofGetLastFrameTime());
if (!ofGetMousePressed())
spin_ += (8.0f + 80.0f * energy) * dt;
pulse_ += (1.0f + 0.20f * kick - pulse_) * 0.25f;
sphere_.setColormap(colormap_);
sphere_.pushSpectrogramColumn(analyzerCh1_.magDown(),
analyzerCh2_.magDown());
@@ -56,9 +65,12 @@ void ofApp::update() {
}
void ofApp::draw() {
const float t = ofGetElapsedTimef();
cam_.begin();
ofPushMatrix();
ofRotateYDeg(spin_);
ofRotateXDeg(16.0f * std::sin(t * 0.27f)); // slow tumble
ofScale(pulse_, pulse_, pulse_); // audio pulse
if (layerA_) sphere_.drawSkin();
if (layerC_) sphere_.drawPoints();
if (layerB_) rings_.draw();
+1
View File
@@ -38,5 +38,6 @@ private:
int colormap_ = 0;
float scopeSr_ = 16.0e6f;
float spin_ = 0.0f;
float pulse_ = 1.0f;
std::string statusText_;
};