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.
This commit is contained in:
L'électron rare
2026-05-07 21:20:46 +02:00
parent a7bbb9af02
commit 2ac6c50a09
10 changed files with 319 additions and 20 deletions
+51
View File
@@ -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);
}
@@ -0,0 +1,7 @@
#version 150
uniform mat4 modelViewProjectionMatrix;
in vec4 position;
void main() {
gl_Position = modelViewProjectionMatrix * position;
}
+9 -6
View File
@@ -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);
}
+64
View File
@@ -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);
}
+7
View File
@@ -0,0 +1,7 @@
#version 150
uniform mat4 modelViewProjectionMatrix;
in vec4 position;
void main() {
gl_Position = modelViewProjectionMatrix * position;
}
+71
View File
@@ -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);
}
+7
View File
@@ -0,0 +1,7 @@
#version 150
uniform mat4 modelViewProjectionMatrix;
in vec4 position;
void main() {
gl_Position = modelViewProjectionMatrix * position;
}
+37 -14
View File
@@ -80,6 +80,9 @@ void ofApp::setup() {
kaleido_ = std::make_unique<oscope::KaleidoVis>();
tunnel_ = std::make_unique<oscope::TunnelVis>();
mesh_ = std::make_unique<oscope::MeshVis>();
metaballs_ = std::make_unique<oscope::ShaderVis>("shaders/metaballs");
voronoi_ = std::make_unique<oscope::ShaderVis>("shaders/voronoi");
twister_ = std::make_unique<oscope::ShaderVis>("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.
+7
View File
@@ -24,6 +24,7 @@
#include "visualizers/KaleidoVis.h"
#include "visualizers/TunnelVis.h"
#include "visualizers/MeshVis.h"
#include "visualizers/ShaderVis.h"
#include <memory>
@@ -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<oscope::KaleidoVis> kaleido_;
std::unique_ptr<oscope::TunnelVis> tunnel_;
std::unique_ptr<oscope::MeshVis> mesh_;
// Demoscene fullscreen FX (shader-based, frag-only).
std::unique_ptr<oscope::ShaderVis> metaballs_;
std::unique_ptr<oscope::ShaderVis> voronoi_;
std::unique_ptr<oscope::ShaderVis> twister_;
std::vector<float> ch1_, ch2_;
Mode mode_ = Mode::Scope4;
+59
View File
@@ -0,0 +1,59 @@
#pragma once
// ShaderVis : visualizer fullscreen-quad piloté par un fragment shader.
// Charge n'importe quel shader (path = "shaders/<nom>") 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 <string>
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<float>(w), static_cast<float>(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