From 03d94f3762ab576ba0675abcaefde888a9da2c3d Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Fri, 27 Jun 2025 10:13:51 -0700 Subject: Initial commit --- src/game.c | 218 ++++++++++++++++++++++++++ src/game.h | 21 +++ src/plugins/CMakeLists.txt | 29 ++++ src/plugins/plugin.h | 52 +++++++ src/plugins/pong.c | 237 ++++++++++++++++++++++++++++ src/plugins/texture_view.c | 144 +++++++++++++++++ src/plugins/viewer.c | 373 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1074 insertions(+) create mode 100644 src/game.c create mode 100644 src/game.h create mode 100644 src/plugins/CMakeLists.txt create mode 100644 src/plugins/plugin.h create mode 100644 src/plugins/pong.c create mode 100644 src/plugins/texture_view.c create mode 100644 src/plugins/viewer.c (limited to 'src') diff --git a/src/game.c b/src/game.c new file mode 100644 index 0000000..51f5cbe --- /dev/null +++ b/src/game.c @@ -0,0 +1,218 @@ +/* + * Main game module with entry point and game loop. + * + * The game module sets up the window and GL context and defers the core game + * logic to a plugin. + */ +#define _GNU_SOURCE 200112L // For readlink() + +#include "game.h" + +#include "plugins/plugin.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include + +#undef _GNU_SOURCE + +static const int WIDTH = 1350; +static const int HEIGHT = 900; +static const int MAX_FPS = 60; + +typedef struct GfxAppState { + Game game; +} GfxAppState; + +/// Initialize the game's plugin. +static bool init_plugin(Game* game) { + assert(game); + assert(game->plugin); + // Plugin state is allowed to be null, either when the plugin does not + // expose an init() or when init() does not initialize a state. + if (plugin_resolve(game->plugin, plugin_init, "init")) { + State* plugin_state = 0; + if (!plugin_call(game->plugin, plugin_init, "init", game, &plugin_state)) { + return false; + } + set_plugin_state(game->plugin, plugin_state); + } + return true; // Plugin does not need to expose an init(). +} + +/// Shutdown the game's plugin. +/// The game's plugin is allowed to be null in the call to this function. +static void shutdown_plugin(Game* game) { + assert(game); + if (game->plugin && + (plugin_resolve(game->plugin, plugin_shutdown, "shutdown"))) { + void* plugin_state = get_plugin_state(game->plugin); + plugin_call(game->plugin, plugin_shutdown, "shutdown", game, plugin_state); + set_plugin_state(game->plugin, 0); + } +} + +/// Boot the game's plugin. +static bool boot_plugin(Game* game) { + assert(game); + assert(game->plugin); + if (plugin_resolve(game->plugin, plugin_boot, "boot")) { + void* plugin_state = get_plugin_state(game->plugin); + return plugin_call(game->plugin, plugin_boot, "boot", game, plugin_state); + } + return true; // Plugin does not need to expose a boot(). +} + +/// Update the plugin's state. +static void update_plugin(Game* game, double t, double dt) { + assert(game); + assert(game->plugin); + if (plugin_resolve(game->plugin, plugin_update, "update")) { + void* plugin_state = get_plugin_state(game->plugin); + plugin_call( + game->plugin, plugin_update, "update", game, plugin_state, t, dt); + } +} + +/// Plugin render. +static void render_plugin(const Game* game) { + assert(game); + assert(game->plugin); + if (plugin_resolve(game->plugin, plugin_render, "render")) { + void* plugin_state = get_plugin_state(game->plugin); + plugin_call(game->plugin, plugin_render, "render", game, plugin_state); + } +} + +/// Plugin resize. +static void resize_plugin(Game* game, int width, int height) { + assert(game); + assert(game->plugin); + if (plugin_resolve(game->plugin, plugin_resize, "resize")) { + void* plugin_state = get_plugin_state(game->plugin); + plugin_call( + game->plugin, plugin_resize, "resize", game, plugin_state, width, + height); + } +} + +static void Shutdown(Game* game); + +static bool Init(Game* game, int argc, const char** argv) { + assert(game); + + if (argc <= 1) { + LOGE("Usage: %s [plugin args]", argv[0]); + return false; + } + + // Syntax: game [plugin args] + // + // Here we consume the arg so that plugins receive the remainder + // args starting from 0. + game->argc = argc - 1; + game->argv = argv + 1; + + char exe_path_buf[NAME_MAX] = {0}; + if (readlink("/proc/self/exe", exe_path_buf, sizeof(exe_path_buf)) == -1) { + LOGE("readlink(/proc/self/exe) failed"); + goto cleanup; + } + + // Replace the last / with a null terminator to remove the exe file from the + // path. This gets the file's parent directory. + *strrchr(exe_path_buf, '/') = 0; + + const mstring exe_dir = mstring_make(exe_path_buf); + const mstring plugins_path = mstring_concat_cstr(exe_dir, "/src/plugins"); + + if (!(game->plugin_engine = new_plugin_engine( + &(PluginEngineDesc){.plugins_dir = mstring_cstr(&plugins_path)}))) { + goto cleanup; + } + + const char* plugin = argv[1]; + if (!(game->plugin = load_plugin(game->plugin_engine, plugin))) { + goto cleanup; + } + + if (!(game->gfx = gfx_init())) { + goto cleanup; + } + + if (!init_plugin(game)) { + goto cleanup; + } + if (!boot_plugin(game)) { + goto cleanup; + } + + return true; + +cleanup: + LOGE("Gfx error: %s", get_error()); + Shutdown(game); + return false; +} + +static void Shutdown(Game* game) { + assert(game); + shutdown_plugin(game); + if (game->gfx) { + gfx_destroy(&game->gfx); + } + if (game->plugin) { + delete_plugin(&game->plugin); + } + if (game->plugin_engine) { + delete_plugin_engine(&game->plugin_engine); + } +} + +static void Update(Game* game, double t, double dt) { + plugin_engine_update(game->plugin_engine); + if (plugin_reloaded(game->plugin)) { + shutdown_plugin(game); + const bool result = init_plugin(game); + assert(result); // TODO: handle error better. + + // Trigger a resize just like the initial resize that occurs when the gfx + // application starts. + resize_plugin(game, game->width, game->height); + } + + update_plugin(game, t, dt); +} + +static void Render(const Game* game) { + GfxCore* gfxcore = gfx_get_core(game->gfx); + gfx_start_frame(gfxcore); + render_plugin(game); + gfx_end_frame(gfxcore); +} + +static void Resize(Game* game, int width, int height) { + game->width = width; + game->height = height; + + GfxCore* gfxcore = gfx_get_core(game->gfx); + gfx_set_viewport(gfxcore, 0, 0, width, height); + + resize_plugin(game, width, height); +} + +GFX_APP_MAIN(WIDTH, HEIGHT, MAX_FPS, "Game"); diff --git a/src/game.h b/src/game.h new file mode 100644 index 0000000..579ba3c --- /dev/null +++ b/src/game.h @@ -0,0 +1,21 @@ +/* + * Header file defining the game state, included by plugins. + */ +#pragma once + +typedef struct PluginEngine PluginEngine; +typedef struct Plugin Plugin; +typedef struct Gfx Gfx; +typedef struct Scene Scene; +typedef struct SceneCamera SceneCamera; + +/// Game state. +typedef struct { + int argc; + const char** argv; + PluginEngine* plugin_engine; + Plugin* plugin; + Gfx* gfx; + int width; + int height; +} Game; 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 @@ +cmake_minimum_required(VERSION 3.0) + +project(plugins) + +set(LINK_LIBRARIES cstring math gfx gfx-app) + +# Viewer + +add_library(viewer SHARED + viewer.c) + +target_link_libraries(viewer PUBLIC + ${LINK_LIBRARIES}) + +# Texture viewer + +add_library(texture_view SHARED + texture_view.c) + +target_link_libraries(texture_view PUBLIC + ${LINK_LIBRARIES}) + +# Pong + +add_library(pong SHARED + pong.c) + +target_link_libraries(pong PUBLIC + ${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 @@ +/* + * Game plugin. + */ +#pragma once + +#include "../game.h" + +#include +#include + +#include + +typedef struct State State; + +/// Initialize the plugin, which may optionally return a state object. +/// +/// This function is called every time the plugin is (re)loaded. +/// +/// It is assumed that the plugin's state is fully encapsulated in the returned +/// state object. The plugin should not store any (mutable) state outside of the +/// returned state object (e.g., no mutable global variables.) +bool init(Game*, State**); + +/// Shut down the plugin. +/// +/// This function is called before the plugin is unloaded. +/// +/// The plugin should perform any destruction needed, but not free the state +/// object; freeing the state object's memory is handled by the caller. +void shutdown(Game*, State*); + +/// Function called the first time the plugin is loaded throughout the +/// application's lifetime. This allows the plugin to do one-time initialization +/// of the game state. +bool boot(Game*, State*); + +/// Update the plugin's and the game's state. +void update(Game*, State*, double t, double dt); + +/// Render hook. +void render(const Game*, const State*); + +/// Called when the game's window is resized. +void resize(Game*, State*, int width, int height); + +// Signatures for the plugin's exposed functions. +typedef bool (*plugin_init)(Game*, State**); +typedef bool (*plugin_shutdown)(Game*, State*); +typedef bool (*plugin_boot)(Game*, State*); +typedef void (*plugin_update)(Game*, State*, double t, double dt); +typedef void (*plugin_render)(const Game*, const State*); +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 @@ +#include "plugin.h" + +#include +#include +#include + +#include +#include +#include + +#include + +static const vec2 PAD_SIZE = (vec2){120, 20}; +static const R PLAYER_Y_OFFSET = 50; +static const R PLAYER_SPEED = 800; + +static const R ENEMY_SPEED = 2; + +static const R BALL_SIZE = 18; +static const R BALL_SPEED = 360; // In each dimension. + +static const R EPS = (R)1e-3; + +typedef struct Player { + vec2 position; +} Player; + +typedef struct Ball { + vec2 position; + vec2 velocity; +} Ball; + +typedef struct State { + bool game_started; + Player human; + Player enemy; + Ball ball; + mat4 viewProjection; +} State; + +bool init(Game* game, State** pp_state) { + assert(game); + + State* state = calloc(1, sizeof(State)); + if (!state) { + return false; + } + + *pp_state = state; + return true; + +cleanup: + free(state); + return false; +} + +void shutdown(Game* game, State* state) { + assert(game); + assert(state); +} + +static void move_ball(Ball* ball, R dt, int width, int height) { + assert(ball); + + const R offset = BALL_SIZE / 2; + + ball->position = vec2_add(ball->position, vec2_scale(ball->velocity, dt)); + + // Right wall. + if (ball->position.x + offset > (R)width) { + ball->position.x = (R)width - offset - EPS; + ball->velocity.x = -ball->velocity.x; + } + // Left wall. + else if (ball->position.x - offset < 0) { + ball->position.x = offset + EPS; + ball->velocity.x = -ball->velocity.x; + } + // Top wall. + if (ball->position.y + offset > (R)height) { + ball->position.y = (R)height - offset - EPS; + ball->velocity.y = -ball->velocity.y; + } + // Bottom wall. + else if (ball->position.y - offset < 0) { + ball->position.y = offset + EPS; + ball->velocity.y = -ball->velocity.y; + } +} + +void move_enemy_player(int width, Player* player, R t) { + const R half_width = (R)width / 2; + const R amplitude = half_width - (PAD_SIZE.x / 2); + player->position.x = half_width + amplitude * sinf(t * ENEMY_SPEED); +} + +void move_human_player(Player* player, R dt) { + assert(player); + + R speed = 0; + if (gfx_app_is_key_pressed('a')) { + speed -= PLAYER_SPEED; + } + if (gfx_app_is_key_pressed('d')) { + speed += PLAYER_SPEED; + } + + player->position.x += speed * dt; +} + +void clamp_player(Player* player, int width) { + assert(player); + + const R offset = PAD_SIZE.x / 2; + + // Left wall. + if (player->position.x + offset > (R)width) { + player->position.x = (R)width - offset; + } + // Right wall. + else if (player->position.x - offset < 0) { + player->position.x = offset; + } +} + +void collide_ball(vec2 old_ball_position, const Player* player, Ball* ball) { + assert(player); + assert(ball); + + // Discrete but simple collision. Checks for intersection and moves the ball + // back by a small epsilon. + + // Player bounding box. + const vec2 player_pmin = vec2_make( + player->position.x - PAD_SIZE.x / 2, player->position.y - PAD_SIZE.y / 2); + const vec2 player_pmax = vec2_make( + player->position.x + PAD_SIZE.x / 2, player->position.y + PAD_SIZE.y / 2); + + // Ball bounding box. + const vec2 ball_pmin = vec2_make( + ball->position.x - BALL_SIZE / 2, ball->position.y - BALL_SIZE / 2); + const vec2 ball_pmax = vec2_make( + ball->position.x + BALL_SIZE / 2, ball->position.y + BALL_SIZE / 2); + + // Check for intersection and update ball. + if (!((ball_pmax.x < player_pmin.x) || (ball_pmin.x > player_pmax.x) || + (ball_pmax.y < player_pmin.y) || (ball_pmin.y > player_pmax.y))) { + ball->position = + vec2_add(old_ball_position, vec2_scale(ball->velocity, -EPS)); + ball->velocity.y = -ball->velocity.y; + } +} + +void update(Game* game, State* state, double t, double dt) { + assert(game); + assert(state); + + // TODO: Move game width/height to GfxApp query functions? + const vec2 old_ball_position = state->ball.position; + move_ball(&state->ball, (R)dt, game->width, game->height); + move_human_player(&state->human, (R)dt); + move_enemy_player(game->width, &state->enemy, (R)t); + clamp_player(&state->human, game->width); + collide_ball(old_ball_position, &state->human, &state->ball); + collide_ball(old_ball_position, &state->enemy, &state->ball); +} + +static void draw_player(ImmRenderer* imm, const Player* player) { + assert(imm); + assert(player); + + const vec2 half_box = vec2_div(PAD_SIZE, vec2_make(2, 2)); + + const vec2 pmin = vec2_sub(player->position, half_box); + const vec2 pmax = vec2_add(player->position, half_box); + const aabb2 box = aabb2_make(pmin, pmax); + + gfx_imm_draw_aabb2(imm, box); +} + +static void draw_ball(ImmRenderer* imm, const Ball* ball) { + assert(imm); + assert(ball); + + const vec2 half_box = vec2_make(BALL_SIZE / 2, BALL_SIZE / 2); + const vec2 pmin = vec2_sub(ball->position, half_box); + const vec2 pmax = vec2_add(ball->position, half_box); + const aabb2 box = aabb2_make(pmin, pmax); + + gfx_imm_draw_aabb2(imm, box); +} + +void render(const Game* game, const State* state) { + assert(game); + assert(state); + + ImmRenderer* imm = gfx_get_imm_renderer(game->gfx); + gfx_imm_start(imm); + gfx_imm_set_view_projection_matrix(imm, &state->viewProjection); + gfx_imm_load_identity(imm); + gfx_imm_set_colour(imm, vec4_make(1, 1, 1, 1)); + draw_player(imm, &state->human); + draw_player(imm, &state->enemy); + draw_ball(imm, &state->ball); + gfx_imm_end(imm); +} + +static R clamp_to_width(int width, R x, R extent) { + return min(x, (R)width - extent); +} + +void resize(Game* game, State* state, int width, int height) { + assert(game); + assert(state); + + state->viewProjection = mat4_ortho(0, (R)width, 0, (R)height, -1, 1); + + state->human.position.y = PLAYER_Y_OFFSET; + state->enemy.position.y = (R)height - PLAYER_Y_OFFSET; + + if (!state->game_started) { + state->human.position.x = (R)width / 2; + state->enemy.position.x = (R)width / 2; + + state->ball.position = + vec2_div(vec2_make((R)width, (R)height), vec2_make(2, 2)); + + state->ball.velocity = vec2_make(BALL_SPEED, BALL_SPEED); + + state->game_started = true; + } else { + state->human.position.x = + clamp_to_width(width, state->human.position.x, PAD_SIZE.x / 2); + state->enemy.position.x = + clamp_to_width(width, state->enemy.position.x, PAD_SIZE.x / 2); + } +} 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 @@ +#include "plugin.h" + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +// Default texture to load if no texture is provided. +static const char* DEFAULT_TEXTURE = "/assets/skybox/clouds1/clouds1_west.bmp"; +// static const char* DEFAULT_TEXTURE = "/assets/checkerboard.jpg"; + +struct State { + Scene* scene; + SceneCamera* camera; +}; + +bool init(Game* game, State** pp_state) { + assert(game); + assert(pp_state); + + State* state = calloc(1, sizeof(State)); + if (!state) { + goto cleanup; + } + + // Usage: [texture file] + const char* texture_file = game->argc > 1 ? game->argv[1] : DEFAULT_TEXTURE; + + GfxCore* gfxcore = gfx_get_core(game->gfx); + + const Texture* texture = gfx_load_texture( + game->gfx, &(LoadTextureCmd){ + .origin = AssetFromFile, + .type = LoadTexture, + .filtering = LinearFiltering, + .mipmaps = false, + .data.texture.filepath = mstring_make(texture_file)}); + if (!texture) { + goto cleanup; + } + + ShaderProgram* shader = gfx_make_view_texture_shader(gfxcore); + if (!shader) { + goto cleanup; + } + + Geometry* geometry = gfx_make_quad_11(gfxcore); + if (!geometry) { + goto cleanup; + } + + MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1}; + material_desc.uniforms[0] = (ShaderUniform){ + .type = UniformTexture, + .value.texture = texture, + .name = sstring_make("Texture")}; + Material* material = gfx_make_material(&material_desc); + if (!material) { + goto cleanup; + } + + const MeshDesc mesh_desc = + (MeshDesc){.geometry = geometry, .material = material, .shader = shader}; + Mesh* mesh = gfx_make_mesh(&mesh_desc); + if (!mesh) { + goto cleanup; + } + + SceneObject* object = + gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}}); + if (!object) { + goto cleanup; + } + + if (!(state->scene = gfx_make_scene())) { + goto cleanup; + } + + SceneNode* node = gfx_make_object_node(object); + if (!node) { + goto cleanup; + } + SceneNode* root = gfx_get_scene_root(state->scene); + if (!root) { + goto cleanup; + } + gfx_set_node_parent(node, root); + + if (!(state->camera = gfx_make_camera())) { + goto cleanup; + } + + *pp_state = state; + return true; + +cleanup: + shutdown(game, state); + if (state) { + free(state); + } + return false; +} + +void shutdown(Game* game, State* state) { + assert(game); + if (state) { + gfx_destroy_camera(&state->camera); + gfx_destroy_scene(&state->scene); + // State freed by plugin engine. + } +} + +void render(const Game* game, const State* state) { + assert(game); + assert(state); + + Renderer* renderer = gfx_get_renderer(game->gfx); + gfx_render_scene( + renderer, &(RenderSceneParams){ + .mode = RenderDefault, + .scene = state->scene, + .camera = state->camera}); +} + +void resize(Game* game, State* state, int width, int height) { + assert(game); + assert(state); + + const R fovy = 90 * TO_RAD; + const R aspect = (R)width / (R)height; + const R near = 0.1; + const R far = 1000; + const mat4 projection = mat4_perspective(fovy, aspect, near, far); + + Camera* camera = gfx_get_camera_camera(state->camera); + camera->projection = projection; +} 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 @@ +#include "plugin.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +// Skybox. +static const char* skybox[6] = { + "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_east.bmp", + "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_west.bmp", + "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_up.bmp", + "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_down.bmp", + "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_south.bmp", + "/home/jeanne/Nextcloud/assets/textures/skybox/clouds1/clouds1_north.bmp", +}; + +// Paths to various scene files. +static const char* BOX = "/home/jeanne/Nextcloud/assets/models/box.gltf"; +static const char* SUZANNE = + "/home/jeanne/Nextcloud/assets/models/suzanne.gltf"; +static const char* SPONZA = "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/" + "2.0/Sponza/glTF/Sponza.gltf"; +static const char* FLIGHT_HELMET = + "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/2.0/FlightHelmet/glTF/" + "FlightHelmet.gltf"; +static const char* DAMAGED_HELMET = + "/home/jeanne/Nextcloud/assets/glTF-Sample-Models/2.0/DamagedHelmet/glTF/" + "DamagedHelmet.gltf"; +static const char* GIRL = + "/home/jeanne/Nextcloud/assets/models/girl/girl-with-ground.gltf"; +static const char* BOXES = + "/home/jeanne/Nextcloud/assets/models/boxes/boxes.gltf"; + +#define DEFAULT_SCENE_FILE GIRL + +static const bool RenderBoundingBoxes = false; +static const R DefaultCameraSpeed = (R)6.0; +static const R DefaultMouseSensitivity = (R)(10 * TO_RAD); +static const vec3 DefaultCameraPosition = (vec3){0, 2, 5}; + +typedef struct CameraCommand { + bool CameraMoveLeft : 1; + bool CameraMoveRight : 1; + bool CameraMoveForward : 1; + bool CameraMoveBackward : 1; +} CameraCommand; + +typedef struct CameraController { + R camera_speed; // Camera movement speed. + R mouse_sensitivity; // Controls the degree with which mouse movements + // rotate the camera. + vec2 prev_mouse_position; // Mouse position in the previous frame. + bool rotating; // When true, subsequent mouse movements cause the + // camera to rotate. +} CameraController; + +typedef struct State { + Scene* scene; + Model* model; + SceneCamera* camera; + CameraController camera_controller; +} State; + +/// Load the skyquad texture. +static const Texture* load_environment_map(Gfx* gfx) { + assert(gfx); + return gfx_load_texture( + gfx, &(LoadTextureCmd){ + .origin = AssetFromFile, + .type = LoadCubemap, + .colour_space = sRGB, + .filtering = NearestFiltering, + .mipmaps = false, + .data.cubemap.filepaths = { + mstring_make(skybox[0]), mstring_make(skybox[1]), + mstring_make(skybox[2]), mstring_make(skybox[3]), + mstring_make(skybox[4]), mstring_make(skybox[5])} + }); +} + +/// Load the skyquad and return the environment light node. +static SceneNode* load_skyquad(Gfx* gfx, SceneNode* root) { + assert(gfx); + assert(root); + + GfxCore* gfxcore = gfx_get_core(gfx); + + const Texture* environment_map = load_environment_map(gfx); + if (!environment_map) { + return 0; + } + + return gfx_setup_skyquad(gfxcore, root, environment_map); +} + +/// Load the model. +static Model* load_model(Game* game, State* state, const char* scene_filepath) { + assert(game); + assert(game->gfx); + assert(state); + assert(state->scene); + + Camera* camera = gfx_get_camera_camera(state->camera); + spatial3_set_position(&camera->spatial, vec3_make(0, 0, 2)); + + SceneNode* root = gfx_get_scene_root(state->scene); + SceneNode* sky_light_node = load_skyquad(game->gfx, root); + if (!sky_light_node) { + return 0; // test + } + + Model* model = gfx_load_model( + game->gfx, &(LoadModelCmd){.origin = AssetFromFile, + .filepath = mstring_make(scene_filepath)}); + if (!model) { + return 0; + } + SceneNode* model_node = gfx_make_model_node(model); + if (!model_node) { + return 0; + } + gfx_set_node_parent(model_node, sky_light_node); + + gfx_log_node_hierarchy(root); + + return model; +} + +bool init(Game* game, State** pp_state) { + assert(game); + + // Usage: + const char* scene_filepath = + game->argc > 1 ? game->argv[1] : DEFAULT_SCENE_FILE; + + State* state = calloc(1, sizeof(State)); + if (!state) { + goto cleanup; + } + + if (!(state->scene = gfx_make_scene())) { + goto cleanup; + } + if (!(state->camera = gfx_make_camera())) { + goto cleanup; + } + + state->model = load_model(game, state, scene_filepath); + if (!state->model) { + goto cleanup; + } + + Anima* anima = gfx_get_model_anima(state->model); + if (anima) { + gfx_play_animation( + anima, &(AnimationPlaySettings){.name = "Walk", .loop = true}); + // TODO: Interpolate animations. + /*gfx_play_animation( + anima, + &(AnimationPlaySettings){.name = "Jumping-jack-lower", .loop = true}); + gfx_play_animation( + anima, &(AnimationPlaySettings){ + .name = "Jumping-jack-arms-mid", .loop = true});*/ + } + + spatial3_set_position( + &gfx_get_camera_camera(state->camera)->spatial, DefaultCameraPosition); + + state->camera_controller.camera_speed = DefaultCameraSpeed; + state->camera_controller.mouse_sensitivity = DefaultMouseSensitivity; + + *pp_state = state; + return true; + +cleanup: + shutdown(game, state); + if (state) { + free(state); + } + return false; +} + +void shutdown(Game* game, State* state) { + assert(game); + if (state) { + gfx_destroy_camera(&state->camera); + gfx_destroy_scene(&state->scene); + // State freed by plugin engine. + } +} + +static void update_camera( + CameraController* controller, R dt, vec2 mouse_position, + CameraCommand command, Spatial3* camera) { + assert(controller); + assert(camera); + + // Translation. + const R move_x = (R)(command.CameraMoveLeft ? -1 : 0) + + (R)(command.CameraMoveRight ? 1 : 0); + const R move_y = (R)(command.CameraMoveForward ? 1 : 0) + + (R)(command.CameraMoveBackward ? -1 : 0); + const vec2 translation = + vec2_scale(vec2_make(move_x, move_y), controller->camera_speed * dt); + spatial3_move_right(camera, translation.x); + spatial3_move_forwards(camera, translation.y); + + // Rotation. + if (controller->rotating) { + const vec2 mouse_delta = + vec2_sub(mouse_position, controller->prev_mouse_position); + + const vec2 rotation = + vec2_scale(mouse_delta, controller->mouse_sensitivity * dt); + + spatial3_global_yaw(camera, -rotation.x); + spatial3_pitch(camera, -rotation.y); + } + + // Update controller state. + controller->prev_mouse_position = mouse_position; +} + +void update(Game* game, State* state, double t, double dt) { + assert(game); + assert(state); + assert(state->scene); + assert(state->camera); + + double mouse_x, mouse_y; + gfx_app_get_mouse_position(&mouse_x, &mouse_y); + const vec2 mouse_position = {(R)mouse_x, (R)mouse_y}; + + const CameraCommand camera_command = (CameraCommand){ + .CameraMoveLeft = gfx_app_is_key_pressed(KeyA), + .CameraMoveRight = gfx_app_is_key_pressed(KeyD), + .CameraMoveForward = gfx_app_is_key_pressed(KeyW), + .CameraMoveBackward = gfx_app_is_key_pressed(KeyS), + }; + + state->camera_controller.rotating = gfx_app_is_mouse_button_pressed(LMB); + + update_camera( + &state->camera_controller, (R)dt, mouse_position, camera_command, + &gfx_get_camera_camera(state->camera)->spatial); + + // const vec3 orbit_point = vec3_make(0, 2, 0); + // Camera* camera = gfx_get_camera_camera(state->camera); + // spatial3_orbit( + // &camera->spatial, orbit_point, + // /*radius=*/5, + // /*azimuth=*/(R)(t * 0.5), /*zenith=*/0); + // spatial3_lookat(&camera->spatial, orbit_point); + + gfx_update(state->scene, state->camera, (R)t); +} + +/// Render the bounding boxes of all scene objects. +static void render_bounding_boxes_rec( + ImmRenderer* imm, const Anima* anima, const mat4* parent_model_matrix, + const SceneNode* node) { + assert(imm); + assert(node); + + const mat4 model_matrix = + mat4_mul(*parent_model_matrix, gfx_get_node_transform(node)); + + const NodeType node_type = gfx_get_node_type(node); + + if (node_type == ModelNode) { + const Model* model = gfx_get_node_model(node); + const SceneNode* root = gfx_get_model_root(model); + render_bounding_boxes_rec(imm, anima, &model_matrix, root); + } else if (node_type == AnimaNode) { + anima = gfx_get_node_anima(node); + } else if (node_type == ObjectNode) { + gfx_imm_set_model_matrix(imm, &model_matrix); + + const SceneObject* obj = gfx_get_node_object(node); + const Skeleton* skeleton = gfx_get_object_skeleton(obj); + + if (skeleton) { // Animated model. + assert(anima); + const size_t num_joints = gfx_get_skeleton_num_joints(skeleton); + for (size_t i = 0; i < num_joints; ++i) { + if (gfx_joint_has_box(anima, skeleton, i)) { + const Box box = gfx_get_joint_box(anima, skeleton, i); + gfx_imm_draw_box3(imm, box.vertices); + } + } + } else { // Static model. + const aabb3 box = gfx_get_object_aabb(obj); + gfx_imm_draw_aabb3(imm, box); + } + } + + // Render children's boxes. + const SceneNode* child = gfx_get_node_child(node); + while (child) { + render_bounding_boxes_rec(imm, anima, &model_matrix, child); + child = gfx_get_node_sibling(child); + } +} + +/// Render the bounding boxes of all scene objects. +static void render_bounding_boxes(const Game* game, const State* state) { + assert(game); + assert(state); + + GfxCore* gfxcore = gfx_get_core(game->gfx); + ImmRenderer* imm = gfx_get_imm_renderer(game->gfx); + assert(gfxcore); + assert(imm); + + const mat4 id = mat4_id(); + Anima* anima = 0; + + gfx_set_blending(gfxcore, true); + gfx_set_depth_mask(gfxcore, false); + gfx_set_polygon_offset(gfxcore, -1.5f, -1.0f); + + gfx_imm_start(imm); + gfx_imm_set_camera(imm, gfx_get_camera_camera(state->camera)); + gfx_imm_set_colour(imm, vec4_make(0.3, 0.3, 0.9, 0.1)); + render_bounding_boxes_rec(imm, anima, &id, gfx_get_scene_root(state->scene)); + gfx_imm_end(imm); + + gfx_reset_polygon_offset(gfxcore); + gfx_set_depth_mask(gfxcore, true); + gfx_set_blending(gfxcore, false); +} + +void render(const Game* game, const State* state) { + assert(state); + assert(game); + assert(game->gfx); + assert(state->scene); + assert(state->camera); + + Renderer* renderer = gfx_get_renderer(game->gfx); + assert(renderer); + + gfx_render_scene( + renderer, &(RenderSceneParams){.mode = RenderDefault, + .scene = state->scene, + .camera = state->camera}); + + if (RenderBoundingBoxes) { + render_bounding_boxes(game, state); + } +} + +void resize(Game* game, State* state, int width, int height) { + assert(game); + assert(state); + + const R fovy = 60 * TO_RAD; + const R aspect = (R)width / (R)height; + const R near = 0.1; + const R far = 1000; + const mat4 projection = mat4_perspective(fovy, aspect, near, far); + + Camera* camera = gfx_get_camera_camera(state->camera); + camera->projection = projection; +} -- cgit v1.2.3