← index
libmx2/libmx/gl_rain/rain.cpp
Source: libmx2/libmx/gl_rain/rain.cpp
#include"mx.hpp"
#include"argz.hpp"

#ifdef __EMSCRIPTEN__
#include <emscripten/emscripten.h>
#include <GLES3/gl3.h>
#endif

#include"gl.hpp"
#include"loadpng.hpp"

#define CHECK_GL_ERROR() \
{ GLenum err = glGetError(); \
if (err != GL_NO_ERROR) \
printf("OpenGL Error: %d at %s:%d\n", err, __FILE__, __LINE__); }

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif


struct WaterParticle {
    float x, y, z;
    float vx, vy, vz;
    float life;
    float size;
    float angle;
    float speed;
    float gravity;
    float friction;
    float rotation;
    float rotationSpeed;
    float color[4];
};

class WaterEmiter {
public:
    static const int MAX_PARTICLES = 10000;
    
    WaterEmiter() = default;
    ~WaterEmiter() {
        releaseResources();
    }

    void load(gl::GLWindow *win) {
        SDL_Surface* waterSurface = png::LoadPNG(win->util.getFilePath("data/water_droplet.png").c_str());
        if(!waterSurface) {
            throw mx::Exception("Failed to load water droplet texture");
        }

        glGenTextures(1, &textureID);
        glBindTexture(GL_TEXTURE_2D, textureID);
        
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);        

        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, waterSurface->w, waterSurface->h, 
                    0, GL_RGBA, GL_UNSIGNED_BYTE, waterSurface->pixels);
        
        SDL_FreeSurface(waterSurface);

#ifdef __EMSCRIPTEN__
const char *vertexShader = R"(#version 300 es
precision highp float;
layout(location = 0) in vec3 position;
layout(location = 1) in vec4 color;
layout(location = 2) in float size;
layout(location = 3) in float life;
out vec4 particleColor;
out float particleLife;
uniform mat4 viewProj;
uniform float time;

void main() {
    vec3 pos = position;
    gl_Position = viewProj * vec4(pos, 1.0);
    float particleScale = size * (0.9 + sin(time * 5.0 + position.y * 10.0) * 0.1);
    float distanceToCamera = gl_Position.w;
    gl_PointSize = particleScale * 60.0 / distanceToCamera;  // Was 30.0
    particleColor = color;
    particleLife = life;
}
)";
        
        const char *fragmentShader = R"(#version 300 es
precision highp float;
in vec4 particleColor;
in float particleLife;
out vec4 fragColor;
uniform sampler2D waterTexture;
uniform float time;

void main() {
    vec2 texCoord = gl_PointCoord;
    
    texCoord.y = (texCoord.y - 0.5) * 2.0 + 0.5;  // Stretch vertically
    
    vec4 texColor = texture(waterTexture, texCoord);
    if(texColor.r < 0.1 && texColor.g < 0.1 && texColor.b < 0.1) discard;
    
    float alpha = texColor.a * particleLife * 0.9;  // Was 0.8
    vec3 waterColor = vec3(0.7, 0.85, 1.0) * texColor.rgb;  // Brighter blue
    
    float highlight = pow(texCoord.y, 2.0) * 0.7;  // Was pow(texCoord.y, 3.0) * 0.5
    waterColor += highlight * vec3(1.0);
    
    float ripple = sin(time * 8.0 + gl_FragCoord.y * 0.2) * 0.05 + 0.95;
    fragColor = vec4(waterColor * ripple * particleColor.rgb, alpha);
    if(alpha < 0.01) discard;
}
)";
#else
        const char *vertexShader = R"(#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec4 color;
layout(location = 2) in float size;
layout(location = 3) in float life;
out vec4 particleColor;
out float particleLife;
uniform mat4 viewProj;
uniform float time;

void main() {
    vec3 pos = position;
    gl_Position = viewProj * vec4(pos, 1.0);
    float particleScale = size * (0.9 + sin(time * 5.0 + position.y * 10.0) * 0.1);
    float distanceToCamera = gl_Position.w;
    gl_PointSize = particleScale * 60.0 / distanceToCamera;  // Was 30.0
    particleColor = color;
    particleLife = life;
}
)";
        
        const char *fragmentShader = R"(#version 330 core
in vec4 particleColor;
in float particleLife;
out vec4 fragColor;
uniform sampler2D waterTexture;
uniform float time;

void main() {
    vec2 texCoord = gl_PointCoord;
    
    texCoord.y = (texCoord.y - 0.5) * 2.0 + 0.5;  // Stretch vertically
    
    vec4 texColor = texture(waterTexture, texCoord);
    if(texColor.r < 0.1 && texColor.g < 0.1 && texColor.b < 0.1) discard;
    
    float alpha = texColor.a * particleLife * 0.9;  // Was 0.8
    vec3 waterColor = vec3(0.7, 0.85, 1.0) * texColor.rgb;  // Brighter blue
    
    float highlight = pow(texCoord.y, 2.0) * 0.7;  // Was pow(texCoord.y, 3.0) * 0.5
    waterColor += highlight * vec3(1.0);
    
    float ripple = sin(time * 8.0 + gl_FragCoord.y * 0.2) * 0.05 + 0.95;
    fragColor = vec4(waterColor * ripple * particleColor.rgb, alpha);
    if(alpha < 0.01) discard;
}
)";
#endif

        if (!waterShader.loadProgramFromText(vertexShader, fragmentShader)) {
            throw mx::Exception("Failed to load water shaders");
        }

        particles.resize(MAX_PARTICLES);
        resetParticles();
        
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
        
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, particles.size() * sizeof(WaterParticle), 
                   particles.data(), GL_DYNAMIC_DRAW);
        
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(WaterParticle), (void*)0);
        glEnableVertexAttribArray(0);
        
        glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, sizeof(WaterParticle), 
                            (void*)(offsetof(WaterParticle, color)));
        glEnableVertexAttribArray(1);
        
        glVertexAttribPointer(2, 1, GL_FLOAT, GL_FALSE, sizeof(WaterParticle), 
                            (void*)(offsetof(WaterParticle, size)));
        glEnableVertexAttribArray(2);
        
        glVertexAttribPointer(3, 1, GL_FLOAT, GL_FALSE, sizeof(WaterParticle), 
                            (void*)(offsetof(WaterParticle, life)));
        glEnableVertexAttribArray(3);
        
        glBindVertexArray(0);

        CHECK_GL_ERROR();
    }

    void resetParticles() {
        for (auto& p : particles) {
            resetParticle(p, true);
        }
    }

    void resetParticle(WaterParticle& p, bool initialSetup = false) {
        p.x = ((float)(rand() % 40) / 100.0f - 0.2f);
        p.y = -0.2f;  
        p.z = ((float)(rand() % 40) / 100.0f - 0.2f);
        
        float angle = ((float)(rand() % 100) / 100.0f) * 2.0f * M_PI;
        float spread = ((float)(rand() % 30) / 100.0f + 0.05f);
        
        p.vx = cos(angle) * spread;
        p.vy = 1.0f + ((float)(rand() % 50) / 100.0f) * 0.8f; 
        p.vz = sin(angle) * spread;
        if (initialSetup) {
            p.life = ((float)(rand() % 100) / 100.0f);
        } else {
            p.life = 1.0f;
        }
        p.size = 0.08f + ((float)(rand() % 60) / 1000.0f);
        p.gravity = 1.2f + ((float)(rand() % 40) / 100.0f);
        p.friction = 0.02f + ((float)(rand() % 2) / 100.0f);
        p.angle = ((float)(rand() % 100) / 100.0f) * 2.0f * M_PI;
        p.rotationSpeed = ((float)(rand() % 100) / 100.0f - 0.5f) * 2.0f;
        float blueVariation = ((float)(rand() % 30) / 100.0f);
        p.color[0] = 0.7f + blueVariation; 
        p.color[1] = 0.8f + blueVariation; 
        p.color[2] = 1.0f;        
        p.color[3] = 0.8f;        
    }

    void update(float deltaTime) {
        rainHitCount = 0; 
        int newRaindrops = 150;  
        for (int i = 0; i < newRaindrops; i++) {
            int idx = rand() % (particles.size() * 3 / 4); 
            if (particles[idx].life <= 0.1f) {
                resetRainParticle(particles[idx]);
            }
        }
        
        for (auto& p : particles) {
            p.vy -= p.gravity * deltaTime;
            p.vx *= (1.0f - p.friction);
            p.vy *= (1.0f - p.friction);
            p.vz *= (1.0f - p.friction);
            float oldY = p.y; 
            p.x += p.vx * deltaTime;
            p.y += p.vy * deltaTime;
            p.z += p.vz * deltaTime;
            
        
            if (p.y <= floorY && oldY > floorY && p.vy < -0.5f) { 
                rainHitCount++;
                createSplash(p.x, floorY, p.z);
                resetRainParticle(p);
            }
            
            p.life -= 0.5f * deltaTime;
            if (p.life <= 0.0f) {
                resetRainParticle(p);
            }
        }
        
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferSubData(GL_ARRAY_BUFFER, 0, particles.size() * sizeof(WaterParticle), 
                       particles.data());
    }

    void draw(gl::GLWindow *win) {
        float camX = sin(cameraRotation) * cameraDistance;
        float camZ = cos(cameraRotation) * cameraDistance;
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), 
                                             (float)win->w / (float)win->h, 
                                             0.1f, 100.0f);
        glm::mat4 view = glm::lookAt(
            glm::vec3(camX, 0.3f, camZ),     
            glm::vec3(0.0f, 0.0f, 0.0f),     
            glm::vec3(0.0f, 1.0f, 0.0f)      
        );
        
        glm::mat4 viewProj = projection * view;
        
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
#ifndef __EMSCRIPTEN__
        glEnable(GL_PROGRAM_POINT_SIZE);
#endif
        glDepthMask(GL_FALSE); 
        
        waterShader.useProgram();
        
        GLint viewProjLoc = glGetUniformLocation(waterShader.id(), "viewProj");
        glUniformMatrix4fv(viewProjLoc, 1, GL_FALSE, glm::value_ptr(viewProj));
        
        GLint texLoc = glGetUniformLocation(waterShader.id(), "waterTexture");
        glUniform1i(texLoc, 0);
        
        GLint timeLoc = glGetUniformLocation(waterShader.id(), "time");
        glUniform1f(timeLoc, SDL_GetTicks() / 1000.0f);
        
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureID);
        
        glBindVertexArray(vao);
        glDrawArrays(GL_POINTS, 0, particles.size());
        glBindVertexArray(0);
    
#ifndef __EMSCRIPTEN__
        glDisable(GL_PROGRAM_POINT_SIZE);
#endif
        glDisable(GL_BLEND);
        glDepthMask(GL_TRUE);
        
        CHECK_GL_ERROR();
    }

    void releaseResources() {
        if (vbo != 0) {
            glDeleteBuffers(1, &vbo);
            vbo = 0;
        }
        if (vao != 0) {
            glDeleteVertexArrays(1, &vao);
            vao = 0;
        }
        if (textureID != 0) {
            glDeleteTextures(1, &textureID);
            textureID = 0;
        }
    }

    void setCameraDistance(float distance) {
        cameraDistance = distance;
    }

    void setCameraRotation(float angle) {
        cameraRotation = angle;
    }

private:
    std::vector<WaterParticle> particles;
    GLuint vao = 0;
    GLuint vbo = 0;
    GLuint textureID = 0;
    gl::ShaderProgram waterShader;
    float cameraDistance = 3.0f;
    float cameraRotation = 0.0f;
    std::vector<WaterParticle> splashParticles;
    const float floorY = 0.0f;  
    int rainHitCount = 0;

public:
    void resetRainParticle(WaterParticle& p, bool initialSetup = false) {
        
        p.x = ((float)(rand() % 200) / 100.0f - 1.0f) * 2.0f; 
        p.y = 3.0f + ((float)(rand() % 100) / 100.0f) * 2.0f; 
        p.z = ((float)(rand() % 200) / 100.0f - 1.0f) * 2.0f; 
        
        p.vx = ((float)(rand() % 20) / 100.0f - 0.1f) * 0.5f;
        p.vy = -3.5f - ((float)(rand() % 100) / 100.0f) * 2.0f;
        p.vz = ((float)(rand() % 20) / 100.0f - 0.1f) * 0.5f;
        
        p.size = 0.15f + ((float)(rand() % 40) / 1000.0f);  
        p.life = 1.0f;
        p.gravity = 0.5f;
        p.friction = 0.01f;
        
        p.color[0] = 0.8f;
        p.color[1] = 0.9f; 
        p.color[2] = 1.0f;
        p.color[3] = 0.9f;  
    }
    
    void createSplash(float x, float y, float z) {
        int splashCount = 5 + rand() % 5;
        for (int i = 0; i < splashCount; i++) {
            int idx = rand() % (particles.size() / 4); 
            WaterParticle& p = particles[idx];
            p.x = x;
            p.y = y + 0.02f;
            p.z = z;
        
            float angle = ((float)(rand() % 100) / 100.0f) * 2.0f * M_PI;
            float speed = 0.5f + ((float)(rand() % 100) / 100.0f) * 0.5f;
            
            p.vx = cos(angle) * speed;
            p.vy = 0.5f + ((float)(rand() % 100) / 100.0f) * 0.5f; 
            p.vz = sin(angle) * speed;
            
            p.size = 0.03f + ((float)(rand() % 30) / 1000.0f); 
            p.life = 0.5f + ((float)(rand() % 30) / 100.0f); 
            p.gravity = 3.0f; 
            
            p.color[0] = 0.8f;
            p.color[1] = 0.9f;
            p.color[2] = 1.0f;
            p.color[3] = 0.8f;
        }
    }
    
    int getRainHitCount() const {
        return rainHitCount;
    }
};

class Floor {
public:
    void load(gl::GLWindow *win) {
        
#ifndef __EMSCRIPTEN__
        const char *vertexShader = R"(#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texCoord;
out vec2 TexCoord;
uniform mat4 viewProj;
void main() {
    gl_Position = viewProj * vec4(position, 1.0);
    TexCoord = texCoord;
}
)";
        
        const char *fragmentShader = R"(#version 330 core
in vec2 TexCoord;
out vec4 fragColor;
uniform sampler2D floorTexture;
uniform float wetness;
void main() {
    vec4 color = texture(floorTexture, TexCoord);
    // Darken the floor where wet
    color.rgb = mix(color.rgb, color.rgb * 0.5 + vec3(0.0, 0.1, 0.2), wetness);
    fragColor = color;
}
)";
#else
    const char *vertexShader = R"(#version 300 es
precision highp float;
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texCoord;
out vec2 TexCoord;
uniform mat4 viewProj;
void main() {
    gl_Position = viewProj * vec4(position, 1.0);
    TexCoord = texCoord;
}
)";
        
        const char *fragmentShader = R"(#version 300 es
precision highp float;
in vec2 TexCoord;
out vec4 fragColor;
uniform sampler2D floorTexture;
uniform float wetness;
void main() {
    vec4 color = texture(floorTexture, TexCoord);
    // Darken the floor where wet
    color.rgb = mix(color.rgb, color.rgb * 0.5 + vec3(0.0, 0.1, 0.2), wetness);
    fragColor = color;
}
)";

#endif
        shader.loadProgramFromText(vertexShader, fragmentShader);
        
        const float size = 5.0f;
        float vertices[] = {
            -size, 0.0f, -size,     0.0f, 0.0f,
             size, 0.0f, -size,     1.0f, 0.0f,
             size, 0.0f,  size,     1.0f, 1.0f,
            -size, 0.0f,  size,     0.0f, 1.0f
        };
        
        unsigned int indices[] = {
            0, 1, 2,
            2, 3, 0
        };
        
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
        
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
        
        glGenBuffers(1, &ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
        
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
        
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(1);
        
        SDL_Surface* floorSurface = png::LoadPNG(win->util.getFilePath("data/ground.png").c_str());
        if(!floorSurface) {
            throw mx::Exception("Failed to load floor texture");  
        }
        
        glGenTextures(1, &textureId);
        glBindTexture(GL_TEXTURE_2D, textureId);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, floorSurface->w, floorSurface->h, 
                    0, GL_RGBA, GL_UNSIGNED_BYTE, floorSurface->pixels);
        
        SDL_FreeSurface(floorSurface);
        
        wetness = 0.0f;
    }
    
    void draw(glm::mat4 viewProj) {
        shader.useProgram();
        
        GLint viewProjLoc = glGetUniformLocation(shader.id(), "viewProj");
        glUniformMatrix4fv(viewProjLoc, 1, GL_FALSE, glm::value_ptr(viewProj));
        
        GLint texLoc = glGetUniformLocation(shader.id(), "floorTexture");
        glUniform1i(texLoc, 0);
        
        GLint wetnessLoc = glGetUniformLocation(shader.id(), "wetness");
        glUniform1f(wetnessLoc, wetness);
        
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureId);
        
        glBindVertexArray(vao);
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
    }
    
    void increaseWetness(float amount) {
        wetness += amount;
        if (wetness > 1.0f) wetness = 1.0f;
    }
    
    void release() {
        if (vbo) glDeleteBuffers(1, &vbo);
        if (vao) glDeleteVertexArrays(1, &vao);
        if (ebo) glDeleteBuffers(1, &ebo);
        if (textureId) glDeleteTextures(1, &textureId);
    }
    
private:
    GLuint vao = 0, vbo = 0, ebo = 0;
    GLuint textureId = 0;
    gl::ShaderProgram shader;
    float wetness = 0.0f;
};

class WaterPool {
public:
    void load(gl::GLWindow *win) {
        
#ifndef __EMSCRIPTEN__
        const char *vertexShader = R"(#version 330 core
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texCoord;
out vec2 TexCoord;
out vec3 FragPos;
uniform mat4 viewProj;
uniform float time;
uniform float size;
void main() {
    vec3 pos = position * vec3(size, 1.0, size);
    pos.y = 0.02f;  // Set higher above floor
    pos.y += sin(time * 2.0 + position.x * 10.0) * 0.01;
    pos.y += cos(time * 1.5 + position.z * 8.0) * 0.01;
    gl_Position = viewProj * vec4(pos, 1.0);
    TexCoord = texCoord;
    FragPos = pos;
}
)";
        
        const char *fragmentShader = R"(#version 330 core
in vec2 TexCoord;
in vec3 FragPos;
out vec4 fragColor;
uniform float time;
uniform sampler2D reflectionTexture;
uniform float poolSize;
void main() {
    
    float dist = length(FragPos.xz) / poolSize;
    float edgeFade = smoothstep(0.95, 0.75, dist);  
    
    
    vec3 waterColor = vec3(0.2, 0.5, 0.8);  
    
    
    vec2 rippleCoord = TexCoord * 5.0;
    rippleCoord.x += sin(time * 0.5) * 0.1;
    rippleCoord.y += cos(time * 0.3) * 0.1;
    
    float ripple1 = sin(rippleCoord.x * 10.0 + time * 3.0) * 0.5 + 0.5;
    float ripple2 = sin(rippleCoord.y * 8.0 + time * 2.5) * 0.5 + 0.5;
    float rippleValue = mix(ripple1, ripple2, 0.5) * 0.3;  
    
    vec3 highlight = vec3(0.8, 0.9, 1.0) * rippleValue;
    vec3 finalColor = waterColor + highlight;
    
    fragColor = vec4(finalColor, 0.85 * edgeFade);  
}
)";
#else
const char *vertexShader = R"(#version 300 es
precision highp float;
layout(location = 0) in vec3 position;
layout(location = 1) in vec2 texCoord;
out vec2 TexCoord;
out vec3 FragPos;
uniform mat4 viewProj;
uniform float time;
uniform float size;
void main() {
    vec3 pos = position * vec3(size, 1.0, size);
    pos.y = 0.02f;  // Set higher above floor
    pos.y += sin(time * 2.0 + position.x * 10.0) * 0.01;
    pos.y += cos(time * 1.5 + position.z * 8.0) * 0.01;
    gl_Position = viewProj * vec4(pos, 1.0);
    TexCoord = texCoord;
    FragPos = pos;
}
)";
        
        const char *fragmentShader = R"(#version 300 es
precision highp float;
in vec2 TexCoord;
in vec3 FragPos;
out vec4 fragColor;
uniform float time;
uniform sampler2D reflectionTexture;
uniform float poolSize;
void main() {
    
    float dist = length(FragPos.xz) / poolSize;
    float edgeFade = smoothstep(0.95, 0.75, dist);  
    
    
    vec3 waterColor = vec3(0.2, 0.5, 0.8);  
    
    
    vec2 rippleCoord = TexCoord * 5.0;
    rippleCoord.x += sin(time * 0.5) * 0.1;
    rippleCoord.y += cos(time * 0.3) * 0.1;
    
    float ripple1 = sin(rippleCoord.x * 10.0 + time * 3.0) * 0.5 + 0.5;
    float ripple2 = sin(rippleCoord.y * 8.0 + time * 2.5) * 0.5 + 0.5;
    float rippleValue = mix(ripple1, ripple2, 0.5) * 0.3;  
    
    vec3 highlight = vec3(0.8, 0.9, 1.0) * rippleValue;
    vec3 finalColor = waterColor + highlight;
    
    fragColor = vec4(finalColor, 0.85 * edgeFade);  
}
)";
#endif
        shader.loadProgramFromText(vertexShader, fragmentShader);
        
        const int segments = 32;
        std::vector<float> vertices;
        std::vector<unsigned int> indices;
        
        vertices.push_back(0.0f); 
        vertices.push_back(0.01f); 
        vertices.push_back(0.0f); 
        vertices.push_back(0.5f); 
        vertices.push_back(0.5f); 
        
        for (int i = 0; i <= segments; i++) {
            float angle = ((float)i / segments) * 2.0f * M_PI;
            float x = sin(angle);
            float z = cos(angle);
            
            vertices.push_back(x); 
            vertices.push_back(0.01f); 
            vertices.push_back(z); 
            vertices.push_back(x * 0.5f + 0.5f); 
            vertices.push_back(z * 0.5f + 0.5f); 
            
            if (i < segments) {
                indices.push_back(0); 
                indices.push_back(i + 1);
                indices.push_back(i + 2);
            }
        }
        
        glGenVertexArrays(1, &vao);
        glBindVertexArray(vao);
        
        glGenBuffers(1, &vbo);
        glBindBuffer(GL_ARRAY_BUFFER, vbo);
        glBufferData(GL_ARRAY_BUFFER, vertices.size() * sizeof(float), vertices.data(), GL_STATIC_DRAW);
        
        glGenBuffers(1, &ebo);
        glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
        glBufferData(GL_ELEMENT_ARRAY_BUFFER, indices.size() * sizeof(unsigned int), indices.data(), GL_STATIC_DRAW);
        
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(0);
        
        glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
        glEnableVertexAttribArray(1);
        
        int texSize = 256;
        SDL_Surface* waterSurface = SDL_CreateRGBSurface(0, texSize, texSize, 32, 0xFF, 0xFF00, 0xFF0000, 0xFF000000);
        
        for(int y = 0; y < texSize; y++) {
            for(int x = 0; x < texSize; x++) {
                Uint8 blue = 180 + (rand() % 50);
                Uint8 green = 130 + (rand() % 40);
                Uint8 red = 100 + (rand() % 30);
                Uint32 color = (blue << 16) | (green << 8) | red;
                ((Uint32*)waterSurface->pixels)[y*texSize + x] = color;
            }
        }
        
        glGenTextures(1, &textureId);
        glBindTexture(GL_TEXTURE_2D, textureId);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, waterSurface->w, waterSurface->h, 
                    0, GL_RGBA, GL_UNSIGNED_BYTE, waterSurface->pixels);
        
        SDL_FreeSurface(waterSurface);
        
        poolSize = 0.2f;  
        maxPoolSize = 3.0f;  
        this->indices = indices;
    }
    
    void update(float deltaTime, int raindropsHit) {
        
        float growAmount = raindropsHit * 0.002f * deltaTime;  
        
        if (poolSize > maxPoolSize * 0.5f) {
            growAmount *= (maxPoolSize - poolSize) / (maxPoolSize * 0.5f);
        }
        
        poolSize += growAmount;
        if (poolSize > maxPoolSize)
            poolSize = maxPoolSize;
    }
    
    void draw(glm::mat4 viewProj, float time) {
        if (poolSize < 0.1f) return; 
        
        shader.useProgram();
        
        GLint viewProjLoc = glGetUniformLocation(shader.id(), "viewProj");
        glUniformMatrix4fv(viewProjLoc, 1, GL_FALSE, glm::value_ptr(viewProj));
        
        GLint timeLoc = glGetUniformLocation(shader.id(), "time");
        glUniform1f(timeLoc, time);
        
        GLint sizeLoc = glGetUniformLocation(shader.id(), "size");
        glUniform1f(sizeLoc, poolSize);
        
        GLint poolSizeLoc = glGetUniformLocation(shader.id(), "poolSize");
        glUniform1f(poolSizeLoc, poolSize);
        
        GLint texLoc = glGetUniformLocation(shader.id(), "reflectionTexture");
        glUniform1i(texLoc, 0);
        
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureId);
        
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
        glDepthMask(GL_FALSE); 
        
        glBindVertexArray(vao);
        glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
        
        glDepthMask(GL_TRUE);  
        glDisable(GL_BLEND);
    }
    
    void release() {
        if (vbo) glDeleteBuffers(1, &vbo);
        if (vao) glDeleteVertexArrays(1, &vao);
        if (ebo) glDeleteBuffers(1, &ebo);
        if (textureId) glDeleteTextures(1, &textureId);
    }
    
    float getSize() const { return poolSize; }
    
private:
    GLuint vao = 0, vbo = 0, ebo = 0;
    GLuint textureId = 0;
    gl::ShaderProgram shader;
    float poolSize = 0.2f;  
    float maxPoolSize = 3.0f;  
    std::vector<unsigned int> indices;
};

class Game : public gl::GLObject {
    WaterEmiter emiter;
    Floor floor;
    WaterPool pool;
    float cameraZoom = 7.0f;       
    float zoomSpeed = 0.2f;        
    float cameraRotation = 0.0f;   
    float rotationSpeed = 0.1f;
public:
    Game() = default;
    virtual ~Game() override {}

    void load(gl::GLWindow *win) override {
        font.loadFont(win->util.getFilePath("data/font.ttf"), 36);
        emiter.load(win);
        floor.load(win);
        pool.load(win);
    }

    void draw(gl::GLWindow *win) override {
        glClearColor(0.18f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        
        Uint32 currentTime = SDL_GetTicks();
        float deltaTime = (currentTime - lastUpdateTime) / 1000.0f;
        lastUpdateTime = currentTime;
        
        update(deltaTime);
        emiter.setCameraDistance(cameraZoom);
        emiter.setCameraRotation(cameraRotation);
        
        float camX = sin(cameraRotation) * cameraZoom;
        float camZ = cos(cameraRotation) * cameraZoom;
        
        glm::mat4 projection = glm::perspective(glm::radians(45.0f),
                                             (float)win->w / (float)win->h,
                                             0.1f, 100.0f);
        glm::mat4 view = glm::lookAt(
            glm::vec3(camX, 2.0f, camZ),
            glm::vec3(0.0f, 0.0f, 0.0f),
            glm::vec3(0.0f, 1.0f, 0.0f)
        );
        
        glm::mat4 viewProj = projection * view;
        
        glEnable(GL_DEPTH_TEST);
        floor.draw(viewProj);
        pool.draw(viewProj, currentTime / 1000.0f);
        emiter.draw(win);
        win->text.setColor({255, 255, 255, 255});
        win->text.printText_Solid(font, 25.0f, 25.0f,
            "Rain Demo - Zoom: " + std::to_string(cameraZoom) +
            " Pool: " + std::to_string(int(pool.getSize() * 100)) + "%");
    }
    
    void event(gl::GLWindow *win, SDL_Event &e) override {
        if (e.type == SDL_KEYDOWN) {
            switch(e.key.keysym.sym) {
                case SDLK_UP:    
                    cameraZoom -= zoomSpeed;
                    if (cameraZoom < 0.5f) cameraZoom = 0.5f;
                    break;
                case SDLK_DOWN:  
                    cameraZoom += zoomSpeed;
                    if (cameraZoom > 10.0f) cameraZoom = 10.0f;
                    break;
                case SDLK_LEFT:  
                    cameraRotation += rotationSpeed;
                    if (cameraRotation > 2.0f * M_PI) 
                        cameraRotation -= 2.0f * M_PI;  
                    break;
                case SDLK_RIGHT: 
                    cameraRotation -= rotationSpeed;
                    if (cameraRotation < 0.0f) 
                        cameraRotation += 2.0f * M_PI;  
                    break;
                default:
                    break;
            }
        }
    }

    void update(float deltaTime) {
        
        emiter.update(deltaTime);
        
        
        int hitCount = emiter.getRainHitCount();
        
        
        pool.update(deltaTime, hitCount);
        
        
        floor.increaseWetness(hitCount * 0.0005f * deltaTime);
    }
private:
    mx::Font font;
    Uint32 lastUpdateTime = SDL_GetTicks();
    gl::GLSprite bg;
    gl::ShaderProgram bg_shader;
    
};

class MainWindow : public gl::GLWindow {
public:
    MainWindow(std::string path, int tw, int th) : gl::GLWindow("Skeleton", tw, th) {
        setPath(path);
        setObject(new Game());
        object->load(this);
    }
    
    ~MainWindow() override {}
    
    virtual void event(SDL_Event &e) override {}
    
    virtual void draw() override {
        glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        glEnable(GL_DEPTH_TEST);
        glViewport(0, 0, w, h);
        object->draw(this);
        swap();
        delay();
    }
};

MainWindow *main_w = nullptr;
void eventProc() {
    main_w->proc();
}

int main(int argc, char **argv) {
#ifdef __EMSCRIPTEN__  
    try {
        MainWindow main_window("",1920, 1080);
        main_w = &main_window;
        emscripten_set_main_loop(eventProc, 0, 1);
    } catch(const mx::Exception &e) {
        mx::system_err << "mx: Exception: " << e.text() << "\n";
        mx::system_err.flush();
        exit(EXIT_FAILURE);
    }           
#else
    Arguments args = proc_args(argc, argv);
    try {
        MainWindow main_window(args.path, args.width, args.height);
        main_window.loop();
    } catch(const mx::Exception &e) {
        mx::system_err << "mx: Exception: " << e.text() << "\n";
        mx::system_err.flush();
        exit(EXIT_FAILURE);
    }
#endif
    return 0;
}