← index
libmx2/libmx/gl_work/stars.cpp
Source: libmx2/libmx/gl_work/stars.cpp
#include "mx.hpp"
#include "argz.hpp"

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

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

#include <random>
#include <string>
#include <vector>
#include <iostream>

#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif

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

#ifndef __EMSCRIPTEN__
const char* vertSource = R"(#version 330 core

layout (location = 0) in vec3 inPosition; 
layout (location = 1) in float inSize;
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;  

out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    gl_PointSize = inSize;
    fragColor = inColor;
}
)";

const char* fragSource = R"(#version 330 core

in vec4 fragColor;
out vec4 FragColor;

uniform sampler2D spriteTexture;

void main() {
    vec4 texColor = texture(spriteTexture, gl_PointCoord);
    
    if (texColor.a < 0.01) {
        discard;
    }
    
    float dist = length(gl_PointCoord - vec2(0.5));
    float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
    
    FragColor = vec4(fragColor.rgb * texColor.rgb, fragColor.a * texColor.a * alpha);
}
)";

const char* lineVertSource = R"(#version 330 core
layout (location = 0) in vec3 inPosition; 
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;  
out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    fragColor = inColor;
}
)";

const char* lineFragSource = R"(#version 330 core
in vec4 fragColor;
out vec4 FragColor;

void main() {
    FragColor = fragColor;
}
)";
#else
const char* vertSource = R"(#version 300 es
precision highp float;

layout (location = 0) in vec3 inPosition; 
layout (location = 1) in float inSize;
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;

out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    gl_PointSize = inSize;
    fragColor = inColor;
}
)";

const char* fragSource = R"(#version 300 es
precision highp float;

in vec4 fragColor;
out vec4 FragColor;

uniform sampler2D spriteTexture;

void main() {
    vec4 texColor = texture(spriteTexture, gl_PointCoord);
    
    if (texColor.a < 0.01) {
        discard;
    }
    
    float dist = length(gl_PointCoord - vec2(0.5));
    float alpha = 1.0 - smoothstep(0.0, 0.5, dist);
    
    FragColor = vec4(fragColor.rgb * texColor.rgb, fragColor.a * texColor.a * alpha);
}
)";

const char* lineVertSource = R"(#version 300 es
precision highp float;
layout (location = 0) in vec3 inPosition; 
layout (location = 2) in vec4 inColor;

uniform mat4 MVP;
out vec4 fragColor;

void main() {
    gl_Position = MVP * vec4(inPosition, 1.0);
    fragColor = inColor;
}
)";

const char* lineFragSource = R"(#version 300 es
precision highp float;
in vec4 fragColor;
out vec4 FragColor;

void main() {
    FragColor = fragColor;
}
)";
#endif

float generateRandomFloat(float min, float max) {
    static std::random_device rd; 
    static std::default_random_engine eng(rd()); 
    std::uniform_real_distribution<float> dist(min, max);
    return dist(eng);
}

class StarField : public gl::GLObject {
public:
    struct Star {
        float x, y, z;   
        float vx, vy, vz; 
        float magnitude;     
        float temperature;   
        float twinkle;
        float size;
        int starType;        
        bool isConstellation;
        
        float origX, origY, origZ;  
        float explodeVx, explodeVy, explodeVz;
        float explosionDelay;  
        float brightness;
        
        float rotationSpeed;     
        float rotationAxisX;     
        float rotationAxisY;     
        float rotationAxisZ;     
        float currentRotation;   
    };

    static constexpr int NUM_STARS = 35000;  
    gl::ShaderProgram program;      
    gl::ShaderProgram lineProgram;  
    GLuint VAO, VBO[3];
    GLuint texture;
    std::vector<Star> stars;
    Uint32 lastUpdateTime = 0;
    
    float cameraX = 0.0f, cameraY = 0.0f, cameraZ = 0.0f;
    float cameraYaw = 0.0f, cameraPitch = 0.0f;
    float cameraSpeed = 25.0f;
    
    float rotationX = 0.0f;
    float rotationY = 0.0f;
    float rotationZ = 0.0f;
    float rotationSpeed = 1.0f;  
    
    float atmosphericTwinkle = 1.0f;
    float lightPollution = 0.1f;  
    GLuint lineVAO, lineVBO;
    std::vector<float> lineVertices;
    float connectionDistance = 25.0f;  
    float lineOpacity = 0.5f;          
    int maxConnections = 5;            
    
    bool isExploding = false;
    float explosionTime = 0.0f;
    float explosionDuration = 15.0f;  
    float explosionForce = 125.0f;    
    bool continuousExplosion = false;
    
    float shockwaveRadius = 0.0f;
    float coreCollapse = 0.0f;
    bool showCore = true;
    
    StarField() : stars(NUM_STARS) {}

    ~StarField() override {
        glDeleteVertexArrays(1, &VAO);
        glDeleteBuffers(3, VBO);
        glDeleteVertexArrays(1, &lineVAO);
        glDeleteBuffers(1, &lineVBO);
        glDeleteTextures(1, &texture);
    }

    void load(gl::GLWindow *win) override {
        if(!program.loadProgramFromText(vertSource, fragSource)) {
            throw mx::Exception("Error loading shader");
        }
        
        if(!lineProgram.loadProgramFromText(lineVertSource, lineFragSource)) {
            throw mx::Exception("Error loading line shader");
        }

        for (int i = 0; i < NUM_STARS; ++i) {
            Star star;
            
            float theta = generateRandomFloat(0.0f, 2.0f * M_PI);
            float phi = acos(generateRandomFloat(-1.0f, 1.0f));
            float radius = generateRandomFloat(50.0f, 200.0f);
            
            star.x = radius * sin(phi) * cos(theta);
            star.y = radius * sin(phi) * sin(theta);
            star.z = radius * cos(phi);
            
            star.origX = star.x;
            star.origY = star.y;
            star.origZ = star.z;
            
            star.vx = generateRandomFloat(-0.001f, 0.001f);
            star.vy = generateRandomFloat(-0.001f, 0.001f);
            star.vz = generateRandomFloat(-0.001f, 0.001f);
            
            float len = sqrt(star.x * star.x + star.y * star.y + star.z * star.z);
            
            float randomFactor = generateRandomFloat(0.5f, 1.5f);
            star.explodeVx = (star.x / len) * explosionForce * randomFactor;
            star.explodeVy = (star.y / len) * explosionForce * randomFactor;
            star.explodeVz = (star.z / len) * explosionForce * randomFactor;
            
            float tangentialForce = explosionForce * 0.3f;
            star.explodeVx += generateRandomFloat(-tangentialForce, tangentialForce);
            star.explodeVy += generateRandomFloat(-tangentialForce, tangentialForce);
            star.explodeVz += generateRandomFloat(-tangentialForce, tangentialForce);
            
            star.explosionDelay = generateRandomFloat(0.0f, 0.5f);
            star.brightness = generateRandomFloat(0.5f, 2.0f);
            
            star.rotationSpeed = generateRandomFloat(50.0f, 500.0f);  
            
            star.rotationAxisX = generateRandomFloat(-1.0f, 1.0f);
            star.rotationAxisY = generateRandomFloat(-1.0f, 1.0f);
            star.rotationAxisZ = generateRandomFloat(-1.0f, 1.0f);
            float axisLen = sqrt(star.rotationAxisX * star.rotationAxisX + 
                               star.rotationAxisY * star.rotationAxisY + 
                               star.rotationAxisZ * star.rotationAxisZ);
            if (axisLen > 0.001f) {
                star.rotationAxisX /= axisLen;
                star.rotationAxisY /= axisLen;
                star.rotationAxisZ /= axisLen;
            } else {
                star.rotationAxisX = 0.0f;
                star.rotationAxisY = 1.0f;
                star.rotationAxisZ = 0.0f;
            }
            
            star.currentRotation = 0.0f;
            
            star.magnitude = generateRandomFloat(-1.5f, 6.5f);
            star.temperature = generateRandomFloat(2000.0f, 40000.0f);
            star.size = magnitudeToSize(star.magnitude);
            star.twinkle = generateRandomFloat(0.5f, 2.0f);
            
            star.starType = 0;
            if (generateRandomFloat(0.0f, 1.0f) < 0.1f) {
                star.starType = 1; 
            } else if (generateRandomFloat(0.0f, 1.0f) < 0.05f) {
                star.starType = 2; 
            }
            
            star.isConstellation = (generateRandomFloat(0.0f, 1.0f) < 0.01f);
            
            stars[i] = star;
        }

        glGenVertexArrays(1, &VAO);
        glGenBuffers(3, VBO);
        glBindVertexArray(VAO);
        
        glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
        glBufferData(GL_ARRAY_BUFFER, NUM_STARS * 3 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, (void*)0);
        
        glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
        glBufferData(GL_ARRAY_BUFFER, NUM_STARS * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(1);
        glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, 0, (void*)0);
        
        glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
        glBufferData(GL_ARRAY_BUFFER, NUM_STARS * 4 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 0, (void*)0);
        
        glGenVertexArrays(1, &lineVAO);
        glGenBuffers(1, &lineVBO);
        
        glBindVertexArray(lineVAO);
        glBindBuffer(GL_ARRAY_BUFFER, lineVBO);
        glBufferData(GL_ARRAY_BUFFER, NUM_STARS * maxConnections * 14 * sizeof(float), nullptr, GL_DYNAMIC_DRAW);
        
        glEnableVertexAttribArray(0);
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)0);
        glEnableVertexAttribArray(2);
        glVertexAttribPointer(2, 4, GL_FLOAT, GL_FALSE, 7 * sizeof(float), (void*)(3 * sizeof(float)));
        
        texture = gl::loadTexture(win->util.getFilePath("data/star.png"));
        lastUpdateTime = SDL_GetTicks();
    }
    
    void triggerExplosion() {
        isExploding = true;
        continuousExplosion = true;
        explosionTime = 0.0f;
        shockwaveRadius = 0.0f;
        coreCollapse = 0.0f;
        showCore = true;
        printf("SUPERNOVA! Core collapse initiated...\n");
    }
    
    void resetPositions() {
        for (auto& star : stars) {
            star.x = star.origX;
            star.y = star.origY;
            star.z = star.origZ;
            star.currentRotation = 0.0f;  
        }
        isExploding = false;
        continuousExplosion = false;
        explosionTime = 0.0f;
        shockwaveRadius = 0.0f;
        showCore = false;
        printf("Stars reset to original positions\n");
    }
    
    void event(gl::GLWindow *win, SDL_Event &e) override {

    }
    
    void draw(gl::GLWindow *win) override {
#ifndef __EMSCRIPTEN__
        glEnable(GL_PROGRAM_POINT_SIZE);
#endif
        glDisable(GL_DEPTH_TEST);
        glEnable(GL_BLEND);
        glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

        Uint32 currentTime = SDL_GetTicks();
        float deltaTime = (currentTime - lastUpdateTime) / 1000.0f; 
        lastUpdateTime = currentTime;

        update(deltaTime);

        glm::mat4 projection = glm::perspective(
            glm::radians(60.0f),  
            (float)win->w / (float)win->h, 
            0.1f, 
            1000.0f
        );

        glm::vec3 front;
        front.x = cos(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front.y = sin(glm::radians(cameraPitch));
        front.z = sin(glm::radians(cameraYaw)) * cos(glm::radians(cameraPitch));
        front = glm::normalize(front);
        glm::vec3 cameraPos(cameraX, cameraY, cameraZ);
        glm::vec3 cameraTarget = cameraPos + front;
        glm::vec3 up(0.0f, 1.0f, 0.0f);
        glm::mat4 view = glm::lookAt(cameraPos, cameraTarget, up);
        
        glm::mat4 model = glm::mat4(1.0f);
        model = glm::rotate(model, glm::radians(rotationX), glm::vec3(1.0f, 0.0f, 0.0f)); 
        model = glm::rotate(model, glm::radians(rotationY), glm::vec3(0.0f, 1.0f, 0.0f)); 
        model = glm::rotate(model, glm::radians(rotationZ), glm::vec3(0.0f, 0.0f, 1.0f)); 
        
        glm::mat4 MVP = projection * view * model;
        
        if (lineVertices.size() > 0) {
            lineProgram.useProgram();
            lineProgram.setUniform("MVP", MVP);
            glBindVertexArray(lineVAO);
            glLineWidth(5.0f);  
            glDrawArrays(GL_LINES, 0, lineVertices.size() / 7);
        }
        
        program.useProgram();
        program.setUniform("MVP", MVP);
        program.setUniform("spriteTexture", 0);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, texture);
        glBindVertexArray(VAO);
        glDrawArrays(GL_POINTS, 0, NUM_STARS);
    }
    
    void update(float deltaTime) {
        if(deltaTime > 0.1f) 
            deltaTime = 0.1f;

        if (isExploding || continuousExplosion) {
            explosionTime += deltaTime;
            
            if (explosionTime < 1.0f) {
                coreCollapse = explosionTime / 1.0f;
            } else {
                coreCollapse = 1.0f;
            }
            
            shockwaveRadius = explosionTime * 200.0f; 
        }

        std::vector<float> positions;
        std::vector<float> sizes;
        std::vector<float> colors;
        positions.reserve(NUM_STARS * 3);
        sizes.reserve(NUM_STARS);
        colors.reserve(NUM_STARS * 4);

        float time = SDL_GetTicks() * 0.001f;

        for (auto& star : stars) {
            if (isExploding || continuousExplosion) {
                float starExplosionTime = explosionTime - star.explosionDelay;
                
                if (starExplosionTime > 0.0f) {
                    star.currentRotation += star.rotationSpeed * deltaTime;
                    if (star.currentRotation > 360.0f) {
                        star.currentRotation -= 360.0f;
                    }
                    
                    if (starExplosionTime < 1.0f) {
                        float collapseAmount = 0.95f; 
                        star.x = star.origX + (star.origX * (collapseAmount - 1.0f)) * starExplosionTime;
                        star.y = star.origY + (star.origY * (collapseAmount - 1.0f)) * starExplosionTime;
                        star.z = star.origZ + (star.origZ * (collapseAmount - 1.0f)) * starExplosionTime;
                    } else {
                        float explosionPhaseTime = starExplosionTime - 1.0f;
                        
                        float acceleration = 1.0f;
                        if (explosionPhaseTime < 2.0f) {
                            acceleration = 1.0f + explosionPhaseTime * 2.0f; 
                        }
                        
                        star.x += star.explodeVx * deltaTime * acceleration;
                        star.y += star.explodeVy * deltaTime * acceleration;
                        star.z += star.explodeVz * deltaTime * acceleration;
                    }
                    
                    float offsetRadius = 2.0f;  
                    
                    float angle = glm::radians(star.currentRotation);
                    float cosAngle = cos(angle);
                    float sinAngle = sin(angle);
                    
                    float ax = star.rotationAxisX;
                    float ay = star.rotationAxisY;
                    float az = star.rotationAxisZ;
                    
                    float vx = offsetRadius;
                    float vy = 0.0f;
                    float vz = 0.0f;
                    
                    float dotProduct = ax * vx + ay * vy + az * vz;
                    float crossX = ay * vz - az * vy;
                    float crossY = az * vx - ax * vz;
                    float crossZ = ax * vy - ay * vx;
                    
                    float rotatedX = vx * cosAngle + crossX * sinAngle + ax * dotProduct * (1.0f - cosAngle);
                    float rotatedY = vy * cosAngle + crossY * sinAngle + ay * dotProduct * (1.0f - cosAngle);
                    float rotatedZ = vz * cosAngle + crossZ * sinAngle + az * dotProduct * (1.0f - cosAngle);
                    
                    star.x += rotatedX * 0.1f;  
                    star.y += rotatedY * 0.1f;
                    star.z += rotatedZ * 0.1f;
                }
            } else {
                star.x += star.vx * deltaTime;
                star.y += star.vy * deltaTime;
                star.z += star.vz * deltaTime;
            }

            positions.push_back(star.x);
            positions.push_back(star.y);
            positions.push_back(star.z);

            float twinkleFactor = 1.0f;
            if (atmosphericTwinkle > 0.0f && !isExploding) {
                twinkleFactor = 0.7f + 0.3f * sin(time * star.twinkle) * atmosphericTwinkle;
            }

            float size = star.size * twinkleFactor;
            if (star.isConstellation) {
                size *= 1.2f; 
            }
            
            float distFromOrigin = sqrt(star.x * star.x + star.y * star.y + star.z * star.z);
            
            if (isExploding || continuousExplosion) {
                float starExplosionTime = explosionTime - star.explosionDelay;
                
                if (starExplosionTime > 0.0f) {
                    if (starExplosionTime < 1.0f) {
                        size *= (1.0f + starExplosionTime * 3.0f) * star.brightness;
                    } else {
                        float explosionPhaseTime = starExplosionTime - 1.0f;
                        
                        if (explosionPhaseTime < 2.0f) {
                            size *= (4.0f + explosionPhaseTime * 2.0f) * star.brightness;
                        } else {
                            float expansionProgress = glm::clamp(explosionPhaseTime / 10.0f, 0.0f, 1.0f);
                            size *= (3.0f + expansionProgress * 2.0f);
                        }
                    }
                    
                    float rotationPulse = 1.0f + 0.2f * sin(glm::radians(star.currentRotation * 2.0f));
                    size *= rotationPulse;
                }
                
                if (distFromOrigin > 500.0f) {
                    float fadeStart = 500.0f;
                    float fadeEnd = 2000.0f; 
                    float fadeFactor = 1.0f - glm::clamp((distFromOrigin - fadeStart) / (fadeEnd - fadeStart), 0.0f, 1.0f);
                    size *= fadeFactor;
                }
            }
            
            sizes.push_back(size);

            glm::vec3 starColor = getStarColor(star.temperature);
            
            if (isExploding || continuousExplosion) {
                float starExplosionTime = explosionTime - star.explosionDelay;
                
                if (starExplosionTime > 0.0f && starExplosionTime < 3.0f) {
                    float flashIntensity = glm::clamp(1.0f - starExplosionTime / 3.0f, 0.0f, 1.0f);
                    starColor.r = glm::mix(starColor.r, 1.0f, flashIntensity * 0.8f);
                    starColor.g = glm::mix(starColor.g, 1.0f, flashIntensity * 0.8f);
                    starColor.b = glm::mix(starColor.b, 1.0f, flashIntensity);
                    
                    float rotationHue = star.currentRotation / 360.0f;
                    starColor.r += sin(rotationHue * M_PI * 2.0f) * 0.1f;
                    starColor.g += sin(rotationHue * M_PI * 2.0f + M_PI * 0.66f) * 0.1f;
                    starColor.b += sin(rotationHue * M_PI * 2.0f + M_PI * 1.33f) * 0.1f;
                    
                    starColor.r = glm::clamp(starColor.r, 0.0f, 1.0f);
                    starColor.g = glm::clamp(starColor.g, 0.0f, 1.0f);
                    starColor.b = glm::clamp(starColor.b, 0.0f, 1.0f);
                }
            }
            
            float alpha = magnitudeToAlpha(star.magnitude) * twinkleFactor;
            
            if (isExploding || continuousExplosion) {
                float starExplosionTime = explosionTime - star.explosionDelay;
                
                if (starExplosionTime > 0.0f) {
                    if (starExplosionTime < 1.0f) {
                        alpha *= (1.0f + starExplosionTime * 5.0f) * star.brightness;
                    } else if (starExplosionTime < 3.0f) {
                        alpha *= 6.0f * star.brightness;
                    } else {
                        float fadeProgress = (starExplosionTime - 3.0f) / 12.0f;
                        alpha *= (1.0f - fadeProgress * 0.7f);
                    }
                }
                
                if (distFromOrigin > 500.0f) {
                    float fadeStart = 500.0f;
                    float fadeEnd = 2000.0f;
                    float fadeFactor = 1.0f - glm::clamp((distFromOrigin - fadeStart) / (fadeEnd - fadeStart), 0.0f, 1.0f);
                    alpha *= fadeFactor;
                }
                
                alpha = glm::clamp(alpha, 0.0f, 1.0f);
            }
            
            colors.push_back(starColor.r);
            colors.push_back(starColor.g);
            colors.push_back(starColor.b);
            colors.push_back(alpha);
        }

        lineVertices.clear();
        
        const float gridSize = connectionDistance;
        std::unordered_map<int, std::vector<int>> grid;
        
        for (int i = 0; i < NUM_STARS; i++) {
            int gridX = static_cast<int>(stars[i].x / gridSize);
            int gridY = static_cast<int>(stars[i].y / gridSize);
            int gridZ = static_cast<int>(stars[i].z / gridSize);
            int key = (gridX * 73856093) ^ (gridY * 19349663) ^ (gridZ * 83492791);
            grid[key].push_back(i);
        }
        
        for (int i = 0; i < NUM_STARS; i++) {
            if (stars[i].magnitude > 5.0f) continue;
            
            int connections = 0;
            
            int gridX = static_cast<int>(stars[i].x / gridSize);
            int gridY = static_cast<int>(stars[i].y / gridSize);
            int gridZ = static_cast<int>(stars[i].z / gridSize);
            
            for (int dx = -1; dx <= 1; dx++) {
                for (int dy = -1; dy <= 1; dy++) {
                    for (int dz = -1; dz <= 1; dz++) {
                        int key = ((gridX + dx) * 73856093) ^ 
                                  ((gridY + dy) * 19349663) ^ 
                                  ((gridZ + dz) * 83492791);
                        
                        if (grid.find(key) == grid.end()) continue;
                        
                        for (int neighborIdx : grid[key]) {
                            if (neighborIdx <= i) continue;
                            
                            auto& neighbor = stars[neighborIdx];
                            if (neighbor.magnitude > 5.0f) continue;
                            
                            float dx = stars[i].x - neighbor.x;
                            float dy = stars[i].y - neighbor.y;
                            float dz = stars[i].z - neighbor.z;
                            float distSq = dx*dx + dy*dy + dz*dz;
                            
                            float effectiveConnectionDist = connectionDistance;
                            if (isExploding || continuousExplosion) {
                                float explosionProgress = glm::clamp(explosionTime / 5.0f, 0.0f, 1.0f);
                                effectiveConnectionDist = connectionDistance * (1.0f + explosionProgress * 20.0f);
                            }
                            
                            if (distSq < effectiveConnectionDist * effectiveConnectionDist) {
                                float distance = sqrt(distSq);
                                float opacity = lineOpacity * (1.0f - distance / effectiveConnectionDist);
                                
                                if (isExploding || continuousExplosion) {
                                    if (explosionTime < 3.0f) {
                                        opacity *= (1.0f + explosionTime * 0.5f); 
                                    }
                                }
                                
                                float dist1 = sqrt(stars[i].x * stars[i].x + stars[i].y * stars[i].y + stars[i].z * stars[i].z);
                                float dist2 = sqrt(neighbor.x * neighbor.x + neighbor.y * neighbor.y + neighbor.z * neighbor.z);
                                
                                if (isExploding || continuousExplosion) {
                                    if (dist1 > 500.0f || dist2 > 500.0f) {
                                        float fadeStart = 500.0f;
                                        float fadeEnd = 2000.0f;
                                        float fadeFactor1 = 1.0f - glm::clamp((dist1 - fadeStart) / (fadeEnd - fadeStart), 0.0f, 1.0f);
                                        float fadeFactor2 = 1.0f - glm::clamp((dist2 - fadeStart) / (fadeEnd - fadeStart), 0.0f, 1.0f);
                                        opacity *= glm::min(fadeFactor1, fadeFactor2);
                                    }
                                }
                                
                                opacity = glm::clamp(opacity, 0.0f, 1.0f);
                                
                                lineVertices.push_back(stars[i].x);
                                lineVertices.push_back(stars[i].y);
                                lineVertices.push_back(stars[i].z);
                                
                                glm::vec3 color1 = getStarColor(stars[i].temperature);
                                
                                if (isExploding && explosionTime < 3.0f) {
                                    float flashIntensity = 1.0f - explosionTime / 3.0f;
                                    color1 = glm::mix(color1, glm::vec3(1.0f, 1.0f, 1.0f), flashIntensity * 0.5f);
                                }
                                
                                lineVertices.push_back(color1.r);
                                lineVertices.push_back(color1.g);
                                lineVertices.push_back(color1.b);
                                lineVertices.push_back(opacity);
                                
                                lineVertices.push_back(neighbor.x);
                                lineVertices.push_back(neighbor.y);
                                lineVertices.push_back(neighbor.z);
                                
                                glm::vec3 color2 = getStarColor(neighbor.temperature);
                                
                                if (isExploding && explosionTime < 3.0f) {
                                    float flashIntensity = 1.0f - explosionTime / 3.0f;
                                    color2 = glm::mix(color2, glm::vec3(1.0f, 1.0f, 1.0f), flashIntensity * 0.5f);
                                }
                                
                                lineVertices.push_back(color2.r);
                                lineVertices.push_back(color2.g);
                                lineVertices.push_back(color2.b);
                                lineVertices.push_back(opacity);
                                
                                connections++;
                                if (connections >= maxConnections) break;
                            }
                        }
                        if (connections >= maxConnections) break;
                    }
                    if (connections >= maxConnections) break;
                }
                if (connections >= maxConnections) break;
            }
        }

        glBindBuffer(GL_ARRAY_BUFFER, VBO[0]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, positions.size() * sizeof(float), positions.data());

        glBindBuffer(GL_ARRAY_BUFFER, VBO[1]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, sizes.size() * sizeof(float), sizes.data());

        glBindBuffer(GL_ARRAY_BUFFER, VBO[2]);
        glBufferSubData(GL_ARRAY_BUFFER, 0, colors.size() * sizeof(float), colors.data());
        
        glBindBuffer(GL_ARRAY_BUFFER, lineVBO);
        if (lineVertices.size() > 0) {
            glBufferData(GL_ARRAY_BUFFER, lineVertices.size() * sizeof(float), lineVertices.data(), GL_DYNAMIC_DRAW);
        }
    }

    glm::vec3 getStarColor(float temperature) {
        float col;
        col = generateRandomFloat(0.1f, 1.0f);
        return glm::vec3(col,col,col);
    }

    float magnitudeToSize(float magnitude) {
        return glm::clamp(15.0f - magnitude * 2.0f, 1.0f, 25.0f);
    }

    float magnitudeToAlpha(float magnitude) {
        float alpha = (6.5f - magnitude) / 6.5f; 
        return glm::clamp(alpha - lightPollution, 0.0f, 1.0f);
    }
};

class Game : public gl::GLObject {
public:
    mx::Font font;
    StarField field; 
    Game()  {}

    ~Game() override {
    }

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

    void draw(gl::GLWindow* win) override {
        field.draw(win);
        win->text.setColor({255, 0, 0, 255});
    }

    void event(gl::GLWindow* win, SDL_Event &e) override {
        const float mouseSensitivity = 0.1f;

        if (e.type == SDL_KEYDOWN) {
            switch (e.key.keysym.sym) {
                case SDLK_SPACE:  
                    field.triggerExplosion();
                    break;
                    
                case SDLK_BACKSPACE:  
                    field.resetPositions();
                    break;
                    
                case SDLK_w: case SDLK_LEFT:
                    field.cameraZ -= field.cameraSpeed * 0.1f;
                    break;
                case SDLK_s: case SDLK_RIGHT:
                    field.cameraZ += field.cameraSpeed * 0.1f;
                    break;
                case SDLK_a: case SDLK_DOWN:
                    field.cameraX -= field.cameraSpeed * 0.1f;
                    field.cameraSpeed += 0.5;
                    break;
                case SDLK_d: case SDLK_UP:
                    field.cameraX += field.cameraSpeed * 0.1f;
                    field.cameraSpeed -= 0.5;
                    break;
                case SDLK_q:
                    field.cameraY += field.cameraSpeed * 0.1f;
                    break;
                case SDLK_e:
                    field.cameraY -= field.cameraSpeed * 0.1f;
                    break;
                case SDLK_1:
                    field.lightPollution = 0.0f;
                    field.atmosphericTwinkle = 0.3f;
                    break;
                case SDLK_2:
                    field.lightPollution = 0.3f;
                    field.atmosphericTwinkle = 0.7f;
                    break;
                case SDLK_3:
                    field.lightPollution = 0.7f;
                    field.atmosphericTwinkle = 1.0f;
                    break;
                case SDLK_z:
                    field.cameraSpeed += 0.1f;
                    break;
                case SDLK_x:
                    field.cameraSpeed -= 0.1f;
                    break;
                
                case SDLK_i:  
                    field.rotationX += field.rotationSpeed;
                    break;
                case SDLK_k:  
                    field.rotationX -= field.rotationSpeed;
                    break;
                case SDLK_j:  
                    field.rotationY += field.rotationSpeed;
                    break;
                case SDLK_l:  
                    field.rotationY -= field.rotationSpeed;
                    break;
                case SDLK_u:  
                    field.rotationZ += field.rotationSpeed;
                    break;
                case SDLK_o:  
                    field.rotationZ -= field.rotationSpeed;
                    break;
                case SDLK_p:  
                    field.rotationX = 0.0f;
                    field.rotationY = 0.0f;
                    field.rotationZ = 0.0f;
                    break;
                case SDLK_LEFTBRACKET:  
                    field.rotationSpeed -= 0.5f;
                    if (field.rotationSpeed < 0.5f) field.rotationSpeed = 0.5f;
                    break;
                case SDLK_RIGHTBRACKET:  
                    field.rotationSpeed += 0.5f;
                    if (field.rotationSpeed > 10.0f) field.rotationSpeed = 10.0f;
                    break;
            
            case SDLK_r:
                field.connectionDistance += 1.0f;
                if (field.connectionDistance > 30.0f) field.connectionDistance = 30.0f;
                break;
            case SDLK_f:
                field.connectionDistance -= 1.0f;
                if (field.connectionDistance < 1.0f) field.connectionDistance = 1.0f;
                break;
                
            case SDLK_t:
                field.lineOpacity += 0.05f;
                if (field.lineOpacity > 1.0f) field.lineOpacity = 1.0f;
                break;
            case SDLK_g:
                field.lineOpacity -= 0.05f;
                if (field.lineOpacity < 0.0f) field.lineOpacity = 0.0f;
                break;
                
            case SDLK_y:
                field.maxConnections += 1;
                if (field.maxConnections > 10) field.maxConnections = 10;
                break;
            case SDLK_h:
                field.maxConnections -= 1;
                if (field.maxConnections < 1) field.maxConnections = 1;
                break;
            }
        }

        if (e.type == SDL_MOUSEMOTION && (e.motion.state & SDL_BUTTON_LMASK)) {
            field.cameraYaw += e.motion.xrel * mouseSensitivity;
            field.cameraPitch -= e.motion.yrel * mouseSensitivity;
        
            if (field.cameraPitch > 89.0f) field.cameraPitch = 89.0f;
            if (field.cameraPitch < -89.0f) field.cameraPitch = -89.0f;
        }
    }

    void update(float deltaTime) {
        CHECK_GL_ERROR();
    }
};

class MainWindow : public gl::GLWindow {
public:
    MainWindow(std::string path, int tw, int th) 
        : gl::GLWindow("Particle Effects [Universal]", tw, th) {
        setPath(path);
        setObject(new Game());
        object->load(this);
    }

    ~MainWindow() override {}

    void event(SDL_Event &e) override {
    }

    void draw() override {
        glClearColor(0.f, 0.f, 0.f, 1.f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        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__
    MainWindow main_window("/", 1920, 1080);
    main_w = &main_window;
    emscripten_set_main_loop(eventProc, 0, 1);

#else
    Argz<std::string> parser(argc, argv);
    parser.addOptionSingle('h', "Display help message")
          .addOptionSingleValue('p', "assets path")
          .addOptionDoubleValue('P', "path", "assets path")
          .addOptionSingleValue('r', "Resolution WidthxHeight")
          .addOptionDoubleValue('R', "resolution", "Resolution WidthxHeight");

    Argument<std::string> arg;
    std::string path;
    int value = 0;
    int tw = 1920, th = 1080;

    try {
        while((value = parser.proc(arg)) != -1) {
            switch(value) {
                case 'h':
                case 'v':
                    parser.help(std::cout);
                    exit(EXIT_SUCCESS);
                    break;
                case 'p':
                case 'P':
                    path = arg.arg_value;
                    break;
                case 'r':
                case 'R': {
                    auto pos = arg.arg_value.find("x");
                    if(pos == std::string::npos)  {
                        mx::system_err << "Error invalid resolution use WidthxHeight\n";
                        mx::system_err.flush();
                        exit(EXIT_FAILURE);
                    }
                    std::string left  = arg.arg_value.substr(0, pos);
                    std::string right = arg.arg_value.substr(pos + 1);
                    tw = atoi(left.c_str());
                    th = atoi(right.c_str());
                }
                break;
            }
        }
    } catch (const ArgException<std::string>& e) {
        mx::system_err << e.text() << "\n";
    }

    if(path.empty()) {
        mx::system_out << "mx: No path provided, trying default current directory.\n";
        path = ".";
    }

    try {
        MainWindow main_window(path, tw, th);
        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;
}