#include "GLFW/glfw3.h" #define CGLM_PRINT_PRECISION 10 #define CGLM_DEFINE_PRINTS 1 #include "cglm/cam.h" #include "cglm/mat4.h" #include "cglm/io.h" #include "ui.h" #include "gpu.h" #include "draw.h" #include "hex.h" #include "arpa/inet.h" #include "vk_mem_alloc.h" #include "vulkan/vk_enum_string_helper.h" #include "vulkan/vulkan_core.h" #include "pthread.h" #include #include #define max(a, b) ((a > b) ? a : b) #define min(a, b) ((a < b) ? a : b) typedef struct ClientContextStruct { GLFWwindow* window; RenderContext* render; UIContext* ui; HexContext* hex; uint32_t clicked_container; uint32_t clicked_element; int32_t clicked_hex[2]; double cursor[2]; vec3 position; vec2 rotation; int32_t key_spin[2]; vec2 cur_spin; double distance; int32_t zoom; bool camera_mode; float key_spin_speed; float cur_spin_speed; float zoom_speed; mat4 inverse; } ClientContext; void* network_thread(void* data) { ClientContext* context = (ClientContext*)data; (void)context; return NULL; } // cos(I*PI/3)/2, sin(I*PI/3)/2 vec3 hex_vertices[] = { { 1.0/2, 0, 0}, { 1.0/4, 0, SQRT3/4}, {-1.0/4, 0, SQRT3/4}, {-1.0/2, 0, 0}, {-1.0/4, 0, -SQRT3/4}, { 1.0/4, 0, -SQRT3/4}, }; vec3 hex_starts[] = { { 0, 0, HEX_Z}, {-HEX_X, 0, HEX_Z/2}, {-HEX_X, 0, -HEX_Z/2}, { 0, 0, -HEX_Z}, { HEX_X, 0, -HEX_Z/2}, { HEX_X, 0, HEX_Z/2}, }; vec3 hex_directions[] = { {-HEX_X, 0, -HEX_Z/2}, { 0, 0, -HEX_Z}, { HEX_X, 0, -HEX_Z/2}, { HEX_X, 0, HEX_Z/2}, { 0, 0, HEX_Z}, {-HEX_X, 0, HEX_Z/2}, }; int hex_indices[] = { 0, 2, 1, 0, 3, 2, 0, 4, 3, 0, 5, 4, 0, 6, 5, 0, 1, 6, }; bool ray_hex_intersect(float* distance, vec4 ray_start, vec4 ray_end, uint32_t region_index, uint32_t hex_index, HexContext* context) { /* Ray-hexagon intersection 1. For each triangle in the hexagon, check for intersection 2. If intersections, return the closest along the ray */ GPUHexRegion* region = &context->regions[region_index]->data; GPUHex* hex = ®ion->hexes[hex_index]; vec3 start; start[0] = ray_start[0]/ray_start[3]; start[1] = ray_start[1]/ray_start[3]; start[2] = ray_start[2]/ray_start[3]; vec3 end; end[0] = ray_end[0]/ray_end[3]; end[1] = ray_end[1]/ray_end[3]; end[2] = ray_end[2]/ray_end[3]; vec3 dir; dir[0] = end[0] - start[0]; dir[1] = end[1] - start[1]; dir[2] = end[2] - start[2]; float dir_len = glm_vec3_norm(dir); glm_vec3_divs(dir, dir_len, dir); vec3 region_offset = { ((float)region->q + (float)region->r/2)*REGION_WIDTH - region->r*HEX_X/2, 0, 0.75*region->r*REGION_HEIGHT + 0.25*region->r*HEX_Z + 0.5*region->q*HEX_Z, }; float center_height = (hex->height[0] + hex->height[1] + hex->height[2] + hex->height[3] + hex->height[4] + hex->height[5])/6; vec3 vertices[7] = { {0, 0, 0}, }; glm_vec3_add(vertices[0], region_offset, vertices[0]); glm_vec3_add(hex_vertices[0], region_offset, vertices[1]); glm_vec3_add(hex_vertices[1], region_offset, vertices[2]); glm_vec3_add(hex_vertices[2], region_offset, vertices[3]); glm_vec3_add(hex_vertices[3], region_offset, vertices[4]); glm_vec3_add(hex_vertices[4], region_offset, vertices[5]); glm_vec3_add(hex_vertices[5], region_offset, vertices[6]); vertices[0][1] += center_height; vertices[1][1] += hex->height[0]; vertices[2][1] += hex->height[1]; vertices[3][1] += hex->height[2]; vertices[4][1] += hex->height[3]; vertices[5][1] += hex->height[4]; vertices[6][1] += hex->height[5]; vec3 hex_offset = {0, 0, 0}; float radius = 0; float ring = 0; int side = 0; if(hex_index != 0) { radius = floor(0.5 + sqrt(12*hex_index-3)/6); ring = hex_index - (3*radius*radius - 3*radius + 1); side = floor(ring/radius); } glm_vec3_muladds(hex_starts[side], radius, hex_offset); glm_vec3_muladds(hex_directions[side], ring-(radius*side), hex_offset); for(uint32_t vertex = 0; vertex < 7; vertex++) { glm_vec3_add(vertices[vertex], hex_offset, vertices[vertex]); } *distance = INFINITY; bool intersect = false; for(uint32_t triangle = 0; triangle < 6; triangle++) { vec3 vert[3]; for(int v_i = 0; v_i < 3; v_i++) { vert[v_i][0] = vertices[hex_indices[triangle*3+v_i]][0]; vert[v_i][1] = vertices[hex_indices[triangle*3+v_i]][1]; vert[v_i][2] = vertices[hex_indices[triangle*3+v_i]][2]; } vec3 v0v1; glm_vec3_sub(vert[1], vert[0], v0v1); vec3 v0v2; glm_vec3_sub(vert[2], vert[0], v0v2); vec3 pvec; glm_vec3_cross(dir, v0v2, pvec); float det = glm_vec3_dot(v0v1, pvec); float det_inv = 1/det; vec3 t; glm_vec3_sub(start, vert[0], t); float u = glm_vec3_dot(t, pvec) * det_inv; if(u < 0 || u > 1) continue; vec3 q; glm_vec3_cross(t, v0v1, q); float v = glm_vec3_dot(dir, q) * det_inv; if(v < 0 || (u+v) > 1) continue; intersect = true; *distance = min(*distance, glm_vec3_dot(v0v2, q) * det_inv); } return intersect; } vec3 up = {0, 1, 0}; VkResult main_thread(ClientContext* context) { VkResult result; GPUString strings[] = { { .pos = {0, 32}, .size = 32, .color = {1.0, 1.0, 1.0, 1.0}, .offset = 0, .length = 0, .font = 0, }, }; GPUDrawable drawables[] = {}; LayerInput layer = { .strings = strings, .num_strings = sizeof(strings)/sizeof(GPUString), .max_codes = 50, .drawables = drawables, .num_drawables = sizeof(drawables)/sizeof(GPUDrawable), }; ContainerInput container = { .id = 1, .size = {WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT}, .layer_count = 1, .layers = &layer, }; create_container(&container, context->render, context->ui); HexRegion* regions[MAX_LOADED_REGIONS]; uint32_t colors[] = { 0xFF0000FF, 0x00FF00FF, 0x0000FFFF, }; context->hex->data.current_map = 0x01; add_transfer(&context->hex->data.current_map, context->hex->context, offsetof(GPUHexContext, current_map), sizeof(uint32_t), context->render->current_frame, context->render); uint32_t region = 0; for(int32_t q = -5; q < 5; q++) { for(int32_t r = -5; r < 5; r++) { VK_RESULT(set_hex_region(q, r, 0, 0x01, ®ions[region], context->hex, context->render)); for(uint32_t i = 0; i < REGION_HEX_COUNT; i++) { for(uint32_t h = 0; h < 6; h++) { regions[region]->data.hexes[i].color[h] = colors[(q+r+50) % (sizeof(colors)/sizeof(uint32_t))]; regions[region]->data.hexes[i].height[h] = (float)i/REGION_HEX_COUNT; } regions[region]->data.hexes[i].color[6] = colors[(q+r+50) % (sizeof(colors)/sizeof(uint32_t))]; } VK_RESULT(add_transfer(®ions[region]->data.hexes, regions[region]->region, offsetof(GPUHexRegion, hexes), sizeof(GPUHex)*REGION_HEX_COUNT, 0, context->render)); region++; } } // uint32_t* mapped_codes = context->ui->containers[0].layers[0].codes_buffer; GPUString* mapped_string = context->ui->containers[0].layers[0].strings_buffer; char str[51]; int frame = 0; double last_frame_time = 0; while(glfwWindowShouldClose(context->window) == 0) { double frame_time = glfwGetTime(); double delta_time = (frame_time - last_frame_time); // Reset callback variables context->clicked_element = 0x00000000; context->clicked_container = 0x00000000; context->clicked_hex[0] = 0; context->clicked_hex[1] = 0; context->zoom = 0; context->cur_spin[0] = 0; context->cur_spin[1] = 0; glfwPollEvents(); if((context->key_spin[0] != 0 || context->key_spin[1] != 0 || context->zoom != 0 || context->cur_spin[0] != 0 || context->cur_spin[1] != 0 || context->render->framebuffer_recreated == true) && frame > 0) { if(context->render->framebuffer_recreated == true) { context->render->framebuffer_recreated = false; glm_perspective( PERSPECTIVE_FOVY, (float)context->render->swapchain_extent.width/(float)context->render->swapchain_extent.height, PERSPECTIVE_NEARZ, PERSPECTIVE_FARZ, context->hex->data.proj); VK_RESULT(add_transfer( &context->hex->data.proj, context->hex->context, offsetof(GPUHexContext, proj), sizeof(mat4), context->render->current_frame, context->render)); } context->rotation[0] += (float)context->key_spin[0]*delta_time*context->key_spin_speed; context->rotation[0] += (float)context->cur_spin[0]*delta_time*context->cur_spin_speed; if(context->rotation[0] > 2*M_PI) { context->rotation[0] -= 2*M_PI; } else if(context->rotation[0] < 0) { context->rotation[0] += 2*M_PI; } context->rotation[1] += (float)context->key_spin[1]*delta_time*context->key_spin_speed; context->rotation[1] += (float)context->cur_spin[1]*delta_time*context->cur_spin_speed; if(context->rotation[1] > (M_PI/2 - 0.1)) { context->rotation[1] = (M_PI/2 - 0.1); } else if(context->rotation[1] < 0) { context->rotation[1] = 0; } context->distance += context->zoom*delta_time*context->zoom_speed; if(context->distance < 1) { context->distance = 1; } vec3 camera = {}; camera[0] = context->position[0] + context->distance*cos(context->rotation[1])*cos(context->rotation[0]); camera[1] = context->position[1] + context->distance*sin(context->rotation[1]); camera[2] = context->position[2] + context->distance*cos(context->rotation[1])*sin(context->rotation[0]); glm_lookat(camera, context->position, up, context->hex->data.view); mat4 regular; glm_mat4_mul(context->hex->data.proj, context->hex->data.view, regular); glm_mat4_inv(regular, context->inverse); add_transfer(&context->hex->data, context->hex->context, 0, 2*sizeof(mat4), context->render->current_frame, context->render); } if(context->clicked_hex[0] != 0 || context->clicked_hex[1] != 0) { snprintf(str, 50, "Clicked: (%d,%d)", context->clicked_hex[0], context->clicked_hex[1]); map_string(str, mapped_codes, 0, 0, context->ui); VK_RESULT(add_transfers( context->ui->containers[0].layers[0].codes_buffer, context->ui->containers[0].layers[0].codes, 0, strlen(str)*sizeof(uint32_t), context->render)); mapped_string->length = strlen(str); VK_RESULT(add_transfers( &context->ui->containers[0].layers[0].strings_buffer[0].length, context->ui->containers[0].layers[0].strings, offsetof(GPUString, length), sizeof(uint32_t), context->render)); } // VkResult result = draw_frame(context->render, context->ui, context->hex, frame_time); if(result != VK_SUCCESS) { fprintf(stderr, "draw_frame error: %s\n", string_VkResult(result)); glfwDestroyWindow(context->window); } frame += 1; last_frame_time = frame_time; } return 0; } void key_callback(GLFWwindow* window, int key, int scancode, int action, int mods) { ClientContext* context = (ClientContext*)glfwGetWindowUserPointer(window); (void)scancode; (void)mods; switch(key) { case GLFW_KEY_A: if(action == GLFW_PRESS) { context->key_spin[0] -= 1; } else if(action == GLFW_RELEASE) { context->key_spin[0] += 1; } break; case GLFW_KEY_D: if(action == GLFW_PRESS) { context->key_spin[0] += 1; } else if(action == GLFW_RELEASE) { context->key_spin[0] -= 1; } break; case GLFW_KEY_W: if(action == GLFW_PRESS) { context->key_spin[1] += 1; } else if(action == GLFW_RELEASE) { context->key_spin[1] -= 1; } break; case GLFW_KEY_S: if(action == GLFW_PRESS) { context->key_spin[1] -= 1; } else if(action == GLFW_RELEASE) { context->key_spin[1] += 1; } break; } } bool contains(double* point, GPUContainer* container, GPUDrawable* rect) { vec2 pos = { container->offset[0] + rect->pos[0], container->offset[1] + rect->pos[1], }; return (point[0] >= pos[0] && point[0] <= pos[0] + rect->size[0]) && (point[1] >= pos[1] && point[1] <= pos[1] + rect->size[1]) && (point[0] >= container->offset[0] && point[0] <= container->offset[0] + container->size[0]) && (point[1] >= container->offset[1] && point[1] <= container->offset[1] + container->size[1]); } void cursor_to_world_ray(ClientContext* context, double cursor[2], vec4 start, vec4 end) { double cursor_scaled[2] = { 2*(cursor[0]*context->render->window_scale[0]/context->render->swapchain_extent.width - 0.5), 2*(cursor[1]*context->render->window_scale[1]/context->render->swapchain_extent.height - 0.5), }; vec4 transformed_start = { cursor_scaled[0], cursor_scaled[1], PERSPECTIVE_NEARZ, 1.0, }; vec4 transformed_end = { PERSPECTIVE_FARZ*cursor_scaled[0], PERSPECTIVE_FARZ*cursor_scaled[1], PERSPECTIVE_FARZ, PERSPECTIVE_FARZ, }; glm_mat4_mulv(context->inverse, transformed_start, start); glm_mat4_mulv(context->inverse, transformed_end, end); } void mouse_button_callback(GLFWwindow* window, int button, int action, int mods) { ClientContext* context = (ClientContext*)glfwGetWindowUserPointer(window); (void)mods; (void)context; double cursor[2]; glfwGetCursorPos(window, &cursor[0], &cursor[1]); switch(button) { // Handle camera hover case GLFW_MOUSE_BUTTON_RIGHT: if(action == GLFW_PRESS) { glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED); context->camera_mode = true; } else { glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL); context->camera_mode = false; } break; case GLFW_MOUSE_BUTTON_LEFT: if(action == GLFW_PRESS) { cursor_to_world_ray(context, cursor, context->hex->data.click_start, context->hex->data.click_end); add_transfer(&context->hex->data.click_start, context->hex->context, offsetof(GPUHexContext, click_start), sizeof(vec4)*2, context->render->current_frame, context->render); // Hex intersections float distance; for(uint32_t r = 0; r < MAX_LOADED_REGIONS; r++) { if(context->hex->regions[r] == NULL) { continue; } else if(context->hex->regions[r]->data.map != context->hex->data.current_map) { continue; } for(uint32_t h = 0; h < REGION_HEX_COUNT; h++) { if(ray_hex_intersect(&distance, context->hex->data.click_start, context->hex->data.click_end, r, h, context->hex)) { context->hex->data.clicked_region = r; context->hex->data.clicked_hex = h; add_transfer(&context->hex->data.clicked_region, context->hex->context, offsetof(GPUHexContext, clicked_region), sizeof(uint32_t)*2, context->render->current_frame, context->render); } } } // UI intersections for(uint32_t c = 0; c < context->ui->max_containers; c++) { if(context->ui->containers[c].id == 0x00000000) { continue; } for(uint32_t l = 0; l < context->ui->containers[c].layer_count; l++) { for(uint32_t d = 0; d < context->ui->containers[c].layers[l].data.num_drawables; d++) { if(context->ui->containers[c].layers[l].drawables_buffer[d].id == 0x00000000) { continue; } if(contains(cursor, &context->ui->containers[c].data, &context->ui->containers[c].layers[l].drawables_buffer[d])) { context->clicked_container = context->ui->containers[c].id; context->clicked_element = context->ui->containers[c].layers[l].drawables_buffer[d].id; } } } } // NOTE: this logic is the same as it would be for right-click(just with different outcomes), so dont repeat yourself // 1. Search through the UI context for the first element that the contains the mouse point // 2. If no UI element intersection, cast a ray through the world scene to find the "clicked entity" // 3. Based on what was clicked, start the mouse click animation at the target area } break; } } void scroll_callback(GLFWwindow* window, double xoffset, double yoffset) { ClientContext* context = (ClientContext*)glfwGetWindowUserPointer(window); (void)xoffset; context->zoom = -yoffset; } void cursor_pos_callback(GLFWwindow* window, double xpos, double ypos) { ClientContext* context = (ClientContext*)glfwGetWindowUserPointer(window); if(context->camera_mode == true) { context->cur_spin[0] = (xpos - context->cursor[0])/context->render->swapchain_extent.width*-100; context->cur_spin[1] = (ypos - context->cursor[1])/context->render->swapchain_extent.height*100; } context->cursor[0] = xpos; context->cursor[1] = ypos; cursor_to_world_ray(context, context->cursor, context->hex->data.hover_start, context->hex->data.hover_end); add_transfer(&context->hex->data.hover_start, context->hex->context, offsetof(GPUHexContext, hover_start), sizeof(vec4)*2, context->render->current_frame, context->render); // Hex intersections float distance; for(uint32_t r = 0; r < MAX_LOADED_REGIONS; r++) { if(context->hex->regions[r] == NULL) { continue; } else if(context->hex->regions[r]->data.map != context->hex->data.current_map) { continue; } for(uint32_t h = 0; h < REGION_HEX_COUNT; h++) { if(ray_hex_intersect(&distance, context->hex->data.hover_start, context->hex->data.hover_end, r, h, context->hex)) { context->hex->data.hovered_region = r; context->hex->data.hovered_hex = h; add_transfer(&context->hex->data.hovered_region, context->hex->context, offsetof(GPUHexContext, hovered_region), sizeof(uint32_t)*2, context->render->current_frame, context->render); } } } } int main() { ClientContext context = { .render = malloc(sizeof(RenderContext)), .ui = malloc(sizeof(UIContext)), .hex = malloc(sizeof(HexContext)), .window = init_window(), .position = {0, 0, 0}, .rotation = {3*M_PI/2, M_PI/4}, .distance = 25, .key_spin_speed = 1.0, .cur_spin_speed = 1.0, .zoom_speed = 1.0, }; if(context.window == NULL || context.render == NULL || context.ui == NULL || context.hex == NULL) { return VK_ERROR_OUT_OF_HOST_MEMORY; } memset(context.render, 0, sizeof(RenderContext)); memset(context.ui, 0, sizeof(UIContext)); memset(context.hex, 0, sizeof(HexContext)); glfwSetWindowUserPointer(context.window, &context); glfwSetKeyCallback(context.window, key_callback); glfwSetMouseButtonCallback(context.window, mouse_button_callback); glfwSetScrollCallback(context.window, scroll_callback); glfwSetCursorPosCallback(context.window, cursor_pos_callback); int error; VkResult result; VK_RESULT(init_vulkan(context.window, context.render)); // TODO: make # of fonts/textures/containers scaling, recreate GPU buffers as necessary VK_RESULT(create_ui_context(10, 10, 10, context.render, context.ui)); VK_RESULT(create_hex_context(context.render, context.hex)); pthread_t network_thread_handle; error = pthread_create(&network_thread_handle, NULL, &network_thread, &context); if(error != 0) { return error; } error = main_thread(&context); if(error != 0) { return error; } error = pthread_join(network_thread_handle, NULL); if(error != 0) { return error; } return 0; }