#include #include #include #include "glad/glad.h" #include #include "stb_image.h" #include #include #include #include #include #include #include #include #include #include "collisions.h" #include "player.h" #include "shader.h" #include "ghost.h" #include "text.h" #include "config.h" // Forward declarations for GLFW callbacks configured at start-up. void framebuffer_size_callback(GLFWwindow* window, int width, int height); void mouse_callback(GLFWwindow* window, double xpos, double ypos); void scroll_callback(GLFWwindow* window, double xoffset, double yoffset); void set_light_front(int xoffset, int yoffset); // player Player *player; // settings unsigned int WINDOWWIDTH = 1600; unsigned int WINDOWHEIGHT = 900; // camera bool first_frame = false; float timed = 0.0f; float last_frame = 0.0f; float camera_speed = 20.0f; float ghost_reset_distance = 10.0f; int ghost_count = 800; // Runtime configuration and palette for the minimap overlay. struct MinimapConfig { bool enabled = true; int width = 200; int height = 200; int margin = 16; float zoom = 2.0f; glm::vec3 wall_color = glm::vec3(0.0f, 0.0f, 0.0f); glm::vec3 background_color = glm::vec3(1.0f, 1.0f, 1.0f); glm::vec3 arrow_color = glm::vec3(1.0f, 0.0f, 0.0f); float breadcrumb_radius = 3.0f; glm::vec3 breadcrumb_color = glm::vec3(1.0f, 0.3f, 0.3f); float breadcrumb_max_distance = 200.0f; float ghost_radius = 4.0f; glm::vec3 ghost_fill_color = glm::vec3(0.9f, 0.9f, 1.0f); glm::vec3 ghost_border_color = glm::vec3(0.2f, 0.2f, 0.4f); }; struct MinimapSegment { glm::vec2 start; glm::vec2 end; }; struct MinimapBounds { float xmin = 0.0f; float xmax = 0.0f; float zmin = 0.0f; float zmax = 0.0f; }; struct TrailConfig { bool enabled = true; float lifetime_minutes = 3.0f; float sample_spacing = 1.0f; float width = 0.25f; float y_position = 0.06f; float simplify_epsilon = 0.15f; float reset_marker_size = 2.0f; glm::vec3 recent_color = glm::vec3(1.0f, 0.0f, 0.0f); glm::vec3 old_color = glm::vec3(0.35f, 0.18f, 0.05f); }; struct TrailPoint { glm::vec3 position; float timestamp; // True for the first point after spawn/reset; prevents drawing teleport segments. bool starts_segment; }; struct TrailVertex { glm::vec3 position; float timestamp; }; struct TrailResetMarker { glm::vec3 position; // Player yaw at reset time, used to orient the flattened marker sprite. float yaw; float timestamp; }; void render_minimap(const glm::vec3 &player_pos, float player_yaw, const std::deque &trail_points, const std::vector &ghosts); bool update_trail(std::deque &trail_points, const glm::vec3 &player_pos, float current_time); bool end_trail_segment(std::deque &trail_points, const glm::vec3 &player_pos, float current_time); bool begin_trail_segment(std::deque &trail_points, const glm::vec3 &player_pos, float current_time); void add_trail_reset_marker(std::deque &reset_markers, const glm::vec3 &player_pos, float player_yaw, float current_time); bool prune_trail_reset_markers(std::deque &reset_markers, float current_time); void render_trail_reset_markers(int ghost_program, unsigned int ghostVAO, const std::deque &reset_markers); void rebuild_trail_mesh(const std::deque &trail_points, std::vector &trail_vertices); float distance_xz(const glm::vec3 &a, const glm::vec3 &b); bool is_redundant_trail_point(const TrailPoint &a, const TrailPoint &b, const TrailPoint &c); glm::vec2 trail_segment_direction(const std::deque &trail_points, size_t from_ix, size_t to_ix); glm::vec3 trail_join_offset(const std::deque &trail_points, size_t point_ix, const glm::vec2 &fallback_normal, float half_width); MinimapConfig minimap_config; TrailConfig trail_config; std::vector minimap_segments; MinimapBounds minimap_bounds; unsigned int minimapVAO = 0; unsigned int minimapVBO = 0; int minimap_program = -1; const char *projectionC = "projection"; const char *viewC = "view"; const char *modelC = "model"; const char *viewposC = "viewPos"; const char *shininessC = "material.shininess"; const char *colorC = "material.color"; const char *lightposC = "light.position"; const char *directionC = "light.direction"; const char *largecutoffC = "light.largecutoff"; const char *smallcutoffC = "light.smallcutoff"; const char *ambientC = "light.ambient"; const char *diffuseC = "light.diffuse"; const char *specularC = "light.specular"; int num_walls; float *wall_vertices; int main(int argc, char *argv[]) { int success; glm::vec3 position; glm::vec3 start_position; float start_yaw; // Load configuration knobs before spinning up the world. Config::loadFromFile("ghostland.json"); camera_speed = Config::getFloat("camera_speed", camera_speed); ghost_reset_distance = Config::getFloat("ghost_reset_distance", ghost_reset_distance); minimap_config.enabled = Config::getInt("minimap_enabled", minimap_config.enabled ? 1 : 0) != 0; minimap_config.width = Config::getInt("minimap_width", minimap_config.width); minimap_config.height = Config::getInt("minimap_height", minimap_config.height); minimap_config.margin = Config::getInt("minimap_margin", minimap_config.margin); minimap_config.zoom = Config::getFloat("minimap_zoom", minimap_config.zoom); minimap_config.wall_color.r = Config::getFloat("minimap_wall_r", minimap_config.wall_color.r); minimap_config.wall_color.g = Config::getFloat("minimap_wall_g", minimap_config.wall_color.g); minimap_config.wall_color.b = Config::getFloat("minimap_wall_b", minimap_config.wall_color.b); minimap_config.background_color.r = Config::getFloat("minimap_background_r", minimap_config.background_color.r); minimap_config.background_color.g = Config::getFloat("minimap_background_g", minimap_config.background_color.g); minimap_config.background_color.b = Config::getFloat("minimap_background_b", minimap_config.background_color.b); minimap_config.arrow_color.r = Config::getFloat("minimap_arrow_r", minimap_config.arrow_color.r); minimap_config.arrow_color.g = Config::getFloat("minimap_arrow_g", minimap_config.arrow_color.g); minimap_config.arrow_color.b = Config::getFloat("minimap_arrow_b", minimap_config.arrow_color.b); minimap_config.breadcrumb_color.r = Config::getFloat("minimap_breadcrumb_r", minimap_config.breadcrumb_color.r); minimap_config.breadcrumb_color.g = Config::getFloat("minimap_breadcrumb_g", minimap_config.breadcrumb_color.g); minimap_config.breadcrumb_color.b = Config::getFloat("minimap_breadcrumb_b", minimap_config.breadcrumb_color.b); minimap_config.breadcrumb_radius = Config::getFloat("minimap_breadcrumb_radius", minimap_config.breadcrumb_radius); minimap_config.breadcrumb_max_distance = Config::getFloat("breadcrumb_max_distance", minimap_config.breadcrumb_max_distance); trail_config.enabled = Config::getInt("trail_enabled", trail_config.enabled ? 1 : 0) != 0; trail_config.lifetime_minutes = Config::getFloat("trail_lifetime_minutes", trail_config.lifetime_minutes); trail_config.sample_spacing = Config::getFloat("trail_sample_spacing", trail_config.sample_spacing); trail_config.width = Config::getFloat("trail_width", trail_config.width); trail_config.y_position = Config::getFloat("trail_y_offset", trail_config.y_position); trail_config.y_position = Config::getFloat("trail_y_position", trail_config.y_position); trail_config.simplify_epsilon = Config::getFloat("trail_simplify_epsilon", trail_config.simplify_epsilon); trail_config.reset_marker_size = Config::getFloat("trail_reset_marker_size", trail_config.reset_marker_size); trail_config.recent_color.r = Config::getFloat("trail_recent_r", trail_config.recent_color.r); trail_config.recent_color.g = Config::getFloat("trail_recent_g", trail_config.recent_color.g); trail_config.recent_color.b = Config::getFloat("trail_recent_b", trail_config.recent_color.b); trail_config.old_color.r = Config::getFloat("trail_old_r", trail_config.old_color.r); trail_config.old_color.g = Config::getFloat("trail_old_g", trail_config.old_color.g); trail_config.old_color.b = Config::getFloat("trail_old_b", trail_config.old_color.b); minimap_config.ghost_fill_color.r = Config::getFloat("minimap_ghost_fill_r", minimap_config.ghost_fill_color.r); minimap_config.ghost_fill_color.g = Config::getFloat("minimap_ghost_fill_g", minimap_config.ghost_fill_color.g); minimap_config.ghost_fill_color.b = Config::getFloat("minimap_ghost_fill_b", minimap_config.ghost_fill_color.b); minimap_config.ghost_border_color.r = Config::getFloat("minimap_ghost_border_r", minimap_config.ghost_border_color.r); minimap_config.ghost_border_color.g = Config::getFloat("minimap_ghost_border_g", minimap_config.ghost_border_color.g); minimap_config.ghost_border_color.b = Config::getFloat("minimap_ghost_border_b", minimap_config.ghost_border_color.b); minimap_config.ghost_radius = Config::getFloat("minimap_ghost_radius", minimap_config.ghost_radius); ghost_count = Config::getInt("ghost_count", ghost_count); // Maze file contains serialized spawn and wall mesh data generated by mazeparser.py. FILE *fp = fopen("maze.txt", "r"); float yaw; if (fscanf(fp, "%f %f %f %f\n", &position.x, &position.y, &position.z, &yaw) == EOF) { printf("1st fscanf failed.\n"); return -1; } start_position = position; start_yaw = yaw; player = new Player(position, yaw); if (fscanf(fp, "%d\n", &num_walls) == EOF) { printf("2nd fscanf failed.\n"); return -1; } player->num_walls = num_walls; // num surfaces * 6 (vertices per point) * 6 (floats per point) int num_wall_vertices = num_walls * 6 * 6; wall_vertices = (float *)calloc(sizeof(float), num_wall_vertices); float xmin_wall, xmax_wall, zmin_wall, zmax_wall; xmin_wall = 9999.9; xmax_wall = -9999.9; zmin_wall = 9999.9; zmax_wall = -9999.9; // read walls for (int i = 0; i < num_walls; i++) { glm::vec2 wall_floor_pts[2]; for (int j = 0; j < 6; j++) { int vix = i*6*6 + j*6; if (fscanf( fp, "%f %f %f %f %f %f\n", &wall_vertices[vix], &wall_vertices[vix+1], &wall_vertices[vix+2], &wall_vertices[vix+3], &wall_vertices[vix+4], &wall_vertices[vix+5] ) == EOF) { printf("3rd fscanf failed.\n"); return -1; } if (wall_vertices[vix] < xmin_wall) { xmin_wall = wall_vertices[vix]; } else if (wall_vertices[vix] > xmax_wall) { xmax_wall = wall_vertices[vix]; } if (wall_vertices[vix + 2] < zmin_wall) { zmin_wall = wall_vertices[vix + 2]; } else if (wall_vertices[vix + 2] > zmax_wall) { zmax_wall = wall_vertices[vix + 2]; } if (j < 2) { wall_floor_pts[j].x = wall_vertices[vix]; wall_floor_pts[j].y = wall_vertices[vix + 2]; if (j == 1) { minimap_segments.push_back({wall_floor_pts[0], wall_floor_pts[1]}); } } } if (fscanf(fp, "\n") == EOF) { printf("4th fscanf failed.\n"); return -1; } } player->wall_vertices = wall_vertices; minimap_bounds.xmin = xmin_wall; minimap_bounds.xmax = xmax_wall; minimap_bounds.zmin = zmin_wall; minimap_bounds.zmax = zmax_wall; // read floor float *floor_vertices = (float *)calloc(sizeof(float), 6*6); for (int j = 0; j < 6; j++) { int vix = j*6; if (fscanf( fp, "%f %f %f %f %f %f\n", &floor_vertices[vix], &floor_vertices[vix+1], &floor_vertices[vix+2], &floor_vertices[vix+3], &floor_vertices[vix+4], &floor_vertices[vix+5] ) == EOF) { printf("floor fscanf failed.\n"); return -1; } } fclose(fp); // Initialize GLFW/GLAD and windowing. glfwInit(); glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3); glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); GLFWmonitor *monitor = glfwGetPrimaryMonitor(); const GLFWvidmode *monitor_mode = glfwGetVideoMode(monitor); WINDOWWIDTH = monitor_mode->width; WINDOWHEIGHT = monitor_mode->height; //WINDOWWIDTH = 1280; //WINDOWHEIGHT = 960; GLFWwindow* window = glfwCreateWindow(WINDOWWIDTH, WINDOWHEIGHT, "Ghostland!", monitor, NULL); //GLFWwindow* window = glfwCreateWindow(WINDOWWIDTH, WINDOWHEIGHT, "Ghostland!", NULL, NULL); if (window == NULL) { printf("Failed to create GLFW window.\n"); glfwTerminate(); return -1; } glfwMakeContextCurrent(window); //glfwSetFramebufferSizeCallback(window, framebuffer_size_callback); glfwSetCursorPosCallback(window, mouse_callback); glfwSetScrollCallback(window, scroll_callback); glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) { printf("Failed to initialize GLAD.\n"); glfwTerminate(); return -1; } printf("OpenGL version: %s\n", glGetString(GL_VERSION)); printf("GLSL version: %s\n", glGetString(GL_SHADING_LANGUAGE_VERSION)); glEnable(GL_DEPTH_TEST); //glEnable(GL_CULL_FACE); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); int wall_program = create_shader_program("vertexshader.glsl", "fragmentshader.glsl"); if (wall_program < 0) { glfwTerminate(); return -1; } // Perhaps there's a better way of doing this, rather than re-compiling the same files? int floor_program = create_shader_program("vertexshader.glsl", "fragmentshader.glsl"); if (floor_program < 0) { glfwTerminate(); return -1; } // do stuff for walls unsigned int wallsVBO, wallsVAO; glGenVertexArrays(1, &wallsVAO); glGenBuffers(1, &wallsVBO); glBindVertexArray(wallsVAO); glBindBuffer(GL_ARRAY_BUFFER, wallsVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(float) * num_wall_vertices, wall_vertices, GL_STATIC_DRAW); // position attribute glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // normal attribute glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); // do stuff for floor unsigned int floorVBO, floorVAO; glGenVertexArrays(1, &floorVAO); glGenBuffers(1, &floorVBO); glBindVertexArray(floorVAO); glBindBuffer(GL_ARRAY_BUFFER, floorVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(float) * 6 * 6 * 1, floor_vertices, GL_STATIC_DRAW); // position attribute glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)0); glEnableVertexAttribArray(0); // normal attribute glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(float), (void*)(3 * sizeof(float))); glEnableVertexAttribArray(1); // The trail is a dynamic ribbon mesh. Each vertex carries the timestamp of // its source path sample so the shader can fade red-to-brown by age. unsigned int trailVBO, trailVAO; glGenVertexArrays(1, &trailVAO); glGenBuffers(1, &trailVBO); glBindVertexArray(trailVAO); glBindBuffer(GL_ARRAY_BUFFER, trailVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(TrailVertex), NULL, GL_DYNAMIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, sizeof(TrailVertex), (void *)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 1, GL_FLOAT, GL_FALSE, sizeof(TrailVertex), (void *)(3 * sizeof(float))); glEnableVertexAttribArray(1); int trail_program = create_shader_program("trailshader.glsl", "trailfragshader.glsl"); if (trail_program < 0) { glfwTerminate(); return -1; } std::deque trail_points; std::vector trail_vertices; std::deque trail_reset_markers; bool trail_dirty = update_trail(trail_points, player->get_pos(), 0.0f); // do stuff for ghosts // create program int ghost_program = create_shader_program("ghostshader.glsl", "ghostfragshader.glsl"); if (ghost_program < 0) { glfwTerminate(); return -1; } // ghost vertices float ghost_vertices[] = { -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, 0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 1.0f, 1.0f, -0.5f, 0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 1.0f, -0.5f, -0.5f, 0.0f, 0.0f, 0.0f, 1.0f, 0.0f, 0.0f, }; unsigned int ghostVBO, ghostVAO; glGenVertexArrays(1, &ghostVAO); glBindVertexArray(ghostVAO); glGenBuffers(1, &ghostVBO); glBindBuffer(GL_ARRAY_BUFFER, ghostVBO); glBufferData(GL_ARRAY_BUFFER, sizeof(ghost_vertices), ghost_vertices, GL_STATIC_DRAW); glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)0); glEnableVertexAttribArray(0); glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(3 * sizeof(float))); glEnableVertexAttribArray(1); glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 8 * sizeof(float), (void *)(6 * sizeof(float))); glEnableVertexAttribArray(2); // ghost texture unsigned int ghost_texture; glGenTextures(1, &ghost_texture); glBindTexture(GL_TEXTURE_2D, ghost_texture); // set the texture wrapping parameters //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_); //glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); // set texture filtering parameters glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); int width, height, numberChannels; //stbi_set_flip_vertically_on_load(true); unsigned char *ghost_data = stbi_load("ghost_facing_right.png", &width, &height, &numberChannels, 0); printf("Num channels: %d\n", numberChannels); if (!ghost_data) { printf("Failed to load texture!\n"); glfwTerminate(); return -1; } glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, ghost_data); glGenerateMipmap(GL_TEXTURE_2D); stbi_image_free(ghost_data); // do stuff for text int text_program; if (init_text(&text_program) < 0) { printf("Failed to load text module!\n"); glfwTerminate(); return -1; } if (minimap_config.enabled) { minimap_program = create_shader_program("minimapshader.glsl", "minimapfragshader.glsl"); if (minimap_program < 0) { glfwTerminate(); return -1; } glGenVertexArrays(1, &minimapVAO); glGenBuffers(1, &minimapVBO); glBindVertexArray(minimapVAO); glBindBuffer(GL_ARRAY_BUFFER, minimapVBO); size_t minimap_buffer_capacity = std::max(minimap_segments.size() * 4 + 12, 256); glBufferData(GL_ARRAY_BUFFER, minimap_buffer_capacity * sizeof(float), NULL, GL_DYNAMIC_DRAW); glEnableVertexAttribArray(0); glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(float), (void*)0); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); } // TODO move these into config file?? int time_secI = 0, num_frames = 1; float time_sec; glm::vec3 wall_color = glm::vec3(0.61f, 0.6f, 0.59f); float wall_shininess = 6.5; glm::vec3 floor_color = glm::vec3(0.11f, 0.1f, 0.09f); float floor_shininess = 3.25; glm::vec3 ambient = glm::vec3(0.007f, 0.006f, 0.005f); glm::vec3 diffuse = glm::vec3(0.61f, 0.60f, 0.59f); glm::vec3 specular = glm::vec3(0.11f, 0.10f, 0.09f); // Persistent danger tint state. The target can change abruptly as ghosts move, // so the displayed intensity is smoothed below to avoid camera flicker. float danger_tint_intensity = 0.0f; bool danger_tint_active = false; float largecutoff = glm::cos(glm::radians(30.0f)); float smallcutoff = glm::cos(glm::radians(7.5f)); std::vector ghosts; for (int i = 0; i < std::max(1, ghost_count); i++) { Ghost *ghost = new Ghost(xmin_wall, xmax_wall, zmin_wall, zmax_wall); // Ensure ghost is not within 30.0 units of start_position while (glm::length(ghost->get_pos() - start_position) <= 30.0f) { ghost->regenerate_position(); } ghosts.push_back(ghost); } player->mouse_callback(window, WINDOWWIDTH/2, WINDOWHEIGHT/2); int FPS = -1; // Main render/game loop. while (!glfwWindowShouldClose(window)) { float current_frame = static_cast(glfwGetTime()); int current_frameI = (int)current_frame; if (current_frameI != time_secI) { // show stats every second FPS = num_frames; glm::vec3 player_pos = player->get_pos(); float yaw, pitch; yaw = player->get_yaw(); pitch = player->get_pitch(); printf("FPS: %d\n", num_frames); printf("Player is at: (%f, %f, %f) facing (%f, %f)\n", player_pos.x, player_pos.y, player_pos.z, yaw, pitch); time_sec = current_frame; time_secI = current_frameI; num_frames = 1; } else { // otherwise increase number of frames num_frames++; } timed = current_frame - last_frame; last_frame = current_frame; if (!first_frame) { first_frame = true; continue; } player->process_input(window); glClearColor(0.0f, 0.0f, 0.0f, 1.0f); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glUseProgram(wall_program); float fov = player->get_fov(); glm::mat4 projection = glm::perspective(glm::radians(fov), (float)WINDOWWIDTH / (float)WINDOWHEIGHT, 0.1f, 256.0f); set_uniform(wall_program, projectionC, projection); glm::vec3 camera_pos, camera_front, camera_up; camera_pos = player->get_camera_pos(); camera_front = player->get_camera_front(); camera_up = player->get_camera_up(); glm::mat4 view = glm::lookAt(camera_pos, camera_pos + camera_front, camera_up); set_uniform(wall_program, viewC, view); glm::mat4 model = glm::mat4(1.0f); set_uniform(wall_program, modelC, model); // light position glm::vec3 light_pos = player->get_light_pos(); //printf("light_pos: (%f, %f, %f)\n", light_pos.x, light_pos.y, light_pos.z); glm::vec3 light_front = player->get_light_front(); //printf("light_pos: (%f, %f, %f)\n", light_front.x, light_front.y, light_front.z); glm::vec3 player_pos = player->get_pos(); if (trail_config.enabled) { trail_dirty = update_trail(trail_points, player_pos, current_frame) || trail_dirty; prune_trail_reset_markers(trail_reset_markers, current_frame); // Upload after sample/prune changes so the world trail VBO is ready // when rendered later in this frame. The minimap reads the deque. if (trail_dirty) { rebuild_trail_mesh(trail_points, trail_vertices); glBindBuffer(GL_ARRAY_BUFFER, trailVBO); glBufferData( GL_ARRAY_BUFFER, std::max(trail_vertices.size(), 1) * sizeof(TrailVertex), trail_vertices.empty() ? NULL : trail_vertices.data(), GL_DYNAMIC_DRAW ); trail_dirty = false; } } // Sort ghosts by distance to player for render-order optimization. struct { bool operator()(const Ghost *a, const Ghost *b) const { glm::vec3 a_diff, b_diff; a_diff = player->get_pos() - a->get_pos(); b_diff = player->get_pos() - b->get_pos(); float a_dist, b_dist; a_dist = glm::length(a_diff); b_dist = glm::length(b_diff); return a_dist < b_dist; } } ghost_compare; // Only the closest 20% need perfect ordering for blending, so partial_sort is cheaper than full sort. std::partial_sort(ghosts.begin(), ghosts.begin() + ghosts.size() / 5, ghosts.end(), ghost_compare); // Drive the danger tint from the actual nearest ghost. Use X/Z distance // so vertical ghost bobbing cannot push the warning in and out. float nearest_ghost_distance = distance_xz(ghosts[0]->get_pos(), player_pos); for (size_t i = 1; i < ghosts.size(); i++) { float ghost_distance = distance_xz(ghosts[i]->get_pos(), player_pos); if (ghost_distance < nearest_ghost_distance) { nearest_ghost_distance = ghost_distance; } } // Interpolate flashlight color based on nearest ghost distance. The // active band has hysteresis so tiny distance changes at the edge do // not flicker the tint on/off. // At 30.0+ units: (0.61, 0.60, 0.59) - normal color // At 10.0 units: (0.99, 0.60, 0.59) - red shift if (nearest_ghost_distance <= 30.0f) { danger_tint_active = true; } else if (nearest_ghost_distance >= 35.0f) { danger_tint_active = false; } float color_factor = 1.0f; if (danger_tint_active) { color_factor = (nearest_ghost_distance - 10.0f) / (30.0f - 10.0f); color_factor = glm::clamp(color_factor, 0.0f, 1.0f); } float target_danger_intensity = 1.0f - color_factor; if (danger_tint_active) { target_danger_intensity = std::max(target_danger_intensity, 0.35f); } // Attack quickly when danger rises, release more slowly when it falls. float tint_response = target_danger_intensity > danger_tint_intensity ? 16.0f : 4.0f; float tint_blend = 1.0f - exp(-tint_response * timed); danger_tint_intensity = glm::mix(danger_tint_intensity, target_danger_intensity, tint_blend); glm::vec3 normal_color = glm::vec3(0.61f, 0.60f, 0.59f); glm::vec3 danger_color = glm::vec3(0.99f, 0.60f, 0.59f); diffuse = glm::mix(normal_color, danger_color, danger_tint_intensity); // fragment requirements set_uniform(wall_program, viewposC, camera_pos); set_uniform(wall_program, shininessC, wall_shininess); set_uniform(wall_program, colorC, wall_color); set_uniform(wall_program, lightposC, light_pos); set_uniform(wall_program, directionC, light_front); set_uniform(wall_program, largecutoffC, largecutoff); set_uniform(wall_program, smallcutoffC, smallcutoff); set_uniform(wall_program, ambientC, ambient); set_uniform(wall_program, diffuseC, diffuse); set_uniform(wall_program, specularC, specular); glBindVertexArray(wallsVAO); glDrawArrays(GL_TRIANGLES, 0, num_wall_vertices); glUseProgram(floor_program); set_uniform(floor_program, projectionC, projection); set_uniform(floor_program, viewC, view); set_uniform(floor_program, modelC, model); set_uniform(floor_program, viewposC, camera_pos); // fragment requirements set_uniform(floor_program, shininessC, floor_shininess); set_uniform(floor_program, colorC, floor_color); set_uniform(floor_program, lightposC, light_pos); set_uniform(floor_program, directionC, light_front); set_uniform(floor_program, largecutoffC, largecutoff); set_uniform(floor_program, smallcutoffC, smallcutoff); set_uniform(floor_program, ambientC, ambient); set_uniform(floor_program, diffuseC, diffuse); set_uniform(floor_program, specularC, specular); glBindVertexArray(floorVAO); glDrawArrays(GL_TRIANGLES, 0, 6 * 6 * 1); glUseProgram(ghost_program); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, ghost_texture); set_uniform(ghost_program, projectionC, projection); set_uniform(ghost_program, viewC, view); bool player_reset_this_frame = false; for (int i = ghosts.size()/5; i >= 0; i--) { glm::mat4 ghost_model = ghosts[i]->get_model(camera_pos); ghosts[i]->apply_movement(current_frame, timed); // When ghosts close in, reset player back to the maze entrance. glm::vec3 ghost_pos = ghosts[i]->get_pos(); glm::vec3 player_pos = player->get_pos(); float distance = glm::length(ghost_pos - player_pos); if (!player_reset_this_frame && distance <= ghost_reset_distance) { glm::vec3 reset_pos = player->get_pos(); float reset_yaw = player->get_yaw(); if (trail_config.enabled) { trail_dirty = end_trail_segment(trail_points, reset_pos, current_frame) || trail_dirty; add_trail_reset_marker(trail_reset_markers, reset_pos, reset_yaw, current_frame); } player->reset_position(start_position, start_yaw); if (trail_config.enabled) { trail_dirty = begin_trail_segment(trail_points, start_position, current_frame) || trail_dirty; } player_reset_this_frame = true; } // Nudge ghosts away from the spawn area so the player gets a breather. float start_distance = glm::length(ghost_pos - start_position); if (start_distance <= 30.0f) { ghosts[i]->reverse_direction_from_position(start_position); } set_uniform(ghost_program, modelC, ghost_model); glBindVertexArray(ghostVAO); glDrawArrays(GL_TRIANGLES, 0, (3 + 3 + 2) * 6); } if (trail_config.enabled) { // Ghost resets can split the trail after the initial upload above. // Rebuild here before drawing so reset markers, world trail, and // minimap all reflect the same reset on the current frame. if (trail_dirty) { rebuild_trail_mesh(trail_points, trail_vertices); glBindBuffer(GL_ARRAY_BUFFER, trailVBO); glBufferData( GL_ARRAY_BUFFER, std::max(trail_vertices.size(), 1) * sizeof(TrailVertex), trail_vertices.empty() ? NULL : trail_vertices.data(), GL_DYNAMIC_DRAW ); trail_dirty = false; } if (!trail_vertices.empty()) { glUseProgram(trail_program); set_uniform(trail_program, projectionC, projection); set_uniform(trail_program, viewC, view); set_uniform(trail_program, modelC, glm::mat4(1.0f)); set_uniform(trail_program, "currentTime", current_frame); set_uniform(trail_program, "maxAge", std::max(0.001f, trail_config.lifetime_minutes * 60.0f)); set_uniform(trail_program, "recentColor", trail_config.recent_color); set_uniform(trail_program, "oldColor", trail_config.old_color); glBindVertexArray(trailVAO); glDrawArrays(GL_TRIANGLES, 0, trail_vertices.size()); } if (!trail_reset_markers.empty()) { glUseProgram(ghost_program); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, ghost_texture); set_uniform(ghost_program, projectionC, projection); set_uniform(ghost_program, viewC, view); render_trail_reset_markers(ghost_program, ghostVAO, trail_reset_markers); } } if (minimap_config.enabled) { render_minimap(player->get_pos(), player->get_yaw(), trail_points, ghosts); } if (FPS != -1) { glUseProgram(text_program); projection = glm::ortho(0.0f, static_cast(WINDOWWIDTH), 0.0f, static_cast(WINDOWHEIGHT)); set_uniform(text_program, projectionC, projection); char fps_buff[256]; sprintf(fps_buff, "FPS: %d", FPS); render_text(text_program, std::string(fps_buff), 25.0F, WINDOWHEIGHT - 35.0f, 0.5f, glm::vec3(1.0f, 0.0f, 0.0f)); } player->apply_movement(timed); glfwSwapBuffers(window); glfwPollEvents(); } glDeleteVertexArrays(1, &wallsVAO); glDeleteBuffers(1, &wallsVBO); glDeleteVertexArrays(1, &floorVAO); glDeleteBuffers(1, &floorVBO); glDeleteVertexArrays(1, &trailVAO); glDeleteBuffers(1, &trailVBO); glDeleteProgram(trail_program); if (minimap_config.enabled) { glDeleteVertexArrays(1, &minimapVAO); glDeleteBuffers(1, &minimapVBO); glDeleteProgram(minimap_program); } glfwTerminate(); return 0; } void framebuffer_size_callback(GLFWwindow* window, int width, int height) { glViewport(0, 0, width, height); } void mouse_callback(GLFWwindow* window, double xpos, double ypos) { player->mouse_callback(window, xpos, ypos); } void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { player->scroll_callback(window, xoffset, yoffset); } float distance_xz(const glm::vec3 &a, const glm::vec3 &b) { float dx = a.x - b.x; float dz = a.z - b.z; return sqrt(dx * dx + dz * dz); } bool is_redundant_trail_point(const TrailPoint &a, const TrailPoint &b, const TrailPoint &c) { if (trail_config.simplify_epsilon <= 0.0f) { return false; } glm::vec2 ab(b.position.x - a.position.x, b.position.z - a.position.z); glm::vec2 ac(c.position.x - a.position.x, c.position.z - a.position.z); float ac_len_sq = glm::dot(ac, ac); if (ac_len_sq <= 0.0001f) { return false; } float t = glm::dot(ab, ac) / ac_len_sq; if (t <= 0.0f || t >= 1.0f) { return false; } glm::vec2 closest = ac * t; return glm::length(ab - closest) <= trail_config.simplify_epsilon; } bool update_trail(std::deque &trail_points, const glm::vec3 &player_pos, float current_time) { if (!trail_config.enabled) { bool had_points = !trail_points.empty(); trail_points.clear(); return had_points; } bool dirty = false; // Keep a time window instead of a fixed sample count so trail length is // stable across different movement speeds and frame rates. float max_age = std::max(0.001f, trail_config.lifetime_minutes * 60.0f); while (!trail_points.empty() && current_time - trail_points.front().timestamp > max_age) { trail_points.pop_front(); dirty = true; } glm::vec3 sample_pos(player_pos.x, trail_config.y_position, player_pos.z); float min_spacing = std::max(0.001f, trail_config.sample_spacing); if (trail_points.empty() || distance_xz(sample_pos, trail_points.back().position) >= min_spacing) { TrailPoint new_point = {sample_pos, current_time, trail_points.empty()}; if (trail_points.size() >= 2 && !trail_points.back().starts_segment) { TrailPoint previous = trail_points.back(); TrailPoint before_previous = trail_points[trail_points.size() - 2]; if (!previous.starts_segment && is_redundant_trail_point(before_previous, previous, new_point)) { trail_points.pop_back(); } } trail_points.push_back(new_point); dirty = true; } return dirty; } bool end_trail_segment(std::deque &trail_points, const glm::vec3 &player_pos, float current_time) { glm::vec3 sample_pos(player_pos.x, trail_config.y_position, player_pos.z); if (trail_points.empty()) { trail_points.push_back({sample_pos, current_time, true}); return true; } if (distance_xz(sample_pos, trail_points.back().position) <= 0.0001f) { // If the last regular sample is already at the reset location, refresh // its age rather than adding a zero-length segment. trail_points.back().position = sample_pos; trail_points.back().timestamp = current_time; return true; } trail_points.push_back({sample_pos, current_time, false}); return true; } bool begin_trail_segment(std::deque &trail_points, const glm::vec3 &player_pos, float current_time) { glm::vec3 sample_pos(player_pos.x, trail_config.y_position, player_pos.z); if (!trail_points.empty() && trail_points.back().starts_segment && distance_xz(sample_pos, trail_points.back().position) <= 0.0001f) { trail_points.back().timestamp = current_time; return true; } TrailPoint new_point = {sample_pos, current_time, true}; trail_points.push_back(new_point); return true; } void add_trail_reset_marker(std::deque &reset_markers, const glm::vec3 &player_pos, float player_yaw, float current_time) { glm::vec3 marker_pos(player_pos.x, trail_config.y_position + 0.002f, player_pos.z); reset_markers.push_back({marker_pos, player_yaw, current_time}); } bool prune_trail_reset_markers(std::deque &reset_markers, float current_time) { bool dirty = false; float max_age = std::max(0.001f, trail_config.lifetime_minutes * 60.0f); while (!reset_markers.empty() && current_time - reset_markers.front().timestamp > max_age) { reset_markers.pop_front(); dirty = true; } return dirty; } void render_trail_reset_markers(int ghost_program, unsigned int ghostVAO, const std::deque &reset_markers) { if (trail_config.reset_marker_size <= 0.0f) { return; } glBindVertexArray(ghostVAO); for (const TrailResetMarker &marker : reset_markers) { glm::mat4 marker_model = glm::mat4(1.0f); marker_model = glm::translate(marker_model, marker.position); marker_model = glm::rotate(marker_model, glm::radians(90.0f - marker.yaw), glm::vec3(0.0f, 1.0f, 0.0f)); marker_model = glm::rotate(marker_model, glm::radians(90.0f), glm::vec3(1.0f, 0.0f, 0.0f)); marker_model = glm::scale(marker_model, glm::vec3(trail_config.reset_marker_size, trail_config.reset_marker_size, 1.0f)); set_uniform(ghost_program, modelC, marker_model); glDrawArrays(GL_TRIANGLES, 0, 6); } } glm::vec2 trail_segment_direction(const std::deque &trail_points, size_t from_ix, size_t to_ix) { glm::vec3 delta = trail_points[to_ix].position - trail_points[from_ix].position; glm::vec2 delta_xz(delta.x, delta.z); float len = glm::length(delta_xz); if (len <= 0.0001f) { return glm::vec2(0.0f, 0.0f); } return delta_xz / len; } glm::vec3 trail_join_offset(const std::deque &trail_points, size_t point_ix, const glm::vec2 &fallback_normal, float half_width) { bool has_prev = point_ix > 0 && !trail_points[point_ix].starts_segment; bool has_next = point_ix + 1 < trail_points.size() && !trail_points[point_ix + 1].starts_segment; if (!has_prev && !has_next) { return glm::vec3(fallback_normal.x * half_width, 0.0f, fallback_normal.y * half_width); } if (!has_prev || !has_next) { glm::vec2 dir = has_prev ? trail_segment_direction(trail_points, point_ix - 1, point_ix) : trail_segment_direction(trail_points, point_ix, point_ix + 1); if (glm::length(dir) <= 0.0001f) { return glm::vec3(fallback_normal.x * half_width, 0.0f, fallback_normal.y * half_width); } glm::vec2 normal(-dir.y, dir.x); return glm::vec3(normal.x * half_width, 0.0f, normal.y * half_width); } glm::vec2 prev_dir = trail_segment_direction(trail_points, point_ix - 1, point_ix); glm::vec2 next_dir = trail_segment_direction(trail_points, point_ix, point_ix + 1); if (glm::length(prev_dir) <= 0.0001f || glm::length(next_dir) <= 0.0001f) { return glm::vec3(fallback_normal.x * half_width, 0.0f, fallback_normal.y * half_width); } glm::vec2 prev_normal(-prev_dir.y, prev_dir.x); glm::vec2 next_normal(-next_dir.y, next_dir.x); glm::vec2 miter = prev_normal + next_normal; float miter_len = glm::length(miter); if (miter_len <= 0.0001f) { return glm::vec3(fallback_normal.x * half_width, 0.0f, fallback_normal.y * half_width); } miter /= miter_len; float denom = glm::dot(miter, fallback_normal); if (denom <= 0.1f) { return glm::vec3(fallback_normal.x * half_width, 0.0f, fallback_normal.y * half_width); } // Clamp miters so tight turns do not produce long spikes in the ribbon. float miter_scale = std::min(half_width / denom, half_width * 2.5f); return glm::vec3(miter.x * miter_scale, 0.0f, miter.y * miter_scale); } void rebuild_trail_mesh(const std::deque &trail_points, std::vector &trail_vertices) { trail_vertices.clear(); if (trail_points.size() < 2 || trail_config.width <= 0.0f) { return; } trail_vertices.reserve((trail_points.size() - 1) * 6); float half_width = trail_config.width * 0.5f; for (size_t i = 1; i < trail_points.size(); i++) { const TrailPoint &p0 = trail_points[i - 1]; const TrailPoint &p1 = trail_points[i]; if (p1.starts_segment) { continue; } glm::vec2 dir = trail_segment_direction(trail_points, i - 1, i); if (glm::length(dir) <= 0.0001f) { continue; } glm::vec2 normal(-dir.y, dir.x); glm::vec3 start_offset = trail_join_offset(trail_points, i - 1, normal, half_width); glm::vec3 end_offset = trail_join_offset(trail_points, i, normal, half_width); TrailVertex v0 = {p0.position + start_offset, p0.timestamp}; TrailVertex v1 = {p0.position - start_offset, p0.timestamp}; TrailVertex v2 = {p1.position + end_offset, p1.timestamp}; TrailVertex v3 = {p1.position - end_offset, p1.timestamp}; trail_vertices.push_back(v0); trail_vertices.push_back(v1); trail_vertices.push_back(v2); trail_vertices.push_back(v2); trail_vertices.push_back(v1); trail_vertices.push_back(v3); } } void render_minimap(const glm::vec3 &player_pos, float player_yaw, const std::deque &trail_points, const std::vector &ghosts) { // 2D overlay renders last so sorting/blending for 3D content is unaffected. if (!minimap_config.enabled || minimap_program < 0) { return; } glDisable(GL_DEPTH_TEST); glUseProgram(minimap_program); glm::mat4 ortho = glm::ortho(0.0f, static_cast(WINDOWWIDTH), 0.0f, static_cast(WINDOWHEIGHT)); set_uniform(minimap_program, projectionC, ortho); glBindVertexArray(minimapVAO); float left = static_cast(minimap_config.margin); float bottom = static_cast(minimap_config.margin); float right = left + minimap_config.width; float top = bottom + minimap_config.height; glEnable(GL_SCISSOR_TEST); glScissor(static_cast(left), static_cast(bottom), minimap_config.width, minimap_config.height); float rect_vertices[] = { left, bottom, right, bottom, right, top, left, top, left, bottom, right, top }; glBindBuffer(GL_ARRAY_BUFFER, minimapVBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(rect_vertices), rect_vertices); set_uniform(minimap_program, "inColor", minimap_config.background_color); glDrawArrays(GL_TRIANGLES, 0, 6); glm::vec2 player_flat(player_pos.x, player_pos.z); glm::vec2 map_center(left + minimap_config.width * 0.5f, bottom + minimap_config.height * 0.5f); float span_x = std::max(1.0f, minimap_bounds.xmax - minimap_bounds.xmin); float span_z = std::max(1.0f, minimap_bounds.zmax - minimap_bounds.zmin); float span_scale = std::min( static_cast(minimap_config.width) / span_x, static_cast(minimap_config.height) / span_z ); float scale = minimap_config.zoom > 0.0f ? minimap_config.zoom : span_scale; static std::vector line_vertices; line_vertices.clear(); line_vertices.reserve(minimap_segments.size() * 4); for (const auto &segment : minimap_segments) { glm::vec2 rs = segment.start - player_flat; glm::vec2 re = segment.end - player_flat; line_vertices.push_back(map_center.x + rs.x * scale); line_vertices.push_back(map_center.y + rs.y * scale); line_vertices.push_back(map_center.x + re.x * scale); line_vertices.push_back(map_center.y + re.y * scale); } if (!line_vertices.empty()) { glBindBuffer(GL_ARRAY_BUFFER, minimapVBO); glBufferData(GL_ARRAY_BUFFER, line_vertices.size() * sizeof(float), line_vertices.data(), GL_DYNAMIC_DRAW); set_uniform(minimap_program, "inColor", minimap_config.wall_color); glDrawArrays(GL_LINES, 0, line_vertices.size() / 2); } // The minimap mirrors the world trail as a real screen-space stroke. The // old breadcrumb radius now acts as stroke half-width in pixels. if (trail_points.size() >= 2 && minimap_config.breadcrumb_radius > 0.0f) { static std::vector trail_line_vertices; trail_line_vertices.clear(); trail_line_vertices.reserve((trail_points.size() - 1) * 12); float max_dist_sq = minimap_config.breadcrumb_max_distance > 0.0f ? minimap_config.breadcrumb_max_distance * minimap_config.breadcrumb_max_distance : -1.0f; float half_width = minimap_config.breadcrumb_radius; for (size_t i = 1; i < trail_points.size(); i++) { if (trail_points[i].starts_segment) { continue; } glm::vec2 rel0 = glm::vec2(trail_points[i - 1].position.x, trail_points[i - 1].position.z) - player_flat; glm::vec2 rel1 = glm::vec2(trail_points[i].position.x, trail_points[i].position.z) - player_flat; if (max_dist_sq > 0.0f && glm::dot(rel0, rel0) > max_dist_sq && glm::dot(rel1, rel1) > max_dist_sq) { continue; } glm::vec2 p0 = map_center + rel0 * scale; glm::vec2 p1 = map_center + rel1 * scale; glm::vec2 segment = p1 - p0; float segment_len = glm::length(segment); if (segment_len <= 0.0001f) { continue; } glm::vec2 normal = glm::vec2(-segment.y, segment.x) / segment_len * half_width; glm::vec2 p0_left = p0 + normal; glm::vec2 p0_right = p0 - normal; glm::vec2 p1_left = p1 + normal; glm::vec2 p1_right = p1 - normal; trail_line_vertices.push_back(p0_left.x); trail_line_vertices.push_back(p0_left.y); trail_line_vertices.push_back(p0_right.x); trail_line_vertices.push_back(p0_right.y); trail_line_vertices.push_back(p1_left.x); trail_line_vertices.push_back(p1_left.y); trail_line_vertices.push_back(p1_left.x); trail_line_vertices.push_back(p1_left.y); trail_line_vertices.push_back(p0_right.x); trail_line_vertices.push_back(p0_right.y); trail_line_vertices.push_back(p1_right.x); trail_line_vertices.push_back(p1_right.y); } if (!trail_line_vertices.empty()) { glBindBuffer(GL_ARRAY_BUFFER, minimapVBO); glBufferData(GL_ARRAY_BUFFER, trail_line_vertices.size() * sizeof(float), trail_line_vertices.data(), GL_DYNAMIC_DRAW); set_uniform(minimap_program, "inColor", minimap_config.breadcrumb_color); glDrawArrays(GL_TRIANGLES, 0, trail_line_vertices.size() / 2); } } // Ghosts are shown as border+fill circles; re-use a small triangle fan per ghost. if (!ghosts.empty() && minimap_config.ghost_radius > 0.0f) { const int ghost_segments = 16; const float ghost_angle_step = glm::two_pi() / static_cast(ghost_segments); std::vector ghost_vertices((ghost_segments + 2) * 2); auto draw_circle = [&](const glm::vec2 ¢er, float radius, const glm::vec3 &color) { if (radius <= 0.0f) return; ghost_vertices[0] = center.x; ghost_vertices[1] = center.y; for (int seg = 0; seg <= ghost_segments; ++seg) { float angle = seg * ghost_angle_step; ghost_vertices[(seg + 1) * 2 + 0] = center.x + cos(angle) * radius; ghost_vertices[(seg + 1) * 2 + 1] = center.y + sin(angle) * radius; } glBindBuffer(GL_ARRAY_BUFFER, minimapVBO); glBufferData(GL_ARRAY_BUFFER, ghost_vertices.size() * sizeof(float), ghost_vertices.data(), GL_DYNAMIC_DRAW); set_uniform(minimap_program, "inColor", color); glDrawArrays(GL_TRIANGLE_FAN, 0, ghost_vertices.size() / 2); }; float border_radius = minimap_config.ghost_radius; float fill_radius = std::max(0.0f, border_radius - 1.0f); for (const Ghost *ghost : ghosts) { glm::vec3 ghost_pos = ghost->get_pos(); glm::vec2 rel = glm::vec2(ghost_pos.x, ghost_pos.z) - player_flat; glm::vec2 center = map_center + rel * scale; draw_circle(center, border_radius, minimap_config.ghost_border_color); draw_circle(center, fill_radius, minimap_config.ghost_fill_color); } } float arrow_length = std::min(minimap_config.width, minimap_config.height) * 0.2f; float arrow_half_width = arrow_length * 0.35f; float yaw_radians = glm::radians(player_yaw - 90.0f); float cos_yaw = cos(yaw_radians); float sin_yaw = sin(yaw_radians); auto rotate = [&](float x, float y) -> glm::vec2 { return glm::vec2( x * cos_yaw - y * sin_yaw, x * sin_yaw + y * cos_yaw ); }; glm::vec2 tip = map_center + rotate(0.0f, arrow_length * 0.5f); glm::vec2 left_pt = map_center + rotate(-arrow_half_width, -arrow_length * 0.5f); glm::vec2 right_pt = map_center + rotate(arrow_half_width, -arrow_length * 0.5f); float arrow_vertices[] = { tip.x, tip.y, left_pt.x, left_pt.y, right_pt.x, right_pt.y }; glBindBuffer(GL_ARRAY_BUFFER, minimapVBO); glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(arrow_vertices), arrow_vertices); set_uniform(minimap_program, "inColor", minimap_config.arrow_color); glDrawArrays(GL_TRIANGLES, 0, 3); glBindBuffer(GL_ARRAY_BUFFER, 0); glBindVertexArray(0); glDisable(GL_SCISSOR_TEST); glEnable(GL_DEPTH_TEST); }