From c8938fe29132a11126013ed1b010c3092f5645e4 Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Wed, 8 Jul 2026 09:07:55 -0400 Subject: feature: improved summer scene --- themes/src/_shaders/sky.frag | 78 +++++++++++++++++++++++ themes/src/_shaders/sky.vert | 10 +++ themes/src/_shaders/water.frag | 63 ++++++++++++++++++ themes/src/_shaders/water.vert | 63 ++++++++++++++++++ themes/src/shaders/sky_frag.cpp | 81 +++++++++++++++++++++++ themes/src/shaders/sky_frag.h | 4 ++ themes/src/shaders/sky_vert.cpp | 13 ++++ themes/src/shaders/sky_vert.h | 4 ++ themes/src/shaders/water_frag.cpp | 66 +++++++++++++++++++ themes/src/shaders/water_frag.h | 4 ++ themes/src/shaders/water_vert.cpp | 66 +++++++++++++++++++ themes/src/shaders/water_vert.h | 4 ++ themes/src/summer/sky_model.cpp | 64 +++++++++++++++++++ themes/src/summer/sky_model.h | 43 +++++++++++++ themes/src/summer/summer_theme.cpp | 30 ++++++++- themes/src/summer/summer_theme.h | 4 ++ themes/src/summer/water_model.cpp | 127 +++++++++++++++++++++++++++++++++++++ themes/src/summer/water_model.h | 70 ++++++++++++++++++++ themes/src/tools/shader.cjs | 41 ++++++++++++ themes/src/tools/shader.js | 41 ------------ 20 files changed, 832 insertions(+), 44 deletions(-) create mode 100644 themes/src/_shaders/sky.frag create mode 100644 themes/src/_shaders/sky.vert create mode 100644 themes/src/_shaders/water.frag create mode 100644 themes/src/_shaders/water.vert create mode 100644 themes/src/shaders/sky_frag.cpp create mode 100644 themes/src/shaders/sky_frag.h create mode 100644 themes/src/shaders/sky_vert.cpp create mode 100644 themes/src/shaders/sky_vert.h create mode 100644 themes/src/shaders/water_frag.cpp create mode 100644 themes/src/shaders/water_frag.h create mode 100644 themes/src/shaders/water_vert.cpp create mode 100644 themes/src/shaders/water_vert.h create mode 100644 themes/src/summer/sky_model.cpp create mode 100644 themes/src/summer/sky_model.h create mode 100644 themes/src/summer/water_model.cpp create mode 100644 themes/src/summer/water_model.h create mode 100644 themes/src/tools/shader.cjs delete mode 100644 themes/src/tools/shader.js (limited to 'themes/src') diff --git a/themes/src/_shaders/sky.frag b/themes/src/_shaders/sky.frag new file mode 100644 index 0000000..a2eb18b --- /dev/null +++ b/themes/src/_shaders/sky.frag @@ -0,0 +1,78 @@ +precision highp float; + +varying vec2 vUv; + +uniform float time; // seconds, for slow cloud drift +uniform float horizon; // screen-space y of the water horizon (~0.5) + +float hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float valueNoise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +float fbm(vec2 p) { + float v = 0.0; + float amp = 0.5; + for (int i = 0; i < 4; i++) { + v += amp * valueNoise(p); + p *= 2.0; + amp *= 0.5; + } + return v; +} + +const vec3 SKY_HORIZON = vec3(0.80, 0.88, 0.98); // matches water horizon fade +const vec3 SKY_TOP = vec3(0.22, 0.48, 0.86); // deeper zenith blue +const vec3 CLOUD_COLOR = vec3(1.00, 0.98, 0.95); // white, slightly warm + +const float DEPTH_BIAS = 0.05; // caps frequency at the horizon (larger = gentler shrink) +const float CLOUD_FREQ = 2.7; // overall cloud scale (larger = more, smaller clouds) +const float X_STRETCH = 0.35; // < 1 stretches clouds horizontally -> wispy +const float CLOUD_THRESH = 0.55; // coverage cutoff; higher = sparser sky +const float CLOUD_SOFT = 0.22; // edge softness of the cloud mask +const float CLOUD_OPACITY = 0.85; // max cloud whiteness +const float DRIFT_SPEED = 0.03; // slow lateral drift (noise units / sec) +const float H_FADE = 0.12; // band over which clouds fade in above horizon + +void main() { + // Height above the water horizon: 0 at the waterline, (1 - horizon) at the top. + float above = max(vUv.y - horizon, 0.0); + + // Vertical gradient. + float g = clamp(above / (1.0 - horizon), 0.0, 1.0); + vec3 col = mix(SKY_HORIZON, SKY_TOP, pow(g, 0.75)); + float haze = 1.0 - smoothstep(0.0, 0.35, g); + col += vec3(0.06, 0.04, 0.00) * haze; + + // Flat cloud-ceiling perspective: distance along the view ~ 1/above, so + // features shrink AND compress vertically toward the horizon. + float depth = 1.0 / (above + DEPTH_BIAS); + vec2 q; + q.x = (vUv.x - 0.5) * depth; + q.y = depth; + + vec2 p = vec2(q.x * X_STRETCH, q.y) * CLOUD_FREQ; + p.x += time * DRIFT_SPEED; + + float n = fbm(p); + float cloud = smoothstep(CLOUD_THRESH, CLOUD_THRESH + CLOUD_SOFT, n); + + // Fade clouds into the haze right at the horizon. + cloud *= smoothstep(0.0, H_FADE, above); + + col = mix(col, CLOUD_COLOR, cloud * CLOUD_OPACITY); + + gl_FragColor = vec4(col, 1.0); +} diff --git a/themes/src/_shaders/sky.vert b/themes/src/_shaders/sky.vert new file mode 100644 index 0000000..249603c --- /dev/null +++ b/themes/src/_shaders/sky.vert @@ -0,0 +1,10 @@ +precision highp float; + +attribute vec2 position; // fullscreen quad, clip-space corners in [-1, 1] + +varying vec2 vUv; + +void main() { + vUv = position * 0.5 + 0.5; // uv.y = 0 bottom, 1 top + gl_Position = vec4(position, 0.0, 1.0); +} diff --git a/themes/src/_shaders/water.frag b/themes/src/_shaders/water.frag new file mode 100644 index 0000000..0ceffeb --- /dev/null +++ b/themes/src/_shaders/water.frag @@ -0,0 +1,63 @@ +precision highp float; + +varying vec3 vWorldPos; +varying vec3 vNormal; + +uniform vec3 cameraPos; +uniform vec3 sunDir; // normalized direction from the water toward the sun +uniform vec3 sunColor; +uniform float time; + +float hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float valueNoise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +void main() { + vec3 N = normalize(vNormal); + vec3 V = normalize(cameraPos - vWorldPos); + + // Fresnel (Schlick). + float f0 = 0.02; + float fres = f0 + (1.0 - f0) * pow(1.0 - max(dot(N, V), 0.0), 5.0); + + // Refraction: depth-tinted water body, wobbled by the surface normal. + float depth01 = clamp((vWorldPos.z + 350.0) / 400.0, 0.0, 1.0); + float wob = (N.x + N.z) * 0.5; + vec3 shallow = vec3(0.10, 0.55, 0.60); + vec3 deep = vec3(0.02, 0.14, 0.28); + vec3 refractCol = mix(shallow, deep, clamp(depth01 + wob * 0.15, 0.0, 1.0)); + + // Reflection: sky gradient + reflected sun glow + broken-up specular glitter. + vec3 R = reflect(-V, N); + vec3 skyHorizon = vec3(0.80, 0.88, 0.98); + vec3 skyTop = vec3(0.30, 0.55, 0.95); + float skyT = clamp(R.y * 2.0, 0.0, 1.0); + vec3 reflectCol = mix(skyHorizon, skyTop, skyT); + + float sunAmt = max(dot(R, sunDir), 0.0); + reflectCol += sunColor * pow(sunAmt, 8.0) * 0.6; + float sparkle = valueNoise(vWorldPos.xz * 0.5 + vec2(time * 1.3, time * 0.7)); + float spec = pow(sunAmt, 200.0) * smoothstep(0.55, 0.9, sparkle); + reflectCol += sunColor * spec * 4.0; + + vec3 color = mix(refractCol, reflectCol, fres); + + // Horizon haze: hide the far straight edge of the plane by fading to sky. + float horizonFade = clamp((vWorldPos.z + 350.0) / 90.0, 0.0, 1.0); + color = mix(skyHorizon, color, horizonFade); + + gl_FragColor = vec4(color, 1.0); // opaque -> occludes the sun below the waterline +} diff --git a/themes/src/_shaders/water.vert b/themes/src/_shaders/water.vert new file mode 100644 index 0000000..7121ab1 --- /dev/null +++ b/themes/src/_shaders/water.vert @@ -0,0 +1,63 @@ +precision highp float; + +attribute vec2 position; // local (x, z) on the plane + +uniform mat4 projection; +uniform mat4 view; +uniform mat4 model; +uniform float time; +uniform float amplitude; + +varying vec3 vWorldPos; +varying vec3 vNormal; + +float hash(vec2 p) { + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float valueNoise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); +} + +// Irregular height: non-harmonic directional sines plus two octaves of +// scrolling value noise so it never reads as a single clean wave. +float waveHeight(vec2 p, float t) { + float h = 0.0; + h += 0.30 * sin(dot(p, vec2(0.90, 0.30)) * 0.35 + t * 1.1); + h += 0.20 * sin(dot(p, vec2(-0.40, 1.00)) * 0.55 - t * 1.7); + h += 0.12 * sin(dot(p, vec2(0.70, -0.80)) * 0.90 + t * 2.3); + h += 0.08 * sin(dot(p, vec2(1.00, 0.60)) * 1.60 - t * 3.1); + h += 0.18 * (valueNoise(p * 0.25 + vec2(t * 0.15, t * 0.10)) - 0.5); + h += 0.09 * (valueNoise(p * 0.70 - vec2(t * 0.20, 0.0)) - 0.5); + return h; +} + +void main() { + vec2 p = position; + float t = time; + + float h = waveHeight(p, t) * amplitude; + + // Analytic normal via central differences of the same height function. + float e = 2.0; + float hL = waveHeight(p - vec2(e, 0.0), t) * amplitude; + float hR = waveHeight(p + vec2(e, 0.0), t) * amplitude; + float hD = waveHeight(p - vec2(0.0, e), t) * amplitude; + float hU = waveHeight(p + vec2(0.0, e), t) * amplitude; + vec3 N = normalize(vec3(hL - hR, 2.0 * e, hD - hU)); + + vec4 worldPos = model * vec4(p.x, h, p.y, 1.0); + vWorldPos = worldPos.xyz; + vNormal = N; // model is translation-only, so no normal matrix needed + + gl_Position = projection * view * worldPos; +} diff --git a/themes/src/shaders/sky_frag.cpp b/themes/src/shaders/sky_frag.cpp new file mode 100644 index 0000000..400f07c --- /dev/null +++ b/themes/src/shaders/sky_frag.cpp @@ -0,0 +1,81 @@ +#include "sky_frag.h" + +const char* shader_sky_frag = "precision highp float; \n" +" \n" +"varying vec2 vUv; \n" +" \n" +"uniform float time; // seconds, for slow cloud drift \n" +"uniform float horizon; // screen-space y of the water horizon (~0.5) \n" +" \n" +"float hash(vec2 p) { \n" +" p = fract(p * vec2(123.34, 456.21)); \n" +" p += dot(p, p + 45.32); \n" +" return fract(p.x * p.y); \n" +"} \n" +" \n" +"float valueNoise(vec2 p) { \n" +" vec2 i = floor(p); \n" +" vec2 f = fract(p); \n" +" float a = hash(i); \n" +" float b = hash(i + vec2(1.0, 0.0)); \n" +" float c = hash(i + vec2(0.0, 1.0)); \n" +" float d = hash(i + vec2(1.0, 1.0)); \n" +" vec2 u = f * f * (3.0 - 2.0 * f); \n" +" return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); \n" +"} \n" +" \n" +"float fbm(vec2 p) { \n" +" float v = 0.0; \n" +" float amp = 0.5; \n" +" for (int i = 0; i < 4; i++) { \n" +" v += amp * valueNoise(p); \n" +" p *= 2.0; \n" +" amp *= 0.5; \n" +" } \n" +" return v; \n" +"} \n" +" \n" +"const vec3 SKY_HORIZON = vec3(0.80, 0.88, 0.98); // matches water horizon fade \n" +"const vec3 SKY_TOP = vec3(0.22, 0.48, 0.86); // deeper zenith blue \n" +"const vec3 CLOUD_COLOR = vec3(1.00, 0.98, 0.95); // white, slightly warm \n" +" \n" +"const float DEPTH_BIAS = 0.05; // caps frequency at the horizon (larger = gentler shrink) \n" +"const float CLOUD_FREQ = 2.7; // overall cloud scale (larger = more, smaller clouds) \n" +"const float X_STRETCH = 0.35; // < 1 stretches clouds horizontally -> wispy \n" +"const float CLOUD_THRESH = 0.55; // coverage cutoff; higher = sparser sky \n" +"const float CLOUD_SOFT = 0.22; // edge softness of the cloud mask \n" +"const float CLOUD_OPACITY = 0.85; // max cloud whiteness \n" +"const float DRIFT_SPEED = 0.03; // slow lateral drift (noise units / sec) \n" +"const float H_FADE = 0.12; // band over which clouds fade in above horizon \n" +" \n" +"void main() { \n" +" // Height above the water horizon: 0 at the waterline, (1 - horizon) at the top. \n" +" float above = max(vUv.y - horizon, 0.0); \n" +" \n" +" // Vertical gradient. \n" +" float g = clamp(above / (1.0 - horizon), 0.0, 1.0); \n" +" vec3 col = mix(SKY_HORIZON, SKY_TOP, pow(g, 0.75)); \n" +" float haze = 1.0 - smoothstep(0.0, 0.35, g); \n" +" col += vec3(0.06, 0.04, 0.00) * haze; \n" +" \n" +" // Flat cloud-ceiling perspective: distance along the view ~ 1/above, so \n" +" // features shrink AND compress vertically toward the horizon. \n" +" float depth = 1.0 / (above + DEPTH_BIAS); \n" +" vec2 q; \n" +" q.x = (vUv.x - 0.5) * depth; \n" +" q.y = depth; \n" +" \n" +" vec2 p = vec2(q.x * X_STRETCH, q.y) * CLOUD_FREQ; \n" +" p.x += time * DRIFT_SPEED; \n" +" \n" +" float n = fbm(p); \n" +" float cloud = smoothstep(CLOUD_THRESH, CLOUD_THRESH + CLOUD_SOFT, n); \n" +" \n" +" // Fade clouds into the haze right at the horizon. \n" +" cloud *= smoothstep(0.0, H_FADE, above); \n" +" \n" +" col = mix(col, CLOUD_COLOR, cloud * CLOUD_OPACITY); \n" +" \n" +" gl_FragColor = vec4(col, 1.0); \n" +"} \n" +" \n"; diff --git a/themes/src/shaders/sky_frag.h b/themes/src/shaders/sky_frag.h new file mode 100644 index 0000000..886b6eb --- /dev/null +++ b/themes/src/shaders/sky_frag.h @@ -0,0 +1,4 @@ +#ifndef SHADER_SKY_FRAG +#define SHADER_SKY_FRAG +extern const char* shader_sky_frag; +#endif diff --git a/themes/src/shaders/sky_vert.cpp b/themes/src/shaders/sky_vert.cpp new file mode 100644 index 0000000..b43aa73 --- /dev/null +++ b/themes/src/shaders/sky_vert.cpp @@ -0,0 +1,13 @@ +#include "sky_vert.h" + +const char* shader_sky_vert = "precision highp float; \n" +" \n" +"attribute vec2 position; // fullscreen quad, clip-space corners in [-1, 1] \n" +" \n" +"varying vec2 vUv; \n" +" \n" +"void main() { \n" +" vUv = position * 0.5 + 0.5; // uv.y = 0 bottom, 1 top \n" +" gl_Position = vec4(position, 0.0, 1.0); \n" +"} \n" +" \n"; diff --git a/themes/src/shaders/sky_vert.h b/themes/src/shaders/sky_vert.h new file mode 100644 index 0000000..114fe63 --- /dev/null +++ b/themes/src/shaders/sky_vert.h @@ -0,0 +1,4 @@ +#ifndef SHADER_SKY_VERT +#define SHADER_SKY_VERT +extern const char* shader_sky_vert; +#endif diff --git a/themes/src/shaders/water_frag.cpp b/themes/src/shaders/water_frag.cpp new file mode 100644 index 0000000..91c50f9 --- /dev/null +++ b/themes/src/shaders/water_frag.cpp @@ -0,0 +1,66 @@ +#include "water_frag.h" + +const char* shader_water_frag = "precision highp float; \n" +" \n" +"varying vec3 vWorldPos; \n" +"varying vec3 vNormal; \n" +" \n" +"uniform vec3 cameraPos; \n" +"uniform vec3 sunDir; // normalized direction from the water toward the sun \n" +"uniform vec3 sunColor; \n" +"uniform float time; \n" +" \n" +"float hash(vec2 p) { \n" +" p = fract(p * vec2(123.34, 456.21)); \n" +" p += dot(p, p + 45.32); \n" +" return fract(p.x * p.y); \n" +"} \n" +" \n" +"float valueNoise(vec2 p) { \n" +" vec2 i = floor(p); \n" +" vec2 f = fract(p); \n" +" float a = hash(i); \n" +" float b = hash(i + vec2(1.0, 0.0)); \n" +" float c = hash(i + vec2(0.0, 1.0)); \n" +" float d = hash(i + vec2(1.0, 1.0)); \n" +" vec2 u = f * f * (3.0 - 2.0 * f); \n" +" return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); \n" +"} \n" +" \n" +"void main() { \n" +" vec3 N = normalize(vNormal); \n" +" vec3 V = normalize(cameraPos - vWorldPos); \n" +" \n" +" // Fresnel (Schlick). \n" +" float f0 = 0.02; \n" +" float fres = f0 + (1.0 - f0) * pow(1.0 - max(dot(N, V), 0.0), 5.0); \n" +" \n" +" // Refraction: depth-tinted water body, wobbled by the surface normal. \n" +" float depth01 = clamp((vWorldPos.z + 350.0) / 400.0, 0.0, 1.0); \n" +" float wob = (N.x + N.z) * 0.5; \n" +" vec3 shallow = vec3(0.10, 0.55, 0.60); \n" +" vec3 deep = vec3(0.02, 0.14, 0.28); \n" +" vec3 refractCol = mix(shallow, deep, clamp(depth01 + wob * 0.15, 0.0, 1.0)); \n" +" \n" +" // Reflection: sky gradient + reflected sun glow + broken-up specular glitter. \n" +" vec3 R = reflect(-V, N); \n" +" vec3 skyHorizon = vec3(0.80, 0.88, 0.98); \n" +" vec3 skyTop = vec3(0.30, 0.55, 0.95); \n" +" float skyT = clamp(R.y * 2.0, 0.0, 1.0); \n" +" vec3 reflectCol = mix(skyHorizon, skyTop, skyT); \n" +" \n" +" float sunAmt = max(dot(R, sunDir), 0.0); \n" +" reflectCol += sunColor * pow(sunAmt, 8.0) * 0.6; \n" +" float sparkle = valueNoise(vWorldPos.xz * 0.5 + vec2(time * 1.3, time * 0.7)); \n" +" float spec = pow(sunAmt, 200.0) * smoothstep(0.55, 0.9, sparkle); \n" +" reflectCol += sunColor * spec * 4.0; \n" +" \n" +" vec3 color = mix(refractCol, reflectCol, fres); \n" +" \n" +" // Horizon haze: hide the far straight edge of the plane by fading to sky. \n" +" float horizonFade = clamp((vWorldPos.z + 350.0) / 90.0, 0.0, 1.0); \n" +" color = mix(skyHorizon, color, horizonFade); \n" +" \n" +" gl_FragColor = vec4(color, 1.0); // opaque -> occludes the sun below the waterline \n" +"} \n" +" \n"; diff --git a/themes/src/shaders/water_frag.h b/themes/src/shaders/water_frag.h new file mode 100644 index 0000000..104cb04 --- /dev/null +++ b/themes/src/shaders/water_frag.h @@ -0,0 +1,4 @@ +#ifndef SHADER_WATER_FRAG +#define SHADER_WATER_FRAG +extern const char* shader_water_frag; +#endif diff --git a/themes/src/shaders/water_vert.cpp b/themes/src/shaders/water_vert.cpp new file mode 100644 index 0000000..44dabd8 --- /dev/null +++ b/themes/src/shaders/water_vert.cpp @@ -0,0 +1,66 @@ +#include "water_vert.h" + +const char* shader_water_vert = "precision highp float; \n" +" \n" +"attribute vec2 position; // local (x, z) on the plane \n" +" \n" +"uniform mat4 projection; \n" +"uniform mat4 view; \n" +"uniform mat4 model; \n" +"uniform float time; \n" +"uniform float amplitude; \n" +" \n" +"varying vec3 vWorldPos; \n" +"varying vec3 vNormal; \n" +" \n" +"float hash(vec2 p) { \n" +" p = fract(p * vec2(123.34, 456.21)); \n" +" p += dot(p, p + 45.32); \n" +" return fract(p.x * p.y); \n" +"} \n" +" \n" +"float valueNoise(vec2 p) { \n" +" vec2 i = floor(p); \n" +" vec2 f = fract(p); \n" +" float a = hash(i); \n" +" float b = hash(i + vec2(1.0, 0.0)); \n" +" float c = hash(i + vec2(0.0, 1.0)); \n" +" float d = hash(i + vec2(1.0, 1.0)); \n" +" vec2 u = f * f * (3.0 - 2.0 * f); \n" +" return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); \n" +"} \n" +" \n" +"// Irregular height: non-harmonic directional sines plus two octaves of \n" +"// scrolling value noise so it never reads as a single clean wave. \n" +"float waveHeight(vec2 p, float t) { \n" +" float h = 0.0; \n" +" h += 0.30 * sin(dot(p, vec2(0.90, 0.30)) * 0.35 + t * 1.1); \n" +" h += 0.20 * sin(dot(p, vec2(-0.40, 1.00)) * 0.55 - t * 1.7); \n" +" h += 0.12 * sin(dot(p, vec2(0.70, -0.80)) * 0.90 + t * 2.3); \n" +" h += 0.08 * sin(dot(p, vec2(1.00, 0.60)) * 1.60 - t * 3.1); \n" +" h += 0.18 * (valueNoise(p * 0.25 + vec2(t * 0.15, t * 0.10)) - 0.5); \n" +" h += 0.09 * (valueNoise(p * 0.70 - vec2(t * 0.20, 0.0)) - 0.5); \n" +" return h; \n" +"} \n" +" \n" +"void main() { \n" +" vec2 p = position; \n" +" float t = time; \n" +" \n" +" float h = waveHeight(p, t) * amplitude; \n" +" \n" +" // Analytic normal via central differences of the same height function. \n" +" float e = 2.0; \n" +" float hL = waveHeight(p - vec2(e, 0.0), t) * amplitude; \n" +" float hR = waveHeight(p + vec2(e, 0.0), t) * amplitude; \n" +" float hD = waveHeight(p - vec2(0.0, e), t) * amplitude; \n" +" float hU = waveHeight(p + vec2(0.0, e), t) * amplitude; \n" +" vec3 N = normalize(vec3(hL - hR, 2.0 * e, hD - hU)); \n" +" \n" +" vec4 worldPos = model * vec4(p.x, h, p.y, 1.0); \n" +" vWorldPos = worldPos.xyz; \n" +" vNormal = N; // model is translation-only, so no normal matrix needed \n" +" \n" +" gl_Position = projection * view * worldPos; \n" +"} \n" +" \n"; diff --git a/themes/src/shaders/water_vert.h b/themes/src/shaders/water_vert.h new file mode 100644 index 0000000..638b22b --- /dev/null +++ b/themes/src/shaders/water_vert.h @@ -0,0 +1,4 @@ +#ifndef SHADER_WATER_VERT +#define SHADER_WATER_VERT +extern const char* shader_water_vert; +#endif diff --git a/themes/src/summer/sky_model.cpp b/themes/src/summer/sky_model.cpp new file mode 100644 index 0000000..d700f34 --- /dev/null +++ b/themes/src/summer/sky_model.cpp @@ -0,0 +1,64 @@ +#include "sky_model.h" +#include "../webgl_context.h" +#include "../mathlib.h" +#include "../shaders/sky_vert.h" +#include "../shaders/sky_frag.h" + +SkyModel::SkyModel() {} + +SkyModel::~SkyModel() { unload(); } + +void SkyModel::load(WebglContext* inContext) { + context = inContext; + shader = loadShader(shader_sky_vert, shader_sky_frag); + useShader(shader); + + attributes.position = getShaderAttribute(shader, "position"); + uniforms.time = getShaderUniform(shader, "time"); + uniforms.horizon = getShaderUniform(shader, "horizon"); + + // Full-screen quad in clip space (two triangles). + Vector2 verts[6] = { + Vector2(-1.f, -1.f), Vector2(1.f, -1.f), Vector2(-1.f, 1.f), + Vector2(-1.f, 1.f), Vector2(1.f, -1.f), Vector2( 1.f, 1.f) + }; + + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(verts), verts, GL_STATIC_DRAW); + + glEnableVertexAttribArray(attributes.position); + glVertexAttribPointer(attributes.position, 2, GL_FLOAT, GL_FALSE, sizeof(Vector2), (GLvoid*)0); + + glBindVertexArray(0); +} + +void SkyModel::update(f32 dtSeconds) { + elapsedTime += dtSeconds; +} + +void SkyModel::render() { + useShader(shader); + + // Background: paint every pixel first, without touching the depth buffer so + // it can neither reject nor be rejected by the sun/water drawn afterward. + glDepthMask(GL_FALSE); + glDisable(GL_DEPTH_TEST); + glDisable(GL_BLEND); + + setShaderFloat(uniforms.time, elapsedTime); + setShaderFloat(uniforms.horizon, horizon); + + glBindVertexArray(vao); + glDrawArrays(GL_TRIANGLES, 0, 6); + glBindVertexArray(0); +} + +void SkyModel::unload() { + if (vao) { glDeleteVertexArrays(1, &vao); vao = 0; } + if (vbo) { glDeleteBuffers(1, &vbo); vbo = 0; } + if (shader) { glDeleteProgram(shader); shader = 0; } +} diff --git a/themes/src/summer/sky_model.h b/themes/src/summer/sky_model.h new file mode 100644 index 0000000..d957f05 --- /dev/null +++ b/themes/src/summer/sky_model.h @@ -0,0 +1,43 @@ +#ifndef SKY_MODEL_H +#define SKY_MODEL_H + +#include "../shader.h" +#include "../types.h" + +struct WebglContext; + +/// A full-screen sky background. Renders a mid-afternoon gradient (from the +/// water horizon up to the top of the screen) and sparse, wispy procedural +/// clouds that shrink toward the horizon. Standalone: owns its own shader and +/// a two-triangle full-screen quad, drawn before the sun and water. +class SkyModel { +public: + SkyModel(); + ~SkyModel(); + + void load(WebglContext* context); + void update(f32 dtSeconds); + void render(); + void unload(); + + /// Screen-space y (0..1) of the water horizon, where the gradient starts. + f32 horizon = 0.5f; + +private: + WebglContext* context = nullptr; + Shader shader = 0; + u32 vao = 0; + u32 vbo = 0; + f32 elapsedTime = 0.f; + + struct { + i32 position = -1; + } attributes; + + struct { + i32 time = -1; + i32 horizon = -1; + } uniforms; +}; + +#endif // SKY_MODEL_H diff --git a/themes/src/summer/summer_theme.cpp b/themes/src/summer/summer_theme.cpp index 6d2cfec..3b237e2 100644 --- a/themes/src/summer/summer_theme.cpp +++ b/themes/src/summer/summer_theme.cpp @@ -4,7 +4,6 @@ #include "../mathlib.h" #include "../shaders/sun_frag.h" #include "../shaders/sun_vert.h" -#include SummerTheme::SummerTheme(WebglContext* context) { @@ -21,20 +20,45 @@ void SummerTheme::load(WebglContext* context) { renderer.load(context, shader_sun_vert, shader_sun_frag); renderer.clearColor = Vector4(0, 181, 286, 255.f).toNormalizedColor(); sun.sectors = 180; - sun.radius = renderer.context->width / 4.f; + sun.radius = renderer.context->width / 6.f; sun.load(&renderer); + + sky.load(context); + water.load(context); + // Align the fake reflection with the sun disk: pointing up and into the + // scene (x = 0 keeps the glitter column centered under the sun). + water.setSun(Vector3(0.f, 0.4f, -1.f), Vector4(249, 215, 28, 255).toNormalizedColor()); } void SummerTheme::update(f32 dtSeconds) { sun.update(dtSeconds); + sky.update(dtSeconds); + water.update(dtSeconds); } void SummerTheme::render() { renderer.render(); + + // Gradient sky + clouds behind everything. It uses its own shader and + // disables depth writes, so restore the sun shader + projection and + // re-enable depth writes before drawing the sun. + sky.render(); + useShader(renderer.shader); + setShaderMat4(renderer.uniforms.projection, renderer.projection); + glDepthMask(GL_TRUE); + // sky.render() disabled blending; the sun's soft glowing edge needs it back, + // otherwise its semi-transparent rim writes opaque as a hard white border. + glEnable(GL_BLEND); + sun.render(&renderer); + // Drawn last: the opaque water paints over the lower half of the sun, + // hiding the part below the waterline while reflecting the part above. + water.render(); } void SummerTheme::unload() { + water.unload(); + sky.unload(); sun.unload(); } @@ -61,7 +85,7 @@ void Sun::load(Renderer2d* renderer) { } mesh.load(&vertices.data[0], vertices.numElements, &indices.data[0], indices.numElements, renderer); - mesh.model = Mat4x4().translateByVec2(Vector2(renderer->context->width / 2.f, renderer->context->height / 2.f)); + mesh.model = Mat4x4().translateByVec2(Vector2(renderer->context->width / 2.f, renderer->context->height * (3.f/ 4.f))); timeUniform = getShaderUniform(renderer->shader, "time"); diff --git a/themes/src/summer/summer_theme.h b/themes/src/summer/summer_theme.h index eb404fd..e048a7a 100644 --- a/themes/src/summer/summer_theme.h +++ b/themes/src/summer/summer_theme.h @@ -2,6 +2,8 @@ #include "../types.h" #include "../renderer_2d.h" #include "../theme.h" +#include "sky_model.h" +#include "water_model.h" #include struct Sun { @@ -22,6 +24,8 @@ public: SummerTheme(WebglContext*); ~SummerTheme(); Sun sun; + SkyModel sky; + WaterModel water{ Vector3(0.f, 0.f, -150.f), Vector3(600.f, 0.6f, 400.f) }; void load(WebglContext*); void update(f32 dtSeconds); void render(); diff --git a/themes/src/summer/water_model.cpp b/themes/src/summer/water_model.cpp new file mode 100644 index 0000000..3ad02bb --- /dev/null +++ b/themes/src/summer/water_model.cpp @@ -0,0 +1,127 @@ +#include "water_model.h" +#include "../webgl_context.h" +#include "../list.h" +#include "../shaders/water_vert.h" +#include "../shaders/water_frag.h" + +WaterModel::WaterModel(Vector3 inPosition, Vector3 inSize) + : position(inPosition), size(inSize) {} + +WaterModel::~WaterModel() { unload(); } + +void WaterModel::setSun(Vector3 direction, Vector4 color) { + sunDir = direction.normalize(); + sunColor = color.toVector3(); +} + +void WaterModel::load(WebglContext* inContext) { + context = inContext; + shader = loadShader(shader_water_vert, shader_water_frag); + useShader(shader); + + attributes.position = getShaderAttribute(shader, "position"); + uniforms.projection = getShaderUniform(shader, "projection"); + uniforms.view = getShaderUniform(shader, "view"); + uniforms.model = getShaderUniform(shader, "model"); + uniforms.time = getShaderUniform(shader, "time"); + uniforms.amplitude = getShaderUniform(shader, "amplitude"); + uniforms.cameraPos = getShaderUniform(shader, "cameraPos"); + uniforms.sunDir = getShaderUniform(shader, "sunDir"); + uniforms.sunColor = getShaderUniform(shader, "sunColor"); + + // Camera: forward almost horizontal with a slight downward pitch so the + // horizon sits near the vertical center and the plane fills the lower half. + cameraPos = Vector3(0.f, 5.f, 20.f); + projection = Mat4x4().getPerspectiveProjection( + 0.1f, 1000.f, 0.9f, + static_cast(context->width) / static_cast(context->height)); + view = Mat4x4().getLookAt(cameraPos, Vector3(0.f, 4.f, -10.f), Vector3(0.f, 1.f, 0.f)); + model = Mat4x4().translate(position); + + // Sensible defaults; the theme overrides these with the actual sun via setSun(). + sunDir = Vector3(0.f, 0.4f, -1.f).normalize(); + sunColor = Vector3(1.0f, 0.95f, 0.75f); + + // Build the grid in local XZ, centered on the origin, extents from size. + matte::List verts; + matte::List idx; + verts.allocate((RES + 1) * (RES + 1)); + idx.allocate(RES * RES * 6); + + for (i32 z = 0; z <= RES; z++) { + for (i32 x = 0; x <= RES; x++) { + f32 fx = ((static_cast(x) / RES) - 0.5f) * size.x; + f32 fz = ((static_cast(z) / RES) - 0.5f) * size.z; + verts.add(Vector2(fx, fz)); + } + } + + const i32 stride = RES + 1; + for (i32 z = 0; z < RES; z++) { + for (i32 x = 0; x < RES; x++) { + u32 tl = z * stride + x; + u32 tr = tl + 1; + u32 bl = (z + 1) * stride + x; + u32 br = bl + 1; + idx.add(tl); idx.add(bl); idx.add(tr); + idx.add(tr); idx.add(bl); idx.add(br); + } + } + numIndices = static_cast(idx.numElements); + + glGenVertexArrays(1, &vao); + glBindVertexArray(vao); + + glGenBuffers(1, &vbo); + glBindBuffer(GL_ARRAY_BUFFER, vbo); + glBufferData(GL_ARRAY_BUFFER, verts.numElements * sizeof(Vector2), &verts.data[0], GL_STATIC_DRAW); + + glGenBuffers(1, &ebo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, idx.numElements * sizeof(u32), &idx.data[0], GL_STATIC_DRAW); + + glEnableVertexAttribArray(attributes.position); + glVertexAttribPointer(attributes.position, 2, GL_FLOAT, GL_FALSE, sizeof(Vector2), (GLvoid*)0); + + glBindVertexArray(0); + verts.deallocate(); + idx.deallocate(); +} + +void WaterModel::update(f32 dtSeconds) { + elapsedTime += dtSeconds; +} + +void WaterModel::render() { + useShader(shader); + + // Opaque, depth-tested. Wipe the depth buffer first: the 2D sun was drawn at + // z=0 (nearest) with depth writes on, so without this the LEQUAL test would + // reject every perspective water fragment. Clearing depth (not color) lets the + // opaque water paint over the lower half of the sun while still self-occluding. + glDisable(GL_BLEND); + glEnable(GL_DEPTH_TEST); + glDepthFunc(GL_LEQUAL); + glDepthMask(GL_TRUE); + glClear(GL_DEPTH_BUFFER_BIT); + + setShaderMat4(uniforms.projection, projection); + setShaderMat4(uniforms.view, view); + setShaderMat4(uniforms.model, model); + setShaderFloat(uniforms.time, elapsedTime); + setShaderFloat(uniforms.amplitude, size.y); + glUniform3f(uniforms.cameraPos, cameraPos.x, cameraPos.y, cameraPos.z); + glUniform3f(uniforms.sunDir, sunDir.x, sunDir.y, sunDir.z); + glUniform3f(uniforms.sunColor, sunColor.x, sunColor.y, sunColor.z); + + glBindVertexArray(vao); + glDrawElements(GL_TRIANGLES, numIndices, GL_UNSIGNED_INT, 0); + glBindVertexArray(0); +} + +void WaterModel::unload() { + if (vao) { glDeleteVertexArrays(1, &vao); vao = 0; } + if (vbo) { glDeleteBuffers(1, &vbo); vbo = 0; } + if (ebo) { glDeleteBuffers(1, &ebo); ebo = 0; } + if (shader) { glDeleteProgram(shader); shader = 0; } +} diff --git a/themes/src/summer/water_model.h b/themes/src/summer/water_model.h new file mode 100644 index 0000000..a99f91d --- /dev/null +++ b/themes/src/summer/water_model.h @@ -0,0 +1,70 @@ +#ifndef WATER_MODEL_H +#define WATER_MODEL_H + +#include "../mathlib.h" +#include "../shader.h" +#include "../types.h" + +struct WebglContext; + +/// A water mesh is a 3D plane in the simulation world. +/// +/// The mesh reflects the sun above it and refracts whatever is +/// below its surface. It owns its own shader, geometry buffers, +/// and perspective camera, so it is fully standalone. +/// +/// The mesh can be rendered and updated. +class WaterModel { +public: + WaterModel(Vector3 position, Vector3 size); + ~WaterModel(); + + /// Compile the water shader, build the grid mesh, and set up the camera. + void load(WebglContext* context); + void update(f32 dtSeconds); + void render(); + void unload(); + + /// Direction pointing toward the sun (normalized internally) and its color, + /// used to fake the surface reflection. + void setSun(Vector3 direction, Vector4 color); + +private: + Vector3 position; + Vector3 size; + + WebglContext* context = nullptr; + Shader shader = 0; + u32 vao = 0; + u32 vbo = 0; + u32 ebo = 0; + i32 numIndices = 0; + f32 elapsedTime = 0.f; + + Mat4x4 projection; + Mat4x4 view; + Mat4x4 model; + + Vector3 cameraPos; + Vector3 sunDir; + Vector3 sunColor; + + static const i32 RES = 200; + + struct { + i32 position = -1; + } attributes; + + struct { + i32 projection = -1; + i32 view = -1; + i32 model = -1; + i32 time = -1; + i32 amplitude = -1; + i32 cameraPos = -1; + i32 sunDir = -1; + i32 sunColor = -1; + } uniforms; +}; + +#endif // WATER_MODEL_H diff --git a/themes/src/tools/shader.cjs b/themes/src/tools/shader.cjs new file mode 100644 index 0000000..10889db --- /dev/null +++ b/themes/src/tools/shader.cjs @@ -0,0 +1,41 @@ +/* + Responsible for generating .h files from GLSL files. + */ + + +const path = require('path'); +const fs = require('fs'); +const directory = path.join(__dirname, "..", "_shaders"); +const out_directory = path.join(__dirname, "..", "shaders"); + +if (!fs.existsSync(out_directory)){ + fs.mkdirSync(out_directory); +} + +const files = fs.readdirSync(directory); +files.forEach(file => { + const filePath = path.join(directory, file); + const text = String(fs.readFileSync(filePath)); + const splitText = text.split('\n'); + const splitName = file.split('.'); + const def = `SHADER_${splitName.join('_').toUpperCase()}`; + let header_result = `#ifndef ${def} \n`; + const filename = splitName.join('_'); + const header_name = filename + ".h"; + const cpp_name = filename + ".cpp"; + header_result += `#define ${def} \n`; + header_result += `extern const char* ${def.toLowerCase()};\n`; + header_result += '#endif\n'; + fs.writeFileSync(path.join(out_directory, header_name), header_result); + + let cpp_result = `#include "${header_name}"\n\n` + cpp_result += `const char* ${def.toLowerCase()} = ` + splitText.forEach((line, index) => { + cpp_result += "\"" + line + " \\n\""; + if (index == splitText.length - 1) + cpp_result += ";\n"; + else + cpp_result += "\n"; + }); + fs.writeFileSync(path.join(out_directory, cpp_name), cpp_result); +}); diff --git a/themes/src/tools/shader.js b/themes/src/tools/shader.js deleted file mode 100644 index 10889db..0000000 --- a/themes/src/tools/shader.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - Responsible for generating .h files from GLSL files. - */ - - -const path = require('path'); -const fs = require('fs'); -const directory = path.join(__dirname, "..", "_shaders"); -const out_directory = path.join(__dirname, "..", "shaders"); - -if (!fs.existsSync(out_directory)){ - fs.mkdirSync(out_directory); -} - -const files = fs.readdirSync(directory); -files.forEach(file => { - const filePath = path.join(directory, file); - const text = String(fs.readFileSync(filePath)); - const splitText = text.split('\n'); - const splitName = file.split('.'); - const def = `SHADER_${splitName.join('_').toUpperCase()}`; - let header_result = `#ifndef ${def} \n`; - const filename = splitName.join('_'); - const header_name = filename + ".h"; - const cpp_name = filename + ".cpp"; - header_result += `#define ${def} \n`; - header_result += `extern const char* ${def.toLowerCase()};\n`; - header_result += '#endif\n'; - fs.writeFileSync(path.join(out_directory, header_name), header_result); - - let cpp_result = `#include "${header_name}"\n\n` - cpp_result += `const char* ${def.toLowerCase()} = ` - splitText.forEach((line, index) => { - cpp_result += "\"" + line + " \\n\""; - if (index == splitText.length - 1) - cpp_result += ";\n"; - else - cpp_result += "\n"; - }); - fs.writeFileSync(path.join(out_directory, cpp_name), cpp_result); -}); -- cgit v1.2.1