#include #include // TODO: Update math/camera to explicitly expose ortho/perspective camera parameters, // not just a baked matrix. //#include #include #include #include #include #include #include #include #include static constexpr int BufferWidth = 160; static constexpr int BufferHeight = 120; static constexpr sgVec2i BufferDims = (sgVec2i){.x = BufferWidth, .y = BufferHeight}; static constexpr R Aspect = (R)BufferWidth / (R)BufferHeight; static const char* WindowTitle = "GAME"; // Window dimensions must be an integer scaling of buffer dimensions. // TODO: Make window dimensions a function of DPI. static constexpr int WindowWidth = 640; static constexpr int WindowHeight = 480; static constexpr sgVec2i WindowDims = (sgVec2i){.x = WindowWidth, .y = WindowHeight}; static const R Fovy = (R)(90 * TO_RAD); #define DEBUG_EVENT_LOOP 1 #ifdef DEBUG_EVENT_LOOP #define EVENT_LOOP_PRINT printf #else #define EVENT_LOOP_PRINT(...) #endif // DEBUG_EVENT_LOOP typedef struct CameraCommand { bool CameraMoveLeft : 1; bool CameraMoveRight : 1; bool CameraMoveForward : 1; bool CameraMoveBackward : 1; bool CameraSlow : 1; // When true, move more slowly. bool CameraRotate : 1; // When true, subsequent mouse movements cause // the camera to rotate. } CameraCommand; typedef struct CameraController { R speed; // Camera movement speed. R rotation_speed; // Controls the degree with which mouse movements rotate // the camera. vec2 prev_mouse_position; // Mouse position in the previous frame. } CameraController; typedef struct Camera { Spatial3 spatial; R fovy; R aspect; R near; R far; } Camera; typedef struct State { SDL_Window* window; swgfx* gfx; sgPixel* colour; Model* model; Camera camera; CameraController camera_controller; Uint64 last_tick; } State; static sgVec3 SgVec3FromMathVec3(vec3 v) { return (sgVec3){v.x, v.y, v.z}; } static CameraCommand CameraCommandFromInput( const bool* keyboard_state, const SDL_MouseButtonFlags mouse_flags) { assert(keyboard_state); if (keyboard_state[SDL_SCANCODE_W]) { printf("W: %d\n", keyboard_state[SDL_SCANCODE_W]); } return (CameraCommand){ .CameraMoveLeft = keyboard_state[SDL_SCANCODE_A], .CameraMoveRight = keyboard_state[SDL_SCANCODE_D], .CameraMoveForward = keyboard_state[SDL_SCANCODE_W], .CameraMoveBackward = keyboard_state[SDL_SCANCODE_S], .CameraSlow = keyboard_state[SDL_SCANCODE_LSHIFT], .CameraRotate = mouse_flags & SDL_BUTTON_MASK(SDL_BUTTON_LEFT), }; } static void UpdateCamera( CameraController* controller, R dt, vec2 mouse_position, CameraCommand command, Camera* camera) { assert(controller); assert(camera); Spatial3* cam = &camera->spatial; // 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 R speed_factor = command.CameraSlow ? 0.3f : 1.f; const vec2 translation = vec2_scale( vec2_normalize(vec2_make(move_x, move_y)), controller->speed * speed_factor * dt); spatial3_move_right(cam, translation.x); spatial3_move_forwards(cam, translation.y); // Rotation. if (command.CameraRotate) { const vec2 mouse_delta = vec2_sub(mouse_position, controller->prev_mouse_position); const vec2 rotation = vec2_scale(mouse_delta, controller->rotation_speed * dt); spatial3_global_yaw(cam, -rotation.x); spatial3_pitch(cam, -rotation.y); } // Update controller state. controller->prev_mouse_position = mouse_position; } static bool Update(State* state, R dt) { assert(state); int num_keys = 0; const bool* keyboard_state = SDL_GetKeyboardState(&num_keys); vec2 mouse = {0}; const SDL_MouseButtonFlags mouse_flags = SDL_GetMouseState(&mouse.x, &mouse.y); const CameraCommand cmd = CameraCommandFromInput(keyboard_state, mouse_flags); UpdateCamera(&state->camera_controller, dt, mouse, cmd, &state->camera); return true; } static void RenderIndexedModel(swgfx* gfx, const IndexedModel* model) { assert(gfx); assert(model); const sgTriIdx* tris = (const sgTriIdx*)(model->data + model->offsetTris); const sgVec3* positions = (const sgVec3*)(model->data + model->offsetPositions); sgTrianglesIndexedNonUniform(gfx, model->numTris, tris, positions); } static void RenderModel(swgfx* gfx, const Model* model) { assert(gfx); assert(model); switch (model->type) { case ModelTypeIndexed: RenderIndexedModel(gfx, &model->indexed); break; case ModelTypeFlat: /* TODO: Render flat models. */ break; default: assert(false); break; } } static void RenderTriangle2d(swgfx* gfx) { assert(gfx); const sgVec2 p0 = (sgVec2){20, 20}; const sgVec2 p1 = (sgVec2){80, 20}; const sgVec2 p2 = (sgVec2){50, 50}; const sgTri2 tri = (sgTri2){p0, p1, p2}; sgTriangles2(gfx, 1, &tri); } static void Checkerboard(swgfx* gfx, int width, int height) { assert(gfx); const sgPixel colour = (sgPixel){255, 0, 255, 255}; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { if (((x ^ y) & 1) == 1) { const sgVec2i position = (sgVec2i){x, y}; sgPixels(gfx, 1, &position, colour); } } } } static bool Render(State* state) { assert(state); assert(state->window); assert(state->gfx); // Locking/unlocking SDL software surfaces is not necessary. // Probably also best to avoid SDL_BlitSurface(); it does pixel format // conversion while blitting one pixel at a time. Instead, make the UI pixel // format match the SDL window's and write to SDL's back buffer directly. SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); assert(window_surface); // Until we make the window resizable, assert dimensions for safety. assert(window_surface->w == WindowWidth); assert(window_surface->h == WindowHeight); #ifdef DEBUG_EVENT_LOOP EVENT_LOOP_PRINT( "Render: window surface: %dx%d\n", window_surface->w, window_surface->h); #endif const Camera* cam = &state->camera; sgColourBuffer(state->gfx, BufferDims, state->colour); sgClear(state->gfx); sgViewport(state->gfx, 0, 0, BufferWidth, BufferHeight); sgCheck(state->gfx); // TODO: For easier debugging, overlay the checkerboard on top of the // other rendered items with alpha blending. //Checkerboard(state->gfx, BufferWidth, BufferHeight); //RenderTriangle2d(state->gfx); sgModelId(state->gfx); sgView(state->gfx, SgVec3FromMathVec3(cam->spatial.p), SgVec3FromMathVec3(cam->spatial.f)); sgPerspective(state->gfx, cam->fovy, cam->aspect, cam->near, cam->far); RenderModel(state->gfx, state->model); /*sgIdx indices[3] = {0, 1, 2}; sgVec3 positions[3] = { (sgVec3){0, 0, 0}, (sgVec3){5, 2, 0}, (sgVec3){8, 8, 0}, }; sgTrianglesIndexed(state->gfx, 3, indices, positions);*/ sgPresent(state->gfx, WindowDims, window_surface->pixels); if (!SDL_UpdateWindowSurface(state->window)) { return false; } return true; } static bool Resize(State* state) { assert(state); // int width, height; // SDL_GetWindowSize(state->window, &width, &height); const SDL_Surface* window_surface = SDL_GetWindowSurface(state->window); if (!window_surface) { return false; } const int width = window_surface->w; const int height = window_surface->h; EVENT_LOOP_PRINT("Resize: %dx%d\n", width, height); return true; } static bool Initialize(State* state) { assert(state); if ((state->window = SDL_CreateWindow( WindowTitle, WindowWidth, WindowHeight, 0)) == NULL) { fprintf(stderr, "SDL_CreateWindow failed\n"); return false; } if (!(state->gfx = sgNew())) { fprintf(stderr, "sgNew failed\n"); return false; } if (!(state->colour = SG_ALIGN_ALLOC(BufferWidth * BufferHeight, sgPixel))) { fprintf(stderr, "Failed to allocate colour buffer\n"); return false; } sgColourBuffer(state->gfx, BufferDims, state->colour); const char* model_path = "/home/jeanne/blender/box.mdl"; if (!(state->model = read_file(model_path))) { fprintf(stderr, "Failed to load model: [%s]\n", model_path); return false; } Camera* camera = &state->camera; camera->fovy = Fovy; camera->aspect = Aspect; camera->near = 0.1f; camera->far = 1000.f; camera->spatial = spatial3_make(); camera->spatial.p = vec3_make(0, 1, 10); state->camera_controller = (CameraController){ .speed = 7.f, .rotation_speed = (R)(90 * TO_RAD), }; state->last_tick = SDL_GetPerformanceCounter(); return true; } static void Shutdown(State* state) { assert(state); if (state->model) { free(state->model); state->model = nullptr; } if (state->colour) { SG_FREE(&state->colour); } if (state->gfx) { sgDel(&state->gfx); } if (state->window) { SDL_DestroyWindow(state->window); state->window = nullptr; } } static R GetDeltaTime(State* state) { assert(state); constexpr Uint64 NS_IN_SEC = 1'000'000'000; const Uint64 this_tick = SDL_GetPerformanceCounter(); const Uint64 freq = SDL_GetPerformanceFrequency(); const Uint64 elapsed_ns = (this_tick - state->last_tick) * NS_IN_SEC / freq; const R elapsed_sec = (R)elapsed_ns / (R)NS_IN_SEC; state->last_tick = this_tick; return elapsed_sec; } int main() { bool success = false; // Controls whether we should keep running. bool running = true; State state = {0}; if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { fprintf(stderr, "SDL_Init failed\n"); goto cleanup; } if (!Initialize(&state)) { fprintf(stderr, "Initialization failed\n"); goto cleanup; } if (!Resize(&state)) { goto cleanup; } success = true; while (success && running) { EVENT_LOOP_PRINT("loop\n"); // Handle events. SDL_Event event = {0}; while (SDL_PollEvent(&event)) { if ((event.type == SDL_EVENT_QUIT) || (event.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED)) { running = false; break; } else if ((event.window.type == SDL_EVENT_WINDOW_DISPLAY_CHANGED) || (event.window.type == SDL_EVENT_WINDOW_RESIZED) || (event.window.type == SDL_EVENT_WINDOW_MOVED)) { // When the window is maximized, an SDL_WINDOWEVENT_MOVED comes in // before an SDL_WINDOWEVENT_SIZE_CHANGED with the window already // resized. This is unfortunate because we cannot rely on the latter // event alone to handle resizing. if (!Resize(&state)) { success = false; break; } } else if (event.type == SDL_EVENT_KEY_DOWN) { if (event.key.mod & SDL_KMOD_LCTRL) { switch (event.key.key) { // Exit. case SDLK_C: case SDLK_D: running = false; break; default: break; } } } else if (event.type == SDL_EVENT_MOUSE_BUTTON_DOWN) { // } else if (event.type == SDL_EVENT_MOUSE_BUTTON_UP) { // } else if (event.type == SDL_EVENT_MOUSE_WHEEL) { // } else { EVENT_LOOP_PRINT("event.window.type = %d\n", event.window.type); } } // events // Draw and update if needed. if (success && running) { const R dt = GetDeltaTime(&state); success = Update(&state, dt); } if (success && running) { success = Render(&state); } } // loop cleanup: if (!success) { fprintf(stderr, "%s\n", SDL_GetError()); } Shutdown(&state); SDL_Quit(); return success ? 0 : 1; }