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/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/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)8
-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/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/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/summer_theme.cpp (renamed from themes/src/summer/SummerTheme.cpp)12
-rw-r--r--themes/src/summer/summer_theme.h (renamed from themes/src/summer/SummerTheme.h)4
-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
58 files changed, 1409 insertions, 780 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/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/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..cc79940 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,
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/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/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/SummerTheme.cpp b/themes/src/summer/summer_theme.cpp
index 1f76b56..6d2cfec 100644
--- a/themes/src/summer/SummerTheme.cpp
+++ b/themes/src/summer/summer_theme.cpp
@@ -1,5 +1,5 @@
-#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"
@@ -59,19 +59,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));
+
+ 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..eb404fd 100644
--- a/themes/src/summer/SummerTheme.h
+++ b/themes/src/summer/summer_theme.h
@@ -1,6 +1,6 @@
#pragma once
#include "../types.h"
-#include "../Renderer2d.h"
+#include "../renderer_2d.h"
#include "../theme.h"
#include <vector>
@@ -8,6 +8,8 @@ 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);
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;