aboutsummaryrefslogtreecommitdiff
path: root/game/src
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2025-06-27 10:18:39 -0700
committer3gg <3gg@shellblade.net>2025-06-27 10:18:39 -0700
commitbd57f345ed9dbed1d81683e48199626de2ea9044 (patch)
tree4221f2f2a7ad2244d2e93052bd68187ec91b8ea9 /game/src
parent9a82ce0083437a4f9f58108b2c23b957d2249ad8 (diff)
Restructure projectHEADmain
Diffstat (limited to 'game/src')
-rw-r--r--game/src/game.c218
-rw-r--r--game/src/game.h21
-rw-r--r--game/src/plugins/CMakeLists.txt29
-rw-r--r--game/src/plugins/plugin.h52
-rw-r--r--game/src/plugins/pong.c237
-rw-r--r--game/src/plugins/texture_view.c144
-rw-r--r--game/src/plugins/viewer.c374
7 files changed, 0 insertions, 1075 deletions
diff --git a/game/src/game.c b/game/src/game.c
deleted file mode 100644
index 51f5cbe..0000000
--- a/game/src/game.c
+++ /dev/null
@@ -1,218 +0,0 @@
1/*
2 * Main game module with entry point and game loop.
3 *
4 * The game module sets up the window and GL context and defers the core game
5 * logic to a plugin.
6 */
7#define _GNU_SOURCE 200112L // For readlink()
8
9#include "game.h"
10
11#include "plugins/plugin.h"
12
13#include <gfx/app.h>
14#include <gfx/core.h>
15#include <gfx/gfx.h>
16#include <gfx/renderer.h>
17
18#include <error.h>
19#include <log/log.h>
20#include <plugin.h>
21
22#include <assert.h>
23#include <stdbool.h>
24#include <stdio.h>
25#include <stdlib.h>
26
27#include <linux/limits.h>
28
29#include <unistd.h>
30
31#undef _GNU_SOURCE
32
33static const int WIDTH = 1350;
34static const int HEIGHT = 900;
35static const int MAX_FPS = 60;
36
37typedef struct GfxAppState {
38 Game game;
39} GfxAppState;
40
41/// Initialize the game's plugin.
42static bool init_plugin(Game* game) {
43 assert(game);
44 assert(game->plugin);
45 // Plugin state is allowed to be null, either when the plugin does not
46 // expose an init() or when init() does not initialize a state.
47 if (plugin_resolve(game->plugin, plugin_init, "init")) {
48 State* plugin_state = 0;
49 if (!plugin_call(game->plugin, plugin_init, "init", game, &plugin_state)) {
50 return false;
51 }
52 set_plugin_state(game->plugin, plugin_state);
53 }
54 return true; // Plugin does not need to expose an init().
55}
56
57/// Shutdown the game's plugin.
58/// The game's plugin is allowed to be null in the call to this function.
59static void shutdown_plugin(Game* game) {
60 assert(game);
61 if (game->plugin &&
62 (plugin_resolve(game->plugin, plugin_shutdown, "shutdown"))) {
63 void* plugin_state = get_plugin_state(game->plugin);
64 plugin_call(game->plugin, plugin_shutdown, "shutdown", game, plugin_state);
65 set_plugin_state(game->plugin, 0);
66 }
67}
68
69/// Boot the game's plugin.
70static bool boot_plugin(Game* game) {
71 assert(game);
72 assert(game->plugin);
73 if (plugin_resolve(game->plugin, plugin_boot, "boot")) {
74 void* plugin_state = get_plugin_state(game->plugin);
75 return plugin_call(game->plugin, plugin_boot, "boot", game, plugin_state);
76 }
77 return true; // Plugin does not need to expose a boot().
78}
79
80/// Update the plugin's state.
81static void update_plugin(Game* game, double t, double dt) {
82 assert(game);
83 assert(game->plugin);
84 if (plugin_resolve(game->plugin, plugin_update, "update")) {
85 void* plugin_state = get_plugin_state(game->plugin);
86 plugin_call(
87 game->plugin, plugin_update, "update", game, plugin_state, t, dt);
88 }
89}
90
91/// Plugin render.
92static void render_plugin(const Game* game) {
93 assert(game);
94 assert(game->plugin);
95 if (plugin_resolve(game->plugin, plugin_render, "render")) {
96 void* plugin_state = get_plugin_state(game->plugin);
97 plugin_call(game->plugin, plugin_render, "render", game, plugin_state);
98 }
99}
100
101/// Plugin resize.
102static void resize_plugin(Game* game, int width, int height) {
103 assert(game);
104 assert(game->plugin);
105 if (plugin_resolve(game->plugin, plugin_resize, "resize")) {
106 void* plugin_state = get_plugin_state(game->plugin);
107 plugin_call(
108 game->plugin, plugin_resize, "resize", game, plugin_state, width,
109 height);
110 }
111}
112
113static void Shutdown(Game* game);
114
115static bool Init(Game* game, int argc, const char** argv) {
116 assert(game);
117
118 if (argc <= 1) {
119 LOGE("Usage: %s <plugin> [plugin args]", argv[0]);
120 return false;
121 }
122
123 // Syntax: game <plugin> [plugin args]
124 //
125 // Here we consume the <plugin> arg so that plugins receive the remainder
126 // args starting from 0.
127 game->argc = argc - 1;
128 game->argv = argv + 1;
129
130 char exe_path_buf[NAME_MAX] = {0};
131 if (readlink("/proc/self/exe", exe_path_buf, sizeof(exe_path_buf)) == -1) {
132 LOGE("readlink(/proc/self/exe) failed");
133 goto cleanup;
134 }
135
136 // Replace the last / with a null terminator to remove the exe file from the
137 // path. This gets the file's parent directory.
138 *strrchr(exe_path_buf, '/') = 0;
139
140 const mstring exe_dir = mstring_make(exe_path_buf);
141 const mstring plugins_path = mstring_concat_cstr(exe_dir, "/src/plugins");
142
143 if (!(game->plugin_engine = new_plugin_engine(
144 &(PluginEngineDesc){.plugins_dir = mstring_cstr(&plugins_path)}))) {
145 goto cleanup;
146 }
147
148 const char* plugin = argv[1];
149 if (!(game->plugin = load_plugin(game->plugin_engine, plugin))) {
150 goto cleanup;
151 }
152
153 if (!(game->gfx = gfx_init())) {
154 goto cleanup;
155 }
156
157 if (!init_plugin(game)) {
158 goto cleanup;
159 }
160 if (!boot_plugin(game)) {
161 goto cleanup;
162 }
163
164 return true;
165
166cleanup:
167 LOGE("Gfx error: %s", get_error());
168 Shutdown(game);
169 return false;
170}
171
172static void Shutdown(Game* game) {
173 assert(game);
174 shutdown_plugin(game);
175 if (game->gfx) {
176 gfx_destroy(&game->gfx);
177 }
178 if (game->plugin) {
179 delete_plugin(&game->plugin);
180 }
181 if (game->plugin_engine) {
182 delete_plugin_engine(&game->plugin_engine);
183 }
184}
185
186static void Update(Game* game, double t, double dt) {
187 plugin_engine_update(game->plugin_engine);
188 if (plugin_reloaded(game->plugin)) {
189 shutdown_plugin(game);
190 const bool result = init_plugin(game);
191 assert(result); // TODO: handle error better.
192
193 // Trigger a resize just like the initial resize that occurs when the gfx
194 // application starts.
195 resize_plugin(game, game->width, game->height);
196 }
197
198 update_plugin(game, t, dt);
199}
200
201static void Render(const Game* game) {
202 GfxCore* gfxcore = gfx_get_core(game->gfx);
203 gfx_start_frame(gfxcore);
204 render_plugin(game);
205 gfx_end_frame(gfxcore);
206}
207
208static void Resize(Game* game, int width, int height) {
209 game->width = width;
210 game->height = height;
211
212 GfxCore* gfxcore = gfx_get_core(game->gfx);
213 gfx_set_viewport(gfxcore, 0, 0, width, height);
214
215 resize_plugin(game, width, height);
216}
217
218GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS, "Game");
diff --git a/game/src/game.h b/game/src/game.h
deleted file mode 100644
index 579ba3c..0000000
--- a/game/src/game.h
+++ /dev/null
@@ -1,21 +0,0 @@
1/*
2 * Header file defining the game state, included by plugins.
3 */
4#pragma once
5
6typedef struct PluginEngine PluginEngine;
7typedef struct Plugin Plugin;
8typedef struct Gfx Gfx;
9typedef struct Scene Scene;
10typedef struct SceneCamera SceneCamera;
11
12/// Game state.
13typedef struct {
14 int argc;
15 const char** argv;
16 PluginEngine* plugin_engine;
17 Plugin* plugin;
18 Gfx* gfx;
19 int width;
20 int height;
21} Game;
diff --git a/game/src/plugins/CMakeLists.txt b/game/src/plugins/CMakeLists.txt
deleted file mode 100644
index 8661598..0000000
--- a/game/src/plugins/CMakeLists.txt
+++ /dev/null
@@ -1,29 +0,0 @@
1cmake_minimum_required(VERSION 3.0)
2
3project(plugins)
4
5set(LINK_LIBRARIES cstring math gfx gfx-app)
6
7# Viewer
8
9add_library(viewer SHARED
10 viewer.c)
11
12target_link_libraries(viewer PUBLIC
13 ${LINK_LIBRARIES})
14
15# Texture viewer
16
17add_library(texture_view SHARED
18 texture_view.c)
19
20target_link_libraries(texture_view PUBLIC
21 ${LINK_LIBRARIES})
22
23# Pong
24
25add_library(pong SHARED
26 pong.c)
27
28target_link_libraries(pong PUBLIC
29 ${LINK_LIBRARIES})
diff --git a/game/src/plugins/plugin.h b/game/src/plugins/plugin.h
deleted file mode 100644
index f7219c6..0000000
--- a/game/src/plugins/plugin.h
+++ /dev/null
@@ -1,52 +0,0 @@
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
13typedef 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.)
22bool 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.
30void 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.
35bool boot(Game*, State*);
36
37/// Update the plugin's and the game's state.
38void update(Game*, State*, double t, double dt);
39
40/// Render hook.
41void render(const Game*, const State*);
42
43/// Called when the game's window is resized.
44void resize(Game*, State*, int width, int height);
45
46// Signatures for the plugin's exposed functions.
47typedef bool (*plugin_init)(Game*, State**);
48typedef bool (*plugin_shutdown)(Game*, State*);
49typedef bool (*plugin_boot)(Game*, State*);
50typedef void (*plugin_update)(Game*, State*, double t, double dt);
51typedef void (*plugin_render)(const Game*, const State*);
52typedef void (*plugin_resize)(Game* game, State* state, int width, int height);
diff --git a/game/src/plugins/pong.c b/game/src/plugins/pong.c
deleted file mode 100644
index c1c55be..0000000
--- a/game/src/plugins/pong.c
+++ /dev/null
@@ -1,237 +0,0 @@
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
13static const vec2 PAD_SIZE = (vec2){120, 20};
14static const R PLAYER_Y_OFFSET = 50;
15static const R PLAYER_SPEED = 800;
16
17static const R ENEMY_SPEED = 2;
18
19static const R BALL_SIZE = 18;
20static const R BALL_SPEED = 360; // In each dimension.
21
22static const R EPS = (R)1e-3;
23
24typedef struct Player {
25 vec2 position;
26} Player;
27
28typedef struct Ball {
29 vec2 position;
30 vec2 velocity;
31} Ball;
32
33typedef struct State {
34 bool game_started;
35 Player human;
36 Player enemy;
37 Ball ball;
38 mat4 viewProjection;
39} State;
40
41bool 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
52cleanup:
53 free(state);
54 return false;
55}
56
57void shutdown(Game* game, State* state) {
58 assert(game);
59 assert(state);
60}
61
62static 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
91void 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
97void 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
111void 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
126void 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
154void 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
168static 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
181static 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
193void 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
208static R clamp_to_width(int width, R x, R extent) {
209 return min(x, (R)width - extent);
210}
211
212void 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/game/src/plugins/texture_view.c b/game/src/plugins/texture_view.c
deleted file mode 100644
index a8b2a94..0000000
--- a/game/src/plugins/texture_view.c
+++ /dev/null
@@ -1,144 +0,0 @@
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.
16static const char* DEFAULT_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp";
17// static const char* DEFAULT_TEXTURE = "/assets/checkerboard.jpg";
18
19struct State {
20 Scene* scene;
21 SceneCamera* camera;
22};
23
24bool 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
103cleanup:
104 shutdown(game, state);
105 if (state) {
106 free(state);
107 }
108 return false;
109}
110
111void 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
120void 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
132void 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/game/src/plugins/viewer.c b/game/src/plugins/viewer.c
deleted file mode 100644
index 23b9ffb..0000000
--- a/game/src/plugins/viewer.c
+++ /dev/null
@@ -1,374 +0,0 @@
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.
16static 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.
26static const char* BOX = "/home/jeanne/Nextcloud/assets/models/box.gltf";
27static const char* SUZANNE =
28 "/home/jeanne/Nextcloud/assets/models/suzanne.gltf";
29static const char* SPONZA = "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/"
30 "2.0/Sponza/glTF/Sponza.gltf";
31static const char* FLIGHT_HELMET =
32 "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/2.0/FlightHelmet/glTF/"
33 "FlightHelmet.gltf";
34static const char* DAMAGED_HELMET =
35 "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/2.0/DamagedHelmet/glTF/"
36 "DamagedHelmet.gltf";
37static const char* GIRL =
38 "/home/jeanne/Nextcloud/assets/models/girl/girl-with-ground.gltf";
39static const char* BOXES =
40 "/home/jeanne/Nextcloud/assets/models/boxes/boxes.gltf";
41
42#define DEFAULT_SCENE_FILE GIRL
43
44static const bool RenderBoundingBoxes = false;
45static const R DefaultCameraSpeed = (R)6.0;
46static const R DefaultMouseSensitivity = (R)(10 * TO_RAD);
47static const vec3 DefaultCameraPosition = (vec3){0, 2, 5};
48
49typedef struct CameraCommand {
50 bool CameraMoveLeft : 1;
51 bool CameraMoveRight : 1;
52 bool CameraMoveForward : 1;
53 bool CameraMoveBackward : 1;
54} CameraCommand;
55
56typedef 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
65typedef struct State {
66 Scene* scene;
67 Model* model;
68 SceneCamera* camera;
69 CameraController camera_controller;
70} State;
71
72/// Load the skyquad texture.
73static 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.
90static 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 3D scene.
105/// Return the loaded model.
106static Model* load_scene(Game* game, State* state, const char* scene_filepath) {
107 assert(game);
108 assert(game->gfx);
109 assert(state);
110 assert(state->scene);
111
112 Camera* camera = gfx_get_camera_camera(state->camera);
113 spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2));
114
115 SceneNode* root = gfx_get_scene_root(state->scene);
116 SceneNode* sky_light_node = load_skyquad(game->gfx, root);
117 if (!sky_light_node) {
118 return 0; // test
119 }
120
121 Model* model = gfx_load_model(
122 game->gfx, &(LoadModelCmd){.origin = AssetFromFile,
123 .filepath = mstring_make(scene_filepath)});
124 if (!model) {
125 return 0;
126 }
127 SceneNode* model_node = gfx_make_model_node(model);
128 if (!model_node) {
129 return 0;
130 }
131 gfx_set_node_parent(model_node, sky_light_node);
132
133 gfx_log_node_hierarchy(root);
134
135 return model;
136}
137
138bool init(Game* game, State** pp_state) {
139 assert(game);
140
141 // Usage: <scene file>
142 const char* scene_filepath =
143 game->argc > 1 ? game->argv[1] : DEFAULT_SCENE_FILE;
144
145 State* state = calloc(1, sizeof(State));
146 if (!state) {
147 goto cleanup;
148 }
149
150 if (!(state->scene = gfx_make_scene())) {
151 goto cleanup;
152 }
153 if (!(state->camera = gfx_make_camera())) {
154 goto cleanup;
155 }
156
157 state->model = load_scene(game, state, scene_filepath);
158 if (!state->model) {
159 goto cleanup;
160 }
161
162 Anima* anima = gfx_get_model_anima(state->model);
163 if (anima) {
164 gfx_play_animation(
165 anima, &(AnimationPlaySettings){.name = "Walk", .loop = true});
166 // TODO: Interpolate animations.
167 /*gfx_play_animation(
168 anima,
169 &(AnimationPlaySettings){.name = "Jumping-jack-lower", .loop = true});
170 gfx_play_animation(
171 anima, &(AnimationPlaySettings){
172 .name = "Jumping-jack-arms-mid", .loop = true});*/
173 }
174
175 spatial3_set_position(
176 &gfx_get_camera_camera(state->camera)->spatial, DefaultCameraPosition);
177
178 state->camera_controller.camera_speed = DefaultCameraSpeed;
179 state->camera_controller.mouse_sensitivity = DefaultMouseSensitivity;
180
181 *pp_state = state;
182 return true;
183
184cleanup:
185 shutdown(game, state);
186 if (state) {
187 free(state);
188 }
189 return false;
190}
191
192void shutdown(Game* game, State* state) {
193 assert(game);
194 if (state) {
195 gfx_destroy_camera(&state->camera);
196 gfx_destroy_scene(&state->scene);
197 // State freed by plugin engine.
198 }
199}
200
201static void update_camera(
202 CameraController* controller, R dt, vec2 mouse_position,
203 CameraCommand command, Spatial3* camera) {
204 assert(controller);
205 assert(camera);
206
207 // Translation.
208 const R move_x = (R)(command.CameraMoveLeft ? -1 : 0) +
209 (R)(command.CameraMoveRight ? 1 : 0);
210 const R move_y = (R)(command.CameraMoveForward ? 1 : 0) +
211 (R)(command.CameraMoveBackward ? -1 : 0);
212 const vec2 translation =
213 vec2_scale(vec2_make(move_x, move_y), controller->camera_speed * dt);
214 spatial3_move_right(camera, translation.x);
215 spatial3_move_forwards(camera, translation.y);
216
217 // Rotation.
218 if (controller->rotating) {
219 const vec2 mouse_delta =
220 vec2_sub(mouse_position, controller->prev_mouse_position);
221
222 const vec2 rotation =
223 vec2_scale(mouse_delta, controller->mouse_sensitivity * dt);
224
225 spatial3_global_yaw(camera, -rotation.x);
226 spatial3_pitch(camera, -rotation.y);
227 }
228
229 // Update controller state.
230 controller->prev_mouse_position = mouse_position;
231}
232
233void update(Game* game, State* state, double t, double dt) {
234 assert(game);
235 assert(state);
236 assert(state->scene);
237 assert(state->camera);
238
239 double mouse_x, mouse_y;
240 gfx_app_get_mouse_position(&mouse_x, &mouse_y);
241 const vec2 mouse_position = {(R)mouse_x, (R)mouse_y};
242
243 const CameraCommand camera_command = (CameraCommand){
244 .CameraMoveLeft = gfx_app_is_key_pressed(KeyA),
245 .CameraMoveRight = gfx_app_is_key_pressed(KeyD),
246 .CameraMoveForward = gfx_app_is_key_pressed(KeyW),
247 .CameraMoveBackward = gfx_app_is_key_pressed(KeyS),
248 };
249
250 state->camera_controller.rotating = gfx_app_is_mouse_button_pressed(LMB);
251
252 update_camera(
253 &state->camera_controller, (R)dt, mouse_position, camera_command,
254 &gfx_get_camera_camera(state->camera)->spatial);
255
256 // const vec3 orbit_point = vec3_make(0, 2, 0);
257 // Camera* camera = gfx_get_camera_camera(state->camera);
258 // spatial3_orbit(
259 // &camera->spatial, orbit_point,
260 // /*radius=*/5,
261 // /*azimuth=*/(R)(t * 0.5), /*zenith=*/0);
262 // spatial3_lookat(&camera->spatial, orbit_point);
263
264 gfx_update(state->scene, state->camera, (R)t);
265}
266
267/// Render the bounding boxes of all scene objects.
268static void render_bounding_boxes_rec(
269 ImmRenderer* imm, const Anima* anima, const mat4* parent_model_matrix,
270 const SceneNode* node) {
271 assert(imm);
272 assert(node);
273
274 const mat4 model_matrix =
275 mat4_mul(*parent_model_matrix, gfx_get_node_transform(node));
276
277 const NodeType node_type = gfx_get_node_type(node);
278
279 if (node_type == ModelNode) {
280 const Model* model = gfx_get_node_model(node);
281 const SceneNode* root = gfx_get_model_root(model);
282 render_bounding_boxes_rec(imm, anima, &model_matrix, root);
283 } else if (node_type == AnimaNode) {
284 anima = gfx_get_node_anima(node);
285 } else if (node_type == ObjectNode) {
286 gfx_imm_set_model_matrix(imm, &model_matrix);
287
288 const SceneObject* obj = gfx_get_node_object(node);
289 const Skeleton* skeleton = gfx_get_object_skeleton(obj);
290
291 if (skeleton) { // Animated model.
292 assert(anima);
293 const size_t num_joints = gfx_get_skeleton_num_joints(skeleton);
294 for (size_t i = 0; i < num_joints; ++i) {
295 if (gfx_joint_has_box(anima, skeleton, i)) {
296 const Box box = gfx_get_joint_box(anima, skeleton, i);
297 gfx_imm_draw_box3(imm, box.vertices);
298 }
299 }
300 } else { // Static model.
301 const aabb3 box = gfx_get_object_aabb(obj);
302 gfx_imm_draw_aabb3(imm, box);
303 }
304 }
305
306 // Render children's boxes.
307 const SceneNode* child = gfx_get_node_child(node);
308 while (child) {
309 render_bounding_boxes_rec(imm, anima, &model_matrix, child);
310 child = gfx_get_node_sibling(child);
311 }
312}
313
314/// Render the bounding boxes of all scene objects.
315static void render_bounding_boxes(const Game* game, const State* state) {
316 assert(game);
317 assert(state);
318
319 GfxCore* gfxcore = gfx_get_core(game->gfx);
320 ImmRenderer* imm = gfx_get_imm_renderer(game->gfx);
321 assert(gfxcore);
322 assert(imm);
323
324 const mat4 id = mat4_id();
325 Anima* anima = 0;
326
327 gfx_set_blending(gfxcore, true);
328 gfx_set_depth_mask(gfxcore, false);
329 gfx_set_polygon_offset(gfxcore, -1.5f, -1.0f);
330
331 gfx_imm_start(imm);
332 gfx_imm_set_camera(imm, gfx_get_camera_camera(state->camera));
333 gfx_imm_set_colour(imm, vec4_make(0.3, 0.3, 0.9, 0.1));
334 render_bounding_boxes_rec(imm, anima, &id, gfx_get_scene_root(state->scene));
335 gfx_imm_end(imm);
336
337 gfx_reset_polygon_offset(gfxcore);
338 gfx_set_depth_mask(gfxcore, true);
339 gfx_set_blending(gfxcore, false);
340}
341
342void render(const Game* game, const State* state) {
343 assert(state);
344 assert(game);
345 assert(game->gfx);
346 assert(state->scene);
347 assert(state->camera);
348
349 Renderer* renderer = gfx_get_renderer(game->gfx);
350 assert(renderer);
351
352 gfx_render_scene(
353 renderer, &(RenderSceneParams){.mode = RenderDefault,
354 .scene = state->scene,
355 .camera = state->camera});
356
357 if (RenderBoundingBoxes) {
358 render_bounding_boxes(game, state);
359 }
360}
361
362void resize(Game* game, State* state, int width, int height) {
363 assert(game);
364 assert(state);
365
366 const R fovy = 60 * TO_RAD;
367 const R aspect = (R)width / (R)height;
368 const R near = 0.1;
369 const R far = 1000;
370 const mat4 projection = mat4_perspective(fovy, aspect, near, far);
371
372 Camera* camera = gfx_get_camera_camera(state->camera);
373 camera->projection = projection;
374}