summaryrefslogtreecommitdiff
path: root/themes/src
diff options
context:
space:
mode:
Diffstat (limited to 'themes/src')
-rw-r--r--themes/src/_shaders/sky.frag78
-rw-r--r--themes/src/_shaders/sky.vert10
-rw-r--r--themes/src/_shaders/water.frag63
-rw-r--r--themes/src/_shaders/water.vert63
-rw-r--r--themes/src/shaders/sky_frag.cpp81
-rw-r--r--themes/src/shaders/sky_frag.h4
-rw-r--r--themes/src/shaders/sky_vert.cpp13
-rw-r--r--themes/src/shaders/sky_vert.h4
-rw-r--r--themes/src/shaders/water_frag.cpp66
-rw-r--r--themes/src/shaders/water_frag.h4
-rw-r--r--themes/src/shaders/water_vert.cpp66
-rw-r--r--themes/src/shaders/water_vert.h4
-rw-r--r--themes/src/summer/sky_model.cpp64
-rw-r--r--themes/src/summer/sky_model.h43
-rw-r--r--themes/src/summer/summer_theme.cpp30
-rw-r--r--themes/src/summer/summer_theme.h4
-rw-r--r--themes/src/summer/water_model.cpp127
-rw-r--r--themes/src/summer/water_model.h70
-rw-r--r--themes/src/tools/shader.cjs (renamed from themes/src/tools/shader.js)0
19 files changed, 791 insertions, 3 deletions
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 <vector>
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 <vector>
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<f32>(context->width) / static_cast<f32>(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<Vector2> verts;
+ matte::List<u32> 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<f32>(x) / RES) - 0.5f) * size.x;
+ f32 fz = ((static_cast<f32>(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<i32>(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.js b/themes/src/tools/shader.cjs
index 10889db..10889db 100644
--- a/themes/src/tools/shader.js
+++ b/themes/src/tools/shader.cjs