diff options
| author | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
|---|---|---|
| committer | 3gg <3gg@shellblade.net> | 2025-12-27 12:03:39 -0800 |
| commit | 5a079a2d114f96d4847d1ee305d5b7c16eeec50e (patch) | |
| tree | 8926ab44f168acf787d8e19608857b3af0f82758 /src | |
Initial commit
Diffstat (limited to 'src')
| -rw-r--r-- | src/main.c | 420 |
1 files changed, 420 insertions, 0 deletions
diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..1c017f0 --- /dev/null +++ b/src/main.c | |||
| @@ -0,0 +1,420 @@ | |||
| 1 | #include <model.h> | ||
| 2 | |||
| 3 | #include <filesystem.h> | ||
| 4 | // TODO: Update math/camera to explicitly expose ortho/perspective camera parameters, | ||
| 5 | // not just a baked matrix. | ||
| 6 | //#include <math/camera.h> | ||
| 7 | #include <math/spatial3.h> | ||
| 8 | #include <math/vec2.h> | ||
| 9 | #include <swgfx.h> | ||
| 10 | #include <SDL3/SDL.h> | ||
| 11 | #include <SDL3/SDL_timer.h> | ||
| 12 | |||
| 13 | #include <assert.h> | ||
| 14 | #include <stdio.h> | ||
| 15 | #include <stdlib.h> | ||
| 16 | |||
| 17 | static constexpr int BufferWidth = 160; | ||
| 18 | static constexpr int BufferHeight = 120; | ||
| 19 | static constexpr sgVec2i BufferDims = (sgVec2i){.x = BufferWidth, .y = BufferHeight}; | ||
| 20 | static constexpr R Aspect = (R)BufferWidth / (R)BufferHeight; | ||
| 21 | |||
| 22 | static const char* WindowTitle = "GAME"; | ||
| 23 | // Window dimensions must be an integer scaling of buffer dimensions. | ||
| 24 | // TODO: Make window dimensions a function of DPI. | ||
| 25 | static constexpr int WindowWidth = 640; | ||
| 26 | static constexpr int WindowHeight = 480; | ||
| 27 | static constexpr sgVec2i WindowDims = (sgVec2i){.x = WindowWidth, .y = WindowHeight}; | ||
| 28 | |||
| 29 | static const R Fovy = (R)(90 * TO_RAD); | ||
| 30 | |||
| 31 | #define DEBUG_EVENT_LOOP 1 | ||
| 32 | |||
| 33 | #ifdef DEBUG_EVENT_LOOP | ||
| 34 | #define EVENT_LOOP_PRINT printf | ||
| 35 | #else | ||
| 36 | #define EVENT_LOOP_PRINT(...) | ||
| 37 | #endif // DEBUG_EVENT_LOOP | ||
| 38 | |||
| 39 | typedef struct CameraCommand { | ||
| 40 | bool CameraMoveLeft : 1; | ||
| 41 | bool CameraMoveRight : 1; | ||
| 42 | bool CameraMoveForward : 1; | ||
| 43 | bool CameraMoveBackward : 1; | ||
| 44 | bool CameraSlow : 1; // When true, move more slowly. | ||
| 45 | bool CameraRotate : 1; // When true, subsequent mouse movements cause | ||
| 46 | // the camera to rotate. | ||
| 47 | } CameraCommand; | ||
| 48 | |||
| 49 | typedef struct CameraController { | ||
| 50 | R speed; // Camera movement speed. | ||
| 51 | R rotation_speed; // Controls the degree with which mouse movements rotate | ||
| 52 | // the camera. | ||
| 53 | vec2 prev_mouse_position; // Mouse position in the previous frame. | ||
| 54 | } CameraController; | ||
| 55 | |||
| 56 | typedef struct Camera { | ||
| 57 | Spatial3 spatial; | ||
| 58 | R fovy; | ||
| 59 | R aspect; | ||
| 60 | R near; | ||
| 61 | R far; | ||
| 62 | } Camera; | ||
| 63 | |||
| 64 | typedef struct State { | ||
| 65 | SDL_Window* window; | ||
| 66 | swgfx* gfx; | ||
| 67 | sgPixel* colour; | ||
| 68 | Model* model; | ||
| 69 | Camera camera; | ||
| 70 | CameraController camera_controller; | ||
| 71 | Uint64 last_tick; | ||
| 72 | } State; | ||
| 73 | |||
| 74 | static sgVec3 SgVec3FromMathVec3(vec3 v) { | ||
| 75 | return (sgVec3){v.x, v.y, v.z}; | ||
| 76 | } | ||
| 77 | |||
| 78 | static CameraCommand CameraCommandFromInput( | ||
| 79 | const bool* keyboard_state, const SDL_MouseButtonFlags mouse_flags) { | ||
| 80 | assert(keyboard_state); | ||
| 81 | if (keyboard_state[SDL_SCANCODE_W]) { | ||
| 82 | printf("W: %d\n", keyboard_state[SDL_SCANCODE_W]); | ||
| 83 | } | ||
| 84 | return (CameraCommand){ | ||
| 85 | .CameraMoveLeft = keyboard_state[SDL_SCANCODE_A], | ||
| 86 | .CameraMoveRight = keyboard_state[SDL_SCANCODE_D], | ||
| 87 | .CameraMoveForward = keyboard_state[SDL_SCANCODE_W], | ||
| 88 | .CameraMoveBackward = keyboard_state[SDL_SCANCODE_S], | ||
| 89 | .CameraSlow = keyboard_state[SDL_SCANCODE_LSHIFT], | ||
| 90 | .CameraRotate = mouse_flags & SDL_BUTTON_MASK(SDL_BUTTON_LEFT), | ||
| 91 | }; | ||
| 92 | } | ||
| 93 | |||
| 94 | static void UpdateCamera( | ||
| 95 | CameraController* controller, R dt, vec2 mouse_position, | ||
| 96 | CameraCommand command, Camera* camera) { | ||
| 97 | assert(controller); | ||
| 98 | assert(camera); | ||
| 99 | |||
| 100 | Spatial3* cam = &camera->spatial; | ||
| 101 | |||
| 102 | // Translation. | ||
| 103 | const R move_x = (R)(command.CameraMoveLeft ? -1 : 0) + | ||
| 104 | (R)(command.CameraMoveRight ? 1 : 0); | ||
| 105 | const R move_y = (R)(command.CameraMoveForward ? 1 : 0) + | ||
| 106 | (R)(command.CameraMoveBackward ? -1 : 0); | ||
| 107 | const R speed_factor = command.CameraSlow ? 0.3f : 1.f; | ||
| 108 | const vec2 translation = vec2_scale( | ||
| 109 | vec2_normalize(vec2_make(move_x, move_y)), | ||
| 110 | controller->speed * speed_factor * dt); | ||
| 111 | spatial3_move_right(cam, translation.x); | ||
| 112 | spatial3_move_forwards(cam, translation.y); | ||
| 113 | |||
| 114 | // Rotation. | ||
| 115 | if (command.CameraRotate) { | ||
| 116 | const vec2 mouse_delta = | ||
| 117 | vec2_sub(mouse_position, controller->prev_mouse_position); | ||
| 118 | |||
| 119 | const vec2 rotation = | ||
| 120 | vec2_scale(mouse_delta, controller->rotation_speed * dt); | ||
| 121 | |||
| 122 | spatial3_global_yaw(cam, -rotation.x); | ||
| 123 | spatial3_pitch(cam, -rotation.y); | ||
| 124 | } | ||
| 125 | |||
| 126 | // Update controller state. | ||
| 127 | controller->prev_mouse_position = mouse_position; | ||
| 128 | } | ||
| 129 | |||
| 130 | static bool Update(State* state, R dt) { | ||
| 131 | assert(state); | ||
| 132 | |||
| 133 | int num_keys = 0; | ||
| 134 | const bool* keyboard_state = SDL_GetKeyboardState(&num_keys); | ||
| 135 | |||
| 136 | vec2 mouse = {0}; | ||
| 137 | const SDL_MouseButtonFlags mouse_flags = SDL_GetMouseState(&mouse.x, &mouse.y); | ||
| 138 | |||
| 139 | const CameraCommand cmd = CameraCommandFromInput(keyboard_state, mouse_flags); | ||
| 140 | UpdateCamera(&state->camera_controller, dt, mouse, cmd, &state->camera); | ||
| 141 | |||
| 142 | return true; | ||
| 143 | } | ||
| 144 | |||
| 145 | static void RenderIndexedModel(swgfx* gfx, const IndexedModel* model) { | ||
| 146 | assert(gfx); | ||
| 147 | assert(model); | ||
| 148 | const sgTriIdx* tris = (const sgTriIdx*)(model->data + model->offsetTris); | ||
| 149 | const sgVec3* positions = (const sgVec3*)(model->data + model->offsetPositions); | ||
| 150 | sgTrianglesIndexedNonUniform(gfx, model->numTris, tris, positions); | ||
| 151 | } | ||
| 152 | |||
| 153 | static void RenderModel(swgfx* gfx, const Model* model) { | ||
| 154 | assert(gfx); | ||
| 155 | assert(model); | ||
| 156 | switch (model->type) { | ||
| 157 | case ModelTypeIndexed: RenderIndexedModel(gfx, &model->indexed); break; | ||
| 158 | case ModelTypeFlat: /* TODO: Render flat models. */ break; | ||
| 159 | default: assert(false); break; | ||
| 160 | } | ||
| 161 | } | ||
| 162 | |||
| 163 | static void RenderTriangle2d(swgfx* gfx) { | ||
| 164 | assert(gfx); | ||
| 165 | const sgVec2 p0 = (sgVec2){20, 20}; | ||
| 166 | const sgVec2 p1 = (sgVec2){80, 20}; | ||
| 167 | const sgVec2 p2 = (sgVec2){50, 50}; | ||
| 168 | const sgTri2 tri = (sgTri2){p0, p1, p2}; | ||
| 169 | sgTriangles2(gfx, 1, &tri); | ||
| 170 | } | ||
| 171 | |||
| 172 | static void Checkerboard(swgfx* gfx, int width, int height) { | ||
| 173 | assert(gfx); | ||
| 174 | const sgPixel colour = (sgPixel){255, 0, 255, 255}; | ||
| 175 | for (int y = 0; y < height; ++y) { | ||
| 176 | for (int x = 0; x < width; ++x) { | ||
| 177 | if (((x ^ y) & 1) == 1) { | ||
| 178 | const sgVec2i position = (sgVec2i){x, y}; | ||
| 179 | sgPixels(gfx, 1, &position, colour); | ||
| 180 | } | ||
| 181 | } | ||
| 182 | } | ||
| 183 | } | ||
| 184 | |||
| 185 | static bool Render(State* state) { | ||
| 186 | assert(state); | ||
| 187 | assert(state->window); | ||
| 188 | assert(state->gfx); | ||
| 189 | |||
| 190 | // Locking/unlocking SDL software surfaces is not necessary. | ||
| 191 | // Probably also best to avoid SDL_BlitSurface(); it does pixel format | ||
| 192 | // conversion while blitting one pixel at a time. Instead, make the UI pixel | ||
| 193 | // format match the SDL window's and write to SDL's back buffer directly. | ||
| 194 | SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); | ||
| 195 | assert(window_surface); | ||
| 196 | |||
| 197 | // Until we make the window resizable, assert dimensions for safety. | ||
| 198 | assert(window_surface->w == WindowWidth); | ||
| 199 | assert(window_surface->h == WindowHeight); | ||
| 200 | |||
| 201 | #ifdef DEBUG_EVENT_LOOP | ||
| 202 | EVENT_LOOP_PRINT( | ||
| 203 | "Render: window surface: %dx%d\n", | ||
| 204 | window_surface->w, window_surface->h); | ||
| 205 | #endif | ||
| 206 | |||
| 207 | const Camera* cam = &state->camera; | ||
| 208 | |||
| 209 | sgColourBuffer(state->gfx, BufferDims, state->colour); | ||
| 210 | sgClear(state->gfx); | ||
| 211 | sgViewport(state->gfx, 0, 0, BufferWidth, BufferHeight); | ||
| 212 | sgCheck(state->gfx); | ||
| 213 | // TODO: For easier debugging, overlay the checkerboard on top of the | ||
| 214 | // other rendered items with alpha blending. | ||
| 215 | //Checkerboard(state->gfx, BufferWidth, BufferHeight); | ||
| 216 | //RenderTriangle2d(state->gfx); | ||
| 217 | sgModelId(state->gfx); | ||
| 218 | sgView(state->gfx, SgVec3FromMathVec3(cam->spatial.p), SgVec3FromMathVec3(cam->spatial.f)); | ||
| 219 | sgPerspective(state->gfx, cam->fovy, cam->aspect, cam->near, cam->far); | ||
| 220 | RenderModel(state->gfx, state->model); | ||
| 221 | /*sgIdx indices[3] = {0, 1, 2}; | ||
| 222 | sgVec3 positions[3] = { | ||
| 223 | (sgVec3){0, 0, 0}, | ||
| 224 | (sgVec3){5, 2, 0}, | ||
| 225 | (sgVec3){8, 8, 0}, | ||
| 226 | }; | ||
| 227 | sgTrianglesIndexed(state->gfx, 3, indices, positions);*/ | ||
| 228 | sgPresent(state->gfx, WindowDims, window_surface->pixels); | ||
| 229 | |||
| 230 | if (!SDL_UpdateWindowSurface(state->window)) { | ||
| 231 | return false; | ||
| 232 | } | ||
| 233 | |||
| 234 | return true; | ||
| 235 | } | ||
| 236 | |||
| 237 | static bool Resize(State* state) { | ||
| 238 | assert(state); | ||
| 239 | |||
| 240 | // int width, height; | ||
| 241 | // SDL_GetWindowSize(state->window, &width, &height); | ||
| 242 | |||
| 243 | const SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); | ||
| 244 | if (!window_surface) { | ||
| 245 | return false; | ||
| 246 | } | ||
| 247 | const int width = window_surface->w; | ||
| 248 | const int height = window_surface->h; | ||
| 249 | |||
| 250 | EVENT_LOOP_PRINT("Resize: %dx%d\n", width, height); | ||
| 251 | |||
| 252 | return true; | ||
| 253 | } | ||
| 254 | |||
| 255 | static bool Initialize(State* state) { | ||
| 256 | assert(state); | ||
| 257 | |||
| 258 | if ((state->window = SDL_CreateWindow( | ||
| 259 | WindowTitle, | ||
| 260 | WindowWidth, | ||
| 261 | WindowHeight, | ||
| 262 | 0)) == NULL) { | ||
| 263 | fprintf(stderr, "SDL_CreateWindow failed\n"); | ||
| 264 | return false; | ||
| 265 | } | ||
| 266 | |||
| 267 | if (!(state->gfx = sgNew())) { | ||
| 268 | fprintf(stderr, "sgNew failed\n"); | ||
| 269 | return false; | ||
| 270 | } | ||
| 271 | |||
| 272 | if (!(state->colour = SG_ALIGN_ALLOC(BufferWidth * BufferHeight, sgPixel))) { | ||
| 273 | fprintf(stderr, "Failed to allocate colour buffer\n"); | ||
| 274 | return false; | ||
| 275 | } | ||
| 276 | |||
| 277 | sgColourBuffer(state->gfx, BufferDims, state->colour); | ||
| 278 | |||
| 279 | const char* model_path = "/home/jeanne/blender/box.mdl"; | ||
| 280 | if (!(state->model = read_file(model_path))) { | ||
| 281 | fprintf(stderr, "Failed to load model: [%s]\n", model_path); | ||
| 282 | return false; | ||
| 283 | } | ||
| 284 | |||
| 285 | Camera* camera = &state->camera; | ||
| 286 | camera->fovy = Fovy; | ||
| 287 | camera->aspect = Aspect; | ||
| 288 | camera->near = 0.1f; | ||
| 289 | camera->far = 1000.f; | ||
| 290 | camera->spatial = spatial3_make(); | ||
| 291 | camera->spatial.p = vec3_make(0, 1, 10); | ||
| 292 | |||
| 293 | state->camera_controller = (CameraController){ | ||
| 294 | .speed = 7.f, | ||
| 295 | .rotation_speed = (R)(90 * TO_RAD), | ||
| 296 | }; | ||
| 297 | |||
| 298 | state->last_tick = SDL_GetPerformanceCounter(); | ||
| 299 | |||
| 300 | return true; | ||
| 301 | } | ||
| 302 | |||
| 303 | static void Shutdown(State* state) { | ||
| 304 | assert(state); | ||
| 305 | |||
| 306 | if (state->model) { | ||
| 307 | free(state->model); | ||
| 308 | state->model = nullptr; | ||
| 309 | } | ||
| 310 | |||
| 311 | if (state->colour) { | ||
| 312 | SG_FREE(&state->colour); | ||
| 313 | } | ||
| 314 | |||
| 315 | if (state->gfx) { | ||
| 316 | sgDel(&state->gfx); | ||
| 317 | } | ||
| 318 | |||
| 319 | if (state->window) { | ||
| 320 | SDL_DestroyWindow(state->window); | ||
| 321 | state->window = nullptr; | ||
| 322 | } | ||
| 323 | } | ||
| 324 | |||
| 325 | static R GetDeltaTime(State* state) { | ||
| 326 | assert(state); | ||
| 327 | constexpr Uint64 NS_IN_SEC = 1'000'000'000; | ||
| 328 | const Uint64 this_tick = SDL_GetPerformanceCounter(); | ||
| 329 | const Uint64 freq = SDL_GetPerformanceFrequency(); | ||
| 330 | const Uint64 elapsed_ns = (this_tick - state->last_tick) * NS_IN_SEC / freq; | ||
| 331 | const R elapsed_sec = (R)elapsed_ns / (R)NS_IN_SEC; | ||
| 332 | state->last_tick = this_tick; | ||
| 333 | return elapsed_sec; | ||
| 334 | } | ||
| 335 | |||
| 336 | int main() { | ||
| 337 | bool success = false; | ||
| 338 | // Controls whether we should keep running. | ||
| 339 | bool running = true; | ||
| 340 | |||
| 341 | State state = {0}; | ||
| 342 | |||
| 343 | if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { | ||
| 344 | fprintf(stderr, "SDL_Init failed\n"); | ||
| 345 | goto cleanup; | ||
| 346 | } | ||
| 347 | |||
| 348 | if (!Initialize(&state)) { | ||
| 349 | fprintf(stderr, "Initialization failed\n"); | ||
| 350 | goto cleanup; | ||
| 351 | } | ||
| 352 | |||
| 353 | if (!Resize(&state)) { | ||
| 354 | goto cleanup; | ||
| 355 | } | ||
| 356 | |||
| 357 | success = true; | ||
| 358 | |||
| 359 | while (success && running) { | ||
| 360 | EVENT_LOOP_PRINT("loop\n"); | ||
| 361 | |||
| 362 | // Handle events. | ||
| 363 | SDL_Event event = {0}; | ||
| 364 | while (SDL_PollEvent(&event)) { | ||
| 365 | if ((event.type == SDL_EVENT_QUIT) || | ||
| 366 | (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)) { | ||
| 367 | running = false; | ||
| 368 | break; | ||
| 369 | } else if ((event.window.type == SDL_EVENT_WINDOW_DISPLAY_CHANGED) || | ||
| 370 | (event.window.type == SDL_EVENT_WINDOW_RESIZED) || | ||
| 371 | (event.window.type == SDL_EVENT_WINDOW_MOVED)) { | ||
| 372 | // When the window is maximized, an SDL_WINDOWEVENT_MOVED comes in | ||
| 373 | // before an SDL_WINDOWEVENT_SIZE_CHANGED with the window already | ||
| 374 | // resized. This is unfortunate because we cannot rely on the latter | ||
| 375 | // event alone to handle resizing. | ||
| 376 | if (!Resize(&state)) { | ||
| 377 | success = false; | ||
| 378 | break; | ||
| 379 | } | ||
| 380 | } else if (event.type == SDL_EVENT_KEY_DOWN) { | ||
| 381 | if (event.key.mod & SDL_KMOD_LCTRL) { | ||
| 382 | switch (event.key.key) { | ||
| 383 | // Exit. | ||
| 384 | case SDLK_C: | ||
| 385 | case SDLK_D: | ||
| 386 | running = false; | ||
| 387 | break; | ||
| 388 | default: | ||
| 389 | break; | ||
| 390 | } | ||
| 391 | } | ||
| 392 | } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { | ||
| 393 | // | ||
| 394 | } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { | ||
| 395 | // | ||
| 396 | } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { | ||
| 397 | // | ||
| 398 | } else { | ||
| 399 | EVENT_LOOP_PRINT("event.window.type = %d\n", event.window.type); | ||
| 400 | } | ||
| 401 | } // events | ||
| 402 | |||
| 403 | // Draw and update if needed. | ||
| 404 | if (success && running) { | ||
| 405 | const R dt = GetDeltaTime(&state); | ||
| 406 | success = Update(&state, dt); | ||
| 407 | } | ||
| 408 | if (success && running) { | ||
| 409 | success = Render(&state); | ||
| 410 | } | ||
| 411 | } // loop | ||
| 412 | |||
| 413 | cleanup: | ||
| 414 | if (!success) { | ||
| 415 | fprintf(stderr, "%s\n", SDL_GetError()); | ||
| 416 | } | ||
| 417 | Shutdown(&state); | ||
| 418 | SDL_Quit(); | ||
| 419 | return success ? 0 : 1; | ||
| 420 | } | ||
