From da0eedbf1733e40613215ecd117e1a4e049089ad Mon Sep 17 00:00:00 2001 From: Matt Kosarek Date: Thu, 19 Feb 2026 16:58:58 -0500 Subject: Removed photo gallery + added cute little grass rendering for the rabbit and a nice gradient background --- themes/dist/output.wasm | Bin 101674 -> 106007 bytes themes/meson.build | 6 +- themes/src/_shaders/grass.frag | 11 +++ themes/src/_shaders/grass.vert | 25 +++++ themes/src/main.cpp | 175 +++++++++++++++++++---------------- themes/src/main_loop.cpp | 48 +++++----- themes/src/renderer_2d.h | 90 +++++++++--------- themes/src/shaders/grass_frag.cpp | 14 +++ themes/src/shaders/grass_frag.h | 4 + themes/src/shaders/grass_vert.cpp | 28 ++++++ themes/src/shaders/grass_vert.h | 4 + themes/src/spring/grass_renderer.cpp | 145 +++++++++++++++++++++++++---- themes/src/spring/grass_renderer.hpp | 58 +++++++++--- themes/src/spring/spring_theme.cpp | 41 +++++++- themes/src/spring/spring_theme.hpp | 64 +++++++------ 15 files changed, 503 insertions(+), 210 deletions(-) create mode 100644 themes/src/_shaders/grass.frag create mode 100644 themes/src/_shaders/grass.vert create mode 100644 themes/src/shaders/grass_frag.cpp create mode 100644 themes/src/shaders/grass_frag.h create mode 100644 themes/src/shaders/grass_vert.cpp create mode 100644 themes/src/shaders/grass_vert.h (limited to 'themes') diff --git a/themes/dist/output.wasm b/themes/dist/output.wasm index 550e5ff..d0b2635 100755 Binary files a/themes/dist/output.wasm and b/themes/dist/output.wasm differ diff --git a/themes/meson.build b/themes/meson.build index 563ddc6..f99bf5c 100644 --- a/themes/meson.build +++ b/themes/meson.build @@ -40,6 +40,8 @@ sources = files( 'src/shaders/sun_vert.cpp', 'src/shaders/snowflake_frag.cpp', 'src/shaders/snowflake_vert.cpp', + 'src/shaders/grass_frag.cpp', + 'src/shaders/grass_vert.cpp', # Autumn theme 'src/autumn/autumn_theme.cpp', @@ -84,7 +86,9 @@ shader_inputs = files( 'src/_shaders/sun.frag', 'src/_shaders/sun.vert', 'src/_shaders/snowflake.frag', - 'src/_shaders/snowflake.vert' + 'src/_shaders/snowflake.vert', + 'src/_shaders/grass.frag', + 'src/_shaders/grass.vert' ) # Custom target that runs whenever shader files change 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 #include 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 #include -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(deltaTime) / 1000.f; + return true; + } + + long deltaTime = time - mainLoop->lastTime; + mainLoop->lastTime = time; + mainLoop->elapsedTime += deltaTime; + mainLoop->numFrames++; + float deltaTimeSeconds = static_cast(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 +#include -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 #include @@ -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 -- cgit v1.2.1