summaryrefslogtreecommitdiff
path: root/game
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2024-01-20 15:54:54 -0800
committer3gg <3gg@shellblade.net>2024-01-20 15:54:54 -0800
commit3cd5b0bcca694630fcb4b977ddf7be7cd1bce153 (patch)
treee1a0e02ab5471d0f2ffb499f18109d6790241798 /game
parent02ec7cd07213e267fda7e1e67b62f55e92a2f32c (diff)
Rename gltfview -> game.
Diffstat (limited to 'game')
-rw-r--r--game/CMakeLists.txt22
-rw-r--r--game/src/game.c216
-rw-r--r--game/src/game.h19
-rw-r--r--game/src/plugins/CMakeLists.txt17
-rw-r--r--game/src/plugins/gltf_view.c196
-rw-r--r--game/src/plugins/plugin.h52
-rw-r--r--game/src/plugins/texture_view.c147
7 files changed, 669 insertions, 0 deletions
diff --git a/game/CMakeLists.txt b/game/CMakeLists.txt
new file mode 100644
index 0000000..3a88bb7
--- /dev/null
+++ b/game/CMakeLists.txt
@@ -0,0 +1,22 @@
1cmake_minimum_required(VERSION 3.0)
2
3add_subdirectory(src/plugins)
4
5project(game)
6
7add_executable(game
8 src/game.c)
9
10target_include_directories(game PRIVATE
11 src/)
12
13target_link_libraries(game PRIVATE
14 cstring
15 error
16 gfx
17 gfx-app
18 list
19 log
20 math
21 mempool
22 plugin)
diff --git a/game/src/game.c b/game/src/game.c
new file mode 100644
index 0000000..64be4f3
--- /dev/null
+++ b/game/src/game.c
@@ -0,0 +1,216 @@
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/gfx.h>
14#include <gfx/gfx_app.h>
15#include <gfx/render_backend.h>
16#include <gfx/renderer.h>
17#include <gfx/scene/camera.h>
18#include <gfx/scene/node.h>
19#include <gfx/scene/object.h>
20#include <gfx/scene/scene.h>
21
22#include <error.h>
23#include <log/log.h>
24#include <math/camera.h>
25#include <plugin.h>
26
27#include <assert.h>
28#include <stdbool.h>
29#include <stdio.h>
30#include <stdlib.h>
31
32#include <linux/limits.h>
33
34#include <unistd.h>
35
36#undef _GNU_SOURCE
37
38static const int WIDTH = 1350;
39static const int HEIGHT = 900;
40static const int MAX_FPS = 60;
41
42/// Initialize the game's plugin.
43static bool init_plugin(Game* game) {
44 assert(game);
45 assert(game->plugin);
46 // Plugin state is allowed to be null, either when the plugin does not
47 // expose an init() or when init() does not initialize a state.
48 if (plugin_resolve(game->plugin, plugin_init, "init")) {
49 State* plugin_state = 0;
50 if (!plugin_call(game->plugin, plugin_init, "init", game, &plugin_state)) {
51 return false;
52 }
53 set_plugin_state(game->plugin, plugin_state);
54 }
55 return true; // Plugin does not need to expose an init().
56}
57
58/// Shutdown the game's plugin.
59/// The game's plugin is allowed to be null in the call to this function.
60static void shutdown_plugin(Game* game) {
61 assert(game);
62 if (game->plugin &&
63 (plugin_resolve(game->plugin, plugin_shutdown, "shutdown"))) {
64 void* plugin_state = get_plugin_state(game->plugin);
65 plugin_call(game->plugin, plugin_shutdown, "shutdown", game, plugin_state);
66 set_plugin_state(game->plugin, 0);
67 }
68}
69
70/// Boot the game's plugin.
71static bool boot_plugin(Game* game) {
72 assert(game);
73 assert(game->plugin);
74 if (plugin_resolve(game->plugin, plugin_boot, "boot")) {
75 void* plugin_state = get_plugin_state(game->plugin);
76 return plugin_call(game->plugin, plugin_boot, "boot", game, plugin_state);
77 }
78 return true; // Plugin does not need to expose a boot().
79}
80
81/// Update the plugin's state.
82static void update_plugin(Game* game, double t, double dt) {
83 assert(game);
84 assert(game->plugin);
85 if (plugin_resolve(game->plugin, plugin_update, "update")) {
86 void* plugin_state = get_plugin_state(game->plugin);
87 plugin_call(
88 game->plugin, plugin_update, "update", game, plugin_state, t, dt);
89 }
90}
91
92/// Plugin render.
93static void render_plugin(const Game* game) {
94 assert(game);
95 assert(game->plugin);
96 if (plugin_resolve(game->plugin, plugin_render, "render")) {
97 void* plugin_state = get_plugin_state(game->plugin);
98 plugin_call(game->plugin, plugin_render, "render", game, plugin_state);
99 }
100}
101
102/// Plugin resize.
103static void resize_plugin(Game* game, int width, int height) {
104 assert(game);
105 assert(game->plugin);
106 if (plugin_resolve(game->plugin, plugin_resize, "resize")) {
107 void* plugin_state = get_plugin_state(game->plugin);
108 plugin_call(
109 game->plugin, plugin_resize, "resize", game, plugin_state, width,
110 height);
111 }
112}
113
114void app_end(Game* game);
115
116bool app_init(const GfxAppDesc* desc, void** app_state) {
117 assert(desc);
118
119 if (desc->argc <= 1) {
120 LOGE("Usage: %s <plugin> [plugin args]", desc->argv[0]);
121 return false;
122 }
123
124 Game* game = calloc(1, sizeof(Game));
125 if (!game) {
126 LOGE("Failed to allocate game state");
127 return false;
128 }
129
130 // Syntax: game <plugin> [plugin args]
131 //
132 // Here we consume the <plugin> arg so that plugins receive the remainder
133 // args starting from 0.
134 game->argc = desc->argc - 1;
135 game->argv = desc->argv + 1;
136
137 char exe_path_buf[NAME_MAX] = {0};
138 if (readlink("/proc/self/exe", exe_path_buf, sizeof(exe_path_buf)) == -1) {
139 LOGE("readlink(/proc/self/exe) failed");
140 goto cleanup;
141 }
142
143 // Replace the last / with a null terminator to remove the exe file from the
144 // path. This gets the file's parent directory.
145 *strrchr(exe_path_buf, '/') = 0;
146
147 const mstring exe_dir = mstring_make(exe_path_buf);
148 const mstring plugins_path = mstring_concat_cstr(exe_dir, "/src/plugins");
149
150 if (!(game->plugin_engine = new_plugin_engine(
151 &(PluginEngineDesc){.plugins_dir = mstring_cstr(&plugins_path)}))) {
152 goto cleanup;
153 }
154
155 const char* plugin = desc->argv[1];
156 if (!(game->plugin = load_plugin(game->plugin_engine, plugin))) {
157 goto cleanup;
158 }
159
160 if (!(game->gfx = gfx_init())) {
161 goto cleanup;
162 }
163
164 if (!init_plugin(game)) {
165 goto cleanup;
166 }
167 if (!boot_plugin(game)) {
168 goto cleanup;
169 }
170
171 *app_state = game;
172 return true;
173
174cleanup:
175 LOGE("Gfx error: %s", get_error());
176 app_end(game);
177 return false;
178}
179
180void app_end(Game* game) {
181 assert(game);
182 shutdown_plugin(game);
183 if (game->gfx) {
184 gfx_destroy(&game->gfx);
185 }
186 if (game->plugin) {
187 delete_plugin(&game->plugin);
188 }
189 if (game->plugin_engine) {
190 delete_plugin_engine(&game->plugin_engine);
191 }
192}
193
194void app_update(Game* game, double t, double dt) {
195 plugin_engine_update(game->plugin_engine);
196 if (plugin_reloaded(game->plugin)) {
197 shutdown_plugin(game);
198 const bool result = init_plugin(game);
199 assert(result); // TODO: handle error better.
200 }
201
202 update_plugin(game, t, dt);
203}
204
205void app_render(const Game* game) {
206 RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
207 gfx_start_frame(render_backend);
208 render_plugin(game);
209 gfx_end_frame(render_backend);
210}
211
212void app_resize(Game* game, int width, int height) {
213 resize_plugin(game, width, height);
214}
215
216GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS);
diff --git a/game/src/game.h b/game/src/game.h
new file mode 100644
index 0000000..93a5e39
--- /dev/null
+++ b/game/src/game.h
@@ -0,0 +1,19 @@
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} Game;
diff --git a/game/src/plugins/CMakeLists.txt b/game/src/plugins/CMakeLists.txt
new file mode 100644
index 0000000..ecb2a45
--- /dev/null
+++ b/game/src/plugins/CMakeLists.txt
@@ -0,0 +1,17 @@
1cmake_minimum_required(VERSION 3.0)
2
3project(plugins)
4
5set(LINK_LIBRARIES cstring math gfx)
6
7add_library(gltf_view SHARED
8 gltf_view.c)
9
10add_library(texture_view SHARED
11 texture_view.c)
12
13target_link_libraries(gltf_view PUBLIC
14 ${LINK_LIBRARIES})
15
16target_link_libraries(texture_view PUBLIC
17 ${LINK_LIBRARIES})
diff --git a/game/src/plugins/gltf_view.c b/game/src/plugins/gltf_view.c
new file mode 100644
index 0000000..c19d1b8
--- /dev/null
+++ b/game/src/plugins/gltf_view.c
@@ -0,0 +1,196 @@
1#include "plugin.h"
2
3#include <gfx/renderer.h>
4#include <gfx/scene.h>
5#include <gfx/util/scene.h>
6#include <gfx/util/skyquad.h>
7#include <gfx/util/texture.h>
8#include <math/camera.h>
9#include <math/spatial3.h>
10
11#include <stdlib.h>
12
13// Paths to various scene files.
14static const char* BOX = "/assets/models/box.gltf";
15static const char* SUZANNE = "/assets/models/suzanne.gltf";
16static const char* SPONZA =
17 "/assets/glTF-Sample-Models/2.0/Sponza/glTF/Sponza.gltf";
18static const char* FLIGHT_HELMET =
19 "/assets/glTF-Sample-Models/2.0/FlightHelmet/glTF/FlightHelmet.gltf";
20static const char* DAMAGED_HELMET =
21 "/assets/glTF-Sample-Models/2.0/DamagedHelmet/glTF/DamagedHelmet.gltf";
22static const char* GIRL =
23 "/home/jeanne/Nextcloud/assets/models/girl/girl-with-ground.gltf";
24
25#define DEFAULT_SCENE_FILE GIRL
26
27struct State {
28 Scene* scene;
29 SceneCamera* camera;
30};
31
32/// Load the skyquad texture.
33static Texture* load_environment_map(RenderBackend* render_backend) {
34 return gfx_load_texture(
35 render_backend,
36 &(LoadTextureCmd){
37 .origin = TextureFromFile,
38 .type = LoadCubemap,
39 .colour_space = sRGB,
40 .filtering = NearestFiltering,
41 .mipmaps = false,
42 .data.cubemap.filepaths = {
43 mstring_make("/assets/skybox/clouds1/clouds1_east.bmp"),
44 mstring_make("/assets/skybox/clouds1/clouds1_west.bmp"),
45 mstring_make("/assets/skybox/clouds1/clouds1_up.bmp"),
46 mstring_make("/assets/skybox/clouds1/clouds1_down.bmp"),
47 mstring_make("/assets/skybox/clouds1/clouds1_south.bmp"),
48 mstring_make("/assets/skybox/clouds1/clouds1_north.bmp")}
49 });
50}
51
52/// Load the skyquad and return the environment light node.
53static SceneNode* load_skyquad(RenderBackend* render_backend, SceneNode* root) {
54 assert(render_backend);
55 assert(root);
56
57 Texture* environment_map = load_environment_map(render_backend);
58 if (!environment_map) {
59 return 0;
60 }
61
62 return gfx_setup_skyquad(render_backend, root, environment_map);
63}
64
65/// Load the 3D scene.
66static SceneNode* load_scene(
67 Game* game, State* state, const char* scene_filepath) {
68 assert(game);
69 assert(game->gfx);
70 assert(state);
71 assert(state->scene);
72
73 SceneNode* root = gfx_get_scene_root(state->scene);
74 RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
75
76 Camera* camera = gfx_get_camera_camera(state->camera);
77 spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2));
78
79 SceneNode* sky_light_node = load_skyquad(render_backend, root);
80 if (!sky_light_node) {
81 return 0;
82 }
83
84 SceneNode* scene_node = gfx_load_scene(
85 game->gfx, sky_light_node,
86 &(LoadSceneCmd){.origin = SceneFromFile, .filepath = scene_filepath});
87 if (!scene_node) {
88 return 0;
89 }
90
91 gfx_log_node_hierarchy(root);
92
93 return scene_node;
94}
95
96bool init(Game* game, State** pp_state) {
97 assert(game);
98
99 State* state = calloc(1, sizeof(State));
100 if (!state) {
101 goto cleanup;
102 }
103
104 if (!(state->scene = gfx_make_scene())) {
105 goto cleanup;
106 }
107 if (!(state->camera = gfx_make_camera())) {
108 goto cleanup;
109 }
110
111 const int argc = game->argc;
112 const char** argv = game->argv;
113
114 // Usage: <scene file>
115 const char* scene_filepath = argc > 1 ? argv[1] : DEFAULT_SCENE_FILE;
116
117 SceneNode* node = load_scene(game, state, scene_filepath);
118 if (!node) {
119 goto cleanup;
120 }
121 Anima* anima = gfx_get_node_anima(node);
122 gfx_play_animation(
123 anima, &(AnimationPlaySettings){.name = "Walk", .loop = true});
124
125 *pp_state = state;
126 return true;
127
128cleanup:
129 shutdown(game, state);
130 if (state) {
131 free(state);
132 }
133 return false;
134}
135
136void shutdown(Game* game, State* state) {
137 assert(game);
138 if (state) {
139 gfx_destroy_camera(&state->camera);
140 gfx_destroy_scene(&state->scene);
141 // State freed by plugin engine.
142 }
143}
144
145void update(Game* game, State* state, double t, double dt) {
146 assert(game);
147 assert(state);
148 assert(state->scene);
149 assert(state->camera);
150
151 gfx_animate_scene(state->scene, (R)t);
152
153 const vec3 orbit_point = vec3_make(0, 2, 0);
154 Camera* camera = gfx_get_camera_camera(state->camera);
155 spatial3_orbit(
156 &camera->spatial, orbit_point,
157 /*radius=*/2.5,
158 /*azimuth=*/t * 0.5, /*zenith=*/0);
159 spatial3_lookat(&camera->spatial, orbit_point);
160}
161
162/// Render the bounding boxes of all scene objects.
163static void render_bounding_boxes(ImmRenderer* imm, const SceneNode* node) {
164 if (gfx_get_node_type(node) == ObjectNode) {
165 // TODO: Look at the scene log. The JointNodes are detached from the
166 // ObjectNodes. This is why the boxes are not being transformed as expected
167 // here. Anima needs to animate boxes? Use OOBB in addition to AABB?
168 const mat4 model = gfx_get_node_global_transform(node);
169 const SceneObject* obj = gfx_get_node_object(node);
170 const aabb3 box = gfx_calc_object_aabb(obj);
171 gfx_imm_set_model_matrix(imm, &model);
172 gfx_imm_draw_aabb(imm, box);
173 }
174
175 // Render children's boxes.
176 for (NodeIter it = gfx_get_node_child(node); it;
177 it = gfx_get_next_child(it)) {
178 render_bounding_boxes(imm, gfx_get_iter_node(it));
179 }
180}
181
182void render(const Game* game, const State* state) {
183 assert(state);
184 assert(game);
185 assert(game->gfx);
186 assert(state->scene);
187 assert(state->camera);
188
189 ImmRenderer* imm = gfx_get_imm_renderer(game->gfx);
190 assert(imm);
191 gfx_imm_start(imm);
192 gfx_imm_set_camera(imm, gfx_get_camera_camera(state->camera));
193 gfx_imm_set_colour(imm, vec4_make(0.2, 0.2, 1.0, 0.3));
194 render_bounding_boxes(imm, gfx_get_scene_root(state->scene));
195 gfx_imm_end(imm);
196}
diff --git a/game/src/plugins/plugin.h b/game/src/plugins/plugin.h
new file mode 100644
index 0000000..a2632cd
--- /dev/null
+++ b/game/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
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* game, State* 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/texture_view.c b/game/src/plugins/texture_view.c
new file mode 100644
index 0000000..b424158
--- /dev/null
+++ b/game/src/plugins/texture_view.c
@@ -0,0 +1,147 @@
1#include "plugin.h"
2
3#include <gfx/render_backend.h>
4#include <gfx/renderer.h>
5#include <gfx/scene.h>
6#include <gfx/util/geometry.h>
7#include <gfx/util/shader.h>
8#include <gfx/util/texture.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 RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
37
38 Texture* texture = gfx_load_texture(
39 render_backend, &(LoadTextureCmd){
40 .origin = TextureFromFile,
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(render_backend);
50 if (!shader) {
51 goto cleanup;
52 }
53
54 Geometry* geometry = gfx_make_quad_11(render_backend);
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 = gfx_make_object();
77 if (!object) {
78 goto cleanup;
79 }
80 gfx_add_object_mesh(object, mesh);
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 RenderBackend* render_backend = gfx_get_render_backend(game->gfx);
137 gfx_set_viewport(render_backend, width, height);
138
139 const R fovy = 90 * TO_RAD;
140 const R aspect = (R)width / (R)height;
141 const R near = 0.1;
142 const R far = 1000;
143 const mat4 projection = mat4_perspective(fovy, aspect, near, far);
144
145 Camera* camera = gfx_get_camera_camera(state->camera);
146 camera->projection = projection;
147}