diff options
Diffstat (limited to 'src/plugins')
| -rw-r--r-- | src/plugins/CMakeLists.txt | 29 | ||||
| -rw-r--r-- | src/plugins/plugin.h | 52 | ||||
| -rw-r--r-- | src/plugins/pong.c | 237 | ||||
| -rw-r--r-- | src/plugins/texture_view.c | 144 | ||||
| -rw-r--r-- | src/plugins/viewer.c | 373 |
5 files changed, 835 insertions, 0 deletions
diff --git a/src/plugins/CMakeLists.txt b/src/plugins/CMakeLists.txt new file mode 100644 index 0000000..8661598 --- /dev/null +++ b/src/plugins/CMakeLists.txt | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | cmake_minimum_required(VERSION 3.0) | ||
| 2 | |||
| 3 | project(plugins) | ||
| 4 | |||
| 5 | set(LINK_LIBRARIES cstring math gfx gfx-app) | ||
| 6 | |||
| 7 | # Viewer | ||
| 8 | |||
| 9 | add_library(viewer SHARED | ||
| 10 | viewer.c) | ||
| 11 | |||
| 12 | target_link_libraries(viewer PUBLIC | ||
| 13 | ${LINK_LIBRARIES}) | ||
| 14 | |||
| 15 | # Texture viewer | ||
| 16 | |||
| 17 | add_library(texture_view SHARED | ||
| 18 | texture_view.c) | ||
| 19 | |||
| 20 | target_link_libraries(texture_view PUBLIC | ||
| 21 | ${LINK_LIBRARIES}) | ||
| 22 | |||
| 23 | # Pong | ||
| 24 | |||
| 25 | add_library(pong SHARED | ||
| 26 | pong.c) | ||
| 27 | |||
| 28 | target_link_libraries(pong PUBLIC | ||
| 29 | ${LINK_LIBRARIES}) | ||
diff --git a/src/plugins/plugin.h b/src/plugins/plugin.h new file mode 100644 index 0000000..f7219c6 --- /dev/null +++ b/src/plugins/plugin.h | |||
| @@ -0,0 +1,52 @@ | |||
| 1 | /* | ||
| 2 | * Game plugin. | ||
| 3 | */ | ||
| 4 | #pragma once | ||
| 5 | |||
| 6 | #include "../game.h" | ||
| 7 | |||
| 8 | #include <gfx/gfx.h> | ||
| 9 | #include <gfx/scene.h> | ||
| 10 | |||
| 11 | #include <stdbool.h> | ||
| 12 | |||
| 13 | typedef struct State State; | ||
| 14 | |||
| 15 | /// Initialize the plugin, which may optionally return a state object. | ||
| 16 | /// | ||
| 17 | /// This function is called every time the plugin is (re)loaded. | ||
| 18 | /// | ||
| 19 | /// It is assumed that the plugin's state is fully encapsulated in the returned | ||
| 20 | /// state object. The plugin should not store any (mutable) state outside of the | ||
| 21 | /// returned state object (e.g., no mutable global variables.) | ||
| 22 | bool init(Game*, State**); | ||
| 23 | |||
| 24 | /// Shut down the plugin. | ||
| 25 | /// | ||
| 26 | /// This function is called before the plugin is unloaded. | ||
| 27 | /// | ||
| 28 | /// The plugin should perform any destruction needed, but not free the state | ||
| 29 | /// object; freeing the state object's memory is handled by the caller. | ||
| 30 | void shutdown(Game*, State*); | ||
| 31 | |||
| 32 | /// Function called the first time the plugin is loaded throughout the | ||
| 33 | /// application's lifetime. This allows the plugin to do one-time initialization | ||
| 34 | /// of the game state. | ||
| 35 | bool boot(Game*, State*); | ||
| 36 | |||
| 37 | /// Update the plugin's and the game's state. | ||
| 38 | void update(Game*, State*, double t, double dt); | ||
| 39 | |||
| 40 | /// Render hook. | ||
| 41 | void render(const Game*, const State*); | ||
| 42 | |||
| 43 | /// Called when the game's window is resized. | ||
| 44 | void resize(Game*, State*, int width, int height); | ||
| 45 | |||
| 46 | // Signatures for the plugin's exposed functions. | ||
| 47 | typedef bool (*plugin_init)(Game*, State**); | ||
| 48 | typedef bool (*plugin_shutdown)(Game*, State*); | ||
| 49 | typedef bool (*plugin_boot)(Game*, State*); | ||
| 50 | typedef void (*plugin_update)(Game*, State*, double t, double dt); | ||
| 51 | typedef void (*plugin_render)(const Game*, const State*); | ||
| 52 | typedef void (*plugin_resize)(Game* game, State* state, int width, int height); | ||
diff --git a/src/plugins/pong.c b/src/plugins/pong.c new file mode 100644 index 0000000..c1c55be --- /dev/null +++ b/src/plugins/pong.c | |||
| @@ -0,0 +1,237 @@ | |||
| 1 | #include "plugin.h" | ||
| 2 | |||
| 3 | #include <gfx/app.h> | ||
| 4 | #include <gfx/gfx.h> | ||
| 5 | #include <gfx/renderer.h> | ||
| 6 | |||
| 7 | #include <math/mat4.h> | ||
| 8 | #include <math/vec2.h> | ||
| 9 | #include <math/vec4.h> | ||
| 10 | |||
| 11 | #include <stdlib.h> | ||
| 12 | |||
| 13 | static const vec2 PAD_SIZE = (vec2){120, 20}; | ||
| 14 | static const R PLAYER_Y_OFFSET = 50; | ||
| 15 | static const R PLAYER_SPEED = 800; | ||
| 16 | |||
| 17 | static const R ENEMY_SPEED = 2; | ||
| 18 | |||
| 19 | static const R BALL_SIZE = 18; | ||
| 20 | static const R BALL_SPEED = 360; // In each dimension. | ||
| 21 | |||
| 22 | static const R EPS = (R)1e-3; | ||
| 23 | |||
| 24 | typedef struct Player { | ||
| 25 | vec2 position; | ||
| 26 | } Player; | ||
| 27 | |||
| 28 | typedef struct Ball { | ||
| 29 | vec2 position; | ||
| 30 | vec2 velocity; | ||
| 31 | } Ball; | ||
| 32 | |||
| 33 | typedef struct State { | ||
| 34 | bool game_started; | ||
| 35 | Player human; | ||
| 36 | Player enemy; | ||
| 37 | Ball ball; | ||
| 38 | mat4 viewProjection; | ||
| 39 | } State; | ||
| 40 | |||
| 41 | bool init(Game* game, State** pp_state) { | ||
| 42 | assert(game); | ||
| 43 | |||
| 44 | State* state = calloc(1, sizeof(State)); | ||
| 45 | if (!state) { | ||
| 46 | return false; | ||
| 47 | } | ||
| 48 | |||
| 49 | *pp_state = state; | ||
| 50 | return true; | ||
| 51 | |||
| 52 | cleanup: | ||
| 53 | free(state); | ||
| 54 | return false; | ||
| 55 | } | ||
| 56 | |||
| 57 | void shutdown(Game* game, State* state) { | ||
| 58 | assert(game); | ||
| 59 | assert(state); | ||
| 60 | } | ||
| 61 | |||
| 62 | static void move_ball(Ball* ball, R dt, int width, int height) { | ||
| 63 | assert(ball); | ||
| 64 | |||
| 65 | const R offset = BALL_SIZE / 2; | ||
| 66 | |||
| 67 | ball->position = vec2_add(ball->position, vec2_scale(ball->velocity, dt)); | ||
| 68 | |||
| 69 | // Right wall. | ||
| 70 | if (ball->position.x + offset > (R)width) { | ||
| 71 | ball->position.x = (R)width - offset - EPS; | ||
| 72 | ball->velocity.x = -ball->velocity.x; | ||
| 73 | } | ||
| 74 | // Left wall. | ||
| 75 | else if (ball->position.x - offset < 0) { | ||
| 76 | ball->position.x = offset + EPS; | ||
| 77 | ball->velocity.x = -ball->velocity.x; | ||
| 78 | } | ||
| 79 | // Top wall. | ||
| 80 | if (ball->position.y + offset > (R)height) { | ||
| 81 | ball->position.y = (R)height - offset - EPS; | ||
| 82 | ball->velocity.y = -ball->velocity.y; | ||
| 83 | } | ||
| 84 | // Bottom wall. | ||
| 85 | else if (ball->position.y - offset < 0) { | ||
| 86 | ball->position.y = offset + EPS; | ||
| 87 | ball->velocity.y = -ball->velocity.y; | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | void move_enemy_player(int width, Player* player, R t) { | ||
| 92 | const R half_width = (R)width / 2; | ||
| 93 | const R amplitude = half_width - (PAD_SIZE.x / 2); | ||
| 94 | player->position.x = half_width + amplitude * sinf(t * ENEMY_SPEED); | ||
| 95 | } | ||
| 96 | |||
| 97 | void move_human_player(Player* player, R dt) { | ||
| 98 | assert(player); | ||
| 99 | |||
| 100 | R speed = 0; | ||
| 101 | if (gfx_app_is_key_pressed('a')) { | ||
| 102 | speed -= PLAYER_SPEED; | ||
| 103 | } | ||
| 104 | if (gfx_app_is_key_pressed('d')) { | ||
| 105 | speed += PLAYER_SPEED; | ||
| 106 | } | ||
| 107 | |||
| 108 | player->position.x += speed * dt; | ||
| 109 | } | ||
| 110 | |||
| 111 | void clamp_player(Player* player, int width) { | ||
| 112 | assert(player); | ||
| 113 | |||
| 114 | const R offset = PAD_SIZE.x / 2; | ||
| 115 | |||
| 116 | // Left wall. | ||
| 117 | if (player->position.x + offset > (R)width) { | ||
| 118 | player->position.x = (R)width - offset; | ||
| 119 | } | ||
| 120 | // Right wall. | ||
| 121 | else if (player->position.x - offset < 0) { | ||
| 122 | player->position.x = offset; | ||
| 123 | } | ||
| 124 | } | ||
| 125 | |||
| 126 | void collide_ball(vec2 old_ball_position, const Player* player, Ball* ball) { | ||
| 127 | assert(player); | ||
| 128 | assert(ball); | ||
| 129 | |||
| 130 | // Discrete but simple collision. Checks for intersection and moves the ball | ||
| 131 | // back by a small epsilon. | ||
| 132 | |||
| 133 | // Player bounding box. | ||
| 134 | const vec2 player_pmin = vec2_make( | ||
| 135 | player->position.x - PAD_SIZE.x / 2, player->position.y - PAD_SIZE.y / 2); | ||
| 136 | const vec2 player_pmax = vec2_make( | ||
| 137 | player->position.x + PAD_SIZE.x / 2, player->position.y + PAD_SIZE.y / 2); | ||
| 138 | |||
| 139 | // Ball bounding box. | ||
| 140 | const vec2 ball_pmin = vec2_make( | ||
| 141 | ball->position.x - BALL_SIZE / 2, ball->position.y - BALL_SIZE / 2); | ||
| 142 | const vec2 ball_pmax = vec2_make( | ||
| 143 | ball->position.x + BALL_SIZE / 2, ball->position.y + BALL_SIZE / 2); | ||
| 144 | |||
| 145 | // Check for intersection and update ball. | ||
| 146 | if (!((ball_pmax.x < player_pmin.x) || (ball_pmin.x > player_pmax.x) || | ||
| 147 | (ball_pmax.y < player_pmin.y) || (ball_pmin.y > player_pmax.y))) { | ||
| 148 | ball->position = | ||
| 149 | vec2_add(old_ball_position, vec2_scale(ball->velocity, -EPS)); | ||
| 150 | ball->velocity.y = -ball->velocity.y; | ||
| 151 | } | ||
| 152 | } | ||
| 153 | |||
| 154 | void update(Game* game, State* state, double t, double dt) { | ||
| 155 | assert(game); | ||
| 156 | assert(state); | ||
| 157 | |||
| 158 | // TODO: Move game width/height to GfxApp query functions? | ||
| 159 | const vec2 old_ball_position = state->ball.position; | ||
| 160 | move_ball(&state->ball, (R)dt, game->width, game->height); | ||
| 161 | move_human_player(&state->human, (R)dt); | ||
| 162 | move_enemy_player(game->width, &state->enemy, (R)t); | ||
| 163 | clamp_player(&state->human, game->width); | ||
| 164 | collide_ball(old_ball_position, &state->human, &state->ball); | ||
| 165 | collide_ball(old_ball_position, &state->enemy, &state->ball); | ||
| 166 | } | ||
| 167 | |||
| 168 | static void draw_player(ImmRenderer* imm, const Player* player) { | ||
| 169 | assert(imm); | ||
| 170 | assert(player); | ||
| 171 | |||
| 172 | const vec2 half_box = vec2_div(PAD_SIZE, vec2_make(2, 2)); | ||
| 173 | |||
| 174 | const vec2 pmin = vec2_sub(player->position, half_box); | ||
| 175 | const vec2 pmax = vec2_add(player->position, half_box); | ||
| 176 | const aabb2 box = aabb2_make(pmin, pmax); | ||
| 177 | |||
| 178 | gfx_imm_draw_aabb2(imm, box); | ||
| 179 | } | ||
| 180 | |||
| 181 | static void draw_ball(ImmRenderer* imm, const Ball* ball) { | ||
| 182 | assert(imm); | ||
| 183 | assert(ball); | ||
| 184 | |||
| 185 | const vec2 half_box = vec2_make(BALL_SIZE / 2, BALL_SIZE / 2); | ||
| 186 | const vec2 pmin = vec2_sub(ball->position, half_box); | ||
| 187 | const vec2 pmax = vec2_add(ball->position, half_box); | ||
| 188 | const aabb2 box = aabb2_make(pmin, pmax); | ||
| 189 | |||
| 190 | gfx_imm_draw_aabb2(imm, box); | ||
| 191 | } | ||
| 192 | |||
| 193 | void render(const Game* game, const State* state) { | ||
| 194 | assert(game); | ||
| 195 | assert(state); | ||
| 196 | |||
| 197 | ImmRenderer* imm = gfx_get_imm_renderer(game->gfx); | ||
| 198 | gfx_imm_start(imm); | ||
| 199 | gfx_imm_set_view_projection_matrix(imm, &state->viewProjection); | ||
| 200 | gfx_imm_load_identity(imm); | ||
| 201 | gfx_imm_set_colour(imm, vec4_make(1, 1, 1, 1)); | ||
| 202 | draw_player(imm, &state->human); | ||
| 203 | draw_player(imm, &state->enemy); | ||
| 204 | draw_ball(imm, &state->ball); | ||
| 205 | gfx_imm_end(imm); | ||
| 206 | } | ||
| 207 | |||
| 208 | static R clamp_to_width(int width, R x, R extent) { | ||
| 209 | return min(x, (R)width - extent); | ||
| 210 | } | ||
| 211 | |||
| 212 | void resize(Game* game, State* state, int width, int height) { | ||
| 213 | assert(game); | ||
| 214 | assert(state); | ||
| 215 | |||
| 216 | state->viewProjection = mat4_ortho(0, (R)width, 0, (R)height, -1, 1); | ||
| 217 | |||
| 218 | state->human.position.y = PLAYER_Y_OFFSET; | ||
| 219 | state->enemy.position.y = (R)height - PLAYER_Y_OFFSET; | ||
| 220 | |||
| 221 | if (!state->game_started) { | ||
| 222 | state->human.position.x = (R)width / 2; | ||
| 223 | state->enemy.position.x = (R)width / 2; | ||
| 224 | |||
| 225 | state->ball.position = | ||
| 226 | vec2_div(vec2_make((R)width, (R)height), vec2_make(2, 2)); | ||
| 227 | |||
| 228 | state->ball.velocity = vec2_make(BALL_SPEED, BALL_SPEED); | ||
| 229 | |||
| 230 | state->game_started = true; | ||
| 231 | } else { | ||
| 232 | state->human.position.x = | ||
| 233 | clamp_to_width(width, state->human.position.x, PAD_SIZE.x / 2); | ||
| 234 | state->enemy.position.x = | ||
| 235 | clamp_to_width(width, state->enemy.position.x, PAD_SIZE.x / 2); | ||
| 236 | } | ||
| 237 | } | ||
diff --git a/src/plugins/texture_view.c b/src/plugins/texture_view.c new file mode 100644 index 0000000..a8b2a94 --- /dev/null +++ b/src/plugins/texture_view.c | |||
| @@ -0,0 +1,144 @@ | |||
| 1 | #include "plugin.h" | ||
| 2 | |||
| 3 | #include <gfx/asset.h> | ||
| 4 | #include <gfx/core.h> | ||
| 5 | #include <gfx/renderer.h> | ||
| 6 | #include <gfx/scene.h> | ||
| 7 | #include <gfx/util/geometry.h> | ||
| 8 | #include <gfx/util/shader.h> | ||
| 9 | |||
| 10 | #include <math/camera.h> | ||
| 11 | |||
| 12 | #include <assert.h> | ||
| 13 | #include <stdlib.h> | ||
| 14 | |||
| 15 | // Default texture to load if no texture is provided. | ||
| 16 | static const char* DEFAULT_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp"; | ||
| 17 | // static const char* DEFAULT_TEXTURE = "/assets/checkerboard.jpg"; | ||
| 18 | |||
| 19 | struct State { | ||
| 20 | Scene* scene; | ||
| 21 | SceneCamera* camera; | ||
| 22 | }; | ||
| 23 | |||
| 24 | bool init(Game* game, State** pp_state) { | ||
| 25 | assert(game); | ||
| 26 | assert(pp_state); | ||
| 27 | |||
| 28 | State* state = calloc(1, sizeof(State)); | ||
| 29 | if (!state) { | ||
| 30 | goto cleanup; | ||
| 31 | } | ||
| 32 | |||
| 33 | // Usage: [texture file] | ||
| 34 | const char* texture_file = game->argc > 1 ? game->argv[1] : DEFAULT_TEXTURE; | ||
| 35 | |||
| 36 | GfxCore* gfxcore = gfx_get_core(game->gfx); | ||
| 37 | |||
| 38 | const Texture* texture = gfx_load_texture( | ||
| 39 | game->gfx, &(LoadTextureCmd){ | ||
| 40 | .origin = AssetFromFile, | ||
| 41 | .type = LoadTexture, | ||
| 42 | .filtering = LinearFiltering, | ||
| 43 | .mipmaps = false, | ||
| 44 | .data.texture.filepath = mstring_make(texture_file)}); | ||
| 45 | if (!texture) { | ||
| 46 | goto cleanup; | ||
| 47 | } | ||
| 48 | |||
| 49 | ShaderProgram* shader = gfx_make_view_texture_shader(gfxcore); | ||
| 50 | if (!shader) { | ||
| 51 | goto cleanup; | ||
| 52 | } | ||
| 53 | |||
| 54 | Geometry* geometry = gfx_make_quad_11(gfxcore); | ||
| 55 | if (!geometry) { | ||
| 56 | goto cleanup; | ||
| 57 | } | ||
| 58 | |||
| 59 | MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1}; | ||
| 60 | material_desc.uniforms[0] = (ShaderUniform){ | ||
| 61 | .type = UniformTexture, | ||
| 62 | .value.texture = texture, | ||
| 63 | .name = sstring_make("Texture")}; | ||
| 64 | Material* material = gfx_make_material(&material_desc); | ||
| 65 | if (!material) { | ||
| 66 | goto cleanup; | ||
| 67 | } | ||
| 68 | |||
| 69 | const MeshDesc mesh_desc = | ||
| 70 | (MeshDesc){.geometry = geometry, .material = material, .shader = shader}; | ||
| 71 | Mesh* mesh = gfx_make_mesh(&mesh_desc); | ||
| 72 | if (!mesh) { | ||
| 73 | goto cleanup; | ||
| 74 | } | ||
| 75 | |||
| 76 | SceneObject* object = | ||
| 77 | gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}}); | ||
| 78 | if (!object) { | ||
| 79 | goto cleanup; | ||
| 80 | } | ||
| 81 | |||
| 82 | if (!(state->scene = gfx_make_scene())) { | ||
| 83 | goto cleanup; | ||
| 84 | } | ||
| 85 | |||
| 86 | SceneNode* node = gfx_make_object_node(object); | ||
| 87 | if (!node) { | ||
| 88 | goto cleanup; | ||
| 89 | } | ||
| 90 | SceneNode* root = gfx_get_scene_root(state->scene); | ||
| 91 | if (!root) { | ||
| 92 | goto cleanup; | ||
| 93 | } | ||
| 94 | gfx_set_node_parent(node, root); | ||
| 95 | |||
| 96 | if (!(state->camera = gfx_make_camera())) { | ||
| 97 | goto cleanup; | ||
| 98 | } | ||
| 99 | |||
| 100 | *pp_state = state; | ||
| 101 | return true; | ||
| 102 | |||
| 103 | cleanup: | ||
| 104 | shutdown(game, state); | ||
| 105 | if (state) { | ||
| 106 | free(state); | ||
| 107 | } | ||
| 108 | return false; | ||
| 109 | } | ||
| 110 | |||
| 111 | void shutdown(Game* game, State* state) { | ||
| 112 | assert(game); | ||
| 113 | if (state) { | ||
| 114 | gfx_destroy_camera(&state->camera); | ||
| 115 | gfx_destroy_scene(&state->scene); | ||
| 116 | // State freed by plugin engine. | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | void render(const Game* game, const State* state) { | ||
| 121 | assert(game); | ||
| 122 | assert(state); | ||
| 123 | |||
| 124 | Renderer* renderer = gfx_get_renderer(game->gfx); | ||
| 125 | gfx_render_scene( | ||
| 126 | renderer, &(RenderSceneParams){ | ||
| 127 | .mode = RenderDefault, | ||
| 128 | .scene = state->scene, | ||
| 129 | .camera = state->camera}); | ||
| 130 | } | ||
| 131 | |||
| 132 | void resize(Game* game, State* state, int width, int height) { | ||
| 133 | assert(game); | ||
| 134 | assert(state); | ||
| 135 | |||
| 136 | const R fovy = 90 * TO_RAD; | ||
| 137 | const R aspect = (R)width / (R)height; | ||
| 138 | const R near = 0.1; | ||
| 139 | const R far = 1000; | ||
| 140 | const mat4 projection = mat4_perspective(fovy, aspect, near, far); | ||
| 141 | |||
| 142 | Camera* camera = gfx_get_camera_camera(state->camera); | ||
| 143 | camera->projection = projection; | ||
| 144 | } | ||
diff --git a/src/plugins/viewer.c b/src/plugins/viewer.c new file mode 100644 index 0000000..1a27f8f --- /dev/null +++ b/src/plugins/viewer.c | |||
| @@ -0,0 +1,373 @@ | |||
| 1 | #include "plugin.h" | ||
| 2 | |||
| 3 | #include <gfx/app.h> | ||
| 4 | #include <gfx/asset.h> | ||
| 5 | #include <gfx/renderer.h> | ||
| 6 | #include <gfx/scene.h> | ||
| 7 | #include <gfx/util/skyquad.h> | ||
| 8 | #include <math/camera.h> | ||
| 9 | #include <math/spatial3.h> | ||
| 10 | |||
| 11 | #include <log/log.h> | ||
| 12 | |||
| 13 | #include <stdlib.h> | ||
| 14 | |||
| 15 | // Skybox. | ||
| 16 | static const char* skybox[6] = { | ||
| 17 | "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_east.bmp", | ||
| 18 | "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_west.bmp", | ||
| 19 | "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_up.bmp", | ||
| 20 | "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_down.bmp", | ||
| 21 | "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_south.bmp", | ||
| 22 | "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_north.bmp", | ||
| 23 | }; | ||
| 24 | |||
| 25 | // Paths to various scene files. | ||
| 26 | static const char* BOX = "/home/jeanne/Nextcloud/assets/models/box.gltf"; | ||
| 27 | static const char* SUZANNE = | ||
| 28 | "/home/jeanne/Nextcloud/assets/models/suzanne.gltf"; | ||
| 29 | static const char* SPONZA = "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/" | ||
| 30 | "2.0/Sponza/glTF/Sponza.gltf"; | ||
| 31 | static const char* FLIGHT_HELMET = | ||
| 32 | "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/2.0/FlightHelmet/glTF/" | ||
| 33 | "FlightHelmet.gltf"; | ||
| 34 | static const char* DAMAGED_HELMET = | ||
| 35 | "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/2.0/DamagedHelmet/glTF/" | ||
| 36 | "DamagedHelmet.gltf"; | ||
| 37 | static const char* GIRL = | ||
| 38 | "/home/jeanne/Nextcloud/assets/models/girl/girl-with-ground.gltf"; | ||
| 39 | static const char* BOXES = | ||
| 40 | "/home/jeanne/Nextcloud/assets/models/boxes/boxes.gltf"; | ||
| 41 | |||
| 42 | #define DEFAULT_SCENE_FILE GIRL | ||
| 43 | |||
| 44 | static const bool RenderBoundingBoxes = false; | ||
| 45 | static const R DefaultCameraSpeed = (R)6.0; | ||
| 46 | static const R DefaultMouseSensitivity = (R)(10 * TO_RAD); | ||
| 47 | static const vec3 DefaultCameraPosition = (vec3){0, 2, 5}; | ||
| 48 | |||
| 49 | typedef struct CameraCommand { | ||
| 50 | bool CameraMoveLeft : 1; | ||
| 51 | bool CameraMoveRight : 1; | ||
| 52 | bool CameraMoveForward : 1; | ||
| 53 | bool CameraMoveBackward : 1; | ||
| 54 | } CameraCommand; | ||
| 55 | |||
| 56 | typedef struct CameraController { | ||
| 57 | R camera_speed; // Camera movement speed. | ||
| 58 | R mouse_sensitivity; // Controls the degree with which mouse movements | ||
| 59 | // rotate the camera. | ||
| 60 | vec2 prev_mouse_position; // Mouse position in the previous frame. | ||
| 61 | bool rotating; // When true, subsequent mouse movements cause the | ||
| 62 | // camera to rotate. | ||
| 63 | } CameraController; | ||
| 64 | |||
| 65 | typedef struct State { | ||
| 66 | Scene* scene; | ||
| 67 | Model* model; | ||
| 68 | SceneCamera* camera; | ||
| 69 | CameraController camera_controller; | ||
| 70 | } State; | ||
| 71 | |||
| 72 | /// Load the skyquad texture. | ||
| 73 | static const Texture* load_environment_map(Gfx* gfx) { | ||
| 74 | assert(gfx); | ||
| 75 | return gfx_load_texture( | ||
| 76 | gfx, &(LoadTextureCmd){ | ||
| 77 | .origin = AssetFromFile, | ||
| 78 | .type = LoadCubemap, | ||
| 79 | .colour_space = sRGB, | ||
| 80 | .filtering = NearestFiltering, | ||
| 81 | .mipmaps = false, | ||
| 82 | .data.cubemap.filepaths = { | ||
| 83 | mstring_make(skybox[0]), mstring_make(skybox[1]), | ||
| 84 | mstring_make(skybox[2]), mstring_make(skybox[3]), | ||
| 85 | mstring_make(skybox[4]), mstring_make(skybox[5])} | ||
| 86 | }); | ||
| 87 | } | ||
| 88 | |||
| 89 | /// Load the skyquad and return the environment light node. | ||
| 90 | static SceneNode* load_skyquad(Gfx* gfx, SceneNode* root) { | ||
| 91 | assert(gfx); | ||
| 92 | assert(root); | ||
| 93 | |||
| 94 | GfxCore* gfxcore = gfx_get_core(gfx); | ||
| 95 | |||
| 96 | const Texture* environment_map = load_environment_map(gfx); | ||
| 97 | if (!environment_map) { | ||
| 98 | return 0; | ||
| 99 | } | ||
| 100 | |||
| 101 | return gfx_setup_skyquad(gfxcore, root, environment_map); | ||
| 102 | } | ||
| 103 | |||
| 104 | /// Load the model. | ||
| 105 | static Model* load_model(Game* game, State* state, const char* scene_filepath) { | ||
| 106 | assert(game); | ||
| 107 | assert(game->gfx); | ||
| 108 | assert(state); | ||
| 109 | assert(state->scene); | ||
| 110 | |||
| 111 | Camera* camera = gfx_get_camera_camera(state->camera); | ||
| 112 | spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2)); | ||
| 113 | |||
| 114 | SceneNode* root = gfx_get_scene_root(state->scene); | ||
| 115 | SceneNode* sky_light_node = load_skyquad(game->gfx, root); | ||
| 116 | if (!sky_light_node) { | ||
| 117 | return 0; // test | ||
| 118 | } | ||
| 119 | |||
| 120 | Model* model = gfx_load_model( | ||
| 121 | game->gfx, &(LoadModelCmd){.origin = AssetFromFile, | ||
| 122 | .filepath = mstring_make(scene_filepath)}); | ||
| 123 | if (!model) { | ||
| 124 | return 0; | ||
| 125 | } | ||
| 126 | SceneNode* model_node = gfx_make_model_node(model); | ||
| 127 | if (!model_node) { | ||
| 128 | return 0; | ||
| 129 | } | ||
| 130 | gfx_set_node_parent(model_node, sky_light_node); | ||
| 131 | |||
| 132 | gfx_log_node_hierarchy(root); | ||
| 133 | |||
| 134 | return model; | ||
| 135 | } | ||
| 136 | |||
| 137 | bool init(Game* game, State** pp_state) { | ||
| 138 | assert(game); | ||
| 139 | |||
| 140 | // Usage: <scene file> | ||
| 141 | const char* scene_filepath = | ||
| 142 | game->argc > 1 ? game->argv[1] : DEFAULT_SCENE_FILE; | ||
| 143 | |||
| 144 | State* state = calloc(1, sizeof(State)); | ||
| 145 | if (!state) { | ||
| 146 | goto cleanup; | ||
| 147 | } | ||
| 148 | |||
| 149 | if (!(state->scene = gfx_make_scene())) { | ||
| 150 | goto cleanup; | ||
| 151 | } | ||
| 152 | if (!(state->camera = gfx_make_camera())) { | ||
| 153 | goto cleanup; | ||
| 154 | } | ||
| 155 | |||
| 156 | state->model = load_model(game, state, scene_filepath); | ||
| 157 | if (!state->model) { | ||
| 158 | goto cleanup; | ||
| 159 | } | ||
| 160 | |||
| 161 | Anima* anima = gfx_get_model_anima(state->model); | ||
| 162 | if (anima) { | ||
| 163 | gfx_play_animation( | ||
| 164 | anima, &(AnimationPlaySettings){.name = "Walk", .loop = true}); | ||
| 165 | // TODO: Interpolate animations. | ||
| 166 | /*gfx_play_animation( | ||
| 167 | anima, | ||
| 168 | &(AnimationPlaySettings){.name = "Jumping-jack-lower", .loop = true}); | ||
| 169 | gfx_play_animation( | ||
| 170 | anima, &(AnimationPlaySettings){ | ||
| 171 | .name = "Jumping-jack-arms-mid", .loop = true});*/ | ||
| 172 | } | ||
| 173 | |||
| 174 | spatial3_set_position( | ||
| 175 | &gfx_get_camera_camera(state->camera)->spatial, DefaultCameraPosition); | ||
| 176 | |||
| 177 | state->camera_controller.camera_speed = DefaultCameraSpeed; | ||
| 178 | state->camera_controller.mouse_sensitivity = DefaultMouseSensitivity; | ||
| 179 | |||
| 180 | *pp_state = state; | ||
| 181 | return true; | ||
| 182 | |||
| 183 | cleanup: | ||
| 184 | shutdown(game, state); | ||
| 185 | if (state) { | ||
| 186 | free(state); | ||
| 187 | } | ||
| 188 | return false; | ||
| 189 | } | ||
| 190 | |||
| 191 | void shutdown(Game* game, State* state) { | ||
| 192 | assert(game); | ||
| 193 | if (state) { | ||
| 194 | gfx_destroy_camera(&state->camera); | ||
| 195 | gfx_destroy_scene(&state->scene); | ||
| 196 | // State freed by plugin engine. | ||
| 197 | } | ||
| 198 | } | ||
| 199 | |||
| 200 | static void update_camera( | ||
| 201 | CameraController* controller, R dt, vec2 mouse_position, | ||
| 202 | CameraCommand command, Spatial3* camera) { | ||
| 203 | assert(controller); | ||
| 204 | assert(camera); | ||
| 205 | |||
| 206 | // Translation. | ||
| 207 | const R move_x = (R)(command.CameraMoveLeft ? -1 : 0) + | ||
| 208 | (R)(command.CameraMoveRight ? 1 : 0); | ||
| 209 | const R move_y = (R)(command.CameraMoveForward ? 1 : 0) + | ||
| 210 | (R)(command.CameraMoveBackward ? -1 : 0); | ||
| 211 | const vec2 translation = | ||
| 212 | vec2_scale(vec2_make(move_x, move_y), controller->camera_speed * dt); | ||
| 213 | spatial3_move_right(camera, translation.x); | ||
| 214 | spatial3_move_forwards(camera, translation.y); | ||
| 215 | |||
| 216 | // Rotation. | ||
| 217 | if (controller->rotating) { | ||
| 218 | const vec2 mouse_delta = | ||
| 219 | vec2_sub(mouse_position, controller->prev_mouse_position); | ||
| 220 | |||
| 221 | const vec2 rotation = | ||
| 222 | vec2_scale(mouse_delta, controller->mouse_sensitivity * dt); | ||
| 223 | |||
| 224 | spatial3_global_yaw(camera, -rotation.x); | ||
| 225 | spatial3_pitch(camera, -rotation.y); | ||
| 226 | } | ||
| 227 | |||
| 228 | // Update controller state. | ||
| 229 | controller->prev_mouse_position = mouse_position; | ||
| 230 | } | ||
| 231 | |||
| 232 | void update(Game* game, State* state, double t, double dt) { | ||
| 233 | assert(game); | ||
| 234 | assert(state); | ||
| 235 | assert(state->scene); | ||
| 236 | assert(state->camera); | ||
| 237 | |||
| 238 | double mouse_x, mouse_y; | ||
| 239 | gfx_app_get_mouse_position(&mouse_x, &mouse_y); | ||
| 240 | const vec2 mouse_position = {(R)mouse_x, (R)mouse_y}; | ||
| 241 | |||
| 242 | const CameraCommand camera_command = (CameraCommand){ | ||
| 243 | .CameraMoveLeft = gfx_app_is_key_pressed(KeyA), | ||
| 244 | .CameraMoveRight = gfx_app_is_key_pressed(KeyD), | ||
| 245 | .CameraMoveForward = gfx_app_is_key_pressed(KeyW), | ||
| 246 | .CameraMoveBackward = gfx_app_is_key_pressed(KeyS), | ||
| 247 | }; | ||
| 248 | |||
| 249 | state->camera_controller.rotating = gfx_app_is_mouse_button_pressed(LMB); | ||
| 250 | |||
| 251 | update_camera( | ||
| 252 | &state->camera_controller, (R)dt, mouse_position, camera_command, | ||
| 253 | &gfx_get_camera_camera(state->camera)->spatial); | ||
| 254 | |||
| 255 | // const vec3 orbit_point = vec3_make(0, 2, 0); | ||
| 256 | // Camera* camera = gfx_get_camera_camera(state->camera); | ||
| 257 | // spatial3_orbit( | ||
| 258 | // &camera->spatial, orbit_point, | ||
| 259 | // /*radius=*/5, | ||
| 260 | // /*azimuth=*/(R)(t * 0.5), /*zenith=*/0); | ||
| 261 | // spatial3_lookat(&camera->spatial, orbit_point); | ||
| 262 | |||
| 263 | gfx_update(state->scene, state->camera, (R)t); | ||
| 264 | } | ||
| 265 | |||
| 266 | /// Render the bounding boxes of all scene objects. | ||
| 267 | static void render_bounding_boxes_rec( | ||
| 268 | ImmRenderer* imm, const Anima* anima, const mat4* parent_model_matrix, | ||
| 269 | const SceneNode* node) { | ||
| 270 | assert(imm); | ||
| 271 | assert(node); | ||
| 272 | |||
| 273 | const mat4 model_matrix = | ||
| 274 | mat4_mul(*parent_model_matrix, gfx_get_node_transform(node)); | ||
| 275 | |||
| 276 | const NodeType node_type = gfx_get_node_type(node); | ||
| 277 | |||
| 278 | if (node_type == ModelNode) { | ||
| 279 | const Model* model = gfx_get_node_model(node); | ||
| 280 | const SceneNode* root = gfx_get_model_root(model); | ||
| 281 | render_bounding_boxes_rec(imm, anima, &model_matrix, root); | ||
| 282 | } else if (node_type == AnimaNode) { | ||
| 283 | anima = gfx_get_node_anima(node); | ||
| 284 | } else if (node_type == ObjectNode) { | ||
| 285 | gfx_imm_set_model_matrix(imm, &model_matrix); | ||
| 286 | |||
| 287 | const SceneObject* obj = gfx_get_node_object(node); | ||
| 288 | const Skeleton* skeleton = gfx_get_object_skeleton(obj); | ||
| 289 | |||
| 290 | if (skeleton) { // Animated model. | ||
| 291 | assert(anima); | ||
| 292 | const size_t num_joints = gfx_get_skeleton_num_joints(skeleton); | ||
| 293 | for (size_t i = 0; i < num_joints; ++i) { | ||
| 294 | if (gfx_joint_has_box(anima, skeleton, i)) { | ||
| 295 | const Box box = gfx_get_joint_box(anima, skeleton, i); | ||
| 296 | gfx_imm_draw_box3(imm, box.vertices); | ||
| 297 | } | ||
| 298 | } | ||
| 299 | } else { // Static model. | ||
| 300 | const aabb3 box = gfx_get_object_aabb(obj); | ||
| 301 | gfx_imm_draw_aabb3(imm, box); | ||
| 302 | } | ||
| 303 | } | ||
| 304 | |||
| 305 | // Render children's boxes. | ||
| 306 | const SceneNode* child = gfx_get_node_child(node); | ||
| 307 | while (child) { | ||
| 308 | render_bounding_boxes_rec(imm, anima, &model_matrix, child); | ||
| 309 | child = gfx_get_node_sibling(child); | ||
| 310 | } | ||
| 311 | } | ||
| 312 | |||
| 313 | /// Render the bounding boxes of all scene objects. | ||
| 314 | static void render_bounding_boxes(const Game* game, const State* state) { | ||
| 315 | assert(game); | ||
| 316 | assert(state); | ||
| 317 | |||
| 318 | GfxCore* gfxcore = gfx_get_core(game->gfx); | ||
| 319 | ImmRenderer* imm = gfx_get_imm_renderer(game->gfx); | ||
| 320 | assert(gfxcore); | ||
| 321 | assert(imm); | ||
| 322 | |||
| 323 | const mat4 id = mat4_id(); | ||
| 324 | Anima* anima = 0; | ||
| 325 | |||
| 326 | gfx_set_blending(gfxcore, true); | ||
| 327 | gfx_set_depth_mask(gfxcore, false); | ||
| 328 | gfx_set_polygon_offset(gfxcore, -1.5f, -1.0f); | ||
| 329 | |||
| 330 | gfx_imm_start(imm); | ||
| 331 | gfx_imm_set_camera(imm, gfx_get_camera_camera(state->camera)); | ||
| 332 | gfx_imm_set_colour(imm, vec4_make(0.3, 0.3, 0.9, 0.1)); | ||
| 333 | render_bounding_boxes_rec(imm, anima, &id, gfx_get_scene_root(state->scene)); | ||
| 334 | gfx_imm_end(imm); | ||
| 335 | |||
| 336 | gfx_reset_polygon_offset(gfxcore); | ||
| 337 | gfx_set_depth_mask(gfxcore, true); | ||
| 338 | gfx_set_blending(gfxcore, false); | ||
| 339 | } | ||
| 340 | |||
| 341 | void render(const Game* game, const State* state) { | ||
| 342 | assert(state); | ||
| 343 | assert(game); | ||
| 344 | assert(game->gfx); | ||
| 345 | assert(state->scene); | ||
| 346 | assert(state->camera); | ||
| 347 | |||
| 348 | Renderer* renderer = gfx_get_renderer(game->gfx); | ||
| 349 | assert(renderer); | ||
| 350 | |||
| 351 | gfx_render_scene( | ||
| 352 | renderer, &(RenderSceneParams){.mode = RenderDefault, | ||
| 353 | .scene = state->scene, | ||
| 354 | .camera = state->camera}); | ||
| 355 | |||
| 356 | if (RenderBoundingBoxes) { | ||
| 357 | render_bounding_boxes(game, state); | ||
| 358 | } | ||
| 359 | } | ||
| 360 | |||
| 361 | void resize(Game* game, State* state, int width, int height) { | ||
| 362 | assert(game); | ||
| 363 | assert(state); | ||
| 364 | |||
| 365 | const R fovy = 60 * TO_RAD; | ||
| 366 | const R aspect = (R)width / (R)height; | ||
| 367 | const R near = 0.1; | ||
| 368 | const R far = 1000; | ||
| 369 | const mat4 projection = mat4_perspective(fovy, aspect, near, far); | ||
| 370 | |||
| 371 | Camera* camera = gfx_get_camera_camera(state->camera); | ||
| 372 | camera->projection = projection; | ||
| 373 | } | ||
