Skip to content

Commit 5d93cf0

Browse files
committed
web: HDR+bloom post, new waves.wgsl layered velvet waves with gold wireframe; strong audio↔visual coupling; drag swirl; smoother pulses; higher contrast
1 parent d7635a7 commit 5d93cf0

File tree

5 files changed

+1151
-51
lines changed

5 files changed

+1151
-51
lines changed

crates/app-core/shaders/post.wgsl

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Fullscreen post-processing: HDR bright pass, separable blur, composite with
2+
// filmic tonemapping, vignette, gentle chroma shift and film grain.
3+
4+
struct VsOut {
5+
@builtin(position) pos: vec4<f32>,
6+
@location(0) uv: vec2<f32>,
7+
};
8+
9+
// Common uniforms used by all post passes
10+
struct PostUniforms {
11+
resolution: vec2<f32>,
12+
time: f32,
13+
ambient: f32,
14+
// For blur
15+
blur_dir: vec2<f32>,
16+
bloom_strength: f32,
17+
threshold: f32,
18+
};
19+
20+
@group(0) @binding(0) var hdr_tex: texture_2d<f32>;
21+
@group(0) @binding(1) var hdr_sampler: sampler;
22+
@group(0) @binding(2) var<uniform> u_post: PostUniforms;
23+
24+
// Optional second texture (used by composite for blurred bloom)
25+
@group(1) @binding(0) var blur_tex: texture_2d<f32>;
26+
@group(1) @binding(1) var blur_sampler: sampler;
27+
28+
@vertex
29+
fn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {
30+
// Fullscreen triangle (no vertex buffer)
31+
var pos = array<vec2<f32>, 3>(
32+
vec2<f32>(-1.0, -3.0),
33+
vec2<f32>(-1.0, 1.0),
34+
vec2<f32>(3.0, 1.0),
35+
);
36+
var uv = array<vec2<f32>, 3>(
37+
vec2<f32>(0.0, 2.0),
38+
vec2<f32>(0.0, 0.0),
39+
vec2<f32>(2.0, 0.0),
40+
);
41+
var out: VsOut;
42+
out.pos = vec4<f32>(pos[vid], 0.0, 1.0);
43+
out.uv = uv[vid];
44+
return out;
45+
}
46+
47+
fn luminance(c: vec3<f32>) -> f32 {
48+
return dot(c, vec3<f32>(0.2126, 0.7152, 0.0722));
49+
}
50+
51+
// BRIGHT PASS: keep only highlights above threshold
52+
@fragment
53+
fn fs_bright(inp: VsOut) -> @location(0) vec4<f32> {
54+
let col = textureSample(hdr_tex, hdr_sampler, inp.uv).rgb;
55+
let thr = u_post.threshold;
56+
let l = luminance(col);
57+
let k = max(l - thr, 0.0);
58+
let outc = col * (k / max(l, 1e-5));
59+
return vec4<f32>(outc, 1.0);
60+
}
61+
62+
// BLUR PASS: simple 9-tap gaussian along blur_dir
63+
@fragment
64+
fn fs_blur(inp: VsOut) -> @location(0) vec4<f32> {
65+
let texel = u_post.blur_dir / u_post.resolution;
66+
let w0 = 0.05;
67+
let w1 = 0.09;
68+
let w2 = 0.12;
69+
let w3 = 0.15;
70+
var acc: vec3<f32> = vec3<f32>(0.0);
71+
acc += textureSample(hdr_tex, hdr_sampler, inp.uv - texel * 3.0).rgb * w0;
72+
acc += textureSample(hdr_tex, hdr_sampler, inp.uv - texel * 2.0).rgb * w1;
73+
acc += textureSample(hdr_tex, hdr_sampler, inp.uv - texel * 1.0).rgb * w2;
74+
acc += textureSample(hdr_tex, hdr_sampler, inp.uv).rgb * w3;
75+
acc += textureSample(hdr_tex, hdr_sampler, inp.uv + texel * 1.0).rgb * w2;
76+
acc += textureSample(hdr_tex, hdr_sampler, inp.uv + texel * 2.0).rgb * w1;
77+
acc += textureSample(hdr_tex, hdr_sampler, inp.uv + texel * 3.0).rgb * w0;
78+
return vec4<f32>(acc, 1.0);
79+
}
80+
81+
fn aces_tonemap(x: vec3<f32>) -> vec3<f32> {
82+
let a = 2.51;
83+
let b = 0.03;
84+
let c = 2.43;
85+
let d = 0.59;
86+
let e = 0.14;
87+
return clamp((x * (a * x + b)) / (x * (c * x + d) + e), vec3<f32>(0.0), vec3<f32>(1.0));
88+
}
89+
90+
fn vignette(uv: vec2<f32>) -> f32 {
91+
let r = length(uv - 0.5);
92+
return smoothstep(0.95, 0.45, r);
93+
}
94+
95+
fn hash2(p: vec2<f32>) -> f32 {
96+
let h = dot(p, vec2<f32>(127.1, 311.7));
97+
return fract(sin(h) * 43758.5453123);
98+
}
99+
100+
// COMPOSITE: tone-map HDR + add bloom + color grading and grain
101+
@fragment
102+
fn fs_composite(inp: VsOut) -> @location(0) vec4<f32> {
103+
var base = textureSample(hdr_tex, hdr_sampler, inp.uv).rgb;
104+
let bloom = textureSample(blur_tex, blur_sampler, inp.uv).rgb * u_post.bloom_strength;
105+
base += bloom;
106+
107+
// Subtle hue warp based on ambient and time
108+
let t = u_post.time * 0.15;
109+
let ambient = u_post.ambient;
110+
let hue = vec3<f32>(sin(t * 1.2) + 1.0, sin(t * 1.5 + 2.0) + 1.0, sin(t * 1.8 + 4.0) + 1.0) * 0.05 * ambient;
111+
base *= (vec3<f32>(1.0) + hue);
112+
113+
// Tonemap
114+
var mapped = aces_tonemap(base);
115+
116+
// Vignette
117+
let vig = vignette(inp.uv);
118+
mapped *= mix(1.0, 0.85, vig);
119+
120+
// Film grain
121+
let noise = hash2(inp.uv * u_post.resolution + u_post.time);
122+
mapped += (noise - 0.5) * 0.02;
123+
124+
return vec4<f32>(mapped, 1.0);
125+
}
126+
127+

crates/app-core/shaders/scene.wgsl

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,25 +27,60 @@ fn vs_main(
2727
return out;
2828
}
2929

30+
// Cheap hash and helpers for micro-detail
31+
fn hash(p: vec2<f32>) -> f32 {
32+
let h = dot(p, vec2<f32>(127.1, 311.7));
33+
return fract(sin(h) * 43758.5453);
34+
}
35+
3036
@fragment
3137
fn fs_main(inf: VsOut) -> @location(0) vec4<f32> {
32-
// Circular mask within the quad (unit circle of radius 0.5)
33-
let r = length(inf.local);
34-
let shape_alpha = 1.0 - smoothstep(0.48, 0.5, r);
38+
// Local in [-0.5,0.5] → map to unit disk coordinates
39+
let p = inf.local * 2.0;
40+
let r = length(p);
41+
let alpha = 1.0 - smoothstep(0.48, 0.5, r); // soft circle mask
42+
if alpha <= 0.0 {
43+
return vec4<f32>(0.0, 0.0, 0.0, 0.0);
44+
}
45+
46+
// Fake sphere normal from signed distance to unit circle
47+
let zz = sqrt(max(1.0 - clamp(r, 0.0, 1.0) * clamp(r, 0.0, 1.0), 0.0));
48+
let n = normalize(vec3<f32>(p, zz));
49+
let light1 = normalize(vec3<f32>(-0.4, 0.2, 0.9));
50+
let light2 = normalize(vec3<f32>(0.6, -0.3, 0.7));
51+
let base_col = inf.color.rgb;
52+
53+
// Diffuse + warm/cool split lighting
54+
let diff1 = max(dot(n, light1), 0.0);
55+
let diff2 = max(dot(n, light2), 0.0);
56+
var col = base_col * (0.25 + 0.9 * diff1) + vec3<f32>(0.9, 0.95, 1.0) * (0.15 * diff2);
57+
58+
// Specular highlights
59+
let view = vec3<f32>(0.0, 0.0, 1.0);
60+
let h1 = normalize(light1 + view);
61+
let spec = pow(max(dot(n, h1), 0.0), 64.0);
62+
col += vec3<f32>(1.0) * (0.25 * spec);
63+
64+
// Pulse-driven inner bloom and moving ring
65+
let pulse = clamp(inf.pulse, 0.0, 1.5);
66+
let core = smoothstep(0.55, 0.0, r) * (0.6 + 1.8 * pulse);
67+
let ring = exp(-40.0 * pow(r - (0.25 + 0.2 * pulse), 2.0)) * (0.3 + 1.2 * pulse);
68+
col *= 0.6 + core;
69+
col += base_col * ring;
3570

36-
// Emissive pulse boosts brightness subtly
37-
let emissive = 0.7 * clamp(inf.pulse, 0.0, 1.5);
38-
var rgb = inf.color.rgb * (1.0 + emissive);
71+
// Iridescent rim
72+
let rim = pow(1.0 - clamp(dot(n, view), 0.0, 1.0), 3.0);
73+
col += vec3<f32>(0.9, 0.8, 1.2) * rim * (0.4 + 0.8 * pulse);
3974

40-
// Subtle outer glow/halo based on radius
41-
let halo = smoothstep(0.75, 0.55, r) * 0.12; // outer ring brightness
42-
rgb += halo * inf.color.rgb;
75+
// Micro sparkle texture
76+
let g = hash(p * 120.0) - 0.5;
77+
col += (0.02 + 0.08 * pulse) * g;
4378

44-
// Add vignette-like radial darkening for ambient mood
45-
let vignette = smoothstep(0.0, 0.7, r);
46-
rgb *= mix(1.0, 0.85, vignette);
79+
// Outer halo beyond the mask for smoother edges (relies on HDR post bloom)
80+
let halo = smoothstep(0.55, 0.45, r) * (0.4 + 1.5 * pulse);
81+
col += base_col * halo;
4782

48-
return vec4<f32>(rgb, shape_alpha * inf.color.a);
83+
return vec4<f32>(col, alpha * inf.color.a);
4984
}
5085

5186

crates/app-core/shaders/waves.wgsl

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Audio-reactive ribbon/heightfield aesthetic rendered in a single fullscreen pass.
2+
// Inspired by smooth velvet waves with golden accents.
3+
4+
struct VsOut {
5+
@builtin(position) pos: vec4<f32>,
6+
@location(0) uv: vec2<f32>,
7+
};
8+
9+
struct Voice {
10+
// xyz position (x,z used), w = pulse (0..1.5)
11+
pos_pulse: vec4<f32>,
12+
// rgb base, a unused
13+
color: vec4<f32>,
14+
};
15+
16+
struct WaveUniforms {
17+
resolution: vec2<f32>,
18+
time: f32,
19+
ambient: f32,
20+
voices: array<Voice, 3>,
21+
swirl_uv: vec2<f32>,
22+
swirl_strength: f32,
23+
swirl_active: f32,
24+
};
25+
26+
@group(0) @binding(0) var<uniform> u: WaveUniforms;
27+
28+
@vertex
29+
fn vs_fullscreen(@builtin(vertex_index) vid: u32) -> VsOut {
30+
var pos = array<vec2<f32>, 3>(
31+
vec2<f32>(-1.0, -3.0),
32+
vec2<f32>(-1.0, 1.0),
33+
vec2<f32>(3.0, 1.0),
34+
);
35+
var uv = array<vec2<f32>, 3>(
36+
vec2<f32>(0.0, 2.0),
37+
vec2<f32>(0.0, 0.0),
38+
vec2<f32>(2.0, 0.0),
39+
);
40+
var out: VsOut;
41+
out.pos = vec4<f32>(pos[vid], 0.0, 1.0);
42+
out.uv = uv[vid];
43+
return out;
44+
}
45+
46+
fn hash2(p: vec2<f32>) -> f32 {
47+
let h = dot(p, vec2<f32>(127.1, 311.7));
48+
return fract(sin(h) * 43758.5453123);
49+
}
50+
51+
fn fbm(p: vec2<f32>) -> f32 {
52+
var a = 0.0;
53+
var b = 0.5;
54+
var f = p;
55+
for (var i = 0; i < 5; i = i + 1) {
56+
a += b * sin(f.x) * cos(f.y);
57+
f *= 2.17;
58+
b *= 0.55;
59+
}
60+
return a;
61+
}
62+
63+
// --- Wireframe helpers and layered waves version ---
64+
fn sd_segment(p: vec2<f32>, a: vec2<f32>, b: vec2<f32>) -> f32 {
65+
let pa = p - a;
66+
let ba = b - a;
67+
let h = clamp(dot(pa, ba) / dot(ba, ba), 0.0, 1.0);
68+
return length(pa - ba * h);
69+
}
70+
71+
fn wireframe_bg(p: vec2<f32>) -> f32 {
72+
var d = 1e9;
73+
let centers = array<vec2<f32>, 3>(
74+
vec2<f32>(0.62, 0.28),
75+
vec2<f32>(0.80, 0.44),
76+
vec2<f32>(0.70, 0.72),
77+
);
78+
let radii = array<f32, 3>(0.28, 0.18, 0.12);
79+
for (var i = 0; i < 3; i = i + 1) {
80+
d = min(d, abs(length(p - centers[i]) - radii[i]));
81+
}
82+
d = min(d, sd_segment(p, vec2<f32>(0.55, 0.20), vec2<f32>(0.85, 0.50)));
83+
d = min(d, sd_segment(p, vec2<f32>(0.62, 0.28), vec2<f32>(0.80, 0.44)));
84+
d = min(d, sd_segment(p, vec2<f32>(0.72, 0.70), vec2<f32>(0.86, 0.52)));
85+
return d;
86+
}
87+
88+
@fragment
89+
fn fs_waves(inp: VsOut) -> @location(0) vec4<f32> {
90+
let uv = inp.uv;
91+
let aspect = u.resolution.x / max(u.resolution.y, 1.0);
92+
let cuv0 = (uv - 0.5) * vec2<f32>(aspect, 1.0);
93+
let t = u.time;
94+
95+
// Background wireframe
96+
let gold = vec3<f32>(1.00, 0.86, 0.46);
97+
let d = wireframe_bg(uv);
98+
var col = vec3<f32>(0.04, 0.055, 0.10);
99+
let line = smoothstep(0.012, 0.002, d);
100+
// Make wireframe more visible; it will bloom subtly
101+
col += gold * line * (0.30 + 0.60 * u.ambient);
102+
103+
// Three layered waves with parallax
104+
for (var L = 0; L < 3; L = L + 1) {
105+
let depth = f32(L);
106+
let par = mix(0.65, 1.25, depth / 2.0);
107+
var cuv = cuv0 * par + vec2<f32>(0.0, -0.10 * depth);
108+
// Swirl displacement around active pointer
109+
if u.swirl_active > 0.5 {
110+
let c = (u.swirl_uv - 0.5) * vec2<f32>(aspect, 1.0) * par;
111+
let v = cuv - c;
112+
let r = length(v);
113+
let ang = u.swirl_strength * 2.5 * exp(-1.8 * r);
114+
let cs = cos(ang);
115+
let sn = sin(ang);
116+
let rot = vec2<f32>(v.x * cs - v.y * sn, v.x * sn + v.y * cs);
117+
cuv = c + rot;
118+
}
119+
// Displace coordinates by nearby voices so dragging clearly affects visuals
120+
var disp = vec2<f32>(0.0);
121+
for (var i = 0; i < 3; i = i + 1) {
122+
let v = u.voices[i];
123+
let p = vec2<f32>(v.pos_pulse.x, v.pos_pulse.z) * 0.33;
124+
let d = distance(cuv, p);
125+
let dir = normalize(cuv - p);
126+
let pulse = clamp(v.pos_pulse.w, 0.0, 1.5);
127+
let str = (0.12 + 0.45 * pulse) * exp(-1.8 * d);
128+
disp += dir * str;
129+
}
130+
cuv += disp;
131+
let tt = t * (0.30 + 0.08 * depth);
132+
let amp = mix(1.0, 2.2, depth / 2.0);
133+
var h = 0.0;
134+
// Broader ribbons along X with mild Y slant
135+
h += amp * (1.05 * sin((6.0 + 1.0 * depth) * cuv.x - 1.2 * tt));
136+
h += amp * (0.65 * sin((9.0 + 1.5 * depth) * cuv.x + 0.8 * tt + 0.7 * cuv.y));
137+
// Envelope so center has more curvature
138+
h *= (1.0 - 0.25 * abs(cuv.y));
139+
// Add velvet noise
140+
h += 0.35 * fbm(cuv * 2.4 + vec2<f32>(0.22 * tt, -0.16 * tt));
141+
for (var i = 0; i < 3; i = i + 1) {
142+
let v = u.voices[i];
143+
let p = vec2<f32>(v.pos_pulse.x, v.pos_pulse.z) * 0.33;
144+
let dd = distance(cuv, p);
145+
let pulse = clamp(v.pos_pulse.w, 0.0, 1.5);
146+
h += (0.65 + 0.9 * pulse) * exp(-2.2 * dd) * sin(14.0 * dd - 2.0 * tt);
147+
h += 0.22 * (1.0 / (1.0 + 6.0 * dd)) * sin(7.0 * (cuv.x - p.x) + 1.5 * tt);
148+
}
149+
let e = 0.002;
150+
let hx = h - (0.55 * sin(6.0 * (cuv.x - e) - 1.4 * tt) + 0.35 * sin(10.0 * (cuv.x - e) + 0.9 * tt + 0.8 * cuv.y) + 0.25 * fbm((cuv - vec2<f32>(e, 0.0)) * 2.5 + vec2<f32>(0.2 * tt, -0.15 * tt)));
151+
let hy = h - (0.55 * sin(6.0 * cuv.x - 1.4 * tt) + 0.35 * sin(10.0 * cuv.x + 0.9 * tt + 0.8 * (cuv.y - e)) + 0.25 * fbm((cuv - vec2<f32>(0.0, e)) * 2.5 + vec2<f32>(0.2 * tt, -0.15 * tt)));
152+
let n = normalize(vec3<f32>(hx, hy, e));
153+
let l1 = normalize(vec3<f32>(-0.4, 0.3, 0.85));
154+
let l2 = normalize(vec3<f32>(0.6, -0.2, 0.75));
155+
let diff = 0.65 * max(dot(n, l1), 0.0) + 0.35 * max(dot(n, l2), 0.0);
156+
let base = mix(vec3<f32>(0.03, 0.04, 0.08), vec3<f32>(0.12, 0.14, 0.26), diff + 0.15 * u.ambient);
157+
let cool = vec3<f32>(0.18, 0.45, 1.05);
158+
let warm = vec3<f32>(1.08, 0.86, 0.40);
159+
let k = clamp(0.5 + 1.1 * h, 0.0, 1.0);
160+
var lay = base + mix(cool * 0.45, warm * 0.55, k);
161+
let stripes = smoothstep(0.45, 0.5, abs(fract(h * 8.0) - 0.5));
162+
lay += (1.0 - stripes) * gold * (0.18 + 0.30 * u.ambient);
163+
let view = vec3<f32>(0.0, 0.0, 1.0);
164+
let h1 = normalize(l1 + view);
165+
lay += vec3<f32>(1.0) * (0.18 * pow(max(dot(n, h1), 0.0), 72.0));
166+
// Emissive crest glow captured by bloom
167+
let crest = smoothstep(0.84, 0.98, k);
168+
lay += gold * crest * (0.75 + 1.4 * u.ambient);
169+
// Visible voice focus points
170+
for (var i = 0; i < 3; i = i + 1) {
171+
let p = vec2<f32>(u.voices[i].pos_pulse.x, u.voices[i].pos_pulse.z) * 0.33;
172+
let dd = distance(cuv, p);
173+
lay += gold * exp(-40.0 * dd * dd) * 0.45;
174+
}
175+
let a = mix(0.55, 0.28, depth / 2.0);
176+
col = col * (1.0 - a) + lay * a;
177+
}
178+
179+
let s = hash2(cuv0 * 600.0 + t);
180+
col += (step(0.992, s) * (s - 0.992) * 240.0) * gold * (0.35 + 0.55 * u.ambient);
181+
182+
return vec4<f32>(col, 1.0);
183+
}
184+
185+

crates/app-core/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ pub mod constants;
22
pub mod music;
33
pub mod state;
44
pub static SCENE_WGSL: &str = include_str!("../shaders/scene.wgsl");
5+
pub static POST_WGSL: &str = include_str!("../shaders/post.wgsl");
6+
pub static WAVES_WGSL: &str = include_str!("../shaders/waves.wgsl");
57

68
pub use constants::*;
79
pub use music::*;

0 commit comments

Comments
 (0)