summaryrefslogtreecommitdiff
path: root/themes/src
diff options
context:
space:
mode:
Diffstat (limited to 'themes/src')
-rw-r--r--themes/src/MainLoop.cpp31
-rw-r--r--themes/src/Renderer2d.h61
-rw-r--r--themes/src/WebglContext.cpp46
-rw-r--r--themes/src/_shaders/grass.frag11
-rw-r--r--themes/src/_shaders/grass.vert25
-rw-r--r--themes/src/_shaders/sky.frag78
-rw-r--r--themes/src/_shaders/sky.vert10
-rw-r--r--themes/src/_shaders/snowflake.frag73
-rw-r--r--themes/src/_shaders/snowflake.vert30
-rw-r--r--themes/src/_shaders/sun.frag40
-rw-r--r--themes/src/_shaders/sun.vert27
-rw-r--r--themes/src/_shaders/water.frag63
-rw-r--r--themes/src/_shaders/water.vert63
-rw-r--r--themes/src/autumn/autumn_theme.cpp (renamed from themes/src/autumn/AutumnTheme.cpp)2
-rw-r--r--themes/src/autumn/autumn_theme.hpp (renamed from themes/src/autumn/AutumnTheme.hpp)6
-rw-r--r--themes/src/autumn/leaf_particle_render.cpp (renamed from themes/src/autumn/LeafParticleRender.cpp)6
-rw-r--r--themes/src/autumn/leaf_particle_render.h (renamed from themes/src/autumn/LeafParticleRender.h)2
-rw-r--r--themes/src/autumn/tree_shape.cpp (renamed from themes/src/autumn/TreeShape.cpp)2
-rw-r--r--themes/src/autumn/tree_shape.h (renamed from themes/src/autumn/TreeShape.h)2
-rw-r--r--themes/src/list.h2
-rw-r--r--themes/src/logger.cpp (renamed from themes/src/Logger.cpp)2
-rw-r--r--themes/src/logger.h (renamed from themes/src/Logger.h)0
-rw-r--r--themes/src/main.cpp174
-rw-r--r--themes/src/main_loop.cpp37
-rw-r--r--themes/src/main_loop.h (renamed from themes/src/MainLoop.h)2
-rw-r--r--themes/src/renderer_2d.cpp (renamed from themes/src/Renderer2d.cpp)6
-rw-r--r--themes/src/renderer_2d.h59
-rw-r--r--themes/src/renderer_3d.cpp (renamed from themes/src/Renderer3d.cpp)26
-rw-r--r--themes/src/renderer_3d.h (renamed from themes/src/Renderer3d.h)0
-rw-r--r--themes/src/shader.cpp (renamed from themes/src/Shader.cpp)2
-rw-r--r--themes/src/shader.h (renamed from themes/src/Shader.h)0
-rw-r--r--themes/src/shaders/grass_frag.cpp14
-rw-r--r--themes/src/shaders/grass_frag.h4
-rw-r--r--themes/src/shaders/grass_vert.cpp28
-rw-r--r--themes/src/shaders/grass_vert.h4
-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/snowflake_frag.cpp76
-rw-r--r--themes/src/shaders/snowflake_frag.h4
-rw-r--r--themes/src/shaders/snowflake_vert.cpp33
-rw-r--r--themes/src/shaders/snowflake_vert.h4
-rw-r--r--themes/src/shaders/sun_frag.cpp40
-rw-r--r--themes/src/shaders/sun_vert.cpp27
-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/shapes_2d.cpp2
-rw-r--r--themes/src/shapes_2d.h2
-rw-r--r--themes/src/spring/GrassRenderer.cpp29
-rw-r--r--themes/src/spring/GrassRenderer.hpp33
-rw-r--r--themes/src/spring/SpringTheme.cpp208
-rw-r--r--themes/src/spring/SpringTheme.hpp45
-rw-r--r--themes/src/spring/grass_renderer.cpp138
-rw-r--r--themes/src/spring/grass_renderer.hpp63
-rw-r--r--themes/src/spring/spring_theme.cpp247
-rw-r--r--themes/src/spring/spring_theme.hpp51
-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.cpp (renamed from themes/src/summer/SummerTheme.cpp)42
-rw-r--r--themes/src/summer/summer_theme.h (renamed from themes/src/summer/SummerTheme.h)8
-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
-rw-r--r--themes/src/webgl_context.cpp46
-rw-r--r--themes/src/webgl_context.h (renamed from themes/src/WebglContext.h)0
-rw-r--r--themes/src/winter/Snowflake.cpp189
-rw-r--r--themes/src/winter/snowflake.cpp185
-rw-r--r--themes/src/winter/snowflake.h (renamed from themes/src/winter/Snowflake.h)35
-rw-r--r--themes/src/winter/windfield.cpp (renamed from themes/src/winter/Windfield.cpp)2
-rw-r--r--themes/src/winter/windfield.hpp (renamed from themes/src/winter/Windfield.hpp)0
-rw-r--r--themes/src/winter/winter_theme.cpp (renamed from themes/src/winter/WinterTheme.cpp)4
-rw-r--r--themes/src/winter/winter_theme.hpp (renamed from themes/src/winter/WinterTheme.hpp)4
75 files changed, 2213 insertions, 788 deletions
diff --git a/themes/src/MainLoop.cpp b/themes/src/MainLoop.cpp
deleted file mode 100644
index 09aa643..0000000
--- a/themes/src/MainLoop.cpp
+++ /dev/null
@@ -1,31 +0,0 @@
-#include "MainLoop.h"
-#include <cstdio>
-#include <cstdlib>
-
-EM_BOOL loop(double time, void* loop) {
- MainLoop* mainLoop = (MainLoop*) loop;
- if (!mainLoop->isRunning) {
- return false;
- }
-
- if (mainLoop->lastTime == 0) {
- mainLoop->lastTime = time;
- return true;
- }
-
- long deltaTime = time - mainLoop->lastTime;
- mainLoop->lastTime = time;
- mainLoop->elapsedTime += deltaTime;
- mainLoop->numFrames++;
- float deltaTimeSeconds = static_cast<float>(deltaTime) / 1000.f;
-
- if (mainLoop->elapsedTime >= 1000.0) {
- printf("FPS: %d\n", mainLoop->numFrames);
-
- mainLoop->elapsedTime = 0.0;
- mainLoop->numFrames = 0;
- }
-
- mainLoop->updateFunc(deltaTimeSeconds, NULL);
- return true;
-} \ No newline at end of file
diff --git a/themes/src/Renderer2d.h b/themes/src/Renderer2d.h
deleted file mode 100644
index 7432894..0000000
--- a/themes/src/Renderer2d.h
+++ /dev/null
@@ -1,61 +0,0 @@
-#pragma once
-
-#include "WebglContext.h"
-#include "types.h"
-#include "Shader.h"
-#include "mathlib.h"
-
-struct WebglContext;
-
-/// Responsible for rendering Mesh2Ds
-struct Renderer2d {
- WebglContext* context = NULL;
- Mat4x4 projection;
- u32 shader;
- Vector4 clearColor;
-
- struct {
- i32 position;
- i32 color;
-
- // TODO: vMatrix is not standard and does not belong here
- i32 vMatrix;
- } attributes;
-
- struct {
- i32 projection;
- i32 model;
- } uniforms;
-
- /// Load with the provided context and shader programs. If the shaders are NULL, the default
- /// shader is used
- void load(WebglContext* context, const char* vertexShader = NULL, const char* fragmentShader = NULL);
- void render();
- void unload();
- f32 get_width();
- f32 get_height();
-};
-
-struct Vertex2D {
- Vector2 position;
- Vector4 color;
- Mat4x4 vMatrix;
-};
-
-struct Mesh2D {
- u32 vao;
- u32 vbo;
- u32 ebo = 0;
- u32 numVertices = 0;
- u32 numIndices = 0;
- Mat4x4 model;
-
- void load(Vertex2D* vertices, u32 numVertices, Renderer2d* renderer);
- void load(Vertex2D* vertices,
- u32 numVertices,
- u32* indices,
- u32 numIndices,
- Renderer2d* renderer);
- void render(Renderer2d* renderer, GLenum drawType = GL_TRIANGLES);
- void unload();
-};
diff --git a/themes/src/WebglContext.cpp b/themes/src/WebglContext.cpp
deleted file mode 100644
index df49c2d..0000000
--- a/themes/src/WebglContext.cpp
+++ /dev/null
@@ -1,46 +0,0 @@
-#include "WebglContext.h"
-#include <cstdio>
-
-
-EM_BOOL onResize(int eventType, const EmscriptenUiEvent *uiEvent, void *userData) {
- WebglContext* context = (WebglContext*)userData;
-
- f64 inWidth, inHeight;
- emscripten_get_element_css_size(context->query, &inWidth, &inHeight);
-
- context->width = static_cast<f32>(inWidth);
- context->height = static_cast<f32>(inHeight);
-
- return true;
-}
-
-void WebglContext::init(const char* inQuery) {
- strcpy(query, inQuery);
- f64 inWidth, inHeight;
- emscripten_get_element_css_size(query, &inWidth, &inHeight);
- width = static_cast<f32>(inWidth);
- height = static_cast<f32>(inHeight);
- emscripten_set_canvas_element_size( query, width, height);
-
- EmscriptenWebGLContextAttributes attrs;
- emscripten_webgl_init_context_attributes(&attrs);
-
- attrs.enableExtensionsByDefault = 1;
- attrs.majorVersion = 3;
- attrs.minorVersion = 0;
-
- context = emscripten_webgl_create_context(query, &attrs);
- makeCurrentContext();
-
- glClearColor(0, 0, 0, 0.0f);
-
- emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false, onResize);
-};
-
-void WebglContext::makeCurrentContext() {
- emscripten_webgl_make_context_current(context);
-};
-
-void WebglContext::destroy() {
- emscripten_webgl_destroy_context(context);
-}
diff --git a/themes/src/_shaders/grass.frag b/themes/src/_shaders/grass.frag
new file mode 100644
index 0000000..a72f078
--- /dev/null
+++ b/themes/src/_shaders/grass.frag
@@ -0,0 +1,11 @@
+varying lowp vec2 vUV;
+
+void main() {
+ lowp float halfWidth = 0.5 * (1.0 - vUV.y);
+ lowp float distFromCenter = abs(vUV.x - 0.5);
+ if (distFromCenter > halfWidth) discard;
+
+ lowp vec3 baseColor = vec3(0.15, 0.45, 0.10);
+ lowp vec3 tipColor = vec3(0.40, 0.75, 0.20);
+ gl_FragColor = vec4(mix(baseColor, tipColor, vUV.y), 1.0);
+}
diff --git a/themes/src/_shaders/grass.vert b/themes/src/_shaders/grass.vert
new file mode 100644
index 0000000..0cf0285
--- /dev/null
+++ b/themes/src/_shaders/grass.vert
@@ -0,0 +1,25 @@
+attribute vec2 position; // Local quad vertex: x in [-0.5, 0.5], y in [0, 1]
+attribute vec3 instancePos; // Per-instance: world-space base of blade
+attribute float instancePhase; // Per-instance: random phase offset for sway
+attribute float instanceHeight; // Per-instance: height scale multiplier
+
+uniform mat4 projection;
+uniform mat4 view;
+uniform float time;
+uniform float bladeWidth;
+uniform float bladeHeight;
+uniform float swayAmount;
+
+varying lowp vec2 vUV;
+
+void main() {
+ vec3 cameraRight = vec3(view[0][0], view[1][0], view[2][0]);
+ float h = bladeHeight * instanceHeight;
+ float sway = sin(time * 1.5 + instancePhase) * swayAmount * position.y;
+ vec3 worldPos = instancePos
+ + cameraRight * (position.x + sway) * bladeWidth
+ + vec3(0.0, 1.0, 0.0) * position.y * h;
+
+ gl_Position = projection * view * vec4(worldPos, 1.0);
+ vUV = vec2(position.x + 0.5, position.y);
+}
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/snowflake.frag b/themes/src/_shaders/snowflake.frag
new file mode 100644
index 0000000..d887cf5
--- /dev/null
+++ b/themes/src/_shaders/snowflake.frag
@@ -0,0 +1,73 @@
+// Procedural star fragment shader
+varying lowp vec2 vUV;
+varying lowp float vSeed;
+varying lowp float vScale;
+
+const lowp float PI = 3.14159265359;
+
+// Simple hash function for deterministic randomness
+lowp float hash(lowp float n) {
+ return fract(sin(n) * 43758.5453123);
+}
+
+// Generate star pattern procedurally
+lowp float starPattern(lowp vec2 uv) {
+ lowp float dist = length(uv);
+ lowp float angle = atan(uv.y, uv.x);
+
+ // Number of star points (4 or 5)
+ lowp float numPoints = 4.0 + floor(hash(vSeed) * 2.0);
+
+ // Create sharp star with triangular points
+ lowp float angleStep = 2.0 * PI / numPoints;
+ lowp float currentAngle = mod(angle + PI / numPoints, angleStep) - angleStep * 0.5;
+
+ // Create triangular points - distance from center to edge varies linearly with angle
+ lowp float normalizedAngle = abs(currentAngle) / (angleStep * 0.5);
+
+ // Outer tip radius and inner valley radius
+ lowp float tipRadius = 0.5;
+ lowp float valleyRadius = 0.15;
+
+ // Linear interpolation creates sharp triangular points
+ lowp float rayEdge = mix(tipRadius, valleyRadius, normalizedAngle);
+
+ // Hard cutoff for sharp edges
+ lowp float star = step(dist, rayEdge);
+
+ return star;
+}
+
+void main() {
+ lowp float pattern = starPattern(vUV);
+
+ // Color variation based on seed - white and blue tints
+ lowp float colorVar = hash(vSeed * 3.0);
+ lowp vec3 starColor;
+
+ if (colorVar < 0.5) {
+ // Pure white
+ starColor = vec3(1.0, 1.0, 1.0);
+ } else if (colorVar < 0.75) {
+ // Light blue tint
+ starColor = vec3(0.9, 0.95, 1.0);
+ } else {
+ // Stronger blue tint
+ starColor = vec3(0.85, 0.92, 1.0);
+ }
+
+ // Scale alpha based on size - smaller stars are more transparent
+ // Normalize scale from range [16, 48] to [0, 1]
+ lowp float sizeRatio = (vScale - 16.0) / (48.0 - 16.0);
+ // Map to alpha range [0.3, 1.0] - smaller stars at 30% opacity, larger at 100%
+ lowp float alpha = mix(0.3, 1.0, sizeRatio);
+
+ lowp vec4 color = vec4(starColor, pattern * alpha);
+
+ // Discard fully transparent pixels for performance
+ if (color.a < 0.01) {
+ discard;
+ }
+
+ gl_FragColor = color;
+}
diff --git a/themes/src/_shaders/snowflake.vert b/themes/src/_shaders/snowflake.vert
new file mode 100644
index 0000000..7cbfb99
--- /dev/null
+++ b/themes/src/_shaders/snowflake.vert
@@ -0,0 +1,30 @@
+// Instanced snowflake vertex shader
+attribute vec2 position; // Base quad vertex position
+attribute vec2 instancePos; // Per-instance: snowflake center position
+attribute float instanceRot; // Per-instance: rotation angle
+attribute float instanceScale; // Per-instance: size scale
+attribute float instanceSeed; // Per-instance: random seed for variation
+
+uniform mat4 projection;
+uniform mat4 model;
+
+varying lowp vec2 vUV; // UV coordinates for fragment shader
+varying lowp float vSeed; // Pass seed to fragment shader
+varying lowp float vScale; // Pass scale to fragment shader
+
+void main() {
+ // Rotate and scale the base quad
+ float c = cos(instanceRot);
+ float s = sin(instanceRot);
+ mat2 rotation = mat2(c, s, -s, c);
+
+ vec2 rotatedPos = rotation * (position * instanceScale);
+ vec2 worldPos = instancePos + rotatedPos;
+
+ gl_Position = projection * model * vec4(worldPos, 0.0, 1.0);
+
+ // Pass UV in range [-1, 1] for procedural generation
+ vUV = position;
+ vSeed = instanceSeed;
+ vScale = instanceScale;
+}
diff --git a/themes/src/_shaders/sun.frag b/themes/src/_shaders/sun.frag
index 8463e06..2170b39 100644
--- a/themes/src/_shaders/sun.frag
+++ b/themes/src/_shaders/sun.frag
@@ -1,5 +1,43 @@
varying lowp vec4 VertexColor;
+varying lowp vec2 TexCoord;
+uniform lowp float time;
+
+// Simple noise function for edge distortion
+lowp float noise(lowp vec2 p) {
+ return sin(p.x * 10.0 + time) * cos(p.y * 10.0 + time * 0.5) * 0.5 + 0.5;
+}
void main() {
- gl_FragColor = VertexColor;
+ // TexCoord is now normalized: center is (0,0), edges are at distance ~1
+ lowp float dist = length(TexCoord);
+
+ // Add animated noise to the edge
+ lowp float angle = atan(TexCoord.y, TexCoord.x);
+ lowp float wave = sin(angle * 8.0 + time * 2.0) * 0.05 + sin(angle * 4.0 - time * 1.5) * 0.03;
+ lowp float noiseValue = noise(TexCoord + time * 0.1) * 0.02;
+
+ // Create soft edge using smoothstep - ensure fade reaches zero at the actual edge
+ lowp float innerEdge = 0.8;
+ lowp float outerEdge = 1.0;
+ lowp float alpha = 1.0 - smoothstep(innerEdge, outerEdge, dist);
+
+ // Apply wave distortion to the edge
+ alpha *= 1.0 - smoothstep(0.85 + wave + noiseValue * 2.0, 1.0, dist);
+
+ // Make edges more transparent but not too much
+ alpha = alpha * alpha;
+
+ // Add slight glow effect at the edge
+ lowp float glow = smoothstep(0.5, 0.8, dist) * (1.0 - smoothstep(0.8, 1.0, dist));
+
+ // Create orange gradient from center
+ lowp vec3 orangeColor = vec3(1.0, 0.5, 0.1);
+ lowp float centerGradient = smoothstep(0.6, 0.0, dist);
+ lowp vec3 baseColor = mix(VertexColor.rgb, orangeColor, centerGradient * 0.6);
+
+ // Mix in the glow with a brighter color
+ lowp vec3 glowColor = baseColor * 1.3;
+ lowp vec3 finalColor = mix(baseColor, glowColor, glow * 0.5);
+
+ gl_FragColor = vec4(finalColor, VertexColor.a * alpha);
}
diff --git a/themes/src/_shaders/sun.vert b/themes/src/_shaders/sun.vert
index 76150f0..5ed77d7 100644
--- a/themes/src/_shaders/sun.vert
+++ b/themes/src/_shaders/sun.vert
@@ -1,13 +1,22 @@
-attribute vec2 position;
-attribute vec4 color;
-attribute mat4 vMatrix;
-uniform mat4 projection;
-uniform mat4 model;
+attribute vec2 position;
+attribute vec4 color;
+attribute mat4 vMatrix;
+uniform mat4 projection;
+uniform mat4 model;
varying lowp vec4 VertexColor;
+varying lowp vec2 TexCoord;
-void main() {
- vec4 fragmentPosition = projection * model * vMatrix * vec4(position.x, position.y, 0, 1);
- gl_Position = fragmentPosition;
- VertexColor = color;
+void main() {
+ vec4 fragmentPosition = projection * model * vMatrix * vec4(position.x, position.y, 0, 1);
+ gl_Position = fragmentPosition;
+ VertexColor = color;
+ // Normalize the position - the center is at (0,0) and edge vertices are at distance 'radius'
+ // We want TexCoord to be in the range roughly [-1, 1] at the edges
+ lowp float maxDist = length(position);
+ if (maxDist > 0.1) {
+ TexCoord = position / maxDist;
+ } else {
+ TexCoord = vec2(0.0, 0.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/autumn/AutumnTheme.cpp b/themes/src/autumn/autumn_theme.cpp
index 4b7a2e2..d88b265 100644
--- a/themes/src/autumn/AutumnTheme.cpp
+++ b/themes/src/autumn/autumn_theme.cpp
@@ -1,4 +1,4 @@
-#include "AutumnTheme.hpp"
+#include "autumn_theme.hpp"
#include "../shapes_2d.h"
#include <memory>
diff --git a/themes/src/autumn/AutumnTheme.hpp b/themes/src/autumn/autumn_theme.hpp
index e3f5748..b61c0f3 100644
--- a/themes/src/autumn/AutumnTheme.hpp
+++ b/themes/src/autumn/autumn_theme.hpp
@@ -1,11 +1,11 @@
#ifndef AUTUMN_THEME_HPP
#define AUTUMN_THEME_HPP
-#include "TreeShape.h"
-#include "LeafParticleRender.h"
+#include "tree_shape.h"
+#include "leaf_particle_render.h"
#include "../types.h"
#include "../theme.h"
-#include "../Renderer2d.h"
+#include "../renderer_2d.h"
#include <memory>
#include <vector>
diff --git a/themes/src/autumn/LeafParticleRender.cpp b/themes/src/autumn/leaf_particle_render.cpp
index fee3df2..569bb2d 100644
--- a/themes/src/autumn/LeafParticleRender.cpp
+++ b/themes/src/autumn/leaf_particle_render.cpp
@@ -1,7 +1,7 @@
-#include "LeafParticleRender.h"
-#include "../Renderer2d.h"
+#include "leaf_particle_render.h"
+#include "../renderer_2d.h"
#include "../mathlib.h"
-#include "TreeShape.h"
+#include "tree_shape.h"
#include "../types.h"
#include <math.h>
diff --git a/themes/src/autumn/LeafParticleRender.h b/themes/src/autumn/leaf_particle_render.h
index f6efe1f..1209e1b 100644
--- a/themes/src/autumn/LeafParticleRender.h
+++ b/themes/src/autumn/leaf_particle_render.h
@@ -1,4 +1,4 @@
-#include "../Renderer2d.h"
+#include "../renderer_2d.h"
#include "../mathlib.h"
#include "../types.h"
diff --git a/themes/src/autumn/TreeShape.cpp b/themes/src/autumn/tree_shape.cpp
index 7c80929..622751b 100644
--- a/themes/src/autumn/TreeShape.cpp
+++ b/themes/src/autumn/tree_shape.cpp
@@ -1,4 +1,4 @@
-#include "TreeShape.h"
+#include "tree_shape.h"
#include "../mathlib.h"
#include <cstdio>
#include <cstdlib>
diff --git a/themes/src/autumn/TreeShape.h b/themes/src/autumn/tree_shape.h
index fc0d11e..0d18415 100644
--- a/themes/src/autumn/TreeShape.h
+++ b/themes/src/autumn/tree_shape.h
@@ -1,4 +1,4 @@
-#include "../Renderer2d.h"
+#include "../renderer_2d.h"
#include "../types.h"
#include "../mathlib.h"
diff --git a/themes/src/list.h b/themes/src/list.h
index 25b236a..9b6a719 100644
--- a/themes/src/list.h
+++ b/themes/src/list.h
@@ -1,7 +1,7 @@
#pragma once
#include <cstdlib>
#include <cstring>
-#include "Logger.h"
+#include "logger.h"
#define FOREACH(list) \
for (i32 idx = 0; idx < list.numElements; idx++) \
diff --git a/themes/src/Logger.cpp b/themes/src/logger.cpp
index 1068d88..bead282 100644
--- a/themes/src/Logger.cpp
+++ b/themes/src/logger.cpp
@@ -1,4 +1,4 @@
-#include "Logger.h"
+#include "logger.h"
#include <chrono>
#include <cstdarg>
#include <cstdio>
diff --git a/themes/src/Logger.h b/themes/src/logger.h
index 7596b6f..7596b6f 100644
--- a/themes/src/Logger.h
+++ b/themes/src/logger.h
diff --git a/themes/src/main.cpp b/themes/src/main.cpp
index 14227c9..ec7630b 100644
--- a/themes/src/main.cpp
+++ b/themes/src/main.cpp
@@ -1,112 +1,132 @@
-#include "WebglContext.h"
-#include "MainLoop.h"
-#include "Renderer2d.h"
+#include "autumn/autumn_theme.hpp"
+#include "main_loop.h"
#include "mathlib.h"
+#include "renderer_2d.h"
+#include "spring/spring_theme.hpp"
+#include "summer/summer_theme.h"
#include "theme.h"
#include "types.h"
-#include "summer/SummerTheme.h"
-#include "autumn/AutumnTheme.hpp"
-#include "spring/SpringTheme.hpp"
-#include "winter/WinterTheme.hpp"
+#include "webgl_context.h"
+#include "winter/winter_theme.hpp"
#include <cstdio>
#include <emscripten/fetch.h>
void load(ThemeType theme);
void unload();
-void update(f32 dtSeconds, void* userData);
-EM_BOOL selectNone(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData);
-EM_BOOL selectAutumn(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData);
-EM_BOOL selectWinter(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData);
-EM_BOOL selectSpring(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData);
-EM_BOOL selectSummer(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData);
+void update(f32 dtSeconds, void *userData);
+EM_BOOL selectNone(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData);
+EM_BOOL selectAutumn(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData);
+EM_BOOL selectWinter(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData);
+EM_BOOL selectSpring(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData);
+EM_BOOL selectSummer(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData);
WebglContext context;
MainLoop mainLoop;
ThemeType type;
-Theme* active_theme;
+Theme *active_theme;
int main() {
- context.init("#theme_canvas");
- emscripten_set_click_callback("#theme_button_default", NULL, false, selectNone);
- emscripten_set_click_callback("#theme_button_autumn", NULL, false, selectAutumn);
- emscripten_set_click_callback("#theme_button_winter", NULL, false, selectWinter);
- emscripten_set_click_callback("#theme_button_spring", NULL, false, selectSpring);
- emscripten_set_click_callback("#theme_button_summer", NULL, false, selectSummer);
-
- return 0;
+ context.init("#theme_canvas");
+ emscripten_set_click_callback("#theme_button_default", NULL, false,
+ selectNone);
+ emscripten_set_click_callback("#theme_button_autumn", NULL, false,
+ selectAutumn);
+ emscripten_set_click_callback("#theme_button_winter", NULL, false,
+ selectWinter);
+ emscripten_set_click_callback("#theme_button_spring", NULL, false,
+ selectSpring);
+ emscripten_set_click_callback("#theme_button_summer", NULL, false,
+ selectSummer);
+
+ return 0;
}
// -- Scene loading, updating, and unloading logic
void load(ThemeType theme) {
- if (type == theme) {
- printf("This theme is already active.\n");
- return;
- }
-
- unload(); // Try and unload before we load, so that we start fresh
-
- type = theme;
- mainLoop.run(update);
-
- switch (type) {
- case ThemeType::Autumn:
- active_theme = new AutumnTheme(&context);
- break;
- case ThemeType::Winter:
- active_theme = new WinterTheme(&context);
- break;
- case ThemeType::Spring:
- active_theme = new SpringTheme(&context);
- break;
- case ThemeType::Summer:
- active_theme = new SummerTheme(&context);
- break;
- default:
- break;
- }
+ if (type == theme) {
+ printf("This theme is already active.\n");
+ return;
+ }
+
+ unload(); // Try and unload before we load, so that we start fresh
+
+ type = theme;
+ mainLoop.run(update);
+
+ switch (type) {
+ case ThemeType::Autumn:
+ active_theme = new AutumnTheme(&context);
+ break;
+ case ThemeType::Winter:
+ active_theme = new WinterTheme(&context);
+ break;
+ case ThemeType::Spring:
+ active_theme = new SpringTheme(&context);
+ break;
+ case ThemeType::Summer:
+ active_theme = new SummerTheme(&context);
+ break;
+ default:
+ break;
+ }
}
-void update(f32 dtSeconds, void* userData) {
- active_theme->update(dtSeconds);
- active_theme->render();
+void update(f32 dtSeconds, void *userData) {
+ if (!active_theme)
+ return;
+ active_theme->update(dtSeconds);
+ active_theme->render();
}
void unload() {
- delete active_theme;
-
- type = ThemeType::Default;
- if (mainLoop.isRunning) {
- mainLoop.stop();
- }
+ delete active_theme;
+ active_theme = nullptr;
+
+ type = ThemeType::Default;
+ glClearColor(0, 0, 0, 0);
+ glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);
+ if (mainLoop.isRunning) {
+ mainLoop.stop();
+ }
}
// -- HTML5 callbacks
-EM_BOOL selectNone(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData) {
- printf("Default theme selected\n");
- unload();
- return true;
+EM_BOOL selectNone(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData) {
+ printf("Default theme selected\n");
+ unload();
+ return true;
}
-EM_BOOL selectAutumn(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData) {
- printf("Autumn theme selected\n");
- load(ThemeType::Autumn);
- return true;
+EM_BOOL selectAutumn(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData) {
+ printf("Autumn theme selected\n");
+ load(ThemeType::Autumn);
+ return true;
}
-EM_BOOL selectWinter(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData) {
- printf("Winter theme selected\n");
- load(ThemeType::Winter);
- return true;
+EM_BOOL selectWinter(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData) {
+ printf("Winter theme selected\n");
+ load(ThemeType::Winter);
+ return true;
}
-EM_BOOL selectSpring(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData) {
- printf("Spring theme selected\n");
- load(ThemeType::Spring);
- return true;
+EM_BOOL selectSpring(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData) {
+ printf("Spring theme selected\n");
+ load(ThemeType::Spring);
+ return true;
}
-EM_BOOL selectSummer(int eventType, const EmscriptenMouseEvent* mouseEvent, void* userData) {
- printf("Summer theme selected\n");
- load(ThemeType::Summer);
- return true;
+EM_BOOL selectSummer(int eventType, const EmscriptenMouseEvent *mouseEvent,
+ void *userData) {
+ printf("Summer theme selected\n");
+ load(ThemeType::Summer);
+ return true;
}
diff --git a/themes/src/main_loop.cpp b/themes/src/main_loop.cpp
new file mode 100644
index 0000000..743892e
--- /dev/null
+++ b/themes/src/main_loop.cpp
@@ -0,0 +1,37 @@
+#include "main_loop.h"
+#include <cstdio>
+#include <cstdlib>
+
+EM_BOOL loop(double time, void *loop) {
+ MainLoop *mainLoop = (MainLoop *)loop;
+ if (!mainLoop->isRunning) {
+ return false;
+ }
+
+ if (mainLoop->lastTime == 0) {
+ mainLoop->lastTime = time;
+ return true;
+ }
+
+ long deltaTime = time - mainLoop->lastTime;
+ mainLoop->lastTime = time;
+ mainLoop->elapsedTime += deltaTime;
+ mainLoop->numFrames++;
+ float deltaTimeSeconds = static_cast<float>(deltaTime) / 1000.f;
+
+ if (mainLoop->elapsedTime >= 1000.0) {
+ printf("FPS: %d\n", mainLoop->numFrames);
+
+ mainLoop->elapsedTime = 0.0;
+ mainLoop->numFrames = 0;
+ }
+
+ // Ignore any update with a greater than 0.1 change. We were
+ // probably tabbed away, so this is uninteresting to us.
+ if (deltaTimeSeconds > 0.1) {
+ return true;
+ }
+
+ mainLoop->updateFunc(deltaTimeSeconds, NULL);
+ return true;
+}
diff --git a/themes/src/MainLoop.h b/themes/src/main_loop.h
index 2573bb8..07520a2 100644
--- a/themes/src/MainLoop.h
+++ b/themes/src/main_loop.h
@@ -26,4 +26,4 @@ struct MainLoop {
void stop() {
isRunning = false;
}
-}; \ No newline at end of file
+};
diff --git a/themes/src/Renderer2d.cpp b/themes/src/renderer_2d.cpp
index f1d78e3..7200669 100644
--- a/themes/src/Renderer2d.cpp
+++ b/themes/src/renderer_2d.cpp
@@ -1,6 +1,6 @@
-#include "Renderer2d.h"
-#include "Shader.h"
-#include "WebglContext.h"
+#include "renderer_2d.h"
+#include "shader.h"
+#include "webgl_context.h"
#include "mathlib.h"
#include <cstdio>
#include "shaders/renderer2d_vert.h"
diff --git a/themes/src/renderer_2d.h b/themes/src/renderer_2d.h
new file mode 100644
index 0000000..16c5cbe
--- /dev/null
+++ b/themes/src/renderer_2d.h
@@ -0,0 +1,59 @@
+#pragma once
+
+#include "mathlib.h"
+#include "shader.h"
+#include "types.h"
+#include "webgl_context.h"
+
+struct WebglContext;
+
+/// Responsible for rendering Mesh2Ds
+struct Renderer2d {
+ WebglContext *context = NULL;
+ Mat4x4 projection;
+ u32 shader;
+ Vector4 clearColor;
+
+ struct {
+ i32 position;
+ i32 color;
+
+ // TODO: vMatrix is not standard and does not belong here
+ i32 vMatrix;
+ } attributes;
+
+ struct {
+ i32 projection;
+ i32 model;
+ } uniforms;
+
+ /// Load with the provided context and shader programs. If the shaders are
+ /// NULL, the default shader is used
+ void load(WebglContext *context, const char *vertexShader = NULL,
+ const char *fragmentShader = NULL);
+ void render();
+ void unload();
+ f32 get_width();
+ f32 get_height();
+};
+
+struct Vertex2D {
+ Vector2 position;
+ Vector4 color;
+ Mat4x4 vMatrix;
+};
+
+struct Mesh2D {
+ u32 vao;
+ u32 vbo;
+ u32 ebo = 0;
+ u32 numVertices = 0;
+ u32 numIndices = 0;
+ Mat4x4 model;
+
+ void load(Vertex2D *vertices, u32 numVertices, Renderer2d *renderer);
+ void load(Vertex2D *vertices, u32 numVertices, u32 *indices, u32 numIndices,
+ Renderer2d *renderer);
+ void render(Renderer2d *renderer, GLenum drawType = GL_TRIANGLES);
+ void unload();
+};
diff --git a/themes/src/Renderer3d.cpp b/themes/src/renderer_3d.cpp
index 00315de..5ba0fb6 100644
--- a/themes/src/Renderer3d.cpp
+++ b/themes/src/renderer_3d.cpp
@@ -1,9 +1,9 @@
-#include "Renderer3d.h"
-#include "Shader.h"
+#include "renderer_3d.h"
+#include "shader.h"
#include "list.h"
#include "mathlib.h"
-#include "WebglContext.h"
-#include "Logger.h"
+#include "webgl_context.h"
+#include "logger.h"
#include <cstdio>
// Note: In the 'transform' attribute, the transform.x is the scale,
@@ -81,7 +81,7 @@ struct LineItem {
};
inline i32 readPastSpaces(i32 i, const char* content) {
- while (content[i] == ' ') i++;
+ while (content[i] == ' ' || content[i] == '\t' || content[i] == '\r') i++;
return i;
}
@@ -93,7 +93,7 @@ inline i32 readPastLine(i32 i, const char* content) {
inline i32 readToken(i32 i, const char* content, char* output) {
i32 tidx = 0;
i = readPastSpaces(i, content);
- while (content[i] != ' ' && content[i] != '\n' && content[i] != '\0') {
+ while (content[i] != ' ' && content[i] != '\t' && content[i] != '\r' && content[i] != '\n' && content[i] != '\0') {
output[tidx] = content[i];
i++;
tidx++;
@@ -143,11 +143,16 @@ Mesh3d Mesh3d_fromObj(Renderer3d* renderer, const char* content, const i32 len)
i = readToken(i, content, buffer);
break;
case LineType_v: {
- while (content[i] != '\n' && content[i] != '\0') {
+ // Bound to 3 components: OBJ lines may carry trailing tokens (e.g. a
+ // stray "\r" from CRLF files, or a 4th vertex weight), and the union
+ // only holds 3 slots — writing past it corrupts the stack.
+ while (content[i] != '\n' && content[i] != '\r' && content[i] != '\0' && lt.idx < 3) {
i = readToken(i, content, buffer);
+ if (buffer[0] == '\0') break;
lt.v.vertices[lt.idx] = atof(buffer);
lt.idx++;
}
+ i = readPastLine(i, content);
float fColor = randomFloatBetween(0.8, 1);
result.vertices.add({
@@ -157,12 +162,15 @@ Mesh3d Mesh3d_fromObj(Renderer3d* renderer, const char* content, const i32 len)
break;
}
case LineType_f: {
- while (content[i] != '\n' && content[i] != '\0') {
+ // Same bound as vertices: only the first 3 indices fit the union.
+ while (content[i] != '\n' && content[i] != '\r' && content[i] != '\0' && lt.idx < 3) {
i = readToken(i, content, buffer);
+ if (buffer[0] == '\0') break;
lt.v.indices[lt.idx] = atoi(buffer);
lt.idx++;
}
-
+ i = readPastLine(i, content);
+
auto v1idx = lt.v.indices[0] - 1;
auto v2idx = lt.v.indices[1] - 1;
auto v3idx = lt.v.indices[2] - 1;
diff --git a/themes/src/Renderer3d.h b/themes/src/renderer_3d.h
index 5b2c8c8..5b2c8c8 100644
--- a/themes/src/Renderer3d.h
+++ b/themes/src/renderer_3d.h
diff --git a/themes/src/Shader.cpp b/themes/src/shader.cpp
index 5f2b00e..ed2cab5 100644
--- a/themes/src/Shader.cpp
+++ b/themes/src/shader.cpp
@@ -1,4 +1,4 @@
-#include "Shader.h"
+#include "shader.h"
#include <string>
GLuint loadIndividualShader(GLenum shaderType, const GLchar* cCode) {
diff --git a/themes/src/Shader.h b/themes/src/shader.h
index bc81764..bc81764 100644
--- a/themes/src/Shader.h
+++ b/themes/src/shader.h
diff --git a/themes/src/shaders/grass_frag.cpp b/themes/src/shaders/grass_frag.cpp
new file mode 100644
index 0000000..5a62cf2
--- /dev/null
+++ b/themes/src/shaders/grass_frag.cpp
@@ -0,0 +1,14 @@
+#include "grass_frag.h"
+
+const char* shader_grass_frag = "varying lowp vec2 vUV; \n"
+" \n"
+"void main() { \n"
+" lowp float halfWidth = 0.5 * (1.0 - vUV.y); \n"
+" lowp float distFromCenter = abs(vUV.x - 0.5); \n"
+" if (distFromCenter > halfWidth) discard; \n"
+" \n"
+" lowp vec3 baseColor = vec3(0.15, 0.45, 0.10); \n"
+" lowp vec3 tipColor = vec3(0.40, 0.75, 0.20); \n"
+" gl_FragColor = vec4(mix(baseColor, tipColor, vUV.y), 1.0); \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/grass_frag.h b/themes/src/shaders/grass_frag.h
new file mode 100644
index 0000000..16cc29a
--- /dev/null
+++ b/themes/src/shaders/grass_frag.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_GRASS_FRAG
+#define SHADER_GRASS_FRAG
+extern const char* shader_grass_frag;
+#endif
diff --git a/themes/src/shaders/grass_vert.cpp b/themes/src/shaders/grass_vert.cpp
new file mode 100644
index 0000000..c9d2955
--- /dev/null
+++ b/themes/src/shaders/grass_vert.cpp
@@ -0,0 +1,28 @@
+#include "grass_vert.h"
+
+const char* shader_grass_vert = "attribute vec2 position; // Local quad vertex: x in [-0.5, 0.5], y in [0, 1] \n"
+"attribute vec3 instancePos; // Per-instance: world-space base of blade \n"
+"attribute float instancePhase; // Per-instance: random phase offset for sway \n"
+"attribute float instanceHeight; // Per-instance: height scale multiplier \n"
+" \n"
+"uniform mat4 projection; \n"
+"uniform mat4 view; \n"
+"uniform float time; \n"
+"uniform float bladeWidth; \n"
+"uniform float bladeHeight; \n"
+"uniform float swayAmount; \n"
+" \n"
+"varying lowp vec2 vUV; \n"
+" \n"
+"void main() { \n"
+" vec3 cameraRight = vec3(view[0][0], view[1][0], view[2][0]); \n"
+" float h = bladeHeight * instanceHeight; \n"
+" float sway = sin(time * 1.5 + instancePhase) * swayAmount * position.y; \n"
+" vec3 worldPos = instancePos \n"
+" + cameraRight * (position.x + sway) * bladeWidth \n"
+" + vec3(0.0, 1.0, 0.0) * position.y * h; \n"
+" \n"
+" gl_Position = projection * view * vec4(worldPos, 1.0); \n"
+" vUV = vec2(position.x + 0.5, position.y); \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/grass_vert.h b/themes/src/shaders/grass_vert.h
new file mode 100644
index 0000000..7ab52b6
--- /dev/null
+++ b/themes/src/shaders/grass_vert.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_GRASS_VERT
+#define SHADER_GRASS_VERT
+extern const char* shader_grass_vert;
+#endif
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/snowflake_frag.cpp b/themes/src/shaders/snowflake_frag.cpp
new file mode 100644
index 0000000..52d535d
--- /dev/null
+++ b/themes/src/shaders/snowflake_frag.cpp
@@ -0,0 +1,76 @@
+#include "snowflake_frag.h"
+
+const char* shader_snowflake_frag = "// Procedural star fragment shader \n"
+"varying lowp vec2 vUV; \n"
+"varying lowp float vSeed; \n"
+"varying lowp float vScale; \n"
+" \n"
+"const lowp float PI = 3.14159265359; \n"
+" \n"
+"// Simple hash function for deterministic randomness \n"
+"lowp float hash(lowp float n) { \n"
+" return fract(sin(n) * 43758.5453123); \n"
+"} \n"
+" \n"
+"// Generate star pattern procedurally \n"
+"lowp float starPattern(lowp vec2 uv) { \n"
+" lowp float dist = length(uv); \n"
+" lowp float angle = atan(uv.y, uv.x); \n"
+" \n"
+" // Number of star points (4 or 5) \n"
+" lowp float numPoints = 4.0 + floor(hash(vSeed) * 2.0); \n"
+" \n"
+" // Create sharp star with triangular points \n"
+" lowp float angleStep = 2.0 * PI / numPoints; \n"
+" lowp float currentAngle = mod(angle + PI / numPoints, angleStep) - angleStep * 0.5; \n"
+" \n"
+" // Create triangular points - distance from center to edge varies linearly with angle \n"
+" lowp float normalizedAngle = abs(currentAngle) / (angleStep * 0.5); \n"
+" \n"
+" // Outer tip radius and inner valley radius \n"
+" lowp float tipRadius = 0.5; \n"
+" lowp float valleyRadius = 0.15; \n"
+" \n"
+" // Linear interpolation creates sharp triangular points \n"
+" lowp float rayEdge = mix(tipRadius, valleyRadius, normalizedAngle); \n"
+" \n"
+" // Hard cutoff for sharp edges \n"
+" lowp float star = step(dist, rayEdge); \n"
+" \n"
+" return star; \n"
+"} \n"
+" \n"
+"void main() { \n"
+" lowp float pattern = starPattern(vUV); \n"
+" \n"
+" // Color variation based on seed - white and blue tints \n"
+" lowp float colorVar = hash(vSeed * 3.0); \n"
+" lowp vec3 starColor; \n"
+" \n"
+" if (colorVar < 0.5) { \n"
+" // Pure white \n"
+" starColor = vec3(1.0, 1.0, 1.0); \n"
+" } else if (colorVar < 0.75) { \n"
+" // Light blue tint \n"
+" starColor = vec3(0.9, 0.95, 1.0); \n"
+" } else { \n"
+" // Stronger blue tint \n"
+" starColor = vec3(0.85, 0.92, 1.0); \n"
+" } \n"
+" \n"
+" // Scale alpha based on size - smaller stars are more transparent \n"
+" // Normalize scale from range [16, 48] to [0, 1] \n"
+" lowp float sizeRatio = (vScale - 16.0) / (48.0 - 16.0); \n"
+" // Map to alpha range [0.3, 1.0] - smaller stars at 30% opacity, larger at 100% \n"
+" lowp float alpha = mix(0.3, 1.0, sizeRatio); \n"
+" \n"
+" lowp vec4 color = vec4(starColor, pattern * alpha); \n"
+" \n"
+" // Discard fully transparent pixels for performance \n"
+" if (color.a < 0.01) { \n"
+" discard; \n"
+" } \n"
+" \n"
+" gl_FragColor = color; \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/snowflake_frag.h b/themes/src/shaders/snowflake_frag.h
new file mode 100644
index 0000000..b34328f
--- /dev/null
+++ b/themes/src/shaders/snowflake_frag.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_SNOWFLAKE_FRAG
+#define SHADER_SNOWFLAKE_FRAG
+extern const char* shader_snowflake_frag;
+#endif
diff --git a/themes/src/shaders/snowflake_vert.cpp b/themes/src/shaders/snowflake_vert.cpp
new file mode 100644
index 0000000..199009d
--- /dev/null
+++ b/themes/src/shaders/snowflake_vert.cpp
@@ -0,0 +1,33 @@
+#include "snowflake_vert.h"
+
+const char* shader_snowflake_vert = "// Instanced snowflake vertex shader \n"
+"attribute vec2 position; // Base quad vertex position \n"
+"attribute vec2 instancePos; // Per-instance: snowflake center position \n"
+"attribute float instanceRot; // Per-instance: rotation angle \n"
+"attribute float instanceScale; // Per-instance: size scale \n"
+"attribute float instanceSeed; // Per-instance: random seed for variation \n"
+" \n"
+"uniform mat4 projection; \n"
+"uniform mat4 model; \n"
+" \n"
+"varying lowp vec2 vUV; // UV coordinates for fragment shader \n"
+"varying lowp float vSeed; // Pass seed to fragment shader \n"
+"varying lowp float vScale; // Pass scale to fragment shader \n"
+" \n"
+"void main() { \n"
+" // Rotate and scale the base quad \n"
+" float c = cos(instanceRot); \n"
+" float s = sin(instanceRot); \n"
+" mat2 rotation = mat2(c, s, -s, c); \n"
+" \n"
+" vec2 rotatedPos = rotation * (position * instanceScale); \n"
+" vec2 worldPos = instancePos + rotatedPos; \n"
+" \n"
+" gl_Position = projection * model * vec4(worldPos, 0.0, 1.0); \n"
+" \n"
+" // Pass UV in range [-1, 1] for procedural generation \n"
+" vUV = position; \n"
+" vSeed = instanceSeed; \n"
+" vScale = instanceScale; \n"
+"} \n"
+" \n";
diff --git a/themes/src/shaders/snowflake_vert.h b/themes/src/shaders/snowflake_vert.h
new file mode 100644
index 0000000..36bf8b0
--- /dev/null
+++ b/themes/src/shaders/snowflake_vert.h
@@ -0,0 +1,4 @@
+#ifndef SHADER_SNOWFLAKE_VERT
+#define SHADER_SNOWFLAKE_VERT
+extern const char* shader_snowflake_vert;
+#endif
diff --git a/themes/src/shaders/sun_frag.cpp b/themes/src/shaders/sun_frag.cpp
index d1ea160..696b3b9 100644
--- a/themes/src/shaders/sun_frag.cpp
+++ b/themes/src/shaders/sun_frag.cpp
@@ -1,8 +1,46 @@
#include "sun_frag.h"
const char* shader_sun_frag = "varying lowp vec4 VertexColor; \n"
+"varying lowp vec2 TexCoord; \n"
+"uniform lowp float time; \n"
+" \n"
+"// Simple noise function for edge distortion \n"
+"lowp float noise(lowp vec2 p) { \n"
+" return sin(p.x * 10.0 + time) * cos(p.y * 10.0 + time * 0.5) * 0.5 + 0.5; \n"
+"} \n"
" \n"
"void main() { \n"
-" gl_FragColor = VertexColor; \n"
+" // TexCoord is now normalized: center is (0,0), edges are at distance ~1 \n"
+" lowp float dist = length(TexCoord); \n"
+" \n"
+" // Add animated noise to the edge \n"
+" lowp float angle = atan(TexCoord.y, TexCoord.x); \n"
+" lowp float wave = sin(angle * 8.0 + time * 2.0) * 0.05 + sin(angle * 4.0 - time * 1.5) * 0.03; \n"
+" lowp float noiseValue = noise(TexCoord + time * 0.1) * 0.02; \n"
+" \n"
+" // Create soft edge using smoothstep - ensure fade reaches zero at the actual edge \n"
+" lowp float innerEdge = 0.8; \n"
+" lowp float outerEdge = 1.0; \n"
+" lowp float alpha = 1.0 - smoothstep(innerEdge, outerEdge, dist); \n"
+" \n"
+" // Apply wave distortion to the edge \n"
+" alpha *= 1.0 - smoothstep(0.85 + wave + noiseValue * 2.0, 1.0, dist); \n"
+" \n"
+" // Make edges more transparent but not too much \n"
+" alpha = alpha * alpha; \n"
+" \n"
+" // Add slight glow effect at the edge \n"
+" lowp float glow = smoothstep(0.5, 0.8, dist) * (1.0 - smoothstep(0.8, 1.0, dist)); \n"
+" \n"
+" // Create orange gradient from center \n"
+" lowp vec3 orangeColor = vec3(1.0, 0.5, 0.1); \n"
+" lowp float centerGradient = smoothstep(0.6, 0.0, dist); \n"
+" lowp vec3 baseColor = mix(VertexColor.rgb, orangeColor, centerGradient * 0.6); \n"
+" \n"
+" // Mix in the glow with a brighter color \n"
+" lowp vec3 glowColor = baseColor * 1.3; \n"
+" lowp vec3 finalColor = mix(baseColor, glowColor, glow * 0.5); \n"
+" \n"
+" gl_FragColor = vec4(finalColor, VertexColor.a * alpha); \n"
"} \n"
" \n";
diff --git a/themes/src/shaders/sun_vert.cpp b/themes/src/shaders/sun_vert.cpp
index ca617c0..bacf3a6 100644
--- a/themes/src/shaders/sun_vert.cpp
+++ b/themes/src/shaders/sun_vert.cpp
@@ -1,16 +1,25 @@
#include "sun_vert.h"
const char* shader_sun_vert = " \n"
-"attribute vec2 position; \n"
-"attribute vec4 color; \n"
-"attribute mat4 vMatrix; \n"
-"uniform mat4 projection; \n"
-"uniform mat4 model; \n"
+"attribute vec2 position; \n"
+"attribute vec4 color; \n"
+"attribute mat4 vMatrix; \n"
+"uniform mat4 projection; \n"
+"uniform mat4 model; \n"
"varying lowp vec4 VertexColor; \n"
+"varying lowp vec2 TexCoord; \n"
" \n"
-"void main() { \n"
-" vec4 fragmentPosition = projection * model * vMatrix * vec4(position.x, position.y, 0, 1); \n"
-" gl_Position = fragmentPosition; \n"
-" VertexColor = color; \n"
+"void main() { \n"
+" vec4 fragmentPosition = projection * model * vMatrix * vec4(position.x, position.y, 0, 1); \n"
+" gl_Position = fragmentPosition; \n"
+" VertexColor = color; \n"
+" // Normalize the position - the center is at (0,0) and edge vertices are at distance 'radius' \n"
+" // We want TexCoord to be in the range roughly [-1, 1] at the edges \n"
+" lowp float maxDist = length(position); \n"
+" if (maxDist > 0.1) { \n"
+" TexCoord = position / maxDist; \n"
+" } else { \n"
+" TexCoord = vec2(0.0, 0.0); \n"
+" } \n"
"} \n"
" \n";
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/shapes_2d.cpp b/themes/src/shapes_2d.cpp
index d5a29ed..e00c521 100644
--- a/themes/src/shapes_2d.cpp
+++ b/themes/src/shapes_2d.cpp
@@ -1,5 +1,5 @@
#include "shapes_2d.h"
-#include "Renderer2d.h"
+#include "renderer_2d.h"
#include "mathlib.h"
#include "list.h"
diff --git a/themes/src/shapes_2d.h b/themes/src/shapes_2d.h
index 8e08504..325d525 100644
--- a/themes/src/shapes_2d.h
+++ b/themes/src/shapes_2d.h
@@ -2,7 +2,7 @@
#define SHAPES_2D
#include "mathlib.h"
-#include "Renderer2d.h"
+#include "renderer_2d.h"
#include "types.h"
class RectangularGradient
diff --git a/themes/src/spring/GrassRenderer.cpp b/themes/src/spring/GrassRenderer.cpp
deleted file mode 100644
index b69d111..0000000
--- a/themes/src/spring/GrassRenderer.cpp
+++ /dev/null
@@ -1,29 +0,0 @@
-#include "GrassRenderer.hpp"
-#include "Renderer3d.h"
-
-void GrassRenderer::load(GrassRendererLoadData params, Renderer3d* renderer) {
- const f32 COLUMN_INCREMENT = GRASS_BLADES_PER_COL / params.area.x;
- const f32 ROW_INCREMENT = GRASS_BLADES_PER_ROW / params.area.y;
- for (i32 r = 0; r < GRASS_BLADES_PER_ROW; r++) {
- i32 indexOffset = r * GRASS_BLADES_PER_ROW;
- f32 y = ROW_INCREMENT * r;
- for (i32 c = 0; c < GRASS_BLADES_PER_COL; c++) {
- f32 x = COLUMN_INCREMENT * c;
- i32 index = indexOffset + c;
- grassBlades[index].position = Vector3(x, y, 0);
- grassBlades[index].top_offset = Vector2(0, 0);
- }
- }
-}
-
-void GrassRenderer::update(f32 seconds) {
-
-}
-
-void GrassRenderer::render(Renderer3d* renderer) {
-
-}
-
-void GrassRenderer::unload() {
-
-}
diff --git a/themes/src/spring/GrassRenderer.hpp b/themes/src/spring/GrassRenderer.hpp
deleted file mode 100644
index 8c96724..0000000
--- a/themes/src/spring/GrassRenderer.hpp
+++ /dev/null
@@ -1,33 +0,0 @@
-#ifndef GRASS_RENDERER_HPP
-#define GRASS_RENDERER_HPP
-
-#include "Renderer3d.h"
-#include "mathlib.h"
-#include "types.h"
-
-const i32 GRASS_BLADES_PER_ROW = 24;
-const i32 GRASS_BLADES_PER_COL = 24;
-const i32 NUM_GRASS_BLADES = GRASS_BLADES_PER_ROW * GRASS_BLADES_PER_COL;
-
-struct GrassRendererLoadData {
- Vector2 origin = Vector2(0, 0);
- Vector2 area = Vector2(480, 480);
- f32 grassHeight = 12.f;
-};
-
-struct GrassUpdateData {
- Vector3 position;
- Vector2 top_offset;
-};
-
-struct GrassRenderer {
-
- GrassUpdateData grassBlades[NUM_GRASS_BLADES];
-
- void load(GrassRendererLoadData params, Renderer3d* renderer);
- void update(f32 dtSeconds);
- void render(Renderer3d* renderer);
- void unload();
-};
-
-#endif
diff --git a/themes/src/spring/SpringTheme.cpp b/themes/src/spring/SpringTheme.cpp
deleted file mode 100644
index e39c138..0000000
--- a/themes/src/spring/SpringTheme.cpp
+++ /dev/null
@@ -1,208 +0,0 @@
-#include "SpringTheme.hpp"
-#include "../Renderer3d.h"
-#include "../shader_fetcher.hpp"
-#include <cstdio>
-#include <emscripten/fetch.h>
-
-void onBunnySuccess(emscripten_fetch_t *fetch) {
- SpringTheme* springTheme = (SpringTheme*)fetch->userData;
- springTheme->state = SpringThemeState::LoadedBunny;
- printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes, fetch->url);
- const i32 len = fetch->numBytes;
- springTheme->bunnyMesh = Mesh3d_fromObj(&springTheme->renderer, fetch->data, len);
- // The data is now available at fetch->data[0] through fetch->data[fetch->numBytes-1];
- emscripten_fetch_close(fetch); // Free data associated with the fetch.
-}
-
-void onBunnyFail(emscripten_fetch_t *fetch) {
- printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url, fetch->status);
- emscripten_fetch_close(fetch); // Also free data on failure.
-}
-
-inline void fetch_bunny(SpringTheme* theme) {
- emscripten_fetch_attr_t attr;
- emscripten_fetch_attr_init(&attr);
- strcpy(attr.requestMethod, "GET");
- attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
- attr.onsuccess = onBunnySuccess;
- attr.onerror = onBunnyFail;
- auto* bunny_fetch = emscripten_fetch(&attr, "themes/resources/bunny.obj");
- bunny_fetch->userData = theme;
-}
-
-inline void on_shaders_loader(ShaderFetchResult* result) {
- SpringTheme* theme = (SpringTheme*)result->user_data;
- theme->renderer.load(theme->renderer.context, result->vertex.c_str(), result->fragment.c_str());
- theme->state = SpringThemeState::LoadedShader;
- fetch_bunny(theme);
-}
-
-SpringTheme::SpringTheme(WebglContext* context)
-{
- load(context);
-}
-
-SpringTheme::~SpringTheme()
-{
- unload();
-}
-
-void SpringTheme::load(WebglContext* context) {
- state = SpringThemeState::Loading;
- renderer.context = context;
- renderer.clearColor = Vector4(160, 231, 160, 255.f).toNormalizedColor();
-
- fetch_shader(
- {
- "themes/src/_shaders/renderer3d.vert",
- "themes/src/_shaders/renderer3d.frag"
- },
- on_shaders_loader,
- this
- );
-}
-
-inline Vector3 bunnyLerp(Vector3& start, Vector3& target, f32 t) {
- t = 3 * t *t - 2 * t * t * t;
- return start + ((target - start) * t);
-}
-
-inline f32 verticalHopLerp(f32 start, f32 target, f32 t) {
- f32 ogt = t;
- t = 3 * t *t - 2 * t * t * t;
- if (ogt >= 0.5f) t = 1.f - t;
- return start + ((target - start) * t);
-}
-
-inline f32 rotationLerp(f32 start, f32 target, f32 t) {
- return start + ((target - start) * t);
-}
-
-void SpringTheme::update(f32 dtSeconds) {
- switch (state) {
- case SpringThemeState::Loading: return;
- case SpringThemeState::LoadedShader: return;
- case SpringThemeState::LoadedBunny:
- state = SpringThemeState::Idle;
- stateTimer = 0.f;
- bunnyHopAnimationTimer = 0.f;
- break;
- case SpringThemeState::Idle: {
- bunnyHopAnimationTimer += dtSeconds;
- const f32 HOP_FREQUENCY = 6.f;
-
- if (bunnyHopAnimationTimer > stateTimer) {
- state = SpringThemeState::PreHop;
- f32 xDir = 1;
- f32 yDir = 1;
- if (bunnyTarget.x > 0) xDir = -1;
- if (bunnyTarget.z > 0) yDir = -1;
- bunnyTarget = bunnyPosition + Vector3(randomFloatBetween(0, xDir * 25), 0, randomFloatBetween(0, yDir * 25));
- auto direction = (bunnyTarget - bunnyPosition);
- auto distance = direction.length();
- direction = direction.normalize();
- numHops = ceil(distance / HOP_FREQUENCY);
- hopCount = 0;
-
- targetRotation = PI - atan2(direction.y, direction.x);
- stateTimer = ((bunnyTarget - bunnyPosition).length() / bunnySpeed) / numHops;
- bunnyHopAnimationTimer = 0.f;
- hopIncrement = (bunnyTarget - bunnyPosition) / numHops;
- }
- break;
- }
- case SpringThemeState::PreHop: {
- const f32 ROTATION_TIME = 0.5f;
- bunnyHopAnimationTimer += dtSeconds;
- f32 current = bunnyRotation + (targetRotation - bunnyRotation) * (bunnyHopAnimationTimer / ROTATION_TIME);
- bunnyMesh.model = Mat4x4().rotate(0, current, 0).translate(bunnyPosition);
-
- if (bunnyHopAnimationTimer > ROTATION_TIME) {
- bunnyRotation = targetRotation;
- bunnyHopAnimationTimer = 0;
- state = SpringThemeState::Hopping;
- }
- break;
- }
- case SpringThemeState::Hopping: {
- bunnyHopAnimationTimer += dtSeconds;
- f32 t = bunnyHopAnimationTimer / stateTimer;
-
- Vector3 nextPosition = bunnyPosition + hopIncrement;
- auto renderPos = bunnyLerp(bunnyPosition, nextPosition, t);
- if ((renderPos - nextPosition).length() < 0.01f) {
- hopCount += 1;
- bunnyHopAnimationTimer = 0.f;
- bunnyPosition = nextPosition;
- }
-
- renderPos.y = verticalHopLerp(0.f, 4.f, t);
-
- const f32 RMAX = PI / 16.f;
- f32 zRotation = 0;
- f32 start = 0.f;
- f32 end = PI / 8.f;
- f32 startTime = 0.f;
- f32 endTime = 0.f;
- bool disableRot = false;
-
- if (t >= 0.9f) {
- disableRot = true;
- }
- else if (t >= 0.7f) {
- start = -RMAX;
- end = 0.f;
- startTime = 0.7f;
- endTime = 0.9f;
- }
- else if (t >= 0.50f) {
- start = 0.f;
- end = -RMAX;
- startTime = 0.50f;
- endTime = 0.70f;
- }
- else if (t >= 0.40f) {
- disableRot = true;
- }
- else if (t >= 0.20f) {
- start = RMAX;
- end = 0.f;
- startTime = 0.20f;
- endTime = 0.40f;
- }
- else {
- start = 0.f;
- end = RMAX;
- startTime = 0.f;
- endTime = 0.20f;
- }
-
-
- if (!disableRot) {
- f32 totalTime = endTime - startTime;
- zRotation = rotationLerp(start, end, (totalTime - (endTime - t)) / totalTime);
- }
-
- bunnyMesh.model = Mat4x4().getZRotationMatrix(zRotation).rotate(0, bunnyRotation, 0).translate(renderPos);
- if (hopCount == numHops) {
- bunnyPosition = bunnyTarget;
- bunnyHopAnimationTimer = 0.f;
- state = SpringThemeState::Idle;
- stateTimer = randomFloatBetween(0.5f, 1.f);
- }
- break;
- }
- }
-}
-
-void SpringTheme::render() {
- renderer.render();
- if (state != SpringThemeState::Loading) {
- bunnyMesh.render(&renderer);
- }
-}
-
-void SpringTheme::unload() {
- renderer.unload();
- bunnyMesh.unload();
-}
diff --git a/themes/src/spring/SpringTheme.hpp b/themes/src/spring/SpringTheme.hpp
deleted file mode 100644
index 64f9cb5..0000000
--- a/themes/src/spring/SpringTheme.hpp
+++ /dev/null
@@ -1,45 +0,0 @@
-#ifndef SPRING_THEME_HPP
-#define SPRING_THEME_HPP
-
-#include "../mathlib.h"
-#include "../types.h"
-#include "../Renderer3d.h"
-#include "../theme.h"
-
-
-enum class SpringThemeState {
- Loading = 0,
- LoadedShader,
- LoadedBunny,
- PreHop,
- Hopping,
- Idle
-};
-
-class SpringTheme : public Theme {
-public:
- SpringTheme(WebglContext*);
- ~SpringTheme();
- Renderer3d renderer;
- SpringThemeState state;
- f32 bunnySpeed = 5.f;
- Vector3 bunnyPosition = Vector3(0, 0, 0);
- Vector3 bunnyTarget = Vector3(0, 0, 0);
- Vector3 hopIncrement = Vector3(0, 0, 0);
-
- f32 numHops = 0;
- f32 hopCount = 0;
- f32 bunnyHopAnimationTimer = 0.f;
- f32 stateTimer = 0.f;
- f32 bunnyRotation = 0.f;
- f32 targetRotation = 0.f;
-
- Mesh3d bunnyMesh;
-
- void load(WebglContext*);
- void update(f32 dtSeconds);
- void render();
- void unload();
-};
-
-#endif
diff --git a/themes/src/spring/grass_renderer.cpp b/themes/src/spring/grass_renderer.cpp
new file mode 100644
index 0000000..e4a210c
--- /dev/null
+++ b/themes/src/spring/grass_renderer.cpp
@@ -0,0 +1,138 @@
+#include "grass_renderer.hpp"
+#include "../renderer_3d.h"
+#include "../shader.h"
+#include "../shaders/grass_frag.h"
+#include "../shaders/grass_vert.h"
+#include "mathlib.h"
+#include <cmath>
+#include <cstddef>
+
+void GrassRenderer::load(GrassRendererLoadData params, Renderer3d *renderer) {
+ bladeHeight = params.grassHeight;
+
+ // Place blades randomly within a circle. Using r = R*sqrt(u) with a
+ // uniform u in [0,1] gives uniform areal density (no center clustering).
+ const f32 radius = fminf(params.area.x, params.area.y) * 0.5f;
+ for (i32 i = 0; i < NUM_GRASS_BLADES; i++) {
+ f32 r = radius * sqrtf(randomFloatBetween(0.f, 1.f));
+ f32 theta = randomFloatBetween(0.f, 2.f * PI);
+ f32 x = params.origin.x + r * cosf(theta);
+ f32 z = params.origin.y + r * sinf(theta);
+ grassBlades[i].position = Vector3(x, 0, z);
+ grassBlades[i].top_offset =
+ Vector2(randomFloatBetween(0.f, 2.f * PI), // sway phase
+ randomFloatBetween(0.5f, 1.5f) // height scale
+ );
+ }
+
+ // Compile grass shader
+ shader = loadShader(shader_grass_vert, shader_grass_frag);
+ useShader(shader);
+
+ // Attribute locations
+ attributes.position = getShaderAttribute(shader, "position");
+ attributes.instancePos = getShaderAttribute(shader, "instancePos");
+ attributes.instancePhase = getShaderAttribute(shader, "instancePhase");
+ attributes.instanceHeight = getShaderAttribute(shader, "instanceHeight");
+
+ // Uniform locations
+ uniforms.projection = getShaderUniform(shader, "projection");
+ uniforms.view = getShaderUniform(shader, "view");
+ uniforms.time = getShaderUniform(shader, "time");
+ uniforms.bladeWidth = getShaderUniform(shader, "bladeWidth");
+ uniforms.bladeHeight = getShaderUniform(shader, "bladeHeight");
+ uniforms.swayAmount = getShaderUniform(shader, "swayAmount");
+
+ // Base quad: two triangles forming a unit quad
+ // x in [-0.5, 0.5], y in [0, 1]
+ Vector2 quadVertices[] = {Vector2(-0.5f, 0.0f), Vector2(0.5f, 0.0f),
+ Vector2(0.5f, 1.0f), Vector2(-0.5f, 0.0f),
+ Vector2(0.5f, 1.0f), Vector2(-0.5f, 1.0f)};
+
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+
+ // Static quad VBO
+ glGenBuffers(1, &quadVbo);
+ glBindBuffer(GL_ARRAY_BUFFER, quadVbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices,
+ GL_STATIC_DRAW);
+ glEnableVertexAttribArray(attributes.position);
+ glVertexAttribPointer(attributes.position, 2, GL_FLOAT, GL_FALSE,
+ sizeof(Vector2), (GLvoid *)0);
+
+ // Dynamic instance VBO
+ glGenBuffers(1, &instanceVbo);
+ glBindBuffer(GL_ARRAY_BUFFER, instanceVbo);
+ glBufferData(GL_ARRAY_BUFFER, NUM_GRASS_BLADES * sizeof(GrassInstanceData),
+ NULL, GL_DYNAMIC_DRAW);
+
+ // instancePos: vec3 (x, y, z) at offset 0
+ glEnableVertexAttribArray(attributes.instancePos);
+ glVertexAttribPointer(attributes.instancePos, 3, GL_FLOAT, GL_FALSE,
+ sizeof(GrassInstanceData),
+ (GLvoid *)offsetof(GrassInstanceData, x));
+ glVertexAttribDivisor(attributes.instancePos, 1);
+
+ // instancePhase: float at offset 12 (after 3 floats)
+ glEnableVertexAttribArray(attributes.instancePhase);
+ glVertexAttribPointer(attributes.instancePhase, 1, GL_FLOAT, GL_FALSE,
+ sizeof(GrassInstanceData),
+ (GLvoid *)offsetof(GrassInstanceData, phaseOffset));
+ glVertexAttribDivisor(attributes.instancePhase, 1);
+
+ // instanceHeight: float at offset 16 (after phaseOffset)
+ glEnableVertexAttribArray(attributes.instanceHeight);
+ glVertexAttribPointer(attributes.instanceHeight, 1, GL_FLOAT, GL_FALSE,
+ sizeof(GrassInstanceData),
+ (GLvoid *)offsetof(GrassInstanceData, heightScale));
+ glVertexAttribDivisor(attributes.instanceHeight, 1);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+}
+
+void GrassRenderer::update(f32 dtSeconds) { time += dtSeconds; }
+
+void GrassRenderer::render(Renderer3d *renderer) {
+ useShader(shader);
+ setShaderMat4(uniforms.projection, renderer->projection);
+ setShaderMat4(uniforms.view, renderer->view);
+ setShaderFloat(uniforms.time, time);
+ setShaderFloat(uniforms.bladeWidth, bladeWidth);
+ setShaderFloat(uniforms.bladeHeight, bladeHeight);
+ setShaderFloat(uniforms.swayAmount, swayAmount);
+
+ // Build and upload instance data
+ GrassInstanceData instanceData[NUM_GRASS_BLADES];
+ for (i32 i = 0; i < NUM_GRASS_BLADES; i++) {
+ instanceData[i].x = grassBlades[i].position.x;
+ instanceData[i].y = grassBlades[i].position.y;
+ instanceData[i].z = grassBlades[i].position.z;
+ instanceData[i].phaseOffset = grassBlades[i].top_offset.x;
+ instanceData[i].heightScale = grassBlades[i].top_offset.y;
+ }
+
+ glBindBuffer(GL_ARRAY_BUFFER, instanceVbo);
+ glBufferSubData(GL_ARRAY_BUFFER, 0,
+ NUM_GRASS_BLADES * sizeof(GrassInstanceData), instanceData);
+
+ glBindVertexArray(vao);
+ glDrawArraysInstanced(GL_TRIANGLES, 0, 6, NUM_GRASS_BLADES);
+ glBindVertexArray(0);
+}
+
+void GrassRenderer::unload() {
+ if (vao)
+ glDeleteVertexArrays(1, &vao);
+ if (quadVbo)
+ glDeleteBuffers(1, &quadVbo);
+ if (instanceVbo)
+ glDeleteBuffers(1, &instanceVbo);
+ if (shader)
+ glDeleteProgram(shader);
+ vao = 0;
+ quadVbo = 0;
+ instanceVbo = 0;
+ shader = 0;
+}
diff --git a/themes/src/spring/grass_renderer.hpp b/themes/src/spring/grass_renderer.hpp
new file mode 100644
index 0000000..14ef067
--- /dev/null
+++ b/themes/src/spring/grass_renderer.hpp
@@ -0,0 +1,63 @@
+#ifndef GRASS_RENDERER_HPP
+#define GRASS_RENDERER_HPP
+
+#include "../renderer_3d.h"
+#include "mathlib.h"
+#include "types.h"
+
+const i32 GRASS_BLADES_PER_ROW = 48;
+const i32 GRASS_BLADES_PER_COL = 48;
+const i32 NUM_GRASS_BLADES = GRASS_BLADES_PER_ROW * GRASS_BLADES_PER_COL;
+
+struct GrassRendererLoadData {
+ Vector2 origin = Vector2(0, 0);
+ Vector2 area = Vector2(480, 480);
+ f32 grassHeight = 4.f;
+};
+
+struct GrassUpdateData {
+ Vector3 position;
+ Vector2 top_offset; // top_offset.x stores per-blade sway phase offset
+};
+
+struct GrassInstanceData {
+ float x, y, z;
+ float phaseOffset;
+ float heightScale;
+};
+
+struct GrassRenderer {
+ GrassUpdateData grassBlades[NUM_GRASS_BLADES];
+
+ u32 vao = 0;
+ u32 quadVbo = 0;
+ u32 instanceVbo = 0;
+ u32 shader = 0;
+ f32 time = 0.f;
+ f32 bladeWidth = 1.5f;
+ f32 bladeHeight = 6.f;
+ f32 swayAmount = 0.3f;
+
+ struct {
+ i32 position;
+ i32 instancePos;
+ i32 instancePhase;
+ i32 instanceHeight;
+ } attributes;
+
+ struct {
+ i32 projection;
+ i32 view;
+ i32 time;
+ i32 bladeWidth;
+ i32 bladeHeight;
+ i32 swayAmount;
+ } uniforms;
+
+ void load(GrassRendererLoadData params, Renderer3d *renderer);
+ void update(f32 dtSeconds);
+ void render(Renderer3d *renderer);
+ void unload();
+};
+
+#endif
diff --git a/themes/src/spring/spring_theme.cpp b/themes/src/spring/spring_theme.cpp
new file mode 100644
index 0000000..4b62795
--- /dev/null
+++ b/themes/src/spring/spring_theme.cpp
@@ -0,0 +1,247 @@
+#include "spring_theme.hpp"
+#include "../renderer_3d.h"
+#include "../shader.h"
+#include "../shader_fetcher.hpp"
+#include "../shapes_2d.h"
+#include <cstdio>
+#include <emscripten/fetch.h>
+
+void onBunnySuccess(emscripten_fetch_t *fetch) {
+ SpringTheme *springTheme = (SpringTheme *)fetch->userData;
+ springTheme->state = SpringThemeState::LoadedBunny;
+ printf("Finished downloading %llu bytes from URL %s.\n", fetch->numBytes,
+ fetch->url);
+ const i32 len = fetch->numBytes;
+ springTheme->bunnyMesh =
+ Mesh3d_fromObj(&springTheme->renderer, fetch->data, len);
+ // The data is now available at fetch->data[0] through
+ // fetch->data[fetch->numBytes-1];
+ emscripten_fetch_close(fetch); // Free data associated with the fetch.
+}
+
+void onBunnyFail(emscripten_fetch_t *fetch) {
+ printf("Downloading %s failed, HTTP failure status code: %d.\n", fetch->url,
+ fetch->status);
+ emscripten_fetch_close(fetch); // Also free data on failure.
+}
+
+inline void fetch_bunny(SpringTheme *theme) {
+ emscripten_fetch_attr_t attr;
+ emscripten_fetch_attr_init(&attr);
+ strcpy(attr.requestMethod, "GET");
+ attr.attributes = EMSCRIPTEN_FETCH_LOAD_TO_MEMORY;
+ attr.onsuccess = onBunnySuccess;
+ attr.onerror = onBunnyFail;
+ auto *bunny_fetch = emscripten_fetch(&attr, "resources/bunny.obj");
+ bunny_fetch->userData = theme;
+}
+
+inline void on_shaders_loader(ShaderFetchResult *result) {
+ SpringTheme *theme = (SpringTheme *)result->user_data;
+ theme->renderer.load(theme->renderer.context, result->vertex.c_str(),
+ result->fragment.c_str());
+ theme->state = SpringThemeState::LoadedShader;
+ fetch_bunny(theme);
+}
+
+SpringTheme::SpringTheme(WebglContext *context) { load(context); }
+
+SpringTheme::~SpringTheme() { unload(); }
+
+void SpringTheme::load(WebglContext *context) {
+ state = SpringThemeState::Loading;
+ renderer.context = context;
+ renderer.clearColor = Vector4(174, 216, 230, 255.f).toNormalizedColor();
+
+ renderer2d.load(context);
+ background = new RectangularGradient(
+ renderer2d, Vector4(174, 216, 230, 255).toNormalizedColor(),
+ Vector4(144, 238, 144, 255).toNormalizedColor(), renderer2d.get_width(),
+ renderer2d.get_height(), {0, 0});
+
+ grassRenderer.load({Vector2(0, -20), Vector2(96, 96), 3.f}, &renderer);
+
+ fetch_shader({"themes/src/_shaders/renderer3d.vert",
+ "themes/src/_shaders/renderer3d.frag"},
+ on_shaders_loader, this);
+}
+
+inline Vector3 bunnyLerp(Vector3 &start, Vector3 &target, f32 t) {
+ t = 3 * t * t - 2 * t * t * t;
+ return start + ((target - start) * t);
+}
+
+inline f32 verticalHopLerp(f32 start, f32 target, f32 t) {
+ f32 ogt = t;
+ t = 3 * t * t - 2 * t * t * t;
+ if (ogt >= 0.5f)
+ t = 1.f - t;
+ return start + ((target - start) * t);
+}
+
+inline f32 rotationLerp(f32 start, f32 target, f32 t) {
+ return start + ((target - start) * t);
+}
+
+void SpringTheme::update(f32 dtSeconds) {
+ if (state != SpringThemeState::Loading) {
+ grassRenderer.update(dtSeconds);
+ }
+ switch (state) {
+ case SpringThemeState::Loading:
+ return;
+ case SpringThemeState::LoadedShader:
+ return;
+ case SpringThemeState::LoadedBunny:
+ state = SpringThemeState::Idle;
+ stateTimer = 0.f;
+ bunnyHopAnimationTimer = 0.f;
+ break;
+ case SpringThemeState::Idle: {
+ bunnyHopAnimationTimer += dtSeconds;
+ const f32 HOP_FREQUENCY = 6.f;
+
+ if (bunnyHopAnimationTimer > stateTimer) {
+ state = SpringThemeState::PreHop;
+ f32 xDir = 1;
+ f32 yDir = 1;
+ if (bunnyTarget.x > 0)
+ xDir = -1;
+ if (bunnyTarget.z > 0)
+ yDir = -1;
+ bunnyTarget = bunnyPosition + Vector3(randomFloatBetween(0, xDir * 25), 0,
+ randomFloatBetween(0, yDir * 25));
+ // Clamp bunnyTarget to within the grass circle (origin=(0,-20), radius=48)
+ const Vector3 grassCenter(0, 0, -20);
+ const f32 grassRadius = 48.f;
+ Vector3 toTarget = bunnyTarget - grassCenter;
+ toTarget.y = 0;
+ if (toTarget.length() > grassRadius) {
+ toTarget = toTarget.normalize() * grassRadius;
+ bunnyTarget = Vector3(grassCenter.x + toTarget.x, 0, grassCenter.z + toTarget.z);
+ }
+ auto direction = (bunnyTarget - bunnyPosition);
+ auto distance = direction.length();
+ direction = direction.normalize();
+ numHops = ceil(distance / HOP_FREQUENCY);
+ hopCount = 0;
+
+ targetRotation = PI - atan2(direction.y, direction.x);
+ stateTimer =
+ ((bunnyTarget - bunnyPosition).length() / bunnySpeed) / numHops;
+ bunnyHopAnimationTimer = 0.f;
+ hopIncrement = (bunnyTarget - bunnyPosition) / numHops;
+ }
+ break;
+ }
+ case SpringThemeState::PreHop: {
+ const f32 ROTATION_TIME = 0.5f;
+ bunnyHopAnimationTimer += dtSeconds;
+ f32 current = bunnyRotation + (targetRotation - bunnyRotation) *
+ (bunnyHopAnimationTimer / ROTATION_TIME);
+ bunnyMesh.model = Mat4x4().rotate(0, current, 0).translate(bunnyPosition);
+
+ if (bunnyHopAnimationTimer > ROTATION_TIME) {
+ bunnyRotation = targetRotation;
+ bunnyHopAnimationTimer = 0;
+ state = SpringThemeState::Hopping;
+ }
+ break;
+ }
+ case SpringThemeState::Hopping: {
+ bunnyHopAnimationTimer += dtSeconds;
+ f32 t = bunnyHopAnimationTimer / stateTimer;
+
+ Vector3 nextPosition = bunnyPosition + hopIncrement;
+ auto renderPos = bunnyLerp(bunnyPosition, nextPosition, t);
+ if ((renderPos - nextPosition).length() < 0.01f) {
+ hopCount += 1;
+ bunnyHopAnimationTimer = 0.f;
+ bunnyPosition = nextPosition;
+ }
+
+ renderPos.y = verticalHopLerp(0.f, 4.f, t);
+
+ const f32 RMAX = PI / 16.f;
+ f32 zRotation = 0;
+ f32 start = 0.f;
+ f32 end = PI / 8.f;
+ f32 startTime = 0.f;
+ f32 endTime = 0.f;
+ bool disableRot = false;
+
+ if (t >= 0.9f) {
+ disableRot = true;
+ } else if (t >= 0.7f) {
+ start = -RMAX;
+ end = 0.f;
+ startTime = 0.7f;
+ endTime = 0.9f;
+ } else if (t >= 0.50f) {
+ start = 0.f;
+ end = -RMAX;
+ startTime = 0.50f;
+ endTime = 0.70f;
+ } else if (t >= 0.40f) {
+ disableRot = true;
+ } else if (t >= 0.20f) {
+ start = RMAX;
+ end = 0.f;
+ startTime = 0.20f;
+ endTime = 0.40f;
+ } else {
+ start = 0.f;
+ end = RMAX;
+ startTime = 0.f;
+ endTime = 0.20f;
+ }
+
+ if (!disableRot) {
+ f32 totalTime = endTime - startTime;
+ zRotation =
+ rotationLerp(start, end, (totalTime - (endTime - t)) / totalTime);
+ }
+
+ bunnyMesh.model = Mat4x4()
+ .getZRotationMatrix(zRotation)
+ .rotate(0, bunnyRotation, 0)
+ .translate(renderPos);
+ if (hopCount == numHops) {
+ bunnyPosition = bunnyTarget;
+ bunnyHopAnimationTimer = 0.f;
+ state = SpringThemeState::Idle;
+ stateTimer = randomFloatBetween(0.5f, 1.f);
+ }
+ break;
+ }
+ }
+}
+
+void SpringTheme::render() {
+ renderer.render();
+
+ // Draw the 2D gradient background without writing to the depth buffer so
+ // the 3D content rendered afterwards is unobstructed.
+ glDepthMask(GL_FALSE);
+ useShader(renderer2d.shader);
+ setShaderMat4(renderer2d.uniforms.projection, renderer2d.projection);
+ background->render();
+ glDepthMask(GL_TRUE);
+
+ if (state != SpringThemeState::Loading) {
+ grassRenderer.render(&renderer);
+ // Restore the 3D renderer's shader after the grass shader took over
+ useShader(renderer.shader);
+ setShaderMat4(renderer.uniforms.projection, renderer.projection);
+ setShaderMat4(renderer.uniforms.view, renderer.view);
+ bunnyMesh.render(&renderer);
+ }
+}
+
+void SpringTheme::unload() {
+ renderer.unload();
+ renderer2d.unload();
+ delete background;
+ bunnyMesh.unload();
+ grassRenderer.unload();
+}
diff --git a/themes/src/spring/spring_theme.hpp b/themes/src/spring/spring_theme.hpp
new file mode 100644
index 0000000..4ee5684
--- /dev/null
+++ b/themes/src/spring/spring_theme.hpp
@@ -0,0 +1,51 @@
+#ifndef SPRING_THEME_HPP
+#define SPRING_THEME_HPP
+
+#include "../mathlib.h"
+#include "../renderer_2d.h"
+#include "../renderer_3d.h"
+#include "../theme.h"
+#include "../types.h"
+#include "grass_renderer.hpp"
+
+class RectangularGradient;
+
+enum class SpringThemeState {
+ Loading = 0,
+ LoadedShader,
+ LoadedBunny,
+ PreHop,
+ Hopping,
+ Idle
+};
+
+class SpringTheme : public Theme {
+public:
+ SpringTheme(WebglContext *);
+ ~SpringTheme();
+ Renderer3d renderer;
+ SpringThemeState state;
+ f32 bunnySpeed = 5.f;
+ Vector3 bunnyPosition = Vector3(0, 0, 0);
+ Vector3 bunnyTarget = Vector3(0, 0, 0);
+ Vector3 hopIncrement = Vector3(0, 0, 0);
+
+ f32 numHops = 0;
+ f32 hopCount = 0;
+ f32 bunnyHopAnimationTimer = 0.f;
+ f32 stateTimer = 0.f;
+ f32 bunnyRotation = 0.f;
+ f32 targetRotation = 0.f;
+
+ Mesh3d bunnyMesh;
+ GrassRenderer grassRenderer;
+ Renderer2d renderer2d;
+ RectangularGradient *background;
+
+ void load(WebglContext *);
+ void update(f32 dtSeconds);
+ void render();
+ void unload();
+};
+
+#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/SummerTheme.cpp b/themes/src/summer/summer_theme.cpp
index 1f76b56..3b237e2 100644
--- a/themes/src/summer/SummerTheme.cpp
+++ b/themes/src/summer/summer_theme.cpp
@@ -1,10 +1,9 @@
-#include "SummerTheme.h"
-#include "../Renderer2d.h"
+#include "summer_theme.h"
+#include "../renderer_2d.h"
#include "../list.h"
#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();
}
@@ -59,19 +83,23 @@ void Sun::load(Renderer2d* renderer) {
indices.add(second);
indices.add(third);
}
-
+
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");
+
vertices.deallocate();
indices.deallocate();
}
void Sun::update(f32 dtSeconds) {
-
+ elapsedTime += dtSeconds;
}
void Sun::render(Renderer2d* renderer) {
setShaderMat4(renderer->uniforms.model, mesh.model);
+ glUniform1f(timeUniform, elapsedTime);
glBindVertexArray(mesh.vao);
glDrawElements(GL_TRIANGLES, mesh.numIndices, GL_UNSIGNED_INT, 0);
glBindVertexArray(0);
diff --git a/themes/src/summer/SummerTheme.h b/themes/src/summer/summer_theme.h
index 2ce6b7f..e048a7a 100644
--- a/themes/src/summer/SummerTheme.h
+++ b/themes/src/summer/summer_theme.h
@@ -1,13 +1,17 @@
#pragma once
#include "../types.h"
-#include "../Renderer2d.h"
+#include "../renderer_2d.h"
#include "../theme.h"
+#include "sky_model.h"
+#include "water_model.h"
#include <vector>
struct Sun {
f32 radius = 20.f;
i32 sectors = 180;
Mesh2D mesh;
+ f32 elapsedTime = 0.f;
+ i32 timeUniform = -1;
void load(Renderer2d* renderer);
void update(f32 dtSeconds);
@@ -20,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
diff --git a/themes/src/webgl_context.cpp b/themes/src/webgl_context.cpp
new file mode 100644
index 0000000..cba4b8c
--- /dev/null
+++ b/themes/src/webgl_context.cpp
@@ -0,0 +1,46 @@
+#include "webgl_context.h"
+#include <cstdio>
+
+EM_BOOL onResize(int eventType, const EmscriptenUiEvent *uiEvent,
+ void *userData) {
+ WebglContext *context = (WebglContext *)userData;
+
+ f64 inWidth, inHeight;
+ emscripten_get_element_css_size(context->query, &inWidth, &inHeight);
+
+ context->width = static_cast<f32>(inWidth);
+ context->height = static_cast<f32>(inHeight);
+ emscripten_set_canvas_element_size(context->query, context->width, context->height);
+
+ return true;
+}
+
+void WebglContext::init(const char *inQuery) {
+ strcpy(query, inQuery);
+ f64 inWidth, inHeight;
+ emscripten_get_element_css_size(query, &inWidth, &inHeight);
+ width = static_cast<f32>(inWidth);
+ height = static_cast<f32>(inHeight);
+ emscripten_set_canvas_element_size(query, width, height);
+
+ EmscriptenWebGLContextAttributes attrs;
+ emscripten_webgl_init_context_attributes(&attrs);
+
+ attrs.enableExtensionsByDefault = 1;
+ attrs.majorVersion = 3;
+ attrs.minorVersion = 0;
+
+ context = emscripten_webgl_create_context(query, &attrs);
+ makeCurrentContext();
+
+ glClearColor(0, 0, 0, 0.0f);
+
+ emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, this, false,
+ onResize);
+};
+
+void WebglContext::makeCurrentContext() {
+ emscripten_webgl_make_context_current(context);
+};
+
+void WebglContext::destroy() { emscripten_webgl_destroy_context(context); }
diff --git a/themes/src/WebglContext.h b/themes/src/webgl_context.h
index 1956092..1956092 100644
--- a/themes/src/WebglContext.h
+++ b/themes/src/webgl_context.h
diff --git a/themes/src/winter/Snowflake.cpp b/themes/src/winter/Snowflake.cpp
deleted file mode 100644
index 57f1a8f..0000000
--- a/themes/src/winter/Snowflake.cpp
+++ /dev/null
@@ -1,189 +0,0 @@
-#include "Snowflake.h"
-#include "../Renderer2d.h"
-#include "../mathlib.h"
-#include "../list.h"
-#include <cstdio>
-
-/*
-
- What else to do?
-
- - Windstream that blows a certain selection of snowflakes in a loop-dee-loop pattern
- - Snowflakes that land on the ground and melt
- - Snowflakes that spin along the Y-axis for a three dimensional effect
-
- */
-
-const Vector4 snowColor = Vector4(1.0, 0.98, 0.98, 1);
-const Vector2 NUM_ARMS_RANGE = Vector2(6.f, 8.f);
-const Vector2 RADIUS_RANGE = Vector2(8.f, 32.f);
-const Vector2 VELOCITY_RANGE_X = Vector2(-10.f, 10.f);
-const Vector2 VELOCITY_RANGE_Y = Vector2(-100.f, -85.f);
-const Vector2 ROTATION_VELOCITY_RANGE = Vector2(-PI / 8.f, PI / 8.f);
-const Vector2 WIND_VELOCITY_RANGE_X = Vector2(-3.f, 3.f);
-const Vector2 WIND_VELOCITY_RANGE_Y = Vector2(3.f, 10.f);
-const f32 GRAVITY = 5.f;
-
-inline void generateSnowflakeArm(f32 width, f32 height, f32 angle, matte::List<Vertex2D>* vertices, Mat4x4 transform = Mat4x4()) {
- f32 halfWidth = width / 2.f;
- Vector2 leftStart = transform * Vector2(-halfWidth, 0).rotate(angle);
- Vector2 leftEnd = transform * Vector2(-halfWidth, height).rotate(angle);
- Vector2 rightStart = transform * Vector2(halfWidth, 0).rotate(angle);
- Vector2 rightEnd = transform * Vector2(halfWidth, height).rotate(angle);
-
- vertices->add({ leftStart, snowColor, Mat4x4() });
- vertices->add({ leftEnd, snowColor, Mat4x4() });
- vertices->add({ rightEnd, snowColor, Mat4x4() });
- vertices->add({ leftStart, snowColor, Mat4x4() });
- vertices->add({ rightEnd, snowColor, Mat4x4() });
- vertices->add({ rightStart, snowColor, Mat4x4() });
-}
-
-/**
- Fills in the vertices array vertices that represent a snowflake shape. The snowflake shape consists
- of numArms jutting out of the center radially. The center of the flake is connected. The radius is
- used to determine the length of the arms. The first third of each arm is barren, after which branches
- extends on either side of the arm at an angle of about 60 degrees. Each branch can itself have tiny
- sub branches jutting out of it, but these should be not nearly as large as the regular branches.
-
- With all of this in mind, we should be able to build a convincing snowflake.
-
- :param vertices List of vertices to be filled in
- :param numArms Number of arms radially sticking out of the snowflake
- :param radius Length of the snowflake arms
- */
-inline void generateSnowflakeShape(matte::List<Vertex2D>* vertices, i32 numArms, f32 radius, f32 armWidthRatio = 0.08f) {
- f32 innerRadius = 0;
- f32 outerRadius = 2 * radius;
- f32 dx = ((2 * PI) / numArms);
- for (i32 armIndex = 0; armIndex < numArms; armIndex++) {
- f32 armAngle = dx * armIndex;
- generateSnowflakeArm(armWidthRatio * radius, radius, armAngle, vertices);
- f32 armLeftAngle = DEG_TO_RAD(60.f);
- f32 armRightAngle = DEG_TO_RAD(-60.f);
-
- const i32 NUM_SUB_ARMS = 4;
- for (i32 subArmIndex = 0; subArmIndex < NUM_SUB_ARMS; subArmIndex++) {
- f32 height = (radius / static_cast<f32>(subArmIndex));
- f32 width = (armWidthRatio / (subArmIndex + 1)) * height;
- f32 transY = (radius / (NUM_SUB_ARMS + 1)) * (subArmIndex + 1);
- Vector2 translation = Vector2(0, transY).rotate(armAngle);
- generateSnowflakeArm(width, height, armAngle, vertices, Mat4x4().translateByVec2(translation).rotate2D(armLeftAngle));
- generateSnowflakeArm(width, height, armAngle, vertices, Mat4x4().translateByVec2(translation).rotate2D(armRightAngle));
- }
- }
-}
-
-inline void initFlake(SnowflakeParticleRenderer* renderer, SnowflakeUpdateData* ud) {
- ud->radius = randomFloatBetween(RADIUS_RANGE.x, RADIUS_RANGE.y);
- ud->vtxIdx = renderer->vertices.numElements;
- generateSnowflakeShape(&renderer->vertices,
- randomFloatBetween(NUM_ARMS_RANGE.x, NUM_ARMS_RANGE.y),
- ud->radius);
-
- ud->numVertices = renderer->vertices.numElements - ud->vtxIdx;
- ud->velocity = Vector2(randomFloatBetween(VELOCITY_RANGE_X.x, VELOCITY_RANGE_X.y), randomFloatBetween(VELOCITY_RANGE_Y.x, VELOCITY_RANGE_Y.y));
- ud->position = Vector2(randomFloatBetween(0, renderer->xMax), randomFloatBetween(renderer->yMax, 4 * renderer->yMax));
- ud->rotateVelocity = randomFloatBetween(ROTATION_VELOCITY_RANGE.x, ROTATION_VELOCITY_RANGE.y);
-}
-
-void SnowflakeParticleRenderer::load(SnowflakeLoadParameters params, Renderer2d* renderer) {
- numSnowflakes = params.numSnowflakes;
-
- updateData = new SnowflakeUpdateData[params.numSnowflakes];
-
- xMax = static_cast<f32>(renderer->context->width);
- yMax = static_cast<f32>(renderer->context->height);
-
- vertices.deallocate();
- vertices.growDynamically = true;
-
- // Initialize each snow flake with its shape
- for (i32 s = 0; s < numSnowflakes; s++) {
- auto ud = &updateData[s];
- initFlake(this, ud);
- }
-
- useShader(renderer->shader);
-
- glGenVertexArrays(1, &vao);
- glBindVertexArray(vao);
-
- glGenBuffers(1, &vbo);
- glBindBuffer(GL_ARRAY_BUFFER, vbo);
- glBufferData(GL_ARRAY_BUFFER, vertices.numElements * sizeof(Vertex2D), &vertices.data[0], GL_DYNAMIC_DRAW);
-
- glEnableVertexAttribArray(renderer->attributes.position);
- glVertexAttribPointer(renderer->attributes.position, 2, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (GLvoid *)0);
-
- glEnableVertexAttribArray(renderer->attributes.color);
- glVertexAttribPointer(renderer->attributes.color, 4, GL_FLOAT, GL_FALSE, sizeof(Vertex2D), (GLvoid *)offsetof(Vertex2D, color));
-
- for (i32 idx = 0; idx < 4; idx++) {
- i32 offset = (4 * sizeof(f32)) * idx;
- glEnableVertexAttribArray(renderer->attributes.vMatrix + idx);
- glVertexAttribPointer(renderer->attributes.vMatrix + idx,
- 4,
- GL_FLOAT,
- GL_FALSE,
- sizeof(Vertex2D),
- (GLvoid *)(offsetof(Vertex2D, vMatrix) + offset));
- }
-
- glBindBuffer(GL_ARRAY_BUFFER, 0);
- glBindVertexArray(0);
-}
-
-inline void resetFlake(SnowflakeParticleRenderer* renderer, SnowflakeUpdateData* ud) {
- ud->position.y = 2 * renderer->yMax;
- ud->velocity = Vector2(randomFloatBetween(-10, 10), randomFloatBetween(-100, -85));
- ud->rotation = 0;
-}
-
-inline void updateFlake(SnowflakeParticleRenderer* renderer, SnowflakeUpdateData* ud, i32 s, f32 dtSeconds) {
- ud->velocity = ud->velocity + Vector2(0, -(GRAVITY * dtSeconds));
- //if (addWind) ud->velocity += renderer->windSpeed;
- ud->position += ud->velocity * dtSeconds;
- ud->rotation += ud->rotateVelocity * dtSeconds;
-
- Mat4x4 m = Mat4x4().translateByVec2(ud->position).rotate2D(ud->rotation);
- for (i32 v = ud->vtxIdx; v < (ud->vtxIdx + ud->numVertices); v++) {
- renderer->vertices.data[v].vMatrix = m;
- }
-
- if (ud->position.y <= -ud->radius) {
- resetFlake(renderer, ud);
- }
-}
-
-void SnowflakeParticleRenderer::update(f32 dtSeconds) {
- timeUntilNextWindSeconds -= dtSeconds;
- if (timeUntilNextWindSeconds < 0) {
- timeUntilNextWindSeconds = randomFloatBetween(2.5f, 10.f);
- }
-
- for (i32 s = 0; s < numSnowflakes; s++) {
- SnowflakeUpdateData* ud = &updateData[s];
- updateFlake(this, ud, s, dtSeconds);
- }
-}
-
-void SnowflakeParticleRenderer::render(Renderer2d* renderer) {
- setShaderMat4(renderer->uniforms.model, model);
-
- glBindBuffer(GL_ARRAY_BUFFER, vbo);
- glBufferSubData(GL_ARRAY_BUFFER, 0, vertices.numElements * sizeof(Vertex2D), &vertices.data[0]);
-
- glBindVertexArray(vao);
- glDrawArrays(GL_TRIANGLES, 0, vertices.numElements);
- glBindVertexArray(0);
-}
-
-void SnowflakeParticleRenderer::unload() {
- glDeleteVertexArrays(1, &vao);
- glDeleteBuffers(1, &vbo);
- vao = 0;
- vbo = 0;
- vertices.deallocate();
- delete [] updateData;
-}
diff --git a/themes/src/winter/snowflake.cpp b/themes/src/winter/snowflake.cpp
new file mode 100644
index 0000000..26b6549
--- /dev/null
+++ b/themes/src/winter/snowflake.cpp
@@ -0,0 +1,185 @@
+#include "snowflake.h"
+#include "../renderer_2d.h"
+#include "../mathlib.h"
+#include "../list.h"
+#include "../shader.h"
+#include "../shaders/snowflake_vert.h"
+#include "../shaders/snowflake_frag.h"
+#include <cstdio>
+
+/*
+
+ What else to do?
+
+ - Windstream that blows a certain selection of snowflakes in a loop-dee-loop pattern
+ - Snowflakes that land on the ground and melt
+ - Snowflakes that spin along the Y-axis for a three dimensional effect
+
+ */
+
+const Vector2 SCALE_RANGE = Vector2(16.f, 48.f);
+const Vector2 VELOCITY_RANGE_X = Vector2(-10.f, 10.f);
+const Vector2 VELOCITY_RANGE_Y = Vector2(-100.f, -85.f);
+const Vector2 ROTATION_VELOCITY_RANGE = Vector2(-PI / 8.f, PI / 8.f);
+const Vector2 WIND_VELOCITY_RANGE_X = Vector2(-3.f, 3.f);
+const Vector2 WIND_VELOCITY_RANGE_Y = Vector2(3.f, 10.f);
+const f32 GRAVITY = 5.f;
+
+inline void initFlake(SnowflakeParticleRenderer* renderer, SnowflakeUpdateData* ud) {
+ ud->scale = randomFloatBetween(SCALE_RANGE.x, SCALE_RANGE.y);
+ ud->seed = randomFloatBetween(0.f, 1000.f);
+
+ // Scale velocity based on star size - larger stars fall faster
+ f32 sizeRatio = (ud->scale - SCALE_RANGE.x) / (SCALE_RANGE.y - SCALE_RANGE.x);
+ f32 velocityY = VELOCITY_RANGE_Y.x + (VELOCITY_RANGE_Y.y - VELOCITY_RANGE_Y.x) * (0.5f + sizeRatio * 0.5f);
+
+ ud->velocity = Vector2(randomFloatBetween(VELOCITY_RANGE_X.x, VELOCITY_RANGE_X.y), velocityY);
+ ud->position = Vector2(randomFloatBetween(0, renderer->xMax), randomFloatBetween(renderer->yMax, 4 * renderer->yMax));
+ ud->rotateVelocity = randomFloatBetween(ROTATION_VELOCITY_RANGE.x, ROTATION_VELOCITY_RANGE.y);
+ ud->rotation = 0.f;
+}
+
+void SnowflakeParticleRenderer::load(SnowflakeLoadParameters params, Renderer2d* renderer) {
+ numSnowflakes = params.numSnowflakes;
+ updateData = new SnowflakeUpdateData[params.numSnowflakes];
+
+ xMax = static_cast<f32>(renderer->context->width);
+ yMax = static_cast<f32>(renderer->context->height);
+
+ // Initialize each snowflake
+ for (i32 s = 0; s < numSnowflakes; s++) {
+ auto ud = &updateData[s];
+ initFlake(this, ud);
+ }
+
+ // Load custom snowflake shader
+ shader = loadShader(shader_snowflake_vert, shader_snowflake_frag);
+ useShader(shader);
+
+ // Get attribute and uniform locations
+ attributes.position = getShaderAttribute(shader, "position");
+ attributes.instancePos = getShaderAttribute(shader, "instancePos");
+ attributes.instanceRot = getShaderAttribute(shader, "instanceRot");
+ attributes.instanceScale = getShaderAttribute(shader, "instanceScale");
+ attributes.instanceSeed = getShaderAttribute(shader, "instanceSeed");
+ uniforms.projection = getShaderUniform(shader, "projection");
+ uniforms.model = getShaderUniform(shader, "model");
+
+ // Create base quad geometry (just a square from -1 to 1)
+ Vector2 quadVertices[] = {
+ Vector2(-1.0f, -1.0f),
+ Vector2( 1.0f, -1.0f),
+ Vector2( 1.0f, 1.0f),
+ Vector2(-1.0f, -1.0f),
+ Vector2( 1.0f, 1.0f),
+ Vector2(-1.0f, 1.0f)
+ };
+
+ // Setup VAO
+ glGenVertexArrays(1, &vao);
+ glBindVertexArray(vao);
+
+ // Create and setup quad VBO (static geometry)
+ glGenBuffers(1, &quadVbo);
+ glBindBuffer(GL_ARRAY_BUFFER, quadVbo);
+ glBufferData(GL_ARRAY_BUFFER, sizeof(quadVertices), quadVertices, GL_STATIC_DRAW);
+ glEnableVertexAttribArray(attributes.position);
+ glVertexAttribPointer(attributes.position, 2, GL_FLOAT, GL_FALSE, sizeof(Vector2), (GLvoid*)0);
+
+ // Create instance VBO (dynamic data)
+ glGenBuffers(1, &instanceVbo);
+ glBindBuffer(GL_ARRAY_BUFFER, instanceVbo);
+ glBufferData(GL_ARRAY_BUFFER, numSnowflakes * sizeof(SnowflakeInstanceData), NULL, GL_DYNAMIC_DRAW);
+
+ // Setup instance attributes
+ glEnableVertexAttribArray(attributes.instancePos);
+ glVertexAttribPointer(attributes.instancePos, 2, GL_FLOAT, GL_FALSE, sizeof(SnowflakeInstanceData), (GLvoid*)offsetof(SnowflakeInstanceData, position));
+ glVertexAttribDivisor(attributes.instancePos, 1);
+
+ glEnableVertexAttribArray(attributes.instanceRot);
+ glVertexAttribPointer(attributes.instanceRot, 1, GL_FLOAT, GL_FALSE, sizeof(SnowflakeInstanceData), (GLvoid*)offsetof(SnowflakeInstanceData, rotation));
+ glVertexAttribDivisor(attributes.instanceRot, 1);
+
+ glEnableVertexAttribArray(attributes.instanceScale);
+ glVertexAttribPointer(attributes.instanceScale, 1, GL_FLOAT, GL_FALSE, sizeof(SnowflakeInstanceData), (GLvoid*)offsetof(SnowflakeInstanceData, scale));
+ glVertexAttribDivisor(attributes.instanceScale, 1);
+
+ glEnableVertexAttribArray(attributes.instanceSeed);
+ glVertexAttribPointer(attributes.instanceSeed, 1, GL_FLOAT, GL_FALSE, sizeof(SnowflakeInstanceData), (GLvoid*)offsetof(SnowflakeInstanceData, seed));
+ glVertexAttribDivisor(attributes.instanceSeed, 1);
+
+ glBindBuffer(GL_ARRAY_BUFFER, 0);
+ glBindVertexArray(0);
+}
+
+inline void resetFlake(SnowflakeParticleRenderer* renderer, SnowflakeUpdateData* ud) {
+ ud->position.y = 2 * renderer->yMax;
+
+ // Scale velocity based on star size - larger stars fall faster
+ f32 sizeRatio = (ud->scale - SCALE_RANGE.x) / (SCALE_RANGE.y - SCALE_RANGE.x);
+ f32 velocityY = VELOCITY_RANGE_Y.x + (VELOCITY_RANGE_Y.y - VELOCITY_RANGE_Y.x) * (0.5f + sizeRatio * 0.5f);
+
+ ud->velocity = Vector2(randomFloatBetween(VELOCITY_RANGE_X.x, VELOCITY_RANGE_X.y), velocityY);
+ ud->rotation = 0;
+}
+
+inline void updateFlake(SnowflakeParticleRenderer* renderer, SnowflakeUpdateData* ud, i32 s, f32 dtSeconds) {
+ ud->velocity = ud->velocity + Vector2(0, -(GRAVITY * dtSeconds));
+ //if (addWind) ud->velocity += renderer->windSpeed;
+ ud->position += ud->velocity * dtSeconds;
+ ud->rotation += ud->rotateVelocity * dtSeconds;
+
+ if (ud->position.y <= -ud->scale) {
+ resetFlake(renderer, ud);
+ }
+}
+
+void SnowflakeParticleRenderer::update(f32 dtSeconds) {
+ timeUntilNextWindSeconds -= dtSeconds;
+ if (timeUntilNextWindSeconds < 0) {
+ timeUntilNextWindSeconds = randomFloatBetween(2.5f, 10.f);
+ }
+
+ for (i32 s = 0; s < numSnowflakes; s++) {
+ SnowflakeUpdateData* ud = &updateData[s];
+ updateFlake(this, ud, s, dtSeconds);
+ }
+}
+
+void SnowflakeParticleRenderer::render(Renderer2d* renderer) {
+ useShader(shader);
+ setShaderMat4(uniforms.projection, renderer->projection);
+ setShaderMat4(uniforms.model, model);
+
+ // Prepare instance data
+ SnowflakeInstanceData* instanceData = new SnowflakeInstanceData[numSnowflakes];
+ for (i32 s = 0; s < numSnowflakes; s++) {
+ instanceData[s].position = updateData[s].position;
+ instanceData[s].rotation = updateData[s].rotation;
+ instanceData[s].scale = updateData[s].scale;
+ instanceData[s].seed = updateData[s].seed;
+ }
+
+ // Upload instance data
+ glBindBuffer(GL_ARRAY_BUFFER, instanceVbo);
+ glBufferSubData(GL_ARRAY_BUFFER, 0, numSnowflakes * sizeof(SnowflakeInstanceData), instanceData);
+
+ // Draw instanced
+ glBindVertexArray(vao);
+ glDrawArraysInstanced(GL_TRIANGLES, 0, 6, numSnowflakes);
+ glBindVertexArray(0);
+
+ delete[] instanceData;
+}
+
+void SnowflakeParticleRenderer::unload() {
+ glDeleteVertexArrays(1, &vao);
+ glDeleteBuffers(1, &quadVbo);
+ glDeleteBuffers(1, &instanceVbo);
+ glDeleteProgram(shader);
+ vao = 0;
+ quadVbo = 0;
+ instanceVbo = 0;
+ shader = 0;
+ delete[] updateData;
+}
diff --git a/themes/src/winter/Snowflake.h b/themes/src/winter/snowflake.h
index ad027f6..e4118f6 100644
--- a/themes/src/winter/Snowflake.h
+++ b/themes/src/winter/snowflake.h
@@ -4,7 +4,7 @@
#include "../types.h"
#include "../mathlib.h"
#include "../list.h"
-#include "Windfield.hpp"
+#include "windfield.hpp"
struct Renderer2d;
struct Vertex2D;
@@ -14,15 +14,20 @@ struct SnowflakeLoadParameters {
f32 windIntervalSeconds = 1.5f;
};
+struct SnowflakeInstanceData {
+ Vector2 position;
+ f32 rotation;
+ f32 scale;
+ f32 seed;
+};
+
struct SnowflakeUpdateData {
Vector2 velocity;
Vector2 position;
f32 rotateVelocity = 0.f;
f32 rotation = 0;
- f32 radius;
-
- i32 vtxIdx = 0;
- i32 numVertices = 0;
+ f32 scale;
+ f32 seed;
};
struct SnowflakeParticleRenderer {
@@ -35,10 +40,24 @@ struct SnowflakeParticleRenderer {
SnowflakeUpdateData* updateData;
u32 vao;
- u32 vbo;
+ u32 quadVbo; // Base quad geometry
+ u32 instanceVbo; // Instance data (position, rotation, scale, seed)
+ u32 shader; // Custom snowflake shader
Mat4x4 model;
- matte::List<Vertex2D> vertices;
-
+
+ struct {
+ i32 position;
+ i32 instancePos;
+ i32 instanceRot;
+ i32 instanceScale;
+ i32 instanceSeed;
+ } attributes;
+
+ struct {
+ i32 projection;
+ i32 model;
+ } uniforms;
+
void load(SnowflakeLoadParameters params, Renderer2d* renderer);
void update(f32 dtSeconds);
void render(Renderer2d* renderer);
diff --git a/themes/src/winter/Windfield.cpp b/themes/src/winter/windfield.cpp
index 88fb74b..f6c3be3 100644
--- a/themes/src/winter/Windfield.cpp
+++ b/themes/src/winter/windfield.cpp
@@ -1,4 +1,4 @@
-#include "Windfield.hpp"
+#include "windfield.hpp"
template <i32 Width, i32 Height, i32 CellDimension>
diff --git a/themes/src/winter/Windfield.hpp b/themes/src/winter/windfield.hpp
index 5bf0c38..5bf0c38 100644
--- a/themes/src/winter/Windfield.hpp
+++ b/themes/src/winter/windfield.hpp
diff --git a/themes/src/winter/WinterTheme.cpp b/themes/src/winter/winter_theme.cpp
index 052670e..a628f18 100644
--- a/themes/src/winter/WinterTheme.cpp
+++ b/themes/src/winter/winter_theme.cpp
@@ -1,5 +1,5 @@
-#include "WinterTheme.hpp"
-#include "../Renderer2d.h"
+#include "winter_theme.hpp"
+#include "../renderer_2d.h"
WinterTheme::WinterTheme(WebglContext* context)
{
diff --git a/themes/src/winter/WinterTheme.hpp b/themes/src/winter/winter_theme.hpp
index 5ba6d94..d1c3e05 100644
--- a/themes/src/winter/WinterTheme.hpp
+++ b/themes/src/winter/winter_theme.hpp
@@ -1,10 +1,10 @@
#ifndef WINTER_THEME_HPP
#define WINTER_THEME_HPP
-#include "Snowflake.h"
+#include "snowflake.h"
#include "../types.h"
#include "../theme.h"
-#include "../Renderer2d.h"
+#include "../renderer_2d.h"
struct WebglContext;