diff options
| author | skal <pascal.massimino@gmail.com> | 2026-02-10 18:19:30 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-02-10 18:19:30 +0100 |
| commit | 225ff0313c5021c3f1ec2bf5dd61114032b925a7 (patch) | |
| tree | 33fce14571651158a409d9acf8494cff69dfe8dc /workspaces/main/shaders/scene1.wgsl | |
| parent | 532220d2abb2b48307f049c4ad5f545d4c8934d6 (diff) | |
feat: Add Scene1 effect from ShaderToy (raymarching cube & sphere)
Converted ShaderToy shader (Saturday cubism experiment) to Scene1Effect
following EFFECT_WORKFLOW.md automation guidelines.
**Changes:**
- Created Scene1Effect (.h, .cc) as scene effect (not post-process)
- Converted GLSL to WGSL with manual fixes:
- Replaced RESOLUTION/iTime with uniforms.resolution/time
- Fixed const expressions (normalize not allowed in const)
- Converted mainImage() to fs_main() return value
- Manual matrix rotation for scene transformation
- Added shader asset to workspaces/main/assets.txt
- Registered in CMakeLists.txt (both GPU_SOURCES sections)
- Added to demo_effects.h and shaders declarations
- Added to timeline.seq at 22.5s for 10s duration
- Added to test_demo_effects.cc scene_effects list
**Shader features:**
- Raymarching cube and sphere with ground plane
- Reflections and soft shadows
- Sky rendering with sun and horizon glow
- ACES tonemapping and sRGB output
- Time-based rotation animation
**Tests:** All effects tests passing (5/9 scene, 9/9 post-process)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Diffstat (limited to 'workspaces/main/shaders/scene1.wgsl')
| -rw-r--r-- | workspaces/main/shaders/scene1.wgsl | 256 |
1 files changed, 256 insertions, 0 deletions
diff --git a/workspaces/main/shaders/scene1.wgsl b/workspaces/main/shaders/scene1.wgsl new file mode 100644 index 0000000..b0ec2ab --- /dev/null +++ b/workspaces/main/shaders/scene1.wgsl @@ -0,0 +1,256 @@ +// Scene1 effect shader - ShaderToy conversion (raymarching cube & sphere) +// Source: Saturday cubism experiment by skal + +#include "common_uniforms" + +@group(0) @binding(0) var<uniform> uniforms: CommonUniforms; + +const PI: f32 = 3.141592654; +const TAU: f32 = 6.283185307; +const TOLERANCE: f32 = 0.0005; +const MAX_RAY_LENGTH: f32 = 20.0; +const MAX_RAY_MARCHES: i32 = 80; +const MAX_SHD_MARCHES: i32 = 20; +const NORM_OFF: f32 = 0.005; + +fn rot(a: f32) -> mat2x2<f32> { + let c = cos(a); + let s = sin(a); + return mat2x2<f32>(c, s, -s, c); +} + +// HSV to RGB conversion +const hsv2rgb_K = vec4<f32>(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0); +fn hsv2rgb(c: vec3<f32>) -> vec3<f32> { + let p = abs(fract(c.xxx + hsv2rgb_K.xyz) * 6.0 - hsv2rgb_K.www); + return c.z * mix(hsv2rgb_K.xxx, clamp(p - hsv2rgb_K.xxx, vec3<f32>(0.0), vec3<f32>(1.0)), c.y); +} + +// Colors (precomputed HSV conversions) +const skyCol = vec3<f32>(0.176, 0.235, 0.25); // HSV(0.57, 0.90, 0.25) +const skylineCol = vec3<f32>(0.5, 0.125, 0.025); // HSV(0.02, 0.95, 0.5) +const sunCol = vec3<f32>(0.5, 0.163, 0.025); // HSV(0.07, 0.95, 0.5) +const diffCol1 = vec3<f32>(0.4, 1.0, 1.0); // HSV(0.60, 0.90, 1.0) +const diffCol2 = vec3<f32>(0.325, 1.0, 0.975); // HSV(0.55, 0.90, 1.0) + +// Lighting (normalized manually) +const sunDir1 = vec3<f32>(0.0, 0.04997, -0.99875); // normalize(0, 0.05, -1) +const lightPos1 = vec3<f32>(10.0, 10.0, 10.0); +const lightPos2 = vec3<f32>(-10.0, 10.0, -10.0); + +fn sRGB(t: vec3<f32>) -> vec3<f32> { + return mix(1.055 * pow(t, vec3<f32>(1.0/2.4)) - 0.055, 12.92 * t, step(t, vec3<f32>(0.0031308))); +} + +fn aces_approx(v_in: vec3<f32>) -> vec3<f32> { + var v = max(v_in, vec3<f32>(0.0)); + v *= 0.6; + let a = 2.51; + let b = 0.03; + let c = 2.43; + let d = 0.59; + let e = 0.14; + return clamp((v * (a * v + b)) / (v * (c * v + d) + e), vec3<f32>(0.0), vec3<f32>(1.0)); +} + +fn tanh_approx(x: f32) -> f32 { + let x2 = x * x; + return clamp(x * (27.0 + x2) / (27.0 + 9.0 * x2), -1.0, 1.0); +} + +fn rayPlane(ro: vec3<f32>, rd: vec3<f32>, plane: vec4<f32>) -> f32 { + return -(dot(ro, plane.xyz) + plane.w) / dot(rd, plane.xyz); +} + +fn box2d(p: vec2<f32>, b: vec2<f32>) -> f32 { + let d = abs(p) - b; + return length(max(d, vec2<f32>(0.0))) + min(max(d.x, d.y), 0.0); +} + +fn box3d(p: vec3<f32>, b: vec3<f32>) -> f32 { + let q = abs(p) - b; + return length(max(q, vec3<f32>(0.0))) + min(max(q.x, max(q.y, q.z)), 0.0); +} + +fn sphere(p: vec3<f32>, r: f32) -> f32 { + return length(p) - r; +} + +var<private> g_rot0: mat2x2<f32>; + +fn render0(ro: vec3<f32>, rd: vec3<f32>) -> vec3<f32> { + var col = vec3<f32>(0.0); + var sf = 1.0001 - max(dot(sunDir1, rd), 0.0); + col += skyCol * pow((1.0 - abs(rd.y)), 8.0); + col += clamp(vec3<f32>(mix(0.0025, 0.125, tanh_approx(0.005 / sf)) / abs(rd.y)) * skylineCol, vec3<f32>(0.0), vec3<f32>(10.0)); + sf *= sf; + col += sunCol * 0.00005 / sf; + + let tp1 = rayPlane(ro, rd, vec4<f32>(0.0, -1.0, 0.0, 6.0)); + if (tp1 > 0.0) { + let pos = ro + tp1 * rd; + let pp = pos.xz; + let db = box2d(pp, vec2<f32>(5.0, 9.0)) - 3.0; + col += vec3<f32>(4.0) * skyCol * rd.y * rd.y * smoothstep(0.25, 0.0, db); + col += vec3<f32>(0.8) * skyCol * exp(-0.5 * max(db, 0.0)); + } + + return clamp(col, vec3<f32>(0.0), vec3<f32>(10.0)); +} + +fn df(p_in: vec3<f32>) -> f32 { + var p = p_in; + p.x = p_in.x * g_rot0[0][0] + p_in.z * g_rot0[0][1]; + p.z = p_in.x * g_rot0[1][0] + p_in.z * g_rot0[1][1]; + + // Cube + var pc = p; + pc -= vec3<f32>(-1.9, 0.0, 0.0); + let dCube = box3d(pc, vec3<f32>(1.6)); + + // Sphere + var ps = p; + ps -= vec3<f32>(1.3, 0.0, 0.0); + let dSphere = sphere(ps, 1.2); + + // Ground plane + let dPlane = p.y + 1.0; + + // Union + var d = min(dCube, dSphere); + d = min(d, dPlane); + + return d; +} + +fn normal(pos: vec3<f32>) -> vec3<f32> { + let eps = vec2<f32>(NORM_OFF, 0.0); + var nor: vec3<f32>; + nor.x = df(pos + eps.xyy) - df(pos - eps.xyy); + nor.y = df(pos + eps.yxy) - df(pos - eps.yxy); + nor.z = df(pos + eps.yyx) - df(pos - eps.yyx); + return normalize(nor); +} + +fn rayMarch(ro: vec3<f32>, rd: vec3<f32>, initt: f32) -> f32 { + var t = initt; + for (var i = 0; i < MAX_RAY_MARCHES; i++) { + if (t > MAX_RAY_LENGTH) { + t = MAX_RAY_LENGTH; + break; + } + let d = df(ro + rd * t); + if (d < TOLERANCE) { + break; + } + t += d; + } + return t; +} + +fn shadow(lp: vec3<f32>, ld: vec3<f32>, mint: f32, maxt: f32) -> f32 { + let ds = 1.0 - 0.4; + var t = mint; + var nd = 1e6; + let soff = 0.05; + let smul = 1.5; + for (var i = 0; i < MAX_SHD_MARCHES; i++) { + let p = lp + ld * t; + let d = df(p); + if (d < TOLERANCE || t >= maxt) { + let sd = 1.0 - exp(-smul * max(t / maxt - soff, 0.0)); + return select(mix(sd, 1.0, smoothstep(0.0, 0.025, nd)), sd, t >= maxt); + } + nd = min(nd, d); + t += ds * d; + } + let sd = 1.0 - exp(-smul * max(t / maxt - soff, 0.0)); + return sd; +} + +fn boxCol(col: vec3<f32>, nsp: vec3<f32>, rd: vec3<f32>, nnor: vec3<f32>, nrcol: vec3<f32>, nshd1: f32, nshd2: f32) -> vec3<f32> { + var nfre = 1.0 + dot(rd, nnor); + nfre *= nfre; + + let nld1 = normalize(lightPos1 - nsp); + let nld2 = normalize(lightPos2 - nsp); + + var ndif1 = max(dot(nld1, nnor), 0.0); + ndif1 *= ndif1; + + var ndif2 = max(dot(nld2, nnor), 0.0); + ndif2 *= ndif2; + + var scol = vec3<f32>(0.0); + let rf = smoothstep(1.0, 0.9, nfre); + scol += diffCol1 * ndif1 * nshd1; + scol += diffCol2 * ndif2 * nshd2; + scol += 0.1 * (skyCol + skylineCol); + scol += nrcol * 0.75 * mix(vec3<f32>(0.25), vec3<f32>(0.5, 0.5, 1.0), nfre); + + return mix(col, scol, rf * smoothstep(90.0, 20.0, dot(nsp, nsp))); +} + +fn render1(ro: vec3<f32>, rd: vec3<f32>) -> vec3<f32> { + let skyCol_local = render0(ro, rd); + var col = skyCol_local; + + let nt = rayMarch(ro, rd, 0.0); + if (nt < MAX_RAY_LENGTH) { + let nsp = ro + rd * nt; + let nnor = normal(nsp); + + let nref = reflect(rd, nnor); + let nrt = rayMarch(nsp, nref, 0.2); + var nrcol = render0(nsp, nref); + + if (nrt < MAX_RAY_LENGTH) { + let nrsp = nsp + nref * nrt; + let nrnor = normal(nrsp); + let nrref = reflect(nref, nrnor); + nrcol = boxCol(nrcol, nrsp, nref, nrnor, render0(nrsp, nrref), 1.0, 1.0); + } + + let nshd1 = mix(0.0, 1.0, shadow(nsp, normalize(lightPos1 - nsp), 0.1, distance(lightPos1, nsp))); + let nshd2 = mix(0.0, 1.0, shadow(nsp, normalize(lightPos2 - nsp), 0.1, distance(lightPos2, nsp))); + + col = boxCol(col, nsp, rd, nnor, nrcol, nshd1, nshd2); + } + + return col; +} + +fn effect(p: vec2<f32>) -> vec3<f32> { + g_rot0 = rot(-0.2 * uniforms.time); + + let fov = tan(TAU / 6.0); + let ro = vec3<f32>(0.0, 2.5, 5.0); + let la = vec3<f32>(0.0, 0.0, 0.0); + let up = vec3<f32>(0.1, 1.0, 0.0); + + let ww = normalize(la - ro); + let uu = normalize(cross(up, ww)); + let vv = cross(ww, uu); + let rd = normalize(-p.x * uu + p.y * vv + fov * ww); + + return render1(ro, rd); +} + +@vertex fn vs_main(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> { + var pos = array<vec2<f32>, 3>( + vec2<f32>(-1.0, -1.0), + vec2<f32>(3.0, -1.0), + vec2<f32>(-1.0, 3.0) + ); + return vec4<f32>(pos[i], 0.0, 1.0); +} + +@fragment fn fs_main(@builtin(position) p: vec4<f32>) -> @location(0) vec4<f32> { + let q = p.xy / uniforms.resolution; + var coord = -1.0 + 2.0 * q; + coord.x *= uniforms.resolution.x / uniforms.resolution.y; + var col = effect(coord); + col = aces_approx(col); + col = sRGB(col); + return vec4<f32>(col, 1.0); +} |
