commit 7473b9c66d6d5a56d33108ee8840e279a568290c Author: Emil Kosz Date: Sun Jun 1 13:37:23 2025 +0200 Stub of a game diff --git a/game/Makefile b/game/Makefile new file mode 100644 index 0000000..27640d1 --- /dev/null +++ b/game/Makefile @@ -0,0 +1,22 @@ +SRC=src/math_3d.c src/main.c src/mesh.c src/program.c src/resources.c src/camera.c \ + src/chunk/chunk.c src/chunk/noise.c src/chunk/chunkmesh.c src/flags.c + +OBJ=$(SRC:.c=.o) +CFLAGS=-g -Werror -Wextra -Wall `pkg-config --cflags sdl3 gl` +LDFLAGS=`pkg-config --libs sdl3 gl` -lm +TARGET=wschod +CC=gcc +LD=gcc + +.PHONY: clean + +all: $(TARGET) + +$(TARGET): $(OBJ) + $(LD) $^ -o $@ $(LDFLAGS) + +.c.o: + $(CC) -c $^ -o $@ $(CFLAGS) + +clean: + $(RM) $(TARGET) $(OBJ) diff --git a/game/res/chunk.frag b/game/res/chunk.frag new file mode 100644 index 0000000..3818fbe --- /dev/null +++ b/game/res/chunk.frag @@ -0,0 +1,11 @@ +#version 100 + +precision lowp float; + +varying vec2 f_uv; +varying float f_height; + +void main () +{ + gl_FragColor = vec4 (f_uv.x, 1.0, f_uv.y, 1.0); +} diff --git a/game/res/chunk.vert b/game/res/chunk.vert new file mode 100644 index 0000000..30886e7 --- /dev/null +++ b/game/res/chunk.vert @@ -0,0 +1,17 @@ +#version 100 + +attribute vec3 position; + +uniform mat4 view; +uniform mat4 projection; + +varying vec2 f_uv; +varying float f_height; + +void main () +{ + f_uv = position.xy / 8.0; + f_height = position.y; + + gl_Position = projection * view * vec4 (position, 1.0); +} diff --git a/game/res/default.frag b/game/res/default.frag new file mode 100644 index 0000000..b541df3 --- /dev/null +++ b/game/res/default.frag @@ -0,0 +1,8 @@ +#version 100 + +precision mediump float; + +void main () +{ + gl_FragColor = vec4 (0.5, 1.0, 0.5, 1.0); +} diff --git a/game/res/default.vert b/game/res/default.vert new file mode 100644 index 0000000..6b02501 --- /dev/null +++ b/game/res/default.vert @@ -0,0 +1,11 @@ +#version 100 + +attribute vec4 position; + +uniform mat4 view; +uniform mat4 projection; + +void main () +{ + gl_Position = projection * view * position; +} diff --git a/game/shell.nix b/game/shell.nix new file mode 100644 index 0000000..3e2446b --- /dev/null +++ b/game/shell.nix @@ -0,0 +1,16 @@ +let + nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-25.05"; + pkgs = import nixpkgs { config = {}; overlays = []; }; +in + + +pkgs.mkShellNoCC { + packages = with pkgs; [ + gcc + pkg-config + sdl3 + gdb + cglm + glew + ]; +} diff --git a/game/src/camera.c b/game/src/camera.c new file mode 100644 index 0000000..b0200ce --- /dev/null +++ b/game/src/camera.c @@ -0,0 +1,48 @@ +#include "camera.h" + +const vec3_t CAMERA_UP = { 0.0f, 1.0f, 0.0f }; + +Camera +Camera_default () +{ + Camera cam = { + .position = { 0.0f, 0.0f, 0.0f }, + .rotation = { 0.0f, 0.0f, 0.0f }, + .fov = 80, + .near = 0.1, + .far = 1000.0 + }; + + return cam; +} + +vec3_t +Camera_get_direction (Camera *camera) +{ + return v3_norm + (vec3 + (sinf (camera->rotation.x) * cosf (camera->rotation.y), + sinf (camera->rotation.y), + cosf (camera->rotation.x) * cosf (camera->rotation.y))); +} + +vec3_t +Camera_get_right (Camera *camera) +{ + return v3_norm (v3_cross (Camera_get_direction (camera), CAMERA_UP)); +} + +mat4_t +Camera_get_view (Camera *camera) +{ + return m4_look_at + (camera->position, v3_add (camera->position, Camera_get_direction (camera)), + CAMERA_UP); +} + +mat4_t +Camera_get_projection (Camera *camera, float aspect) +{ + return m4_perspective + (camera->fov, aspect, camera->near, camera->far); +} diff --git a/game/src/camera.h b/game/src/camera.h new file mode 100644 index 0000000..5e8ef89 --- /dev/null +++ b/game/src/camera.h @@ -0,0 +1,20 @@ +#pragma once + +#include "math_3d.h" +#include "util.h" + +typedef struct Camera +{ + vec3_t position; + vec3_t rotation; + int fov; + float near; + float far; +} Camera; + +Camera Camera_default (); + +vec3_t Camera_get_direction (Camera *camera); +vec3_t Camera_get_right (Camera *camera); +mat4_t Camera_get_view (Camera *camera); +mat4_t Camera_get_projection (Camera *camera, float aspect); diff --git a/game/src/chunk/chunk.c b/game/src/chunk/chunk.c new file mode 100644 index 0000000..bc5edd6 --- /dev/null +++ b/game/src/chunk/chunk.c @@ -0,0 +1,52 @@ +#include "chunk.h" + +Chunk +Chunk_new () +{ + Chunk chunk; + chunk.heightmap = malloc (sizeof (float) * CHUNK_SIZE); + chunk.materialmap = malloc (sizeof (char) * CHUNK_SIZE); + INFO ("Created new Chunk. Memory size: %zu bytes", + sizeof (float) * CHUNK_SIZE + sizeof (char) * CHUNK_SIZE); + return chunk; +} + +void +Chunk_noise (Chunk *chunk, int xoffset, int yoffset) +{ + for (int y = 0; y < CHUNK_DIM; ++y) + { + for (int x = 0; x < CHUNK_DIM; ++x) + { + float val = noise2 ((x + xoffset * CHUNK_DIM), (y + yoffset * CHUNK_DIM)) * 8; + chunk->heightmap[INDEX (x, y)] = val; + chunk->materialmap[INDEX (x, y)] = 0; + } + } +} + +void +Chunk_free (Chunk *chunk) +{ + free (chunk->heightmap); + free (chunk->materialmap); +} + +void +Chunk_heightmap_print (Chunk *chunk) +{ + INFO ("== Chunk heightmap =="); + + for (int y = 0; y < CHUNK_DIM; ++y) + { + char row[CHUNK_DIM+1]; + + for (int x = 0; x < CHUNK_DIM; ++x) + { + row[x] = (int) chunk->heightmap[INDEX (x, y)] + 48; + } + + row[CHUNK_DIM] = '\0'; + INFO ("%s", row); + } +} diff --git a/game/src/chunk/chunk.h b/game/src/chunk/chunk.h new file mode 100644 index 0000000..4e06635 --- /dev/null +++ b/game/src/chunk/chunk.h @@ -0,0 +1,20 @@ +#pragma once + +#include "../util.h" +#include "noise.h" + +#define CHUNK_DIM 16 +#define CHUNK_SIZE (CHUNK_DIM*CHUNK_DIM) + +#define INDEX(x, y) (CHUNK_DIM * y + x) + +typedef struct Chunk +{ + float *heightmap; + char *materialmap; +} Chunk; + +Chunk Chunk_new (); +void Chunk_noise (Chunk *chunk, int xoffset, int yoffset); +void Chunk_free (Chunk *chunk); +void Chunk_heightmap_print (Chunk *chunk); diff --git a/game/src/chunk/chunkmesh.c b/game/src/chunk/chunkmesh.c new file mode 100644 index 0000000..83bf1ee --- /dev/null +++ b/game/src/chunk/chunkmesh.c @@ -0,0 +1,91 @@ +#include "chunkmesh.h" + +extern GameFlags game_flags; + +ChunkMesh +ChunkMesh_from_Chunk_and_neighbors (Chunk *chunk, Chunk *nd, Chunk *nr, Chunk *nc) +/* nd --- down neighbor; nr --- right neighbor; nc --- corner neighbor */ +{ + vec3_t *positions = malloc (sizeof (vec3_t) * CHUNKMESH_SIZE); + unsigned int *indices = malloc (sizeof (unsigned int) * CHUNKMESH_SIZE * 6); + + int i_i = 0; + for (int y = 0; y < CHUNKMESH_DIM; ++y) + { + for (int x = 0; x < CHUNKMESH_DIM; ++x) + { + float height = chunk->heightmap[INDEX (x, y)]; + + if (x == CHUNK_DIM && y < CHUNKMESH_DIM && nr != NULL) + { + height = nr->heightmap[INDEX (0, y)]; + } + + if (y == CHUNK_DIM && x < CHUNKMESH_DIM && nd != NULL) + { + height = nd->heightmap[INDEX (x, 0)]; + } + + if (x == CHUNK_DIM && y == CHUNK_DIM && nc != NULL) + { + height = nc->heightmap[INDEX (0, 0)]; + } + + positions[CMINDEX (x, y)] = vec3 (x, height, y); + + if (x < CHUNK_DIM && y < CHUNK_DIM) + { + int i0 = CMINDEX (x, y); + int i1 = CMINDEX (x + 1, y); + int i2 = CMINDEX (x, y + 1); + int i3 = CMINDEX (x + 1, y + 1); + + /* + i0----i1 + | | + | | + i2----i3 + */ + + indices[i_i++] = i0; + indices[i_i++] = i2; + indices[i_i++] = i3; + indices[i_i++] = i0; + indices[i_i++] = i3; + indices[i_i++] = i1; + } + } + } + + ChunkMesh mesh; + glGenBuffers (1, &mesh.pbo); + glGenBuffers (1, &mesh.ebo); + + glBindBuffer (GL_ARRAY_BUFFER, mesh.pbo); + glBufferData (GL_ARRAY_BUFFER, sizeof (vec3_t) * CHUNKMESH_SIZE, positions, GL_STATIC_DRAW); + glEnableVertexAttribArray (0); + glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, sizeof (vec3_t), 0); + + glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, mesh.ebo); + glBufferData (GL_ELEMENT_ARRAY_BUFFER, sizeof (unsigned int) * CHUNKMESH_SIZE * 6, indices, GL_STATIC_DRAW); + + free (positions); + free (indices); + + return mesh; +} + +void +ChunkMesh_render (ChunkMesh *mesh) +{ + glBindBuffer (GL_ELEMENT_ARRAY_BUFFER, mesh->ebo); + + if (game_flags.wireframe_enabled) + { + glDrawElements (GL_LINES, CHUNKMESH_SIZE * 6, GL_UNSIGNED_INT, 0); + } + else + { + glDrawElements (GL_TRIANGLES, CHUNKMESH_SIZE * 6, GL_UNSIGNED_INT, 0); + } +} diff --git a/game/src/chunk/chunkmesh.h b/game/src/chunk/chunkmesh.h new file mode 100644 index 0000000..05484c4 --- /dev/null +++ b/game/src/chunk/chunkmesh.h @@ -0,0 +1,21 @@ +#pragma once + +#include + +#include "../flags.h" +#include "../math_3d.h" +#include "chunk.h" + +#define CHUNKMESH_DIM (CHUNK_SIZE+1) +#define CHUNKMESH_SIZE (CHUNKMESH_DIM*CHUNKMESH_DIM) +#define CMINDEX(x, y) (CHUNKMESH_DIM * y + x) + +typedef struct ChunkMesh +{ + unsigned int pbo; + /* Element Buffer Object, the indices. */ + unsigned int ebo; +} ChunkMesh; + +ChunkMesh ChunkMesh_from_Chunk_and_neighbors (Chunk *chunk, Chunk *nd, Chunk *nr, Chunk *nc); +void ChunkMesh_render (ChunkMesh *mesh); diff --git a/game/src/chunk/noise.c b/game/src/chunk/noise.c new file mode 100644 index 0000000..583ede3 --- /dev/null +++ b/game/src/chunk/noise.c @@ -0,0 +1,43 @@ +#include "noise.h" + +static u_int8_t *perm; + +void +noise_seed (int seed) +{ + srand (seed); + perm = malloc (sizeof (u_int8_t) * 256); + + for (size_t i = 0; i < 256; ++i) { + perm[i] = i; + } + + for (size_t i = 0; i < 255; ++i) { + size_t j = i + rand() / (RAND_MAX / (256 - i) + 1); + u_int8_t t = perm[j]; + perm[j] = perm[i]; + perm[i] = t; + } +} + +float +noise2 (float x, float y) +{ + int X = (int) FASTFLOOR(x) & 0xff; + int Y = (int) FASTFLOOR(y) & 0xff; + x -= FASTFLOOR(x); + y -= FASTFLOOR(y); + float u = FADE(x); + float v = FADE(y); + int A = (perm[X] + Y) & 0xff; + int B = (perm[X + 1] + Y) & 0xff; + + return LERP + (v, LERP + (u, GRAD2 + (perm[A], x, y), + GRAD2(perm[B], x - 1, y)), + LERP(u, GRAD2 + (perm[A + 1], x, y - 1), + GRAD2(perm[B + 1], x - 1, y - 1))); +} diff --git a/game/src/chunk/noise.h b/game/src/chunk/noise.h new file mode 100644 index 0000000..4f09dc1 --- /dev/null +++ b/game/src/chunk/noise.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#define FADE(t) (((6 * t - 15) * t + 10) * t * t * t) +#define LERP(t, a, b) (a + t * (b - a)) +#define FASTFLOOR(x) (((int) (x) < (x)) ? ((int) x) : ((int) x - 1)) + +static inline float +GRAD2 (int hash, float x, float y) +{ + return ((hash & 1) ? -x : x) + ((hash & 2) ? -y : y); +} + +void noise_seed (int seed); +float noise2 (float x, float y); diff --git a/game/src/flags.c b/game/src/flags.c new file mode 100644 index 0000000..65ac461 --- /dev/null +++ b/game/src/flags.c @@ -0,0 +1,3 @@ +#include "flags.h" + +GameFlags game_flags = { 0 }; diff --git a/game/src/flags.h b/game/src/flags.h new file mode 100644 index 0000000..91ef453 --- /dev/null +++ b/game/src/flags.h @@ -0,0 +1,18 @@ +#pragma once + +typedef struct GameFlags +{ + /* Volatile state flags, zeroed at start. */ + int in_main_menu: 1; + int in_pause_menu: 1; + int in_game: 1; + + int can_free_look: 1; + int moving_forward: 1; + int moving_backward: 1; + int moving_left: 1; + int moving_right: 1; + + int wireframe_enabled: 1; + /* Saved state flags, loaded from game save/map. */ +} GameFlags; diff --git a/game/src/main.c b/game/src/main.c new file mode 100644 index 0000000..a01f72a --- /dev/null +++ b/game/src/main.c @@ -0,0 +1,218 @@ +#define SDL_MAIN_USE_CALLBACKS 1 +#include +#include + +#include + +#include "chunk/chunkmesh.h" +#include "state.h" +#include "flags.h" +#include "camera.h" +#include "mesh.h" +#include "resources.h" + +/* TODO: switch back to cglm.h, since it's faster or something */ + +static SDL_Window *window = NULL; +static SDL_GLContext context = NULL; + +extern GameState game_state; +extern GameFlags game_flags; +extern Resources resources; + +static ChunkMesh test_chunkmesh; +static Camera camera; + +SDL_AppResult +SDL_AppInit (void **appstate, int argc, char **argv) +{ + (void) appstate; (void) argc; (void) argv; + + SDL_SetAppMetadata (GAME_NAME, GAME_VERSION, "eur.chopininteractive.wschod-game"); + + if (!SDL_Init (SDL_INIT_VIDEO)) + { + FATAL ("Couldn't initialize SDL: %s", SDL_GetError ()); + } + + window = SDL_CreateWindow (GAME_NAME, 640, 480, SDL_WINDOW_OPENGL); + + if (!window) + { + FATAL ("Couldn't create window: %s", SDL_GetError ()); + } + + /* We use OpenGL ES 2.0. It should support everything, and it's more + than enough for the game. */ + SDL_GL_SetAttribute (SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_ES); + SDL_GL_SetAttribute (SDL_GL_CONTEXT_MAJOR_VERSION, 2); + SDL_GL_SetAttribute (SDL_GL_CONTEXT_MINOR_VERSION, 0); + + context = SDL_GL_CreateContext (window); + + if (!context) + { + FATAL ("Couldn't create OpenGL context: %s", SDL_GetError ()); + } + + SDL_GL_MakeCurrent (window, context); + SDL_SetWindowRelativeMouseMode (window, true); + + INFO ("Renderer: %s", glGetString (GL_RENDERER)); + INFO ("OpenGL version: %s", glGetString (GL_VERSION)); + INFO ("GLSL version: %s", glGetString (GL_SHADING_LANGUAGE_VERSION)); + INFO ("Max vertex attributes: %d", GL_MAX_VERTEX_ATTRIBS); + + glViewport(0, 0, 640, 480); + glClearColor (0, 0.3, 0.8, 1); + + /* Initialize and load resources. */ + Resources_init (); + noise_seed (1337); + + /* Generate a test chunk. */ + Chunk chunk = Chunk_new (); + Chunk_noise (&chunk, 0, 0); + test_chunkmesh = ChunkMesh_from_Chunk_and_neighbors (&chunk, NULL, NULL, NULL); + + program_use (resources.programs[1]); + program_print (resources.programs[1]); + + /* Set up game state. */ + game_state.last_time = SDL_GetTicks (); + + /* Set some basic game flags for testing. */ + game_flags.can_free_look = 1; + + /* Initialize the default camera. */ + camera = Camera_default (); + camera.position.z = -1.0; + + return SDL_APP_CONTINUE; +} + +SDL_AppResult +SDL_AppEvent (void *appstate, SDL_Event *event) +{ + (void) appstate; + + if (event->type == SDL_EVENT_QUIT) + { + return SDL_APP_SUCCESS; + } + + if (event->type == SDL_EVENT_MOUSE_MOTION && game_flags.can_free_look) + { + camera.rotation.x -= event->motion.xrel / 100.f; + camera.rotation.y -= event->motion.yrel / 100.f; + camera.rotation.y = + CLAMP (camera.rotation.y, -M_PI / 2 + 0.025, M_PI / 2 - 0.025); + } + + if (event->type == SDL_EVENT_KEY_DOWN) + { + switch (event->key.key) + { + case SDLK_W: + game_flags.moving_forward = 1; + break; + case SDLK_A: + game_flags.moving_left = 1; + break; + case SDLK_S: + game_flags.moving_backward = 1; + break; + case SDLK_D: + game_flags.moving_right = 1; + break; + case SDLK_F1: + game_flags.wireframe_enabled = !game_flags.wireframe_enabled; + break; + } + } + + if (event->type == SDL_EVENT_KEY_UP) + { + switch (event->key.key) + { + case SDLK_W: + game_flags.moving_forward = 0; + break; + case SDLK_A: + game_flags.moving_left = 0; + break; + case SDLK_S: + game_flags.moving_backward = 0; + break; + case SDLK_D: + game_flags.moving_right = 0; + break; + } + } + + return SDL_APP_CONTINUE; +} + +SDL_AppResult +SDL_AppIterate (void *appstate) +{ + (void) appstate; + + /* Caluclate stuff per frame. */ + int now = SDL_GetTicks (); + game_state.delta_time = (now - game_state.last_time) / 1000.0; + game_state.last_time = now; + + if (game_flags.moving_forward) + { + camera.position = v3_add + (camera.position, v3_muls + (Camera_get_direction (&camera), game_state.delta_time)); + } + + if (game_flags.moving_backward) + { + camera.position = v3_sub + (camera.position, v3_muls + (Camera_get_direction (&camera), game_state.delta_time)); + } + + if (game_flags.moving_right) + { + camera.position = v3_add + (camera.position, v3_muls + (Camera_get_right (&camera), game_state.delta_time)); + } + + if (game_flags.moving_left) + { + camera.position = v3_sub + (camera.position, v3_muls + (Camera_get_right (&camera), game_state.delta_time)); + } + + /* Render stuff. */ + + glClear (GL_COLOR_BUFFER_BIT); + + mat4_t view = Camera_get_view (&camera); + mat4_t projection = Camera_get_projection (&camera, 640.0 / 480.0); + + program_use (resources.programs[1]); + program_set_mat4 (resources.programs[1], "view", view); + program_set_mat4 (resources.programs[1], "projection", projection); + ChunkMesh_render (&test_chunkmesh); + + SDL_GL_SwapWindow (window); + + return SDL_APP_CONTINUE; +} + +void +SDL_AppQuit (void *appstate, SDL_AppResult result) +{ + (void) appstate; (void) result; + Resources_free (); + SDL_GL_DestroyContext (context); + SDL_DestroyWindow (window); +} diff --git a/game/src/math_3d.c b/game/src/math_3d.c new file mode 100644 index 0000000..9e94a34 --- /dev/null +++ b/game/src/math_3d.c @@ -0,0 +1,2 @@ +#define MATH_3D_IMPLEMENTATION +#include "math_3d.h" diff --git a/game/src/math_3d.h b/game/src/math_3d.h new file mode 100644 index 0000000..cdc6101 --- /dev/null +++ b/game/src/math_3d.h @@ -0,0 +1,627 @@ +/** + +Math 3D v1.0 +By Stephan Soller and Tobias Malmsheimer +Licensed under the MIT license + +Math 3D is a compact C99 library meant to be used with OpenGL. It provides basic +3D vector and 4x4 matrix operations as well as functions to create transformation +and projection matrices. The OpenGL binary layout is used so you can just upload +vectors and matrices into shaders and work with them without any conversions. + +It's an stb style single header file library. Define MATH_3D_IMPLEMENTATION +before you include this file in *one* C file to create the implementation. + + +QUICK NOTES + +- If not explicitly stated by a parameter name all angles are in radians. +- The matrices use column-major indices. This is the same as in OpenGL and GLSL. + The matrix documentation below for details. +- Matrices are passed by value. This is probably a bit inefficient but + simplifies code quite a bit. Most operations will be inlined by the compiler + anyway so the difference shouldn't matter that much. A matrix fits into 4 of + the 16 SSE2 registers anyway. If profiling shows significant slowdowns the + matrix type might change but ease of use is more important than every last + percent of performance. +- When combining matrices with multiplication the effects apply right to left. + This is the convention used in mathematics and OpenGL. Source: + https://en.wikipedia.org/wiki/Transformation_matrix#Composing_and_inverting_transformations + Direct3D does it differently. +- The `m4_mul_pos()` and `m4_mul_dir()` functions do a correct perspective + divide (division by w) when necessary. This is a bit slower but ensures that + the functions will properly work with projection matrices. If profiling shows + this is a bottleneck special functions without perspective division can be + added. But the normal multiplications should avoid any surprises. +- The library consistently uses a right-handed coordinate system. The old + `glOrtho()` broke that rule and `m4_ortho()` has be slightly modified so you + can always think of right-handed cubes that are projected into OpenGLs + normalized device coordinates. +- Special care has been taken to document all complex operations and important + sources. Most code is covered by test cases that have been manually calculated + and checked on the whiteboard. Since indices and math code is prone to be + confusing we used pair programming to avoid mistakes. + + +FURTHER IDEARS + +These are ideas for future work on the library. They're implemented as soon as +there is a proper use case and we can find good names for them. + +- bool v3_is_null(vec3_t v, float epsilon) + To check if the length of a vector is smaller than `epsilon`. +- vec3_t v3_length_default(vec3_t v, float default_length, float epsilon) + Returns `default_length` if the length of `v` is smaller than `epsilon`. + Otherwise same as `v3_length()`. +- vec3_t v3_norm_default(vec3_t v, vec3_t default_vector, float epsilon) + Returns `default_vector` if the length of `v` is smaller than `epsilon`. + Otherwise the same as `v3_norm()`. +- mat4_t m4_invert(mat4_t matrix) + Matrix inversion that works with arbitrary matrices. `m4_invert_affine()` can + already invert translation, rotation, scaling, mirroring, reflection and + shearing matrices. So a general inversion might only be useful to invert + projection matrices for picking. But with orthographic and perspective + projection it's probably simpler to calculate the ray into the scene directly + based on the screen coordinates. + + +VERSION HISTORY + +v1.0 2016-02-15 Initial release + +**/ + +#ifndef MATH_3D_HEADER +#define MATH_3D_HEADER + +#include +#include + + +// Define PI directly because we would need to define the _BSD_SOURCE or +// _XOPEN_SOURCE feature test macros to get it from math.h. That would be a +// rather harsh dependency. So we define it directly if necessary. +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + + +// +// 3D vectors +// +// Use the `vec3()` function to create vectors. All other vector functions start +// with the `v3_` prefix. +// +// The binary layout is the same as in GLSL and everything else (just 3 floats). +// So you can just upload the vectors into shaders as they are. +// + +typedef struct { float x, y, z; } vec3_t; +static inline vec3_t vec3(float x, float y, float z) { return (vec3_t){ x, y, z }; } + +static inline vec3_t v3_add (vec3_t a, vec3_t b) { return (vec3_t){ a.x + b.x, a.y + b.y, a.z + b.z }; } +static inline vec3_t v3_adds (vec3_t a, float s) { return (vec3_t){ a.x + s, a.y + s, a.z + s }; } +static inline vec3_t v3_sub (vec3_t a, vec3_t b) { return (vec3_t){ a.x - b.x, a.y - b.y, a.z - b.z }; } +static inline vec3_t v3_subs (vec3_t a, float s) { return (vec3_t){ a.x - s, a.y - s, a.z - s }; } +static inline vec3_t v3_mul (vec3_t a, vec3_t b) { return (vec3_t){ a.x * b.x, a.y * b.y, a.z * b.z }; } +static inline vec3_t v3_muls (vec3_t a, float s) { return (vec3_t){ a.x * s, a.y * s, a.z * s }; } +static inline vec3_t v3_div (vec3_t a, vec3_t b) { return (vec3_t){ a.x / b.x, a.y / b.y, a.z / b.z }; } +static inline vec3_t v3_divs (vec3_t a, float s) { return (vec3_t){ a.x / s, a.y / s, a.z / s }; } +static inline float v3_length(vec3_t v) { return sqrtf(v.x*v.x + v.y*v.y + v.z*v.z); } +static inline vec3_t v3_norm (vec3_t v); +static inline float v3_dot (vec3_t a, vec3_t b) { return a.x*b.x + a.y*b.y + a.z*b.z; } +static inline vec3_t v3_proj (vec3_t v, vec3_t onto); +static inline vec3_t v3_cross (vec3_t a, vec3_t b); +static inline float v3_angle_between(vec3_t a, vec3_t b); + + +// +// 4x4 matrices +// +// Use the `mat4()` function to create a matrix. You can write the matrix +// members in the same way as you would write them on paper or on a whiteboard: +// +// mat4_t m = mat4( +// 1, 0, 0, 7, +// 0, 1, 0, 5, +// 0, 0, 1, 3, +// 0, 0, 0, 1 +// ) +// +// This creates a matrix that translates points by vec3(7, 5, 3). All other +// matrix functions start with the `m4_` prefix. Among them functions to create +// identity, translation, rotation, scaling and projection matrices. +// +// The matrix is stored in column-major order, just as OpenGL expects. Members +// can be accessed by indices or member names. When you write a matrix on paper +// or on the whiteboard the indices and named members correspond to these +// positions: +// +// | m[0][0] m[1][0] m[2][0] m[3][0] | +// | m[0][1] m[1][1] m[2][1] m[3][1] | +// | m[0][2] m[1][2] m[2][2] m[3][2] | +// | m[0][3] m[1][3] m[2][3] m[3][3] | +// +// | m00 m10 m20 m30 | +// | m01 m11 m21 m31 | +// | m02 m12 m22 m32 | +// | m03 m13 m23 m33 | +// +// The first index or number in a name denotes the column, the second the row. +// So m[i][j] denotes the member in the ith column and the jth row. This is the +// same as in GLSL (source: GLSL v1.3 specification, 5.6 Matrix Components). +// + +typedef union { + // The first index is the column index, the second the row index. The memory + // layout of nested arrays in C matches the memory layout expected by OpenGL. + float m[4][4]; + // OpenGL expects the first 4 floats to be the first column of the matrix. + // So we need to define the named members column by column for the names to + // match the memory locations of the array elements. + struct { + float m00, m01, m02, m03; + float m10, m11, m12, m13; + float m20, m21, m22, m23; + float m30, m31, m32, m33; + }; +} mat4_t; + +static inline mat4_t mat4( + float m00, float m10, float m20, float m30, + float m01, float m11, float m21, float m31, + float m02, float m12, float m22, float m32, + float m03, float m13, float m23, float m33 +); + +static inline mat4_t m4_identity (); +static inline mat4_t m4_translation (vec3_t offset); +static inline mat4_t m4_scaling (vec3_t scale); +static inline mat4_t m4_rotation_x (float angle_in_rad); +static inline mat4_t m4_rotation_y (float angle_in_rad); +static inline mat4_t m4_rotation_z (float angle_in_rad); + mat4_t m4_rotation (float angle_in_rad, vec3_t axis); + + mat4_t m4_ortho (float left, float right, float bottom, float top, float back, float front); + mat4_t m4_perspective (float vertical_field_of_view_in_deg, float aspect_ratio, float near_view_distance, float far_view_distance); + mat4_t m4_look_at (vec3_t from, vec3_t to, vec3_t up); + +static inline mat4_t m4_transpose (mat4_t matrix); +static inline mat4_t m4_mul (mat4_t a, mat4_t b); + mat4_t m4_invert_affine(mat4_t matrix); + vec3_t m4_mul_pos (mat4_t matrix, vec3_t position); + vec3_t m4_mul_dir (mat4_t matrix, vec3_t direction); + + void m4_print (mat4_t matrix); + void m4_printp (mat4_t matrix, int width, int precision); + void m4_fprint (FILE* stream, mat4_t matrix); + void m4_fprintp (FILE* stream, mat4_t matrix, int width, int precision); + + + +// +// 3D vector functions header implementation +// + +static inline vec3_t v3_norm(vec3_t v) { + float len = v3_length(v); + if (len > 0) + return (vec3_t){ v.x / len, v.y / len, v.z / len }; + else + return (vec3_t){ 0, 0, 0}; +} + +static inline vec3_t v3_proj(vec3_t v, vec3_t onto) { + return v3_muls(onto, v3_dot(v, onto) / v3_dot(onto, onto)); +} + +static inline vec3_t v3_cross(vec3_t a, vec3_t b) { + return (vec3_t){ + a.y * b.z - a.z * b.y, + a.z * b.x - a.x * b.z, + a.x * b.y - a.y * b.x + }; +} + +static inline float v3_angle_between(vec3_t a, vec3_t b) { + return acosf( v3_dot(a, b) / (v3_length(a) * v3_length(b)) ); +} + + +// +// Matrix functions header implementation +// + +static inline mat4_t mat4( + float m00, float m10, float m20, float m30, + float m01, float m11, float m21, float m31, + float m02, float m12, float m22, float m32, + float m03, float m13, float m23, float m33 +) { + return (mat4_t){ + .m[0][0] = m00, .m[1][0] = m10, .m[2][0] = m20, .m[3][0] = m30, + .m[0][1] = m01, .m[1][1] = m11, .m[2][1] = m21, .m[3][1] = m31, + .m[0][2] = m02, .m[1][2] = m12, .m[2][2] = m22, .m[3][2] = m32, + .m[0][3] = m03, .m[1][3] = m13, .m[2][3] = m23, .m[3][3] = m33 + }; +} + +static inline mat4_t m4_identity() { + return mat4( + 1, 0, 0, 0, + 0, 1, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ); +} + +static inline mat4_t m4_translation(vec3_t offset) { + return mat4( + 1, 0, 0, offset.x, + 0, 1, 0, offset.y, + 0, 0, 1, offset.z, + 0, 0, 0, 1 + ); +} + +static inline mat4_t m4_scaling(vec3_t scale) { + float x = scale.x, y = scale.y, z = scale.z; + return mat4( + x, 0, 0, 0, + 0, y, 0, 0, + 0, 0, z, 0, + 0, 0, 0, 1 + ); +} + +static inline mat4_t m4_rotation_x(float angle_in_rad) { + float s = sinf(angle_in_rad), c = cosf(angle_in_rad); + return mat4( + 1, 0, 0, 0, + 0, c, -s, 0, + 0, s, c, 0, + 0, 0, 0, 1 + ); +} + +static inline mat4_t m4_rotation_y(float angle_in_rad) { + float s = sinf(angle_in_rad), c = cosf(angle_in_rad); + return mat4( + c, 0, s, 0, + 0, 1, 0, 0, + -s, 0, c, 0, + 0, 0, 0, 1 + ); +} + +static inline mat4_t m4_rotation_z(float angle_in_rad) { + float s = sinf(angle_in_rad), c = cosf(angle_in_rad); + return mat4( + c, -s, 0, 0, + s, c, 0, 0, + 0, 0, 1, 0, + 0, 0, 0, 1 + ); +} + +static inline mat4_t m4_transpose(mat4_t matrix) { + return mat4( + matrix.m00, matrix.m01, matrix.m02, matrix.m03, + matrix.m10, matrix.m11, matrix.m12, matrix.m13, + matrix.m20, matrix.m21, matrix.m22, matrix.m23, + matrix.m30, matrix.m31, matrix.m32, matrix.m33 + ); +} + +/** + * Multiplication of two 4x4 matrices. + * + * Implemented by following the row times column rule and illustrating it on a + * whiteboard with the proper indices in mind. + * + * Further reading: https://en.wikipedia.org/wiki/Matrix_multiplication + * But note that the article use the first index for rows and the second for + * columns. + */ +static inline mat4_t m4_mul(mat4_t a, mat4_t b) { + mat4_t result; + + for(int i = 0; i < 4; i++) { + for(int j = 0; j < 4; j++) { + float sum = 0; + for(int k = 0; k < 4; k++) { + sum += a.m[k][j] * b.m[i][k]; + } + result.m[i][j] = sum; + } + } + + return result; +} + +#endif // MATH_3D_HEADER + + +#ifdef MATH_3D_IMPLEMENTATION + +/** + * Creates a matrix to rotate around an axis by a given angle. The axis doesn't + * need to be normalized. + * + * Sources: + * + * https://en.wikipedia.org/wiki/Rotation_matrix#Rotation_matrix_from_axis_and_angle + */ +mat4_t m4_rotation(float angle_in_rad, vec3_t axis) { + vec3_t normalized_axis = v3_norm(axis); + float x = normalized_axis.x, y = normalized_axis.y, z = normalized_axis.z; + float c = cosf(angle_in_rad), s = sinf(angle_in_rad); + + return mat4( + c + x*x*(1-c), x*y*(1-c) - z*s, x*z*(1-c) + y*s, 0, + y*x*(1-c) + z*s, c + y*y*(1-c), y*z*(1-c) - x*s, 0, + z*x*(1-c) - y*s, z*y*(1-c) + x*s, c + z*z*(1-c), 0, + 0, 0, 0, 1 + ); +} + + +/** + * Creates an orthographic projection matrix. It maps the right handed cube + * defined by left, right, bottom, top, back and front onto the screen and + * z-buffer. You can think of it as a cube you move through world or camera + * space and everything inside is visible. + * + * This is slightly different from the traditional glOrtho() and from the linked + * sources. These functions require the user to negate the last two arguments + * (creating a left-handed coordinate system). We avoid that here so you can + * think of this function as moving a right-handed cube through world space. + * + * The arguments are ordered in a way that for each axis you specify the minimum + * followed by the maximum. Thats why it's bottom to top and back to front. + * + * Implementation details: + * + * To be more exact the right-handed cube is mapped into normalized device + * coordinates, a left-handed cube where (-1 -1) is the lower left corner, + * (1, 1) the upper right corner and a z-value of -1 is the nearest point and + * 1 the furthest point. OpenGL takes it from there and puts it on the screen + * and into the z-buffer. + * + * Sources: + * + * https://msdn.microsoft.com/en-us/library/windows/desktop/dd373965(v=vs.85).aspx + * https://unspecified.wordpress.com/2012/06/21/calculating-the-gluperspective-matrix-and-other-opengl-matrix-maths/ + */ +mat4_t m4_ortho(float left, float right, float bottom, float top, float back, float front) { + float l = left, r = right, b = bottom, t = top, n = front, f = back; + float tx = -(r + l) / (r - l); + float ty = -(t + b) / (t - b); + float tz = -(f + n) / (f - n); + return mat4( + 2 / (r - l), 0, 0, tx, + 0, 2 / (t - b), 0, ty, + 0, 0, 2 / (f - n), tz, + 0, 0, 0, 1 + ); +} + +/** + * Creates a perspective projection matrix for a camera. + * + * The camera is at the origin and looks in the direction of the negative Z axis. + * `near_view_distance` and `far_view_distance` have to be positive and > 0. + * They are distances from the camera eye, not values on an axis. + * + * `near_view_distance` can be small but not 0. 0 breaks the projection and + * everything ends up at the max value (far end) of the z-buffer. Making the + * z-buffer useless. + * + * The matrix is the same as `gluPerspective()` builds. The view distance is + * mapped to the z-buffer with a reciprocal function (1/x). Therefore the z-buffer + * resolution for near objects is very good while resolution for far objects is + * limited. + * + * Sources: + * + * https://unspecified.wordpress.com/2012/06/21/calculating-the-gluperspective-matrix-and-other-opengl-matrix-maths/ + */ +mat4_t m4_perspective(float vertical_field_of_view_in_deg, float aspect_ratio, float near_view_distance, float far_view_distance) { + float fovy_in_rad = vertical_field_of_view_in_deg / 180 * M_PI; + float f = 1.0f / tanf(fovy_in_rad / 2.0f); + float ar = aspect_ratio; + float nd = near_view_distance, fd = far_view_distance; + + return mat4( + f / ar, 0, 0, 0, + 0, f, 0, 0, + 0, 0, (fd+nd)/(nd-fd), (2*fd*nd)/(nd-fd), + 0, 0, -1, 0 + ); +} + +/** + * Builds a transformation matrix for a camera that looks from `from` towards + * `to`. `up` defines the direction that's upwards for the camera. All three + * vectors are given in world space and `up` doesn't need to be normalized. + * + * Sources: Derived on whiteboard. + * + * Implementation details: + * + * x, y and z are the right-handed base vectors of the cameras subspace. + * x has to be normalized because the cross product only produces a normalized + * output vector if both input vectors are orthogonal to each other. And up + * probably isn't orthogonal to z. + * + * These vectors are then used to build a 3x3 rotation matrix. This matrix + * rotates a vector by the same amount the camera is rotated. But instead we + * need to rotate all incoming vertices backwards by that amount. That's what a + * camera matrix is for: To move the world so that the camera is in the origin. + * So we take the inverse of that rotation matrix and in case of an rotation + * matrix this is just the transposed matrix. That's why the 3x3 part of the + * matrix are the x, y and z vectors but written horizontally instead of + * vertically. + * + * The translation is derived by creating a translation matrix to move the world + * into the origin (thats translate by minus `from`). The complete lookat matrix + * is then this translation followed by the rotation. Written as matrix + * multiplication: + * + * lookat = rotation * translation + * + * Since we're right-handed this equals to first doing the translation and after + * that doing the rotation. During that multiplication the rotation 3x3 part + * doesn't change but the translation vector is multiplied with each rotation + * axis. The dot product is just a more compact way to write the actual + * multiplications. + */ +mat4_t m4_look_at(vec3_t from, vec3_t to, vec3_t up) { + vec3_t z = v3_muls(v3_norm(v3_sub(to, from)), -1); + vec3_t x = v3_norm(v3_cross(up, z)); + vec3_t y = v3_cross(z, x); + + return mat4( + x.x, x.y, x.z, -v3_dot(from, x), + y.x, y.y, y.z, -v3_dot(from, y), + z.x, z.y, z.z, -v3_dot(from, z), + 0, 0, 0, 1 + ); +} + + +/** + * Inverts an affine transformation matrix. That are translation, scaling, + * mirroring, reflection, rotation and shearing matrices or any combination of + * them. + * + * Implementation details: + * + * - Invert the 3x3 part of the 4x4 matrix to handle rotation, scaling, etc. + * correctly (see source). + * - Invert the translation part of the 4x4 matrix by multiplying it with the + * inverted rotation matrix and negating it. + * + * When a 3D point is multiplied with a transformation matrix it is first + * rotated and then translated. The inverted transformation matrix is the + * inverse translation followed by the inverse rotation. Written as a matrix + * multiplication (remember, the effect applies right to left): + * + * inv(matrix) = inv(rotation) * inv(translation) + * + * The inverse translation is a translation into the opposite direction, just + * the negative translation. The rotation part isn't changed by that + * multiplication but the translation part is multiplied by the inverse rotation + * matrix. It's the same situation as with `m4_look_at()`. But since we don't + * store the rotation matrix as 3D vectors we can't use the dot product and have + * to write the matrix multiplication operations by hand. + * + * Sources for 3x3 matrix inversion: + * + * https://www.khanacademy.org/math/precalculus/precalc-matrices/determinants-and-inverses-of-large-matrices/v/inverting-3x3-part-2-determinant-and-adjugate-of-a-matrix + */ +mat4_t m4_invert_affine(mat4_t matrix) { + // Create shorthands to access matrix members + float m00 = matrix.m00, m10 = matrix.m10, m20 = matrix.m20, m30 = matrix.m30; + float m01 = matrix.m01, m11 = matrix.m11, m21 = matrix.m21, m31 = matrix.m31; + float m02 = matrix.m02, m12 = matrix.m12, m22 = matrix.m22, m32 = matrix.m32; + + // Invert 3x3 part of the 4x4 matrix that contains the rotation, etc. + // That part is called R from here on. + + // Calculate cofactor matrix of R + float c00 = m11*m22 - m12*m21, c10 = -(m01*m22 - m02*m21), c20 = m01*m12 - m02*m11; + float c01 = -(m10*m22 - m12*m20), c11 = m00*m22 - m02*m20, c21 = -(m00*m12 - m02*m10); + float c02 = m10*m21 - m11*m20, c12 = -(m00*m21 - m01*m20), c22 = m00*m11 - m01*m10; + + // Caclculate the determinant by using the already calculated determinants + // in the cofactor matrix. + // Second sign is already minus from the cofactor matrix. + float det = m00*c00 + m10*c10 + m20 * c20; + if (fabsf(det) < 0.00001) + return m4_identity(); + + // Calcuate inverse of R by dividing the transposed cofactor matrix by the + // determinant. + float i00 = c00 / det, i10 = c01 / det, i20 = c02 / det; + float i01 = c10 / det, i11 = c11 / det, i21 = c12 / det; + float i02 = c20 / det, i12 = c21 / det, i22 = c22 / det; + + // Combine the inverted R with the inverted translation + return mat4( + i00, i10, i20, -(i00*m30 + i10*m31 + i20*m32), + i01, i11, i21, -(i01*m30 + i11*m31 + i21*m32), + i02, i12, i22, -(i02*m30 + i12*m31 + i22*m32), + 0, 0, 0, 1 + ); +} + +/** + * Multiplies a 4x4 matrix with a 3D vector representing a point in 3D space. + * + * Before the matrix multiplication the vector is first expanded to a 4D vector + * (x, y, z, 1). After the multiplication the vector is reduced to 3D again by + * dividing through the 4th component (if it's not 0 or 1). + */ +vec3_t m4_mul_pos(mat4_t matrix, vec3_t position) { + vec3_t result = vec3( + matrix.m00 * position.x + matrix.m10 * position.y + matrix.m20 * position.z + matrix.m30, + matrix.m01 * position.x + matrix.m11 * position.y + matrix.m21 * position.z + matrix.m31, + matrix.m02 * position.x + matrix.m12 * position.y + matrix.m22 * position.z + matrix.m32 + ); + + float w = matrix.m03 * position.x + matrix.m13 * position.y + matrix.m23 * position.z + matrix.m33; + if (w != 0 && w != 1) + return vec3(result.x / w, result.y / w, result.z / w); + + return result; +} + +/** + * Multiplies a 4x4 matrix with a 3D vector representing a direction in 3D space. + * + * Before the matrix multiplication the vector is first expanded to a 4D vector + * (x, y, z, 0). For directions the 4th component is set to 0 because directions + * are only rotated, not translated. After the multiplication the vector is + * reduced to 3D again by dividing through the 4th component (if it's not 0 or + * 1). This is necessary because the matrix might contains something other than + * (0, 0, 0, 1) in the bottom row which might set w to something other than 0 + * or 1. + */ +vec3_t m4_mul_dir(mat4_t matrix, vec3_t direction) { + vec3_t result = vec3( + matrix.m00 * direction.x + matrix.m10 * direction.y + matrix.m20 * direction.z, + matrix.m01 * direction.x + matrix.m11 * direction.y + matrix.m21 * direction.z, + matrix.m02 * direction.x + matrix.m12 * direction.y + matrix.m22 * direction.z + ); + + float w = matrix.m03 * direction.x + matrix.m13 * direction.y + matrix.m23 * direction.z; + if (w != 0 && w != 1) + return vec3(result.x / w, result.y / w, result.z / w); + + return result; +} + +void m4_print(mat4_t matrix) { + m4_fprintp(stdout, matrix, 6, 2); +} + +void m4_printp(mat4_t matrix, int width, int precision) { + m4_fprintp(stdout, matrix, width, precision); +} + +void m4_fprint(FILE* stream, mat4_t matrix) { + m4_fprintp(stream, matrix, 6, 2); +} + +void m4_fprintp(FILE* stream, mat4_t matrix, int width, int precision) { + mat4_t m = matrix; + int w = width, p = precision; + for(int r = 0; r < 4; r++) { + fprintf(stream, "| %*.*f %*.*f %*.*f %*.*f |\n", + w, p, m.m[0][r], w, p, m.m[1][r], w, p, m.m[2][r], w, p, m.m[3][r] + ); + } +} + +#endif // MATH_3D_IMPLEMENTATION \ No newline at end of file diff --git a/game/src/mesh.c b/game/src/mesh.c new file mode 100644 index 0000000..f58e730 --- /dev/null +++ b/game/src/mesh.c @@ -0,0 +1,21 @@ +#include "mesh.h" + +Mesh +Mesh_new (size_t vertices_len, float *vertices) +{ + Mesh mesh = { vertices_len, vertices, 0 }; + glGenBuffers (1, &mesh.vbo); + glBindBuffer (GL_ARRAY_BUFFER, mesh.vbo); + glBufferData (GL_ARRAY_BUFFER, vertices_len * sizeof (float), vertices, GL_STATIC_DRAW); + glVertexAttribPointer (0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof (float), 0); + glEnableVertexAttribArray (0); + + return mesh; +} + +void +Mesh_draw (Mesh *mesh) +{ + glBindBuffer (GL_ARRAY_BUFFER, mesh->vbo); + glDrawArrays (GL_TRIANGLES, 0, mesh->vertices_len); +} diff --git a/game/src/mesh.h b/game/src/mesh.h new file mode 100644 index 0000000..bb36702 --- /dev/null +++ b/game/src/mesh.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "util.h" + +typedef struct Mesh +{ + size_t vertices_len; + float *vertices; + GLuint vbo; +} Mesh; + +Mesh Mesh_new (size_t vertices_len, float *vertices); +void Mesh_draw (Mesh *mesh); diff --git a/game/src/program.c b/game/src/program.c new file mode 100644 index 0000000..882a50e --- /dev/null +++ b/game/src/program.c @@ -0,0 +1,123 @@ +#include "program.h" + +void +shader_fail (int shader) +{ + int info_len; + glGetShaderiv (shader, GL_INFO_LOG_LENGTH, &info_len); + int shader_type; + glGetShaderiv (shader, GL_SHADER_TYPE, &shader_type); + const char *type = shader_type == GL_VERTEX_SHADER ? "vertex" : "fragment"; + + if (info_len > 1) + { + char *info = malloc (sizeof (char) * info_len); + glGetShaderInfoLog (shader, info_len, NULL, info); + WARN ("Compiling %s shader failed:\n%s", type, info); + free (info); + } + else + { + WARN ("Compiling %s shader failed without information.", type); + } +} + +void +program_fail (int program) +{ + int info_len; + glGetProgramiv (program, GL_INFO_LOG_LENGTH, &info_len); + + if (info_len > 1) + { + char *info = malloc (sizeof (char) * info_len); + glGetProgramInfoLog (program, info_len, NULL, info); + WARN ("Linking program failed:\n%s", info); + free (info); + } + else + { + WARN ("Linking program failed without information."); + } +} + +int +program_create (const char *v_src, const char *f_src) +{ + int success = 0; + + int vertex = glCreateShader (GL_VERTEX_SHADER); + glShaderSource (vertex, 1, &v_src, NULL); + glCompileShader (vertex); + glGetShaderiv (vertex, GL_COMPILE_STATUS, &success); + if (!success) shader_fail (vertex); + + int fragment = glCreateShader (GL_FRAGMENT_SHADER); + glShaderSource (fragment, 1, &f_src, NULL); + glCompileShader (fragment); + glGetShaderiv (fragment, GL_COMPILE_STATUS, &success); + if (!success) shader_fail (fragment); + + int program = glCreateProgram (); + glAttachShader (program, vertex); + glAttachShader (program, fragment); + glLinkProgram (program); + glGetProgramiv (program, GL_LINK_STATUS, &success); + if (!success) program_fail (program); + else INFO ("Compiled shaders and linked program %d successfuly.", program); + + glDeleteShader (vertex); + glDeleteShader (fragment); + + return program; +} + +void +program_set_mat4 (int program, const char *name, mat4_t value) +{ + glUniformMatrix4fv (glGetUniformLocation (program, name), 1, GL_FALSE, (float *) &value); +} + +static const char * +gl_type_to_string (GLenum type) +{ + switch (type) + { + case GL_FLOAT: return "float"; + case GL_FLOAT_VEC3: return "vec3"; + case GL_FLOAT_VEC4: return "vec4"; + case GL_INT: return "int"; + case GL_INT_VEC3: return "ivec3"; + case GL_INT_VEC4: return "ivec4"; + case GL_BOOL: return "bool"; + case GL_FLOAT_MAT4: return "mat4"; + case GL_SAMPLER_2D: return "sampler2D"; + default: return "unknown type"; + } +} + +void +program_print (int program) +{ + int bufsize; + glGetProgramiv (program, GL_ACTIVE_UNIFORM_MAX_LENGTH, &bufsize); + char name[bufsize]; + + int uniform_no, attribute_no; + glGetProgramiv (program, GL_ACTIVE_UNIFORMS, &uniform_no); + glGetProgramiv (program, GL_ACTIVE_ATTRIBUTES, &attribute_no); + + INFO ("== Program %d: %d active attribute(s) / %d active uniform(s) ==", program, attribute_no, uniform_no); + + GLenum type; + + for (GLint i = 0; i < attribute_no; i++) { + glGetActiveAttrib (program, i, bufsize, NULL, NULL, &type, name); + INFO ("Attribute #%d: %s %s", i, gl_type_to_string (type), name); + } + + for (GLint i = 0; i < uniform_no; i++) { + glGetActiveUniform (program, i, bufsize, NULL, NULL, &type, name); + INFO ("Uniform #%d: %s %s", i, gl_type_to_string (type), name); + } +} diff --git a/game/src/program.h b/game/src/program.h new file mode 100644 index 0000000..f3dab10 --- /dev/null +++ b/game/src/program.h @@ -0,0 +1,15 @@ +#pragma once + +#include + +#include "math_3d.h" +#include "util.h" + +/* Just for clarity sake. */ +#define program_use glUseProgram + +void shader_fail (int shader); +void program_fail (int program); +int program_create (const char *v_src, const char *f_src); +void program_set_mat4 (int program, const char *name, mat4_t value); +void program_print (int program); diff --git a/game/src/resources.c b/game/src/resources.c new file mode 100644 index 0000000..5697200 --- /dev/null +++ b/game/src/resources.c @@ -0,0 +1,64 @@ +#include "resources.h" + +Resources resources; + +void +Resources_add_program (const char *v_src, const char *f_src) +{ + resources.programs_len++; + resources.programs = realloc (resources.programs, sizeof (int) * resources.programs_len); + resources.programs[resources.programs_len - 1] = program_create (v_src, f_src); +} + +void +Resources_load_program (const char *v_path, const char *f_path) +{ + FILE *v_file = fopen (v_path, "r"); + + if (v_file == NULL) + { + WARN ("File `%s' does not exist. Not loading program.", v_path); + return; + } + + fseek (v_file, 0L, SEEK_END); + size_t v_len = ftell (v_file) + 1; + char *v_src = calloc (v_len, sizeof (char)); + rewind (v_file); + INFO ("Read %zu bytes from vertex source.", fread (v_src, sizeof (char), v_len, v_file)); + + FILE *f_file = fopen (f_path, "r"); + + if (f_file == NULL) + { + WARN ("File `%s' does not exist. Not loading program.", f_path); + return; + } + + fseek (f_file, 0L, SEEK_END); + size_t f_len = ftell (f_file) + 1; + char *f_src = calloc (f_len, sizeof (char)); + rewind (f_file); + INFO ("Read %zu bytes from fragment source.", fread (f_src, sizeof (char), f_len, f_file)); + + Resources_add_program (v_src, f_src); + + free (v_src); + free (f_src); +} + +void +Resources_init () +{ + resources.programs_len = 0; + resources.programs = malloc (sizeof (int)); + Resources_load_program ("res/default.vert", "res/default.frag"); + Resources_load_program ("res/chunk.vert", "res/chunk.frag"); +} + +void +Resources_free () +{ + free (resources.programs); +} + diff --git a/game/src/resources.h b/game/src/resources.h new file mode 100644 index 0000000..af80666 --- /dev/null +++ b/game/src/resources.h @@ -0,0 +1,13 @@ +#pragma once + +#include "program.h" + +typedef struct Resources { + size_t programs_len; + int *programs; +} Resources; + +void Resources_add_program (const char *v_src, const char *f_src); +void Resources_load_program (const char *v_path, const char *f_path); +void Resources_init (); +void Resources_free (); diff --git a/game/src/state.h b/game/src/state.h new file mode 100644 index 0000000..5a848ea --- /dev/null +++ b/game/src/state.h @@ -0,0 +1,9 @@ +#pragma once + +typedef struct GameState +{ + int last_time; + float delta_time; +} GameState; + +GameState game_state; diff --git a/game/src/util.h b/game/src/util.h new file mode 100644 index 0000000..671ae37 --- /dev/null +++ b/game/src/util.h @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +#define GAME_NAME "Wschód" +#define GAME_VERSION "0.1" + +#define COLOR_BLUE "\x1B[34m" +#define COLOR_RED "\x1B[31m" +#define COLOR_YELLOW "\x1B[33m" +#define COLOR_RESET "\x1B[0m" + +#define INFO(fmt, args...) (fprintf (stderr, COLOR_BLUE "[I] %s:%d:%s(): " COLOR_RESET fmt "\n", __FILE__, __LINE__, __func__, ##args)) +#define WARN(fmt, args...) (fprintf (stderr, COLOR_YELLOW "[W] %s:%d:%s(): " COLOR_RESET fmt "\n", __FILE__, __LINE__, __func__, ##args)) +#define FATAL(fmt, args...) (fprintf (stderr, COLOR_RED "[!] %s:%d:%s(): " COLOR_RESET fmt "\n", __FILE__, __LINE__, __func__, ##args), exit (-1)) + +#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x))) diff --git a/mapedit/Makefile b/mapedit/Makefile new file mode 100644 index 0000000..f87635e --- /dev/null +++ b/mapedit/Makefile @@ -0,0 +1,21 @@ +SRC=src/mapedit.c + +OBJ=$(SRC:.c=.o) +CFLAGS=-g -Werror -Wextra -Wall `pkg-config --cflags gtk4` +LDFLAGS=`pkg-config --libs gtk4` +TARGET=mapedit +CC=gcc +LD=gcc + +.PHONY: clean + +all: $(TARGET) + +$(TARGET): $(OBJ) + $(LD) $^ -o $@ $(LDFLAGS) + +.c.o: + $(CC) -c $^ -o $@ $(CFLAGS) + +clean: + $(RM) $(TARGET) $(OBJ) diff --git a/mapedit/game.c b/mapedit/game.c new file mode 100644 index 0000000..9498866 --- /dev/null +++ b/mapedit/game.c @@ -0,0 +1,67 @@ +#define SDL_MAIN_USE_CALLBACKS 1 +#include +#include + +#include + +#include "shader.h" + +static SDL_Window *window = NULL; +static SDL_GLContext context = NULL; + +SDL_AppResult +SDL_AppInit (void **appstate, int argc, char **argv) +{ + (void) appstate; (void) argc; (void) argv; + + INFO ("Running game with debugging facilities."); + SDL_SetAppMetadata ("Wschód", VERSION, "eur.chopininteractive.wschod-game"); + + if (!SDL_Init (SDL_INIT_VIDEO)) { + ERROR ("Couldn't initialize SDL: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + window = SDL_CreateWindow ("Wschód", 640, 480, SDL_WINDOW_OPENGL); + + if (!window) { + ERROR ("Couldn't create window: %s", SDL_GetError()); + return SDL_APP_FAILURE; + } + + context = SDL_GL_CreateContext (window); + + return SDL_APP_CONTINUE; +} + +SDL_AppResult +SDL_AppEvent (void *appstate, SDL_Event *event) +{ + (void) appstate; + + if (event->type == SDL_EVENT_QUIT) { + return SDL_APP_SUCCESS; + } + + return SDL_APP_CONTINUE; +} + +SDL_AppResult +SDL_AppIterate (void *appstate) +{ + (void) appstate; + + glClearColor (0, 0, 0, 1); + glClear (GL_COLOR_BUFFER_BIT); + SDL_GL_SwapWindow (window); + + return SDL_APP_CONTINUE; +} + +void +SDL_AppQuit (void *appstate, SDL_AppResult result) +{ + (void) appstate; + (void) result; + SDL_GL_DestroyContext (context); +} diff --git a/mapedit/shell.nix b/mapedit/shell.nix new file mode 100644 index 0000000..8ed8896 --- /dev/null +++ b/mapedit/shell.nix @@ -0,0 +1,13 @@ +let + nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-25.05"; + pkgs = import nixpkgs { config = {}; overlays = []; }; +in + + +pkgs.mkShellNoCC { + packages = with pkgs; [ + gcc + pkg-config + gtk4 + ]; +} diff --git a/mapedit/src/mapedit.c b/mapedit/src/mapedit.c new file mode 100644 index 0000000..7311589 --- /dev/null +++ b/mapedit/src/mapedit.c @@ -0,0 +1,7 @@ +#include + +int +main () +{ + return 0; +}