summaryrefslogtreecommitdiff
path: root/themes/src
diff options
context:
space:
mode:
authorMatt Kosarek <matt.kosarek@canonical.com>2026-02-19 16:58:58 -0500
committerMatt Kosarek <matt.kosarek@canonical.com>2026-02-19 16:58:58 -0500
commitda0eedbf1733e40613215ecd117e1a4e049089ad (patch)
treed83d5dc63b50efbd45084d692ae037cbe0f02b25 /themes/src
parent4d1beea73810af4641d074f974ad9c196a7e8d6e (diff)
Removed photo gallery + added cute little grass rendering for the rabbit and a nice gradient backgroundHEADmaster
Diffstat (limited to 'themes/src')
-rw-r--r--themes/src/_shaders/grass.frag11
-rw-r--r--themes/src/_shaders/grass.vert25
-rw-r--r--themes/src/main.cpp175
-rw-r--r--themes/src/main_loop.cpp48
-rw-r--r--themes/src/renderer_2d.h90
-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/spring/grass_renderer.cpp145
-rw-r--r--themes/src/spring/grass_renderer.hpp58
-rw-r--r--themes/src/spring/spring_theme.cpp41
-rw-r--r--themes/src/spring/spring_theme.hpp64
13 files changed, 498 insertions, 209 deletions
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/main.cpp b/themes/src/main.cpp
index 60e6aed..ec7630b 100644
--- a/themes/src/main.cpp
+++ b/themes/src/main.cpp
@@ -1,117 +1,132 @@
-#include "webgl_context.h"
+#include "autumn/autumn_theme.hpp"
#include "main_loop.h"
-#include "renderer_2d.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/summer_theme.h"
-#include "autumn/autumn_theme.hpp"
-#include "spring/spring_theme.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) {
- if (!active_theme)
- return;
- 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;
- 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();
- }
+ 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
index e5397ca..743892e 100644
--- a/themes/src/main_loop.cpp
+++ b/themes/src/main_loop.cpp
@@ -2,30 +2,36 @@
#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;
- }
+EM_BOOL loop(double time, void *loop) {
+ MainLoop *mainLoop = (MainLoop *)loop;
+ if (!mainLoop->isRunning) {
+ return false;
+ }
- long deltaTime = time - mainLoop->lastTime;
+ if (mainLoop->lastTime == 0) {
mainLoop->lastTime = time;
- mainLoop->elapsedTime += deltaTime;
- mainLoop->numFrames++;
- float deltaTimeSeconds = static_cast<float>(deltaTime) / 1000.f;
+ 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);
+ if (mainLoop->elapsedTime >= 1000.0) {
+ printf("FPS: %d\n", mainLoop->numFrames);
- mainLoop->elapsedTime = 0.0;
- mainLoop->numFrames = 0;
- }
+ mainLoop->elapsedTime = 0.0;
+ mainLoop->numFrames = 0;
+ }
- mainLoop->updateFunc(deltaTimeSeconds, NULL);
+ // 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;
-} \ No newline at end of file
+ }
+
+ mainLoop->updateFunc(deltaTimeSeconds, NULL);
+ return true;
+}
diff --git a/themes/src/renderer_2d.h b/themes/src/renderer_2d.h
index d572533..16c5cbe 100644
--- a/themes/src/renderer_2d.h
+++ b/themes/src/renderer_2d.h
@@ -1,61 +1,59 @@
#pragma once
-#include "webgl_context.h"
-#include "types.h"
-#include "shader.h"
#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();
+ 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;
+ 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();
+ 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/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/spring/grass_renderer.cpp b/themes/src/spring/grass_renderer.cpp
index 685f733..e4a210c 100644
--- a/themes/src/spring/grass_renderer.cpp
+++ b/themes/src/spring/grass_renderer.cpp
@@ -1,29 +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) {
- 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::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);
-void GrassRenderer::update(f32 seconds) {
-
+ // 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::render(Renderer3d* renderer) {
+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
index 88879f3..14ef067 100644
--- a/themes/src/spring/grass_renderer.hpp
+++ b/themes/src/spring/grass_renderer.hpp
@@ -5,29 +5,59 @@
#include "mathlib.h"
#include "types.h"
-const i32 GRASS_BLADES_PER_ROW = 24;
-const i32 GRASS_BLADES_PER_COL = 24;
+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 = 12.f;
+ Vector2 origin = Vector2(0, 0);
+ Vector2 area = Vector2(480, 480);
+ f32 grassHeight = 4.f;
};
struct GrassUpdateData {
- Vector3 position;
- Vector2 top_offset;
+ 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];
-
- void load(GrassRendererLoadData params, Renderer3d* renderer);
- void update(f32 dtSeconds);
- void render(Renderer3d* renderer);
- void unload();
+ 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
index 8b09366..4b62795 100644
--- a/themes/src/spring/spring_theme.cpp
+++ b/themes/src/spring/spring_theme.cpp
@@ -1,6 +1,8 @@
#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>
@@ -49,7 +51,15 @@ SpringTheme::~SpringTheme() { unload(); }
void SpringTheme::load(WebglContext *context) {
state = SpringThemeState::Loading;
renderer.context = context;
- renderer.clearColor = Vector4(160, 231, 160, 255.f).toNormalizedColor();
+ 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"},
@@ -74,6 +84,9 @@ inline f32 rotationLerp(f32 start, f32 target, f32 t) {
}
void SpringTheme::update(f32 dtSeconds) {
+ if (state != SpringThemeState::Loading) {
+ grassRenderer.update(dtSeconds);
+ }
switch (state) {
case SpringThemeState::Loading:
return;
@@ -98,6 +111,15 @@ void SpringTheme::update(f32 dtSeconds) {
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();
@@ -197,12 +219,29 @@ void SpringTheme::update(f32 dtSeconds) {
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
index 6079958..4ee5684 100644
--- a/themes/src/spring/spring_theme.hpp
+++ b/themes/src/spring/spring_theme.hpp
@@ -2,44 +2,50 @@
#define SPRING_THEME_HPP
#include "../mathlib.h"
-#include "../types.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
+ 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();
+ 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