From 2ac6c50a09072e8453335f55fcc06741d579d1d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?L=27=C3=A9lectron=20rare?= <108685187+electron-rare@users.noreply.github.com> Date: Thu, 7 May 2026 21:20:46 +0200 Subject: [PATCH] feat(of): aces tonemap + 3 demoscene shaders Vague 1 : - postfx.frag : Reinhard -> ACES filmic (Narkowicz). Courbe S filmique, contraste hautes lumieres. - 3 nouveaux shaders fullscreen frag-only : * metaballs : 7 balls inverse-square potential, threshold module par kick, halo extra * voronoi : F1/F2 cells animees + bordure F2-F1 phosphor * twister : 4 colonnes Amiga-style rotation Y, faceting, palette IQ + scanlines - ShaderVis.h : wrapper Visualizer mince qui charge n'importe quel shader/uniforms standards (uTime/uRes/uBass/uMid/ uTreble/uKick/uBpm). - DemoScene::background = BgKind enum (Tunnel/Starfield/ Metaballs/Voronoi/Twister), choisi par scene narrative. - Demo 1 AMIGA -> Twister bg, demo 6 GLITCH -> Voronoi bg. --- oscope-of/bin/data/shaders/metaballs.frag | 51 ++++++++++++++++ oscope-of/bin/data/shaders/metaballs.vert | 7 +++ oscope-of/bin/data/shaders/postfx.frag | 15 +++-- oscope-of/bin/data/shaders/twister.frag | 64 ++++++++++++++++++++ oscope-of/bin/data/shaders/twister.vert | 7 +++ oscope-of/bin/data/shaders/voronoi.frag | 71 +++++++++++++++++++++++ oscope-of/bin/data/shaders/voronoi.vert | 7 +++ oscope-of/src/ofApp.cpp | 51 +++++++++++----- oscope-of/src/ofApp.h | 7 +++ oscope-of/src/visualizers/ShaderVis.h | 59 +++++++++++++++++++ 10 files changed, 319 insertions(+), 20 deletions(-) create mode 100644 oscope-of/bin/data/shaders/metaballs.frag create mode 100644 oscope-of/bin/data/shaders/metaballs.vert create mode 100644 oscope-of/bin/data/shaders/twister.frag create mode 100644 oscope-of/bin/data/shaders/twister.vert create mode 100644 oscope-of/bin/data/shaders/voronoi.frag create mode 100644 oscope-of/bin/data/shaders/voronoi.vert create mode 100644 oscope-of/src/visualizers/ShaderVis.h diff --git a/oscope-of/bin/data/shaders/metaballs.frag b/oscope-of/bin/data/shaders/metaballs.frag new file mode 100644 index 0000000..348813c --- /dev/null +++ b/oscope-of/bin/data/shaders/metaballs.frag @@ -0,0 +1,51 @@ +#version 150 + +// Metaballs : champ scalaire = somme de potentiels rᵢ²/distᵢ², seuil +// iso-surface, anneaux additionnels pour le contour. Audio-réactif : +// position des balls modulée par bands, threshold par kick. + +out vec4 fragColor; + +uniform vec2 uRes; +uniform float uTime; +uniform float uBass; +uniform float uMid; +uniform float uTreble; +uniform float uKick; + +#define NB 7 + +void main() { + vec2 p = (gl_FragCoord.xy / uRes) * 2.0 - 1.0; + p.x *= uRes.x / uRes.y; + + float field = 0.0; + for (int i = 0; i < NB; i++) { + float fi = float(i); + // Position des balls : orbites lentes + modulation audio + vec2 c = vec2( + cos(uTime * (0.3 + fi * 0.07) + fi * 1.1) * (0.6 + uMid * 0.3), + sin(uTime * (0.4 + fi * 0.05) + fi * 1.7) * (0.4 + uBass * 0.3) + ); + float r = 0.18 + 0.05 * sin(uTime * 0.7 + fi); + float d = length(p - c); + // Inverse-square potential (smooth falloff) + field += (r * r) / max(d * d, 0.001); + } + + // Seuil iso-surface modulé par kick (le blob "respire" sur kick) + float thresh = 4.5 - uKick * 1.5; + float surf = smoothstep(thresh - 0.5, thresh + 0.5, field); + + // Couleur dégradée par champ — intérieur chaud, contour cyan + vec3 inside = vec3(1.0, 0.4 + uTreble * 0.4, 0.2); + vec3 contour = vec3(0.2, 0.8, 1.0); + vec3 col = mix(contour, inside, surf); + + // Halo extérieur (anneau plus large pour l'épaisseur visuelle) + float halo = smoothstep(thresh - 1.5, thresh - 0.4, field) * + (1.0 - smoothstep(thresh - 0.4, thresh, field)); + col += vec3(0.3, 0.9, 1.0) * halo * 0.6; + + fragColor = vec4(col, 1.0); +} diff --git a/oscope-of/bin/data/shaders/metaballs.vert b/oscope-of/bin/data/shaders/metaballs.vert new file mode 100644 index 0000000..0bf069e --- /dev/null +++ b/oscope-of/bin/data/shaders/metaballs.vert @@ -0,0 +1,7 @@ +#version 150 + +uniform mat4 modelViewProjectionMatrix; +in vec4 position; +void main() { + gl_Position = modelViewProjectionMatrix * position; +} diff --git a/oscope-of/bin/data/shaders/postfx.frag b/oscope-of/bin/data/shaders/postfx.frag index 6217ce8..1469faa 100644 --- a/oscope-of/bin/data/shaders/postfx.frag +++ b/oscope-of/bin/data/shaders/postfx.frag @@ -156,11 +156,14 @@ void main() { col *= mix(1.0, v, uVignette); } - // Final tone-map / clamp — without this, bloom + grain + feedback - // saturate to pure white very quickly on bright visualizers. - // Reinhard avec exposition : col = (col*e) / (1 + col*e), e=0.6 → la - // valeur d'entrée 1.0 mappe à ~0.375, 5.0 à ~0.75, jamais à 1.0. - col = (col * 0.6) / (1.0 + col * 0.6); - col = clamp(col, 0.0, 1.0); + // Tone-map ACES filmic (Narkowicz approximation) — courbe S filmique + // bien meilleure que Reinhard pour le contraste et la saturation des + // hautes lumières. Empêche le clip + garde du peps dans les blacks. + // y = (x*(a*x+b)) / (x*(c*x+d)+e) + { + const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14; + col = clamp((col * (a * col + b)) / (col * (c * col + d) + e), + 0.0, 1.0); + } fragColor = vec4(col, 1.0); } diff --git a/oscope-of/bin/data/shaders/twister.frag b/oscope-of/bin/data/shaders/twister.frag new file mode 100644 index 0000000..d239fbf --- /dev/null +++ b/oscope-of/bin/data/shaders/twister.frag @@ -0,0 +1,64 @@ +#version 150 + +// Twister bars : 4 colonnes verticales tournant sur axe Y, gradient +// palette par hauteur. Mythe Amiga (Spaceballs etc.). Audio-réactif : +// vitesse de rotation suit BPM, couleur cycle modulé par mid. + +out vec4 fragColor; + +uniform vec2 uRes; +uniform float uTime; +uniform float uBpm; +uniform float uMid; +uniform float uKick; + +#define NCOL 4 + +vec3 palette(float t) { + return 0.5 + 0.5 * cos(6.28318 * + (vec3(1.0, 0.6, 0.4) * t + vec3(0.0, 0.33, 0.67))); +} + +void main() { + vec2 p = (gl_FragCoord.xy / uRes) * 2.0 - 1.0; + p.x *= uRes.x / uRes.y; + + // Repère colonne : on divise l'écran horizontalement en NCOL bandes. + float cw = 0.55; // largeur chaque bar + float speed = 0.4 + uBpm * 0.005; + vec3 col = vec3(0.02, 0.04, 0.06); // fond bleu nuit + + for (int i = 0; i < NCOL; i++) { + float fi = float(i); + // Centre X de la barre, espacement régulier + float cx = -1.5 + 1.0 * fi; + // Phase rotation propre + offset par barre + float phase = uTime * speed * (1.0 + fi * 0.08) + fi * 0.7; + + // Coord locale dans la barre + float lx = (p.x - cx); + float ly = p.y; + + // "Rotation" sur axe Y : la largeur visible varie en sin(phase) + // → projection 3D simulée. lx normalisé sur cw. + float xn = lx / cw; + if (abs(xn) <= 1.0) { + // l'angle apparent du faceting du twister + float ang = atan(xn, sin(phase + ly * 1.6)); + // Couleur gradient vertical + cycle hue par phase + float t = ly * 0.5 + 0.5; + vec3 base = palette(t * 0.7 + phase * 0.05 + uMid * 0.3); + // Faceting = ombre additionnelle selon angle (faux 3D) + float facet = 0.5 + 0.5 * cos(ang * 4.0 + phase * 2.0); + col = mix(col, base * (0.4 + facet * 1.0), 0.95); + // Bord clair sur les arêtes + float edge = smoothstep(0.85, 1.0, abs(xn)); + col += vec3(1.0, 0.9, 0.7) * edge * 0.6 * (0.8 + uKick * 1.5); + } + } + + // Scanlines CRT + col *= 0.85 + 0.15 * sin(gl_FragCoord.y * 1.5); + + fragColor = vec4(col, 1.0); +} diff --git a/oscope-of/bin/data/shaders/twister.vert b/oscope-of/bin/data/shaders/twister.vert new file mode 100644 index 0000000..0bf069e --- /dev/null +++ b/oscope-of/bin/data/shaders/twister.vert @@ -0,0 +1,7 @@ +#version 150 + +uniform mat4 modelViewProjectionMatrix; +in vec4 position; +void main() { + gl_Position = modelViewProjectionMatrix * position; +} diff --git a/oscope-of/bin/data/shaders/voronoi.frag b/oscope-of/bin/data/shaders/voronoi.frag new file mode 100644 index 0000000..171007c --- /dev/null +++ b/oscope-of/bin/data/shaders/voronoi.frag @@ -0,0 +1,71 @@ +#version 150 + +// Voronoi / Worley cells (Steven Worley 1996 + Inigo Quilez approach). +// Pour chaque pixel : distance F1 au site le plus proche, F2 au 2e. +// Couleur basée sur F1 (bordure cellulaire = F2-F1), animation temporelle. + +out vec4 fragColor; + +uniform vec2 uRes; +uniform float uTime; +uniform float uBass; +uniform float uMid; +uniform float uTreble; +uniform float uKick; + +vec2 hash2(vec2 p) { + p = vec2(dot(p, vec2(127.1, 311.7)), + dot(p, vec2(269.5, 183.3))); + return fract(sin(p) * 43758.5453); +} + +void main() { + vec2 uv = gl_FragCoord.xy / uRes.xx * 6.0; // ~6 cells horizontalement + uv.y *= uRes.x / uRes.y; + + vec2 ip = floor(uv); + vec2 fp = fract(uv); + + float F1 = 9.9, F2 = 9.9; + vec2 closest = vec2(0.0); + + // Recherche 3x3 cells autour du fragment + for (int j = -1; j <= 1; j++) { + for (int i = -1; i <= 1; i++) { + vec2 g = vec2(float(i), float(j)); + vec2 o = hash2(ip + g); + // Site animé sur orbite lente + bass shake + o = 0.5 + 0.5 * sin(uTime * (0.5 + uBass * 0.6) + 6.2831 * o); + vec2 r = g + o - fp; + float d = dot(r, r); + if (d < F1) { F2 = F1; F1 = d; closest = ip + g; } + else if (d < F2) { F2 = d; } + } + } + + F1 = sqrt(F1); + F2 = sqrt(F2); + + // Bordure cellulaire (F2-F1 = distance à la frontière) + float edge = smoothstep(0.0, 0.10, F2 - F1); + + // Couleur par cell : hash de la position pour stabilité + vec3 cellColor = 0.5 + 0.5 * sin( + vec3(0.0, 2.1, 4.2) + hash2(closest).x * 6.28 + uTime * 0.3 + ); + cellColor *= 0.4 + 0.8 * smoothstep(0.7, 0.0, F1); // assombrit centre + + // Mix bordure phosphor cyan + vec3 borderColor = vec3(0.4, 0.9, 1.0) * (1.0 - edge) * (1.0 + uTreble * 1.5); + + vec3 col = mix(borderColor, cellColor, edge); + + // Pulse sur kick : flash global + col += vec3(uKick * 0.3); + + // Vignette + vec2 v = (gl_FragCoord.xy / uRes) - 0.5; + col *= smoothstep(1.2, 0.3, length(v) * 1.5); + + fragColor = vec4(col, 1.0); +} diff --git a/oscope-of/bin/data/shaders/voronoi.vert b/oscope-of/bin/data/shaders/voronoi.vert new file mode 100644 index 0000000..0bf069e --- /dev/null +++ b/oscope-of/bin/data/shaders/voronoi.vert @@ -0,0 +1,7 @@ +#version 150 + +uniform mat4 modelViewProjectionMatrix; +in vec4 position; +void main() { + gl_Position = modelViewProjectionMatrix * position; +} diff --git a/oscope-of/src/ofApp.cpp b/oscope-of/src/ofApp.cpp index 56c0bc0..be77f79 100644 --- a/oscope-of/src/ofApp.cpp +++ b/oscope-of/src/ofApp.cpp @@ -80,6 +80,9 @@ void ofApp::setup() { kaleido_ = std::make_unique(); tunnel_ = std::make_unique(); mesh_ = std::make_unique(); + metaballs_ = std::make_unique("shaders/metaballs"); + voronoi_ = std::make_unique("shaders/voronoi"); + twister_ = std::make_unique("shaders/twister"); lissajous_->setup(W, H); spectro_->setup(W, H / 4); reactive_->setup(W, H); @@ -90,6 +93,9 @@ void ofApp::setup() { kaleido_->setup(W, H); tunnel_->setup(W, H); mesh_->setup(W, H); + metaballs_->setup(W, H); + voronoi_->setup(W, H); + twister_->setup(W, H); postfx_.setup(W, H); @@ -238,6 +244,9 @@ void ofApp::update() { kaleido_->update(frame); tunnel_->update(frame); mesh_->update(frame); + metaballs_->update(frame); + voronoi_->update(frame); + twister_->update(frame); applyOscFx(); @@ -354,12 +363,14 @@ void ofApp::initDemos() { demos_[0] = {"AMIGA TRIBUTE", { {"INSERT DISK", 18.0f, stars(), SS::Neon, " *** AMIGA 500 RELOADED *** KICKSTART 1.3 " - " INSERT WORKBENCH DISK ", "M"}, + " INSERT WORKBENCH DISK ", "M", BgKind::Starfield}, {"SCROLLZ", 35.0f, all(), SS::Classic, " GREETINGS FROM 1989 COPPER BARS AND BOBS FOREVER " - " AMIGA NEVER DIES HELLO TO ALL DEMOSCENE VETERANS ", "M"}, + " AMIGA NEVER DIES HELLO TO ALL DEMOSCENE VETERANS ", "M", + BgKind::Twister}, {"COPPER STORM", 30.0f, all(), SS::Rainbow, - " COPPER LIST IS POETRY EVERY SCANLINE A NEW COLOR ", "G"}, + " COPPER LIST IS POETRY EVERY SCANLINE A NEW COLOR ", "G", + BgKind::Twister}, }}; // ─── 2 · C64 LOWLIFE ───────────────────────────────────── @@ -413,12 +424,12 @@ void ofApp::initDemos() { // ─── 6 · GLITCH WORLD ──────────────────────────────────── demos_[5] = {"GLITCH WORLD", { {"PROBE", 15.0f, stars(), SS::Neon, - " SCANNING ANOMALY REALITY UNSTABLE ", "V"}, + " SCANNING ANOMALY REALITY UNSTABLE ", "V", BgKind::Voronoi}, {"CORRUPT", 40.0f, all(), SS::Glitch, " BUFFER OVERFLOW SIGNAL CORRUPTED " - " THE GHOSTS IN THE WIRES ARE WAKING UP ", "V"}, + " THE GHOSTS IN THE WIRES ARE WAKING UP ", "V", BgKind::Voronoi}, {"CRASH", 30.0f, all(), SS::Glitch, - " KERNEL PANIC BUT THE BEAT GOES ON ", "Q"}, + " KERNEL PANIC BUT THE BEAT GOES ON ", "Q", BgKind::Voronoi}, }}; // ─── 7 · AMBIENT VOID ──────────────────────────────────── @@ -660,14 +671,26 @@ void ofApp::drawScope4(int W, int H) { demo_.bobsEnabled() = scope4_.bobs; demo_.starfieldEnabled() = scope4_.starfield; - // 1) Fond : starfield remplace le tunnel si toggle actif, sinon tunnel. - if (scope4_.starfield) { - ofBackground(0); - demo_.drawStarfield(W, H); - } else if (scope4_.tunnel) { - tunnel_->draw(0, 0, W, H); - } else { - ofBackground(0); + // 1) Fond : selon scène narrative active (BgKind), sinon tunnel/starfield + // classique en mode live. + BgKind bg = BgKind::Tunnel; + if (narrativeMode_ && currentDemo_ < (int)demos_.size() && + narrativeIdx_ < (int)demos_[currentDemo_].scenes.size()) { + bg = demos_[currentDemo_].scenes[narrativeIdx_].background; + } else if (scope4_.starfield) { + bg = BgKind::Starfield; + } else if (!scope4_.tunnel) { + bg = BgKind::Tunnel; // pas de fond -> tunnel skip below + } + switch (bg) { + case BgKind::Tunnel: + if (scope4_.tunnel) tunnel_->draw(0, 0, W, H); + else ofBackground(0); + break; + case BgKind::Starfield: ofBackground(0); demo_.drawStarfield(W, H); break; + case BgKind::Metaballs: metaballs_->draw(0, 0, W, H); break; + case BgKind::Voronoi: voronoi_->draw(0, 0, W, H); break; + case BgKind::Twister: twister_->draw(0, 0, W, H); break; } // 1bis) HUD pseudo-aléatoire de valeurs sub-10 Hz. diff --git a/oscope-of/src/ofApp.h b/oscope-of/src/ofApp.h index df1ec39..c02285e 100644 --- a/oscope-of/src/ofApp.h +++ b/oscope-of/src/ofApp.h @@ -24,6 +24,7 @@ #include "visualizers/KaleidoVis.h" #include "visualizers/TunnelVis.h" #include "visualizers/MeshVis.h" +#include "visualizers/ShaderVis.h" #include @@ -89,6 +90,7 @@ private: // 10 démoparties narratives multi-actes (touches 1..9 + 0). // Chaque démo = liste de scènes qui défilent dans le temps avec // narration scénarisée + track SC + transitions. + enum class BgKind { Tunnel, Starfield, Metaballs, Voronoi, Twister }; struct DemoScene { const char* name; float durSec; @@ -96,6 +98,7 @@ private: oscope::ScrollerStyle scroller; const char* narration; const char* albumLetter; + BgKind background = BgKind::Tunnel; }; struct Demo { const char* name; @@ -123,6 +126,10 @@ private: std::unique_ptr kaleido_; std::unique_ptr tunnel_; std::unique_ptr mesh_; + // Demoscene fullscreen FX (shader-based, frag-only). + std::unique_ptr metaballs_; + std::unique_ptr voronoi_; + std::unique_ptr twister_; std::vector ch1_, ch2_; Mode mode_ = Mode::Scope4; diff --git a/oscope-of/src/visualizers/ShaderVis.h b/oscope-of/src/visualizers/ShaderVis.h new file mode 100644 index 0000000..7e1bffd --- /dev/null +++ b/oscope-of/src/visualizers/ShaderVis.h @@ -0,0 +1,59 @@ +#pragma once + +// ShaderVis : visualizer fullscreen-quad piloté par un fragment shader. +// Charge n'importe quel shader (path = "shaders/") et passe les +// uniforms standards uTime / uRes + bandes audio (bass/mid/treble/kick). +// Sert de wrapper léger pour les FX type demoscene (metaballs, voronoi, +// twister, plasma fbm, rotozoomer, etc.) sans code redondant. + +#include "Visualizer.h" +#include "ofMain.h" + +#include + +namespace oscope { + +class ShaderVis : public Visualizer { +public: + explicit ShaderVis(const std::string& shaderPath) + : shaderPath_(shaderPath) {} + + void setup(int w, int h) override { w_ = w; h_ = h; reloadShaders(); } + void update(const VisFrame& frame) override { + bass_ = frame.bands.bass; + mid_ = frame.bands.mid; + treble_ = frame.bands.treble; + kick_ = frame.bands.kick; + bpm_ = frame.osc.bpm(); + } + void draw(int x, int y, int w, int h) override { + if (!shader_.isLoaded()) { + ofPushStyle(); + ofSetColor(60); + ofDrawRectangle(x, y, w, h); + ofSetColor(255, 80, 80); + ofDrawBitmapString(shaderPath_ + " missing", x + 12, y + 24); + ofPopStyle(); + return; + } + shader_.begin(); + shader_.setUniform2f("uRes", static_cast(w), static_cast(h)); + shader_.setUniform1f("uTime", ofGetElapsedTimef()); + shader_.setUniform1f("uBass", bass_); + shader_.setUniform1f("uMid", mid_); + shader_.setUniform1f("uTreble",treble_); + shader_.setUniform1f("uKick", kick_); + shader_.setUniform1f("uBpm", bpm_); + ofDrawRectangle(x, y, w, h); + shader_.end(); + } + void reloadShaders() override { shader_.load(shaderPath_); } + +private: + std::string shaderPath_; + int w_ = 0, h_ = 0; + float bass_ = 0, mid_ = 0, treble_ = 0, kick_ = 0, bpm_ = 120; + ofShader shader_; +}; + +} // namespace oscope