diff --git a/dbc.cpp b/dbc.cpp new file mode 100644 index 0000000..c25d32a --- /dev/null +++ b/dbc.cpp @@ -0,0 +1,40 @@ +#include "dbc.hpp" + +void dbc::log(const string &message) { + fmt::print("{}\n", message); +} + +void dbc::sentinel(const string &message) { + string err = fmt::format("[SENTINEL!] {}\n", message); + throw dbc::SentinelError{err}; +} + +void dbc::pre(const string &message, bool test) { + if(!test) { + string err = fmt::format("[PRE!] {}\n", message); + throw dbc::PreCondError{err}; + } +} + +void dbc::pre(const string &message, std::function tester) { + dbc::pre(message, tester()); +} + +void dbc::post(const string &message, bool test) { + if(!test) { + string err = fmt::format("[POST!] {}\n", message); + throw dbc::PostCondError{err}; + } +} + +void dbc::post(const string &message, std::function tester) { + dbc::post(message, tester()); +} + +void dbc::check(bool test, const string &message) { + if(!test) { + string err = fmt::format("[CHECK!] {}\n", message); + fmt::println("{}", err); + throw dbc::CheckError{err}; + } +} diff --git a/dbc.hpp b/dbc.hpp new file mode 100644 index 0000000..919d729 --- /dev/null +++ b/dbc.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include +#include +#include + +using std::string; + +namespace dbc { + class Error { + public: + const string message; + Error(string m) : message{m} {} + Error(const char *m) : message{m} {} + }; + + class CheckError : public Error {}; + class SentinelError : public Error {}; + class PreCondError : public Error {}; + class PostCondError : public Error {}; + + void log(const string &message); + void sentinel(const string &message); + void pre(const string &message, bool test); + void pre(const string &message, std::function tester); + void post(const string &message, bool test); + void post(const string &message, std::function tester); + void check(bool test, const string &message); +} diff --git a/main.cpp b/main.cpp deleted file mode 100644 index 48a9543..0000000 --- a/main.cpp +++ /dev/null @@ -1,34 +0,0 @@ -/* -Copyright (c) 2004, Lode Vandevenne -All rights reserved. -*/ - -#include -#include -#include -#include - -#include "quickcg.h" -using namespace QuickCG; -using namespace std; - -//place the example code below here: - -int main(int argc, char *argv[]) -{ - screen(256, 256, 0, "Small Test Script"); - for (int x = 0; x < w; x++) - for (int y = 0; y < h; y++) - { - pset(x, y, ColorRGBA(x, y, 128, 255)); - } - print("Hello, world!", 8, 8); - - std::string test; - test.resize(20); - - redraw(); - sleep(); - - return 0; -} diff --git a/matrix.cpp b/matrix.cpp new file mode 100644 index 0000000..85301ee --- /dev/null +++ b/matrix.cpp @@ -0,0 +1,94 @@ +#include "matrix.hpp" +#include "dbc.hpp" +#include +#include +#include + +using namespace fmt; +using std::min, std::max; + +namespace matrix { + + flood::flood(Matrix &mat, Point start, int old_val, int new_val) : + mat(mat), start(start), old_val(old_val), new_val(new_val), + x(start.x), y(start.y), dirs{mat, start.x, start.y} + { + dbc::check(old_val != new_val, "what you doing?"); + current_loc = start; + q.push(start); + } + + bool flood::next() { + if(!q.empty()) { + if(!dirs.next()) { + // box is done reset it + auto current_loc = q.front(); + q.pop(); + + dirs = matrix::compass{mat, current_loc.x, current_loc.y}; + dirs.next(); + } + + // get the next thing + if(mat[dirs.y][dirs.x] <= old_val) { + mat[dirs.y][dirs.x] = new_val; + x = dirs.x; + y = dirs.y; + + q.push({.x=dirs.x, .y=dirs.y}); + } + + return true; + } else { + return false; + } + } + + line::line(Point start, Point end) : + x(start.x), y(start.y), + x1(end.x), y1(end.y) + { + dx = std::abs(x1 - x); + sx = x < x1 ? 1 : -1; + dy = std::abs(y1 - y) * -1; + sy = y < y1 ? 1 : -1; + error = dx + dy; + } + + bool line::next() { + if(x != x1 || y != y1) { + int e2 = 2 * error; + + if(e2 >= dy) { + error = error + dy; + x = x + sx; + } + + if(e2 <= dx) { + error = error + dx; + y = y + sy; + } + return true; + } else { + return false; + } + } + + void dump(const std::string &msg, Matrix &map, int show_x, int show_y) { + println("----------------- {}", msg); + + for(each_row it{map}; it.next();) { + int cell = map[it.y][it.x]; + + if(int(it.x) == show_x && int(it.y) == show_y) { + print("{:x}<", cell); + } else if(cell > 15) { + print("* "); + } else { + print("{:x} ", cell); + } + + if(it.row) print("\n"); + } + } +} diff --git a/matrix.hpp b/matrix.hpp new file mode 100644 index 0000000..e9266fa --- /dev/null +++ b/matrix.hpp @@ -0,0 +1,305 @@ +#pragma once +#include +#include +#include +#include +#include +#include "point.hpp" + +namespace matrix { + using std::vector, std::queue, std::array; + using std::min, std::max, std::floor; + + typedef vector Row; + typedef vector Matrix; + + /* + * Just a quick thing to reset a matrix to a value. + */ + template + inline void assign(MAT &out, VAL new_value) { + for(auto &row : out) { + row.assign(row.size(), new_value); + } + } + + template + inline bool inbounds(MAT &mat, size_t x, size_t y) { + // since Point.x and Point.y are size_t any negatives are massive + bool res = (y < mat.size()) && (x < mat[0].size()); + return res; + } + + template + inline size_t width(MAT &mat) { + return mat[0].size(); + } + + template + inline size_t height(MAT &mat) { + return mat.size(); + } + + inline size_t next_x(size_t x, size_t width) { + return (x + 1) * ((x + 1) < width); + } + + inline size_t next_y(size_t x, size_t y) { + return y + (x == 0); + } + + inline bool at_end(size_t y, size_t height) { + return y < height; + } + + inline bool end_row(size_t x, size_t width) { + return x == width - 1; + } + + void dump(const std::string &msg, Matrix &map, int show_x=-1, int show_y=-1); + + template + struct each_cell_t { + size_t x = ~0; + size_t y = ~0; + size_t width = 0; + size_t height = 0; + + each_cell_t(MAT &mat) + { + height = matrix::height(mat); + width = matrix::width(mat); + } + + bool next() { + x = next_x(x, width); + y = next_y(x, y); + return at_end(y, height); + } + }; + + template + struct viewport_t { + Point start; + // this is the point in the map + size_t x; + size_t y; + // this is the point inside the box, start at 0 + size_t view_x = ~0; + size_t view_y = ~0; + // viewport width/height + size_t width; + size_t height; + + viewport_t(MAT &mat, Point start, int max_x, int max_y) : + start(start), + x(start.x-1), + y(start.y-1) + { + width = std::min(size_t(max_x), matrix::width(mat) - start.x); + height = std::min(size_t(max_y), matrix::height(mat) - start.y); + fmt::println("viewport_t max_x, max_y {},{} vs matrix {},{}, x={}, y={}", + max_x, max_y, matrix::width(mat), matrix::height(mat), x, y); + } + + bool next() { + y = next_y(x, y); + x = next_x(x, width); + view_x = next_x(view_x, width); + view_y = next_y(view_x, view_y); + return at_end(y, height); + } + }; + + using viewport = viewport_t; + + using each_cell = each_cell_t; + + template + struct each_row_t { + size_t x = ~0; + size_t y = ~0; + size_t width = 0; + size_t height = 0; + bool row = false; + + each_row_t(MAT &mat) { + height = matrix::height(mat); + width = matrix::width(mat); + } + + bool next() { + x = next_x(x, width); + y = next_y(x, y); + row = end_row(x, width); + return at_end(y, height); + } + }; + + using each_row = each_row_t; + + template + struct box_t { + size_t from_x; + size_t from_y; + size_t x = 0; // these are set in constructor + size_t y = 0; // again, no fancy ~ trick needed + size_t left = 0; + size_t top = 0; + size_t right = 0; + size_t bottom = 0; + + box_t(MAT &mat, size_t at_x, size_t at_y, size_t size) : + from_x(at_x), from_y(at_y) + { + size_t h = matrix::height(mat); + size_t w = matrix::width(mat); + + // keeps it from going below zero + // need extra -1 to compensate for the first next() + left = max(from_x, size) - size; + x = left - 1; // must be -1 for next() + // keeps it from going above width + right = min(from_x + size + 1, w); + + // same for these two + top = max(from_y, size) - size; + y = top - (left == 0); + bottom = min(from_y + size + 1, h); + } + + bool next() { + // calc next but allow to go to 0 for next + x = next_x(x, right); + // x will go to 0, which signals new line + y = next_y(x, y); // this must go here + // if x==0 then this moves it to min_x + x = max(x, left); + // and done + + return at_end(y, bottom); + } + + float distance() { + int dx = from_x - x; + int dy = from_y - y; + + return sqrt((dx * dx) + (dy * dy)); + } + }; + + using box = box_t; + + template + struct compass_t { + size_t x = 0; // these are set in constructor + size_t y = 0; // again, no fancy ~ trick needed + array x_dirs{0, 1, 0, -1}; + array y_dirs{-1, 0, 1, 0}; + size_t max_dirs=0; + size_t dir = ~0; + + compass_t(MAT &mat, size_t x, size_t y) : + x(x), y(y) + { + array x_in{0, 1, 0, -1}; + array y_in{-1, 0, 1, 0}; + + for(size_t i = 0; i < 4; i++) { + int nx = x + x_in[i]; + int ny = y + y_in[i]; + if(matrix::inbounds(mat, nx, ny)) { + x_dirs[max_dirs] = nx; + y_dirs[max_dirs] = ny; + max_dirs++; + } + } + } + + bool next() { + dir++; + if(dir < max_dirs) { + x = x_dirs[dir]; + y = y_dirs[dir]; + return true; + } else { + return false; + } + } + }; + + using compass = compass_t; + + struct flood { + Matrix &mat; + Point start; + int old_val; + int new_val; + queue q; + Point current_loc; + int x; + int y; + matrix::compass dirs; + + flood(Matrix &mat, Point start, int old_val, int new_val); + bool next(); + bool next_working(); + }; + + struct line { + int x; + int y; + int x1; + int y1; + int sx; + int sy; + int dx; + int dy; + int error; + + line(Point start, Point end); + bool next(); + }; + + template + struct circle_t { + float center_x; + float center_y; + float radius = 0.0f; + int y = 0; + int dx = 0; + int dy = 0; + int left = 0; + int right = 0; + int top = 0; + int bottom = 0; + int width = 0; + int height = 0; + + circle_t(MAT &mat, Point center, float radius) : + center_x(center.x), center_y(center.y), radius(radius) + { + width = matrix::width(mat); + height = matrix::height(mat); + top = max(int(floor(center_y - radius)), 0); + bottom = min(int(floor(center_y + radius)), height - 1); + + y = top; + } + + bool next() { + y++; + if(y <= bottom) { + dy = y - center_y; + dx = floor(sqrt(radius * radius - dy * dy)); + left = max(0, int(center_x) - dx); + right = min(width, int(center_x) + dx + 1); + return true; + } else { + return false; + } + } + }; + + using circle = circle_t; +} diff --git a/meson.build b/meson.build index b82f73b..48dc97f 100644 --- a/meson.build +++ b/meson.build @@ -1,27 +1,25 @@ -project('lodecaster', 'cpp', - default_options: ['cpp_std=c++20']) +project('raycaster', 'cpp', + default_options: ['cpp_std=c++20']) catch2 = dependency('catch2-with-main') fmt = dependency('fmt') json = dependency('nlohmann_json') -sdl2 = dependency('sdl2') -sdl2_main = dependency('sdl2main') +sfml = dependency('sfml') dependencies = [ - sdl2, sdl2_main, - fmt, json -] + fmt, json, sfml + ] executable('runtests', [ - 'quickcg.cpp', - 'main.cpp', - ], - win_subsystem: 'windows', - dependencies: dependencies) + 'dbc.cpp', + 'matrix.cpp', + 'tests/base.cpp', + ], + dependencies: dependencies + [catch2]) -executable('lodecaster', [ - 'quickcg.cpp', - 'raycaster_flat.cpp', - ], - win_subsystem: 'windows', - dependencies: dependencies) +executable('raycaster', [ + 'dbc.cpp', + 'matrix.cpp', + 'raycaster.cpp', + ], + dependencies: dependencies) diff --git a/point.hpp b/point.hpp new file mode 100644 index 0000000..42ed9db --- /dev/null +++ b/point.hpp @@ -0,0 +1,19 @@ +#pragma once +#include + +struct Point { + size_t x = 0; + size_t y = 0; + + bool operator==(const Point& other) const { + return other.x == x && other.y == y; + } +}; + +typedef std::vector PointList; + +struct PointHash { + size_t operator()(const Point& p) const { + return std::hash()(p.x) ^ std::hash()(p.y); + } +}; diff --git a/pycaster.py b/pycaster.py new file mode 100644 index 0000000..d130d98 --- /dev/null +++ b/pycaster.py @@ -0,0 +1,126 @@ +import pygame +import sys +import math + +SCREEN_HEIGHT=480 +SCREEN_WIDTH=SCREEN_HEIGHT * 2 +MAP_SIZE=8 +TILE_SIZE=int((SCREEN_WIDTH / 2) / MAP_SIZE) +FOV=math.pi / 3 +HALF_FOV = FOV / 2 +CASTED_RAYS=30 +STEP_ANGLE = FOV / CASTED_RAYS +MAX_DEPTH = int(MAP_SIZE * TILE_SIZE) +SCALE = (SCREEN_WIDTH / 2) / CASTED_RAYS + + +player_x = (SCREEN_WIDTH/2)/2 +player_y = (SCREEN_WIDTH/2)/2 +player_angle = math.pi + +MAP = ('########' + '# # #' + '# # ###' + '# #' + '## #' + '# ### #' + '# # #' + '########') + +pygame.init() +win = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) +pygame.display.set_caption("Ray-Casting") +clock = pygame.time.Clock() + +def draw_map(): + light_grey = (191, 191, 191) + dark_grey = (65,65,65) + + for i in range(MAP_SIZE): + for j in range(MAP_SIZE): + square = i * MAP_SIZE + j + + pygame.draw.rect(win, + light_grey if MAP[square] == '#' else dark_grey, + (j * TILE_SIZE, i * TILE_SIZE, TILE_SIZE -1, TILE_SIZE - 1)) + +def ray_casting(): + # left angle of FOV + start_angle = player_angle - HALF_FOV + + for ray in range(CASTED_RAYS): + for depth in range(1,MAX_DEPTH): + target_x = player_x - math.sin(start_angle) * depth + target_y = player_y + math.cos(start_angle) * depth + col = int(target_x / TILE_SIZE) + row = int(target_y / TILE_SIZE) + square = row * MAP_SIZE + col + + if MAP[square] == '#': + pygame.draw.rect(win, + (195, 137, 38), + (col * TILE_SIZE, + row * TILE_SIZE, + TILE_SIZE -1, TILE_SIZE-1)) + + pygame.draw.line(win, (233, 166, 49), + (player_x, player_y), + (target_x, target_y)) + + # wall shading + color = 255 / (1 + depth * depth * 0.0001) + + # fix fish eye effect + depth *= math.cos(player_angle - start_angle) + + # calculate wall height + wall_height = 21000 / (depth) + + if wall_height > SCREEN_HEIGHT: + wall_height = SCREEN_HEIGHT + + pygame.draw.rect(win, + (color, color, color), + (SCREEN_HEIGHT + ray * SCALE, + (SCREEN_HEIGHT / 2) - wall_height/2, + SCALE, wall_height)) + + break + + start_angle += STEP_ANGLE + +while True: + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit(0) + + # update 2d background + pygame.draw.rect(win, (0,0,0), (0, 0, SCREEN_HEIGHT, SCREEN_HEIGHT)) + + # update 3d background + pygame.draw.rect(win, (100, 100, 100), (480, SCREEN_HEIGHT / 2, SCREEN_HEIGHT, SCREEN_HEIGHT)) + pygame.draw.rect(win, (200, 200, 200), (480, -SCREEN_HEIGHT / 2, SCREEN_HEIGHT, SCREEN_HEIGHT)) + + draw_map() + ray_casting() + + keys = pygame.key.get_pressed() + if keys[pygame.K_LEFT]: + # working with radians, not degrees + player_angle -= 0.1 + elif keys[pygame.K_RIGHT]: + player_angle += 0.1 + elif keys[pygame.K_UP]: + forward = True + player_x += -1 * math.sin(player_angle) * 5 + player_y += math.cos(player_angle) * 5 + elif keys[pygame.K_DOWN]: + forward = False + player_x -= -1 * math.sin(player_angle) * 5 + player_y -= math.cos(player_angle) * 5 + + # update the display + pygame.display.flip() + + clock.tick(30) diff --git a/raycaster.cpp b/raycaster.cpp new file mode 100644 index 0000000..cd50742 --- /dev/null +++ b/raycaster.cpp @@ -0,0 +1,102 @@ +#include +#include +#include +#include +#include "matrix.hpp" + +using matrix::Matrix; +using namespace fmt; + +const int SCREEN_HEIGHT=480; +const int SCREEN_WIDTH=SCREEN_HEIGHT * 2; +const int MAP_SIZE=8; +const int TILE_SIZE=(SCREEN_WIDTH/2) / MAP_SIZE; +const float FOV = std::numbers::pi / 3.0; +const float HALF_FOV = FOV / 2; +const int CASTED_RAYS=30; +const float STEP_ANGLE = FOV / CASTED_RAYS; +const int MAX_DEPTH = MAP_SIZE * TILE_SIZE; +const float SCALE = (SCREEN_WIDTH / 2) / CASTED_RAYS; + +Matrix MAP{ + {1,1,1,1,1,1,1,1}, + {1,0,1,0,0,0,0,1}, + {1,0,1,0,0,1,1,1}, + {1,0,0,0,0,0,0,1}, + {1,1,0,0,0,0,0,1}, + {1,0,0,1,1,1,0,1}, + {1,0,0,0,1,0,0,1}, + {1,1,1,1,1,1,1,1} +}; + +float player_x = SCREEN_WIDTH / 4; +float player_y = SCREEN_WIDTH / 4; +float player_angle = std::numbers::pi; + +void draw_map_rect(sf::RenderWindow &window, sf::Color color, int x, int y) { + sf::RectangleShape rect({TILE_SIZE-1, TILE_SIZE-1}); + rect.setFillColor(color); + rect.setPosition(x * TILE_SIZE, y * TILE_SIZE); + window.draw(rect); +} + +void draw_map(sf::RenderWindow &window, Matrix &map) { + sf::Color light_grey{191, 191, 191}; + sf::Color dark_grey{65,65,65}; + + for(size_t y = 0; y < matrix::height(map); y++) { + for(size_t x = 0; x < matrix::width(map); x++) { + draw_map_rect(window, map[y][x] == 1 ? light_grey : dark_grey, x, y); + } + } +} + +void draw_line(sf::RenderWindow &window, sf::Vector2f start, sf::Vector2f end) { + sf::Vertex line[] = { + sf::Vertex(start), + sf::Vertex(end) + }; + + window.draw(line, 2, sf::Lines); +} + +void ray_casting(sf::RenderWindow &window, Matrix& map) { + float start_angle = player_angle - HALF_FOV; + + for(int ray = 0; ray < CASTED_RAYS; ray++, start_angle += STEP_ANGLE) + { + for(int depth = 1; depth < MAX_DEPTH; depth++) { + float target_x = player_x - std::sin(start_angle) * depth; + float target_y = player_y + std::cos(start_angle) * depth; + + int col = int(target_x / TILE_SIZE); + int row = int(target_y / TILE_SIZE); + + if(map[row][col] == 1) { + draw_map_rect(window, {195, 137, 38}, col & TILE_SIZE, row * TILE_SIZE); + draw_line(window, {player_x, player_y}, {target_x, target_y}); + break; + } + } + } +} + +int main() { + sf::RenderWindow window(sf::VideoMode(SCREEN_WIDTH, SCREEN_HEIGHT), "Raycaster"); + + while(window.isOpen()) { + sf::Event event; + + draw_map(window, MAP); + ray_casting(window, MAP); + window.display(); + + while(window.pollEvent(event)) { + if(event.type == sf::Event::Closed) { + window.close(); + } + } + } + + return 0; +} diff --git a/quickcg.cpp b/scratchpad/quickcg.cpp similarity index 99% rename from quickcg.cpp rename to scratchpad/quickcg.cpp index 2102492..123e6d6 100644 --- a/quickcg.cpp +++ b/scratchpad/quickcg.cpp @@ -46,6 +46,7 @@ QuickCG can handle some things that standard C++ doesn't but that are commonly u #include #include #include +#include namespace QuickCG { @@ -65,7 +66,7 @@ namespace QuickCG SDL_Renderer* render; SDL_Texture* tex; SDL_PixelFormat *fmt; - const Uint8* inkeys; + const Uint8* inkeys = NULL; SDL_Event event = { 0 }; @@ -76,7 +77,9 @@ namespace QuickCG bool keyDown(int key) //this checks if the key is held down, returns true all the time until the key is up { - return (inkeys[key] != 0); + assert(inkeys != NULL && "inkeys is not initialized!"); + // return (inkeys[key] != 0); + return false; } bool keyPressed(int key) //this checks if the key is *just* pressed, returns true only once until the key is up again @@ -171,6 +174,8 @@ namespace QuickCG SDL_Quit(); std::exit(1); } + + inkeys = SDL_GetKeyboardState(NULL); } //Locks the screen diff --git a/quickcg.h b/scratchpad/quickcg.h similarity index 100% rename from quickcg.h rename to scratchpad/quickcg.h diff --git a/raycaster_flat.cpp b/scratchpad/raycaster_flat.cpp similarity index 82% rename from raycaster_flat.cpp rename to scratchpad/raycaster_flat.cpp index f052663..06a72bb 100644 --- a/raycaster_flat.cpp +++ b/scratchpad/raycaster_flat.cpp @@ -79,6 +79,7 @@ int main(int /*argc*/, char */*argv*/[]) double oldTime = 0; //time of previous frame screen(screenWidth, screenHeight, 0, "Raycaster"); + while(!done()) { for(int x = 0; x < w; x++) @@ -176,7 +177,7 @@ int main(int /*argc*/, char */*argv*/[]) if(drawEnd >= h) drawEnd = h - 1; //choose wall color - ColorRGB color; + ColorRGBA color; switch(worldMap[mapX][mapY]) { case 1: color = RGB_Red; break; //red @@ -198,45 +199,52 @@ int main(int /*argc*/, char */*argv*/[]) double frameTime = (time - oldTime) / 1000.0; //frameTime is the time this frame has taken, in seconds print(1.0 / frameTime); //FPS counter redraw(); - cls(); //speed modifiers double moveSpeed = frameTime * 5.0; //the constant value is in squares/second double rotSpeed = frameTime * 3.0; //the constant value is in radians/second - readKeys(); - //move forward if no wall in front of you - if(keyDown(SDLK_UP)) - { - if(worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed; - if(worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed; - } - //move backwards if no wall behind you - if(keyDown(SDLK_DOWN)) - { - if(worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed; - if(worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed; - } - //rotate to the right - if(keyDown(SDLK_RIGHT)) - { - //both camera direction and camera plane must be rotated - double oldDirX = dirX; - dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed); - dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed); - double oldPlaneX = planeX; - planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed); - planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed); - } - //rotate to the left - if(keyDown(SDLK_LEFT)) - { - //both camera direction and camera plane must be rotated - double oldDirX = dirX; - dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed); - dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed); - double oldPlaneX = planeX; - planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed); - planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed); + + SDL_Event event; + while(SDL_PollEvent(&event)) { + if(event.type != SDL_KEYDOWN) continue; + + cls(); + //move forward if no wall in front of you + if(event.key.keysym.sym == SDLK_UP) + { + if(worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed; + if(worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed; + } + //move backwards if no wall behind you + if(event.key.keysym.sym == SDLK_DOWN) + { + if(worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed; + if(worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed; + } + //rotate to the right + if(event.key.keysym.sym == SDLK_RIGHT) + { + //both camera direction and camera plane must be rotated + double oldDirX = dirX; + dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed); + dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed); + double oldPlaneX = planeX; + planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed); + planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed); + } + //rotate to the left + if(event.key.keysym.sym == SDLK_LEFT) + { + //both camera direction and camera plane must be rotated + double oldDirX = dirX; + dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed); + dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed); + double oldPlaneX = planeX; + planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed); + planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed); + } } } + + return 0; } diff --git a/scratchpad/raycaster_sprites.cpp b/scratchpad/raycaster_sprites.cpp new file mode 100644 index 0000000..8120204 --- /dev/null +++ b/scratchpad/raycaster_sprites.cpp @@ -0,0 +1,471 @@ +/* +Copyright (c) 2004-2020, Lode Vandevenne + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include + +#include "quickcg.h" +using namespace QuickCG; + +/* +g++ *.cpp -lSDL -O3 -W -Wall -ansi -pedantic +g++ *.cpp -lSDL +*/ + + +#define screenWidth 640 +#define screenHeight 480 +#define texWidth 64 // must be power of two +#define texHeight 64 // must be power of two +#define mapWidth 24 +#define mapHeight 24 + +int worldMap[mapWidth][mapHeight] = +{ + {8,8,8,8,8,8,8,8,8,8,8,4,4,6,4,4,6,4,6,4,4,4,6,4}, + {8,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,0,0,0,0,0,0,4}, + {8,0,3,3,0,0,0,0,0,8,8,4,0,0,0,0,0,0,0,0,0,0,0,6}, + {8,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,6}, + {8,0,3,3,0,0,0,0,0,8,8,4,0,0,0,0,0,0,0,0,0,0,0,4}, + {8,0,0,0,0,0,0,0,0,0,8,4,0,0,0,0,0,6,6,6,0,6,4,6}, + {8,8,8,8,0,8,8,8,8,8,8,4,4,4,4,4,4,6,0,0,0,0,0,6}, + {7,7,7,7,0,7,7,7,7,0,8,0,8,0,8,0,8,4,0,4,0,6,0,6}, + {7,7,0,0,0,0,0,0,7,8,0,8,0,8,0,8,8,6,0,0,0,0,0,6}, + {7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,0,0,0,0,4}, + {7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,8,6,0,6,0,6,0,6}, + {7,7,0,0,0,0,0,0,7,8,0,8,0,8,0,8,8,6,4,6,0,6,6,6}, + {7,7,7,7,0,7,7,7,7,8,8,4,0,6,8,4,8,3,3,3,0,3,3,3}, + {2,2,2,2,0,2,2,2,2,4,6,4,0,0,6,0,6,3,0,0,0,0,0,3}, + {2,2,0,0,0,0,0,2,2,4,0,0,0,0,0,0,4,3,0,0,0,0,0,3}, + {2,0,0,0,0,0,0,0,2,4,0,0,0,0,0,0,4,3,0,0,0,0,0,3}, + {1,0,0,0,0,0,0,0,1,4,4,4,4,4,6,0,6,3,3,0,0,0,3,3}, + {2,0,0,0,0,0,0,0,2,2,2,1,2,2,2,6,6,0,0,5,0,5,0,5}, + {2,2,0,0,0,0,0,2,2,2,0,0,0,2,2,0,5,0,5,0,0,0,5,5}, + {2,0,0,0,0,0,0,0,2,0,0,0,0,0,2,5,0,5,0,5,0,5,0,5}, + {1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,5}, + {2,0,0,0,0,0,0,0,2,0,0,0,0,0,2,5,0,5,0,5,0,5,0,5}, + {2,2,0,0,0,0,0,2,2,2,0,0,0,2,2,0,5,0,5,0,0,0,5,5}, + {2,2,2,2,1,2,2,2,2,2,2,1,2,2,2,5,5,5,5,5,5,5,5,5} +}; + +struct Sprite +{ + double x; + double y; + int texture; +}; + +#define numSprites 19 + +Sprite sprite[numSprites] = +{ + {20.5, 11.5, 10}, //green light in front of playerstart + //green lights in every room + {18.5,4.5, 10}, + {10.0,4.5, 10}, + {10.0,12.5,10}, + {3.5, 6.5, 10}, + {3.5, 20.5,10}, + {3.5, 14.5,10}, + {14.5,20.5,10}, + + //row of pillars in front of wall: fisheye test + {18.5, 10.5, 9}, + {18.5, 11.5, 9}, + {18.5, 12.5, 9}, + + //some barrels around the map + {21.5, 1.5, 8}, + {15.5, 1.5, 8}, + {16.0, 1.8, 8}, + {16.2, 1.2, 8}, + {3.5, 2.5, 8}, + {9.5, 15.5, 8}, + {10.0, 15.1,8}, + {10.5, 15.8,8}, +}; + +Uint32 buffer[screenHeight][screenWidth]; // y-coordinate first because it works per scanline + +//1D Zbuffer +double ZBuffer[screenWidth]; + +//arrays used to sort the sprites +int spriteOrder[numSprites]; +double spriteDistance[numSprites]; + +//function used to sort the sprites +void sortSprites(int* order, double* dist, int amount); + +int main(int /*argc*/, char */*argv*/[]) +{ + double posX = 22.0, posY = 11.5; //x and y start position + double dirX = -1.0, dirY = 0.0; //initial direction vector + double planeX = 0.0, planeY = 0.66; //the 2d raycaster version of camera plane + + double time = 0; //time of current frame + double oldTime = 0; //time of previous frame + + std::vector texture[11]; + for(int i = 0; i < 11; i++) texture[i].resize(texWidth * texHeight); + + screen(screenWidth,screenHeight, 0, "Raycaster"); + + //load some textures + unsigned long tw, th, error = 0; + error |= loadImage(texture[0], tw, th, "pics/eagle.png"); + error |= loadImage(texture[1], tw, th, "pics/redbrick.png"); + error |= loadImage(texture[2], tw, th, "pics/purplestone.png"); + error |= loadImage(texture[3], tw, th, "pics/greystone.png"); + error |= loadImage(texture[4], tw, th, "pics/bluestone.png"); + error |= loadImage(texture[5], tw, th, "pics/mossy.png"); + error |= loadImage(texture[6], tw, th, "pics/wood.png"); + error |= loadImage(texture[7], tw, th, "pics/colorstone.png"); + if(error) { std::cout << "error loading images" << std::endl; return 1; } + + //load some sprite textures + error |= loadImage(texture[8], tw, th, "pics/barrel.png"); + error |= loadImage(texture[9], tw, th, "pics/pillar.png"); + error |= loadImage(texture[10], tw, th, "pics/greenlight.png"); + if(error) { std::cout << "error loading images" << std::endl; return 1; } + + //start the main loop + while(!done()) + { + //FLOOR CASTING + for(int y = screenHeight / 2 + 1; y < screenHeight; ++y) + { + // rayDir for leftmost ray (x = 0) and rightmost ray (x = w) + float rayDirX0 = dirX - planeX; + float rayDirY0 = dirY - planeY; + float rayDirX1 = dirX + planeX; + float rayDirY1 = dirY + planeY; + + // Current y position compared to the center of the screen (the horizon) + int p = y - screenHeight / 2; + + // Vertical position of the camera. + float posZ = 0.5 * screenHeight; + + // Horizontal distance from the camera to the floor for the current row. + // 0.5 is the z position exactly in the middle between floor and ceiling. + float rowDistance = posZ / p; + + // calculate the real world step vector we have to add for each x (parallel to camera plane) + // adding step by step avoids multiplications with a weight in the inner loop + float floorStepX = rowDistance * (rayDirX1 - rayDirX0) / screenWidth; + float floorStepY = rowDistance * (rayDirY1 - rayDirY0) / screenWidth; + + // real world coordinates of the leftmost column. This will be updated as we step to the right. + float floorX = posX + rowDistance * rayDirX0; + float floorY = posY + rowDistance * rayDirY0; + + for(int x = 0; x < screenWidth; ++x) + { + // the cell coord is simply got from the integer parts of floorX and floorY + int cellX = (int)(floorX); + int cellY = (int)(floorY); + + // get the texture coordinate from the fractional part + int tx = (int)(texWidth * (floorX - cellX)) & (texWidth - 1); + int ty = (int)(texHeight * (floorY - cellY)) & (texHeight - 1); + + floorX += floorStepX; + floorY += floorStepY; + + // choose texture and draw the pixel + int checkerBoardPattern = (int(cellX + cellY)) & 1; + int floorTexture; + if(checkerBoardPattern == 0) floorTexture = 3; + else floorTexture = 4; + int ceilingTexture = 6; + Uint32 color; + + // floor + color = texture[floorTexture][texWidth * ty + tx]; + color = (color >> 1) & 8355711; // make a bit darker + buffer[y][x] = color; + + //ceiling (symmetrical, at screenHeight - y - 1 instead of y) + color = texture[ceilingTexture][texWidth * ty + tx]; + color = (color >> 1) & 8355711; // make a bit darker + buffer[screenHeight - y - 1][x] = color; + } + } + + // WALL CASTING + for(int x = 0; x < w; x++) + { + //calculate ray position and direction + double cameraX = 2 * x / double(w) - 1; //x-coordinate in camera space + double rayDirX = dirX + planeX * cameraX; + double rayDirY = dirY + planeY * cameraX; + + //which box of the map we're in + int mapX = int(posX); + int mapY = int(posY); + + //length of ray from current position to next x or y-side + double sideDistX; + double sideDistY; + + //length of ray from one x or y-side to next x or y-side + double deltaDistX = (rayDirX == 0) ? 1e30 : std::abs(1 / rayDirX); + double deltaDistY = (rayDirY == 0) ? 1e30 : std::abs(1 / rayDirY); + double perpWallDist; + + //what direction to step in x or y-direction (either +1 or -1) + int stepX; + int stepY; + + int hit = 0; //was there a wall hit? + int side; //was a NS or a EW wall hit? + + //calculate step and initial sideDist + if(rayDirX < 0) + { + stepX = -1; + sideDistX = (posX - mapX) * deltaDistX; + } + else + { + stepX = 1; + sideDistX = (mapX + 1.0 - posX) * deltaDistX; + } + if(rayDirY < 0) + { + stepY = -1; + sideDistY = (posY - mapY) * deltaDistY; + } + else + { + stepY = 1; + sideDistY = (mapY + 1.0 - posY) * deltaDistY; + } + //perform DDA + while (hit == 0) + { + //jump to next map square, either in x-direction, or in y-direction + if(sideDistX < sideDistY) + { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } + else + { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + //Check if ray has hit a wall + if(worldMap[mapX][mapY] > 0) hit = 1; + } + + //Calculate distance of perpendicular ray (Euclidean distance would give fisheye effect!) + if(side == 0) perpWallDist = (sideDistX - deltaDistX); + else perpWallDist = (sideDistY - deltaDistY); + + //Calculate height of line to draw on screen + int lineHeight = (int)(h / perpWallDist); + + //calculate lowest and highest pixel to fill in current stripe + int drawStart = -lineHeight / 2 + h / 2; + if(drawStart < 0) drawStart = 0; + int drawEnd = lineHeight / 2 + h / 2; + if(drawEnd >= h) drawEnd = h - 1; + //texturing calculations + int texNum = worldMap[mapX][mapY] - 1; //1 subtracted from it so that texture 0 can be used! + + //calculate value of wallX + double wallX; //where exactly the wall was hit + if (side == 0) wallX = posY + perpWallDist * rayDirY; + else wallX = posX + perpWallDist * rayDirX; + wallX -= floor((wallX)); + + //x coordinate on the texture + int texX = int(wallX * double(texWidth)); + if(side == 0 && rayDirX > 0) texX = texWidth - texX - 1; + if(side == 1 && rayDirY < 0) texX = texWidth - texX - 1; + + // TODO: an integer-only bresenham or DDA like algorithm could make the texture coordinate stepping faster + // How much to increase the texture coordinate per screen pixel + double step = 1.0 * texHeight / lineHeight; + // Starting texture coordinate + double texPos = (drawStart - h / 2 + lineHeight / 2) * step; + for(int y = drawStart; y < drawEnd; y++) + { + // Cast the texture coordinate to integer, and mask with (texHeight - 1) in case of overflow + int texY = (int)texPos & (texHeight - 1); + texPos += step; + Uint32 color = texture[texNum][texHeight * texY + texX]; + //make color darker for y-sides: R, G and B byte each divided through two with a "shift" and an "and" + if(side == 1) color = (color >> 1) & 8355711; + buffer[y][x] = color; + } + + //SET THE ZBUFFER FOR THE SPRITE CASTING + ZBuffer[x] = perpWallDist; //perpendicular distance is used + } + + //SPRITE CASTING + //sort sprites from far to close + for(int i = 0; i < numSprites; i++) + { + spriteOrder[i] = i; + spriteDistance[i] = ((posX - sprite[i].x) * (posX - sprite[i].x) + (posY - sprite[i].y) * (posY - sprite[i].y)); //sqrt not taken, unneeded + } + sortSprites(spriteOrder, spriteDistance, numSprites); + + //after sorting the sprites, do the projection and draw them + for(int i = 0; i < numSprites; i++) + { + //translate sprite position to relative to camera + double spriteX = sprite[spriteOrder[i]].x - posX; + double spriteY = sprite[spriteOrder[i]].y - posY; + + //transform sprite with the inverse camera matrix + // [ planeX dirX ] -1 [ dirY -dirX ] + // [ ] = 1/(planeX*dirY-dirX*planeY) * [ ] + // [ planeY dirY ] [ -planeY planeX ] + + double invDet = 1.0 / (planeX * dirY - dirX * planeY); //required for correct matrix multiplication + + double transformX = invDet * (dirY * spriteX - dirX * spriteY); + double transformY = invDet * (-planeY * spriteX + planeX * spriteY); //this is actually the depth inside the screen, that what Z is in 3D, the distance of sprite to player, matching sqrt(spriteDistance[i]) + + int spriteScreenX = int((w / 2) * (1 + transformX / transformY)); + + //parameters for scaling and moving the sprites + #define uDiv 1 + #define vDiv 1 + #define vMove 0.0 + int vMoveScreen = int(vMove / transformY); + + //calculate height of the sprite on screen + int spriteHeight = abs(int(h / (transformY))) / vDiv; //using "transformY" instead of the real distance prevents fisheye + //calculate lowest and highest pixel to fill in current stripe + int drawStartY = -spriteHeight / 2 + h / 2 + vMoveScreen; + if(drawStartY < 0) drawStartY = 0; + int drawEndY = spriteHeight / 2 + h / 2 + vMoveScreen; + if(drawEndY >= h) drawEndY = h - 1; + + //calculate width of the sprite + int spriteWidth = abs(int (h / (transformY))) / uDiv; // same as height of sprite, given that it's square + int drawStartX = -spriteWidth / 2 + spriteScreenX; + if(drawStartX < 0) drawStartX = 0; + int drawEndX = spriteWidth / 2 + spriteScreenX; + if(drawEndX > w) drawEndX = w; + + //loop through every vertical stripe of the sprite on screen + for(int stripe = drawStartX; stripe < drawEndX; stripe++) + { + int texX = int(256 * (stripe - (-spriteWidth / 2 + spriteScreenX)) * texWidth / spriteWidth) / 256; + //the conditions in the if are: + //1) it's in front of camera plane so you don't see things behind you + //2) ZBuffer, with perpendicular distance + if(transformY > 0 && transformY < ZBuffer[stripe]) + { + for(int y = drawStartY; y < drawEndY; y++) //for every pixel of the current stripe + { + int d = (y - vMoveScreen) * 256 - h * 128 + spriteHeight * 128; //256 and 128 factors to avoid floats + int texY = ((d * texHeight) / spriteHeight) / 256; + Uint32 color = texture[sprite[spriteOrder[i]].texture][texWidth * texY + texX]; //get current color from the texture + if((color & 0x00FFFFFF) != 0) buffer[y][stripe] = color; //paint pixel if it isn't black, black is the invisible color + } + } + } + } + + drawBuffer(buffer[0]); + // No need to clear the screen here, since everything is overdrawn with floor and ceiling + + //timing for input and FPS counter + oldTime = time; + time = getTicks(); + double frameTime = (time - oldTime) / 1000.0; //frametime is the time this frame has taken, in seconds + print(1.0 / frameTime); //FPS counter + redraw(); + + //speed modifiers + double moveSpeed = frameTime * 3.0; //the constant value is in squares/second + double rotSpeed = frameTime * 2.0; //the constant value is in radians/second + + SDL_Event event; + while(SDL_PollEvent(&event)) { + if(event.type != SDL_KEYDOWN) continue; + //move forward if no wall in front of you + if(event.key.keysym.sym == SDLK_UP) + { + if(worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed; + if(worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed; + } + //move backwards if no wall behind you + if(event.key.keysym.sym == SDLK_DOWN) + { + if(worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed; + if(worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed; + } + //rotate to the right + if(event.key.keysym.sym == SDLK_RIGHT) + { + //both camera direction and camera plane must be rotated + double oldDirX = dirX; + dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed); + dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed); + double oldPlaneX = planeX; + planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed); + planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed); + } + //rotate to the left + if(event.key.keysym.sym == SDLK_LEFT) + { + //both camera direction and camera plane must be rotated + double oldDirX = dirX; + dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed); + dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed); + double oldPlaneX = planeX; + planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed); + planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed); + } + } + } +} + +//sort the sprites based on distance +void sortSprites(int* order, double* dist, int amount) +{ + std::vector> sprites(amount); + for(int i = 0; i < amount; i++) { + sprites[i].first = dist[i]; + sprites[i].second = order[i]; + } + std::sort(sprites.begin(), sprites.end()); + // restore in reverse order to go from farthest to nearest + for(int i = 0; i < amount; i++) { + dist[i] = sprites[amount - i - 1].first; + order[i] = sprites[amount - i - 1].second; + } +} diff --git a/scratchpad/raycaster_textured.cpp b/scratchpad/raycaster_textured.cpp new file mode 100644 index 0000000..179fee6 --- /dev/null +++ b/scratchpad/raycaster_textured.cpp @@ -0,0 +1,293 @@ +/* +Copyright (c) 2004-2019, Lode Vandevenne + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +#include +#include +#include +#include + +#include "quickcg.h" +using namespace QuickCG; + +/* +g++ *.cpp -lSDL -O3 -W -Wall -ansi -pedantic +g++ *.cpp -lSDL +*/ + + +#define screenWidth 640 +#define screenHeight 480 +#define texWidth 64 +#define texHeight 64 +#define mapWidth 24 +#define mapHeight 24 + +int worldMap[mapWidth][mapHeight]= +{ + {4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,7,7,7,7,7,7,7,7}, + {4,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,7}, + {4,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7}, + {4,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7}, + {4,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,7,0,0,0,0,0,0,7}, + {4,0,4,0,0,0,0,5,5,5,5,5,5,5,5,5,7,7,0,7,7,7,7,7}, + {4,0,5,0,0,0,0,5,0,5,0,5,0,5,0,5,7,0,0,0,7,7,7,1}, + {4,0,6,0,0,0,0,5,0,0,0,0,0,0,0,5,7,0,0,0,0,0,0,8}, + {4,0,7,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,7,7,7,1}, + {4,0,8,0,0,0,0,5,0,0,0,0,0,0,0,5,7,0,0,0,0,0,0,8}, + {4,0,0,0,0,0,0,5,0,0,0,0,0,0,0,5,7,0,0,0,7,7,7,1}, + {4,0,0,0,0,0,0,5,5,5,5,0,5,5,5,5,7,7,7,7,7,7,7,1}, + {6,6,6,6,6,6,6,6,6,6,6,0,6,6,6,6,6,6,6,6,6,6,6,6}, + {8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,4}, + {6,6,6,6,6,6,0,6,6,6,6,0,6,6,6,6,6,6,6,6,6,6,6,6}, + {4,4,4,4,4,4,0,4,4,4,6,0,6,2,2,2,2,2,2,2,3,3,3,3}, + {4,0,0,0,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,0,0,0,2}, + {4,0,0,0,0,0,0,0,0,0,0,0,6,2,0,0,5,0,0,2,0,0,0,2}, + {4,0,0,0,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,2,0,2,2}, + {4,0,6,0,6,0,0,0,0,4,6,0,0,0,0,0,5,0,0,0,0,0,0,2}, + {4,0,0,5,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,2,0,2,2}, + {4,0,6,0,6,0,0,0,0,4,6,0,6,2,0,0,5,0,0,2,0,0,0,2}, + {4,0,0,0,0,0,0,0,0,4,6,0,6,2,0,0,0,0,0,2,0,0,0,2}, + {4,4,4,4,4,4,4,4,4,4,1,1,1,2,2,2,2,2,2,3,3,3,3,3} +}; + +Uint32 buffer[screenHeight][screenWidth]; + +int main(int /*argc*/, char */*argv*/[]) +{ + double posX = 22.0, posY = 11.5; //x and y start position + double dirX = -1.0, dirY = 0.0; //initial direction vector + double planeX = 0.0, planeY = 0.66; //the 2d raycaster version of camera plane + + double time = 0; //time of current frame + double oldTime = 0; //time of previous frame + + std::vector texture[8]; + for(int i = 0; i < 8; i++) texture[i].resize(texWidth * texHeight); + + screen(screenWidth,screenHeight, 0, "Raycaster"); + + //generate some textures +#if 0 + for(int x = 0; x < texWidth; x++) + for(int y = 0; y < texHeight; y++) + { + int xorcolor = (x * 256 / texWidth) ^ (y * 256 / texHeight); + //int xcolor = x * 256 / texWidth; + int ycolor = y * 256 / texHeight; + int xycolor = y * 128 / texHeight + x * 128 / texWidth; + texture[0][texWidth * y + x] = 65536 * 254 * (x != y && x != texWidth - y); //flat red texture with black cross + texture[1][texWidth * y + x] = xycolor + 256 * xycolor + 65536 * xycolor; //sloped greyscale + texture[2][texWidth * y + x] = 256 * xycolor + 65536 * xycolor; //sloped yellow gradient + texture[3][texWidth * y + x] = xorcolor + 256 * xorcolor + 65536 * xorcolor; //xor greyscale + texture[4][texWidth * y + x] = 256 * xorcolor; //xor green + texture[5][texWidth * y + x] = 65536 * 192 * (x % 16 && y % 16); //red bricks + texture[6][texWidth * y + x] = 65536 * ycolor; //red gradient + texture[7][texWidth * y + x] = 128 + 256 * 128 + 65536 * 128; //flat grey texture + } +#else + //generate some textures + unsigned long tw, th; + loadImage(texture[0], tw, th, "pics/eagle.png"); + loadImage(texture[1], tw, th, "pics/redbrick.png"); + loadImage(texture[2], tw, th, "pics/purplestone.png"); + loadImage(texture[3], tw, th, "pics/greystone.png"); + loadImage(texture[4], tw, th, "pics/bluestone.png"); + loadImage(texture[5], tw, th, "pics/mossy.png"); + loadImage(texture[6], tw, th, "pics/wood.png"); + loadImage(texture[7], tw, th, "pics/colorstone.png"); +#endif + + //start the main loop + while(!done()) + { + for(int x = 0; x < w; x++) + { + //calculate ray position and direction + double cameraX = 2 * x / (double)w - 1; //x-coordinate in camera space + double rayDirX = dirX + planeX*cameraX; + double rayDirY = dirY + planeY*cameraX; + + //which box of the map we're in + int mapX = int(posX); + int mapY = int(posY); + + //length of ray from current position to next x or y-side + double sideDistX; + double sideDistY; + + //length of ray from one x or y-side to next x or y-side + double deltaDistX = (rayDirX == 0) ? 1e30 : std::abs(1 / rayDirX); + double deltaDistY = (rayDirY == 0) ? 1e30 : std::abs(1 / rayDirY); + double perpWallDist; + + //what direction to step in x or y-direction (either +1 or -1) + int stepX; + int stepY; + + int hit = 0; //was there a wall hit? + int side; //was a NS or a EW wall hit? + + //calculate step and initial sideDist + if(rayDirX < 0) + { + stepX = -1; + sideDistX = (posX - mapX) * deltaDistX; + } + else + { + stepX = 1; + sideDistX = (mapX + 1.0 - posX) * deltaDistX; + } + if(rayDirY < 0) + { + stepY = -1; + sideDistY = (posY - mapY) * deltaDistY; + } + else + { + stepY = 1; + sideDistY = (mapY + 1.0 - posY) * deltaDistY; + } + //perform DDA + while (hit == 0) + { + //jump to next map square, either in x-direction, or in y-direction + if(sideDistX < sideDistY) + { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } + else + { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + //Check if ray has hit a wall + if(worldMap[mapX][mapY] > 0) hit = 1; + } + + //Calculate distance of perpendicular ray (Euclidean distance would give fisheye effect!) + if(side == 0) perpWallDist = (sideDistX - deltaDistX); + else perpWallDist = (sideDistY - deltaDistY); + + //Calculate height of line to draw on screen + int lineHeight = (int)(h / perpWallDist); + + + int pitch = 100; + + //calculate lowest and highest pixel to fill in current stripe + int drawStart = -lineHeight / 2 + h / 2 + pitch; + if(drawStart < 0) drawStart = 0; + int drawEnd = lineHeight / 2 + h / 2 + pitch; + if(drawEnd >= h) drawEnd = h - 1; + + //texturing calculations + int texNum = worldMap[mapX][mapY] - 1; //1 subtracted from it so that texture 0 can be used! + + //calculate value of wallX + double wallX; //where exactly the wall was hit + if(side == 0) wallX = posY + perpWallDist * rayDirY; + else wallX = posX + perpWallDist * rayDirX; + wallX -= floor((wallX)); + + //x coordinate on the texture + int texX = int(wallX * double(texWidth)); + if(side == 0 && rayDirX > 0) texX = texWidth - texX - 1; + if(side == 1 && rayDirY < 0) texX = texWidth - texX - 1; + + // TODO: an integer-only bresenham or DDA like algorithm could make the texture coordinate stepping faster + // How much to increase the texture coordinate per screen pixel + double step = 1.0 * texHeight / lineHeight; + // Starting texture coordinate + double texPos = (drawStart - pitch - h / 2 + lineHeight / 2) * step; + for(int y = drawStart; y < drawEnd; y++) + { + // Cast the texture coordinate to integer, and mask with (texHeight - 1) in case of overflow + int texY = (int)texPos & (texHeight - 1); + texPos += step; + Uint32 color = texture[texNum][texHeight * texY + texX]; + //make color darker for y-sides: R, G and B byte each divided through two with a "shift" and an "and" + if(side == 1) color = (color >> 1) & 8355711; + buffer[y][x] = color; + } + } + + for(int y = 0; y < h; y++) for(int x = 0; x < w; x++) buffer[y][x] = 0; //clear the buffer instead of cls() + drawBuffer(buffer[0]); + + //timing for input and FPS counter + oldTime = time; + time = getTicks(); + double frameTime = (time - oldTime) / 1000.0; //frametime is the time this frame has taken, in seconds + print(1.0 / frameTime); //FPS counter + redraw(); + + //speed modifiers + double moveSpeed = frameTime * 5.0; //the constant value is in squares/second + double rotSpeed = frameTime * 3.0; //the constant value is in radians/second + + SDL_Event event; + while(SDL_PollEvent(&event)) { + if(event.type != SDL_KEYDOWN) continue; + //move forward if no wall in front of you + if(event.key.keysym.sym == SDLK_UP) + { + if(worldMap[int(posX + dirX * moveSpeed)][int(posY)] == false) posX += dirX * moveSpeed; + if(worldMap[int(posX)][int(posY + dirY * moveSpeed)] == false) posY += dirY * moveSpeed; + } + //move backwards if no wall behind you + if(event.key.keysym.sym == SDLK_DOWN) + { + if(worldMap[int(posX - dirX * moveSpeed)][int(posY)] == false) posX -= dirX * moveSpeed; + if(worldMap[int(posX)][int(posY - dirY * moveSpeed)] == false) posY -= dirY * moveSpeed; + } + //rotate to the right + if(event.key.keysym.sym == SDLK_RIGHT) + { + //both camera direction and camera plane must be rotated + double oldDirX = dirX; + dirX = dirX * cos(-rotSpeed) - dirY * sin(-rotSpeed); + dirY = oldDirX * sin(-rotSpeed) + dirY * cos(-rotSpeed); + double oldPlaneX = planeX; + planeX = planeX * cos(-rotSpeed) - planeY * sin(-rotSpeed); + planeY = oldPlaneX * sin(-rotSpeed) + planeY * cos(-rotSpeed); + } + //rotate to the left + if(event.key.keysym.sym == SDLK_LEFT) + { + //both camera direction and camera plane must be rotated + double oldDirX = dirX; + dirX = dirX * cos(rotSpeed) - dirY * sin(rotSpeed); + dirY = oldDirX * sin(rotSpeed) + dirY * cos(rotSpeed); + double oldPlaneX = planeX; + planeX = planeX * cos(rotSpeed) - planeY * sin(rotSpeed); + planeY = oldPlaneX * sin(rotSpeed) + planeY * cos(rotSpeed); + } + } + } + + return 0; +} diff --git a/sdlprog.cpp b/scratchpad/sdlprog.cpp similarity index 100% rename from sdlprog.cpp rename to scratchpad/sdlprog.cpp diff --git a/scratchpad/timcaster.cpp b/scratchpad/timcaster.cpp new file mode 100644 index 0000000..aae265e --- /dev/null +++ b/scratchpad/timcaster.cpp @@ -0,0 +1,553 @@ +#include +#include +#include +#include +#include + +#define ASSERT(_e, ...) if (!(_e)) { fprintf(stderr, __VA_ARGS__); exit(1); } + +typedef float f32; +typedef double f64; +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +typedef uint64_t u64; +typedef int8_t i8; +typedef int16_t i16; +typedef int32_t i32; +typedef int64_t i64; +typedef size_t usize; +typedef ssize_t isize; + +#define SCREEN_SIZE_X 640 +#define SCREEN_SIZE_Y 360 + +#define TILE_WIDTH 1.0f +#define WALL_HEIGHT 1.2f + +typedef struct v2_s {f32 x, y;} v2; +typedef struct v2i_s { i32 x, y;} v2i; + +#define dot(v0, v1) \ + ({ const v2 _v0 = (v0), _v1 = (v1); (_v0.x * _v1.x) + (_v0.y * _v1.y); }) +#define length(v) ({ const v2 _v = (v); sqrtf(dot(_v, _v)); }) +#define normalize(u) ({ \ + const v2 _u = (u); \ + const f32 l = length(_u); \ + (v2) { _u.x/l, _u.y/l }; \ + }) +#define rotr(v) ({ const v2 _v = (v); (v2) { -_v.y, _v.x }; }) +#define min(a, b) ({ __typeof__(a) _a = (a), _b = (b); _a < _b ? _a : _b; }) +#define max(a, b) ({ __typeof__(a) _a = (a), _b = (b); _a > _b ? _a : _b; }) + +static u8 MAPDATA[8*13] = { + 1, 1, 1, 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 3, 0, 0, 4, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 4, 1, + 1, 0, 2, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 0, 2, 2, 0, 0, 0, 1, + 1, 0, 2, 2, 0, 3, 0, 1, + 1, 0, 0, 0, 0, 3, 0, 1, + 1, 0, 0, 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, 1, 1, 1, +}; + +enum KeyboardKeyState { + KeyboardKeyState_Depressed = 0, // No recent event, key is still up + KeyboardKeyState_Released = 1, // Last event was a released event + KeyboardKeyState_Held = 2, // No recent event, key is still down + KeyboardKeyState_Pressed = 3, // Last event was a pressed event + KeyboardKeyState_COUNT = 4 +}; + +struct KeyBoardState { + enum KeyboardKeyState up; + enum KeyboardKeyState down; + enum KeyboardKeyState right; + enum KeyboardKeyState left; + enum KeyboardKeyState a; + enum KeyboardKeyState s; + enum KeyboardKeyState d; + enum KeyboardKeyState w; + enum KeyboardKeyState q; + enum KeyboardKeyState e; + + enum KeyboardKeyState one; + enum KeyboardKeyState two; + enum KeyboardKeyState three; + enum KeyboardKeyState four; + enum KeyboardKeyState five; + enum KeyboardKeyState six; + enum KeyboardKeyState seven; + enum KeyboardKeyState eight; +}; + +void clear_keyboard_state(struct KeyBoardState* kbs) { + kbs->up = KeyboardKeyState_Depressed; + kbs->down = KeyboardKeyState_Depressed; + kbs->right = KeyboardKeyState_Depressed; + kbs->left = KeyboardKeyState_Depressed; + kbs->a = KeyboardKeyState_Depressed; + kbs->s = KeyboardKeyState_Depressed; + kbs->d = KeyboardKeyState_Depressed; + kbs->w = KeyboardKeyState_Depressed; + kbs->q = KeyboardKeyState_Depressed; + kbs->e = KeyboardKeyState_Depressed; + + kbs->one = KeyboardKeyState_Depressed; + kbs->two = KeyboardKeyState_Depressed; + kbs->three = KeyboardKeyState_Depressed; + kbs->four = KeyboardKeyState_Depressed; + kbs->five = KeyboardKeyState_Depressed; + kbs->six = KeyboardKeyState_Depressed; + kbs->seven = KeyboardKeyState_Depressed; + kbs->eight = KeyboardKeyState_Depressed; +} + +void decay_keyboard_state(struct KeyBoardState* kbs) { + static enum KeyboardKeyState to_depressed_state[KeyboardKeyState_COUNT] = { + KeyboardKeyState_Depressed, + KeyboardKeyState_Depressed, + KeyboardKeyState_Held, + KeyboardKeyState_Held + }; + + kbs->up = to_depressed_state[kbs->up]; + kbs->down = to_depressed_state[kbs->down]; + kbs->right = to_depressed_state[kbs->right]; + kbs->left = to_depressed_state[kbs->left]; + kbs->a = to_depressed_state[kbs->a]; + kbs->s = to_depressed_state[kbs->s]; + kbs->d = to_depressed_state[kbs->d]; + kbs->w = to_depressed_state[kbs->w]; + kbs->q = to_depressed_state[kbs->q]; + kbs->e = to_depressed_state[kbs->e]; + + kbs->one = to_depressed_state[kbs->one]; + kbs->two = to_depressed_state[kbs->two]; + kbs->three = to_depressed_state[kbs->three]; + kbs->four = to_depressed_state[kbs->four]; + kbs->five = to_depressed_state[kbs->five]; + kbs->six = to_depressed_state[kbs->six]; + kbs->seven = to_depressed_state[kbs->seven]; + kbs->eight = to_depressed_state[kbs->eight]; +} + +bool is_pressed(enum KeyboardKeyState state) { + static bool lookup[KeyboardKeyState_COUNT] = {0, 0, 1, 1}; + return lookup[state]; +} + +// TODO: Could we store the pixels in column-major? We're always rendering +// in vertical lines, so I suspect that would be more efficient. +struct { + SDL_Window *window; + SDL_Texture *texture; + SDL_Renderer *renderer; + u32 pixels[SCREEN_SIZE_X * SCREEN_SIZE_Y]; + bool quit; + + v2 camera_pos; + v2 camera_dir; + v2 camera_dir_rotr; + f32 camera_width; + f32 camera_height; + f32 camera_z; + + v2 player_speed; + f32 player_omega; + + struct KeyBoardState keyboard_state; +} state; + + +static void tick(f32 dt) { + + v2 input_dir = {0.0, 0.0}; // In the body frame, which is right-handed, so y points left. + if (is_pressed(state.keyboard_state.w)) { + input_dir.x += 1.0; + } + if (is_pressed(state.keyboard_state.s)) { + input_dir.x -= 1.0; + } + if (is_pressed(state.keyboard_state.d)) { + input_dir.y -= 1.0; + } + if (is_pressed(state.keyboard_state.a)) { + input_dir.y += 1.0; + } + + int input_rot_dir = 0; // Right-hand rotation in plane (CCW) + if (is_pressed(state.keyboard_state.q)) { + input_rot_dir += 1; + } + if (is_pressed(state.keyboard_state.e)) { + input_rot_dir -= 1; + } + + if (is_pressed(state.keyboard_state.three)) { + state.camera_z *= 0.95; + printf("camera z: %.3f\n", state.camera_z); + } + if (is_pressed(state.keyboard_state.four)) { + state.camera_z /= 0.95; + printf("camera z: %.3f\n", state.camera_z); + } + if (is_pressed(state.keyboard_state.five)) { + state.camera_height *= 0.95; + printf("camera height: %.3f\n", state.camera_height); + } + if (is_pressed(state.keyboard_state.six)) { + state.camera_height /= 0.95; + printf("camera height: %.3f\n", state.camera_height); + } + if (is_pressed(state.keyboard_state.seven)) { + state.camera_width *= 0.95; + printf("camera width: %.3f\n", state.camera_width); + } + if (is_pressed(state.keyboard_state.eight)) { + state.camera_width /= 0.95; + printf("camera width: %.3f\n", state.camera_width); + } + + // Update the player's velocity + const f32 kPlayerInputAccel = 7.5; + const f32 kPlayerInputAngularAccel = 9.5; + const f32 kPlayerMaxSpeed = 7.0; + const f32 kPlayerMaxOmega = 7.0; + const f32 kAirFriction = 0.9; + const f32 kAirFrictionRot = 0.85; + + // Note: Speed is in the global frame + state.player_speed.x += (state.camera_dir.x*input_dir.x + state.camera_dir_rotr.x*input_dir.y) * kPlayerInputAccel * dt; + state.player_speed.y += (state.camera_dir.y*input_dir.x + state.camera_dir_rotr.y*input_dir.y) * kPlayerInputAccel * dt; + state.player_omega += input_rot_dir * kPlayerInputAngularAccel * dt; + + // Clamp the velocity to a maximum magnitude + f32 speed = length(state.player_speed); + if (speed > kPlayerMaxSpeed) { + state.player_speed.x *= kPlayerMaxSpeed / speed; + state.player_speed.y *= kPlayerMaxSpeed / speed; + } + if (state.player_omega > kPlayerMaxOmega) { + state.player_omega *= kPlayerMaxOmega / state.player_omega; + } else if (state.player_omega < -kPlayerMaxOmega) { + state.player_omega *= - kPlayerMaxOmega / state.player_omega; + } + + // Update the player's position + state.camera_pos.x += state.player_speed.x * dt; + state.camera_pos.y += state.player_speed.y * dt; + + // Update the player's rotational heading + float theta = atan2(state.camera_dir.y, state.camera_dir.x); + theta += state.player_omega * dt; + state.camera_dir = ((v2) {cos(theta), sin(theta)}); + state.camera_dir_rotr = rotr((state.camera_dir)); + + // Apply air friction + state.player_speed.x *= kAirFriction; + state.player_speed.y *= kAirFriction; + state.player_omega *= kAirFrictionRot; +} + + +// Fill all pixels in the vertical line at x between y0 and y1 with the given color. +static void draw_column(int x, int y0, int y1, u32 color) { + for (int y = y0; y <= y1; y++) { + state.pixels[(y * SCREEN_SIZE_X) + x] = color; + } +} + +static void render() { + static u32 color_wall[4] = { + 0xFFFF0000, + 0xFF00FF00, + 0xFF00FFFF, + 0xFF0000FF + }; + static u32 color_wall_light[4] = { + 0xFFFF3333, + 0xFF66FF66, + 0xFF88FFFF, + 0xFF3333FF + }; + const u32 color_floor = 0xFF666666; + const u32 color_ceil = 0xFF444444; + + // Get camera location's cell coordinates + int x_ind_cam = (int)(floorf(state.camera_pos.x / TILE_WIDTH)); + int y_ind_cam = (int)(floorf(state.camera_pos.y / TILE_WIDTH)); + f32 x_rem_cam = state.camera_pos.x - TILE_WIDTH*x_ind_cam; + f32 y_rem_cam = state.camera_pos.y - TILE_WIDTH*y_ind_cam; + + for (int x = 0; x < SCREEN_SIZE_X; x++) { + + // Camera to pixel column + const f32 dw = state.camera_width/2 - (state.camera_width*x)/SCREEN_SIZE_X; + const v2 cp = { + state.camera_dir.x + dw*state.camera_dir_rotr.x, + state.camera_dir.y + dw*state.camera_dir_rotr.y + }; + + // Distance from the camera to the column + const f32 cam_len = length( (cp) ); + + // Ray direction through this column + const v2 dir = {cp.x / cam_len, cp.y /cam_len}; + + // Start at the camera pos + int x_ind = x_ind_cam; + int y_ind = y_ind_cam; + f32 x_rem = x_rem_cam; + f32 y_rem = y_rem_cam; + + // We will be raycasting through cells of unit width. + // Our ray's position vs time is: + // x(t) = x_rem + dir.x * dt + // y(t) = y_rem + dir.y * dt + + // We cross x = 0 if dir.x < 0, at dt = -x_rem/dir.x + // We cross x = TILE_WIDTH if dir.x > 0, at dt = (TILE_WIDTH-x_rem)/dir.x + // We cross y = 0 if dir.y < 0, at dt = -y_rem/dir.y + // We cross y = TILE_WIDTH if dir.y > 0, at dt = (TILE_WIDTH-y_rem)/dir.y + + // We can generalize this to: + // dx_ind_dir = -1 if dir.x < 0, at dt = -1/dir.x * x_rem + 0.0 + // dx_ind_dir = 1 if dir.x > 0, at dt = -1/dir.x * x_rem + TILE_WIDTH/dir.x + // dx_ind_dir = 0 if dir.x = 0, at dt = 0 * x_rem + INFINITY + // dy_ind_dir = -1 if dir.y < 0, at dt = -1/dir.y * y_rem + 0.0 + // dy_ind_dir = 1 if dir.y > 0, at dt = -1/dir.y * y_rem + TILE_WIDTH/dir.y + // dy_ind_dir = 0 if dir.x = 0, at dt = 0 * y_rem + INFINITY + + int dx_ind_dir = 0; + f32 dx_a = 0.0; + f32 dx_b = INFINITY; + if (dir.x < 0) { + dx_ind_dir = -1; + dx_a = -1.0f/dir.x; + dx_b = 0.0; + } else if (dir.x > 0) { + dx_ind_dir = 1; + dx_a = -1.0f/dir.x; + dx_b = TILE_WIDTH/dir.x; + } + + int dy_ind_dir = 0; + f32 dy_a = 0.0; + f32 dy_b = INFINITY; + if (dir.y < 0) { + dy_ind_dir = -1; + dy_a = -1.0f/dir.y; + dy_b = 0.0; + } else if (dir.y > 0) { + dy_ind_dir = 1; + dy_a = -1.0f/dir.y; + dy_b = TILE_WIDTH/dir.y; + } + + // Step through cells until we hit an occupied cell + int n_steps = 0; + int dx_ind, dy_ind; + while (n_steps < 100) { + n_steps += 1; + + f32 dt_best = INFINITY; + dx_ind = 0; + dy_ind = 0; + + f32 dt_x = dx_a*x_rem + dx_b; + f32 dt_y = dy_a*y_rem + dy_b; + if (dt_x < dt_y) { + dt_best = dt_x; + dx_ind = dx_ind_dir; + dy_ind = 0; + } else { + dt_best = dt_y; + dx_ind = 0; + dy_ind = dy_ind_dir; + } + + // Move up to the next cell + x_ind += dx_ind; + y_ind += dy_ind; + x_rem += dir.x * dt_best - TILE_WIDTH*dx_ind; + y_rem += dir.y * dt_best - TILE_WIDTH*dy_ind; + + // Check to see if the new cell is solid + if (MAPDATA[y_ind*8 + x_ind] > 0) { + break; + } + } + + // Calculate the collision location + const v2 collision = { + TILE_WIDTH*x_ind + x_rem, + TILE_WIDTH*y_ind + y_rem + }; + + // Calculate the ray length + const f32 ray_len = length( ((v2) {collision.x - state.camera_pos.x, collision.y - state.camera_pos.y}) ); + + // Calculate the pixel bounds that we fill the wall in for + int y_lo = (int)(SCREEN_SIZE_Y/2.0f - cam_len*state.camera_z/ray_len * SCREEN_SIZE_Y / state.camera_height); + int y_hi = (int)(SCREEN_SIZE_Y/2.0f + cam_len*(WALL_HEIGHT - state.camera_z)/ray_len * SCREEN_SIZE_Y / state.camera_height); + y_lo = max(y_lo, 0); + y_hi = min(y_hi, SCREEN_SIZE_Y-1); + + u32 color_wall_to_render = (dx_ind == 0) ? color_wall[MAPDATA[y_ind*8 + x_ind]-1] : color_wall_light[MAPDATA[y_ind*8 + x_ind]-1]; + + draw_column(x, 0, y_lo-1, color_floor); + draw_column(x, y_lo, y_hi, color_wall_to_render); + draw_column(x, y_hi + 1, SCREEN_SIZE_Y-1, color_ceil); + } +} + +int main(int argc, char *argv[]) { + // Initialize SDL + ASSERT( + SDL_Init(SDL_INIT_VIDEO) == 0, + "SDL initialization failed: %s\n", + SDL_GetError() + ); + + // Create a window + state.window = SDL_CreateWindow( + "TOOM", + SDL_WINDOWPOS_CENTERED_DISPLAY(1), + SDL_WINDOWPOS_CENTERED_DISPLAY(1), + SCREEN_SIZE_X, + SCREEN_SIZE_Y, + SDL_WINDOW_ALLOW_HIGHDPI); + ASSERT(state.window, "Error creating SDL window: %s\n", SDL_GetError()); + + // Create a renderer + state.renderer = SDL_CreateRenderer(state.window, -1, SDL_RENDERER_PRESENTVSYNC); + ASSERT(state.renderer, "Error creating SDL renderer: %s\n", SDL_GetError()); + + // Create a texture + state.texture = SDL_CreateTexture( + state.renderer, + SDL_PIXELFORMAT_ABGR8888, + SDL_TEXTUREACCESS_STREAMING, + SCREEN_SIZE_X, + SCREEN_SIZE_Y); + ASSERT(state.texture, "Error creating SDL texture: %s\n", SDL_GetError()); + + // Init camera + state.camera_pos = (v2) { 5.0f, 5.0f }; + state.camera_dir = ((v2) {cos(0.0), sin(0.0)}); + state.camera_dir_rotr = rotr((state.camera_dir)); + state.camera_width = 1.5f; + state.camera_height = state.camera_width * SCREEN_SIZE_Y / SCREEN_SIZE_X; + state.camera_z = 0.4; + + // Init player state + state.player_speed = (v2) { 0.0f, 0.0f }; + state.player_omega = 0.0f; + + // Init keyboard + clear_keyboard_state(&state.keyboard_state); + + // Time structs + struct timeval timeval_start, timeval_end; + + // Main loop + u32 time_prev_tick = SDL_GetTicks(); + state.quit = 0; + while (state.quit == 0) { + const u32 time_start = SDL_GetTicks(); + gettimeofday(&timeval_start, NULL); + + SDL_Event event; + while (SDL_PollEvent(&event)) { + if (event.type == SDL_QUIT) { + state.quit = 1; + break; + } else if (event.type == SDL_KEYDOWN) { + switch (event.key.keysym.sym) { + case (SDLK_UP) : state.keyboard_state.up = KeyboardKeyState_Pressed; break; + case (SDLK_DOWN) : state.keyboard_state.down = KeyboardKeyState_Pressed; break; + case (SDLK_LEFT) : state.keyboard_state.left = KeyboardKeyState_Pressed; break; + case (SDLK_RIGHT) : state.keyboard_state.right = KeyboardKeyState_Pressed; break; + case (SDLK_a) : state.keyboard_state.a = KeyboardKeyState_Pressed; break; + case (SDLK_s) : state.keyboard_state.s = KeyboardKeyState_Pressed; break; + case (SDLK_d) : state.keyboard_state.d = KeyboardKeyState_Pressed; break; + case (SDLK_w) : state.keyboard_state.w = KeyboardKeyState_Pressed; break; + case (SDLK_q) : state.keyboard_state.q = KeyboardKeyState_Pressed; break; + case (SDLK_e) : state.keyboard_state.e = KeyboardKeyState_Pressed; break; + case (SDLK_1) : state.keyboard_state.one = KeyboardKeyState_Pressed; break; + case (SDLK_2) : state.keyboard_state.two = KeyboardKeyState_Pressed; break; + case (SDLK_3) : state.keyboard_state.three = KeyboardKeyState_Pressed; break; + case (SDLK_4) : state.keyboard_state.four = KeyboardKeyState_Pressed; break; + case (SDLK_5) : state.keyboard_state.five = KeyboardKeyState_Pressed; break; + case (SDLK_6) : state.keyboard_state.six = KeyboardKeyState_Pressed; break; + case (SDLK_7) : state.keyboard_state.seven = KeyboardKeyState_Pressed; break; + case (SDLK_8) : state.keyboard_state.eight = KeyboardKeyState_Pressed; break; + } + } else if (event.type == SDL_KEYUP) { + switch (event.key.keysym.sym) { + case (SDLK_UP) : state.keyboard_state.up = KeyboardKeyState_Released; break; + case (SDLK_DOWN) : state.keyboard_state.down = KeyboardKeyState_Released; break; + case (SDLK_LEFT) : state.keyboard_state.left = KeyboardKeyState_Released; break; + case (SDLK_RIGHT) : state.keyboard_state.right = KeyboardKeyState_Released; break; + case (SDLK_a) : state.keyboard_state.a = KeyboardKeyState_Released; break; + case (SDLK_s) : state.keyboard_state.s = KeyboardKeyState_Released; break; + case (SDLK_d) : state.keyboard_state.d = KeyboardKeyState_Released; break; + case (SDLK_w) : state.keyboard_state.w = KeyboardKeyState_Released; break; + case (SDLK_q) : state.keyboard_state.q = KeyboardKeyState_Released; break; + case (SDLK_e) : state.keyboard_state.e = KeyboardKeyState_Released; break; + case (SDLK_1) : state.keyboard_state.one = KeyboardKeyState_Released; break; + case (SDLK_2) : state.keyboard_state.two = KeyboardKeyState_Released; break; + case (SDLK_3) : state.keyboard_state.three = KeyboardKeyState_Released; break; + case (SDLK_4) : state.keyboard_state.four = KeyboardKeyState_Released; break; + case (SDLK_5) : state.keyboard_state.five = KeyboardKeyState_Released; break; + case (SDLK_6) : state.keyboard_state.six = KeyboardKeyState_Released; break; + case (SDLK_7) : state.keyboard_state.seven = KeyboardKeyState_Released; break; + case (SDLK_8) : state.keyboard_state.eight = KeyboardKeyState_Released; break; + } + } + } + + // TODO: Move to more accurate timing? + const u32 time_tick_start = SDL_GetTicks(); + const f32 dt = (time_tick_start - time_prev_tick) / 1000.0f; + tick(dt); + time_prev_tick = time_tick_start; + + render(); + + decay_keyboard_state(&state.keyboard_state); + + // Get timer end for all the non-SDL stuff + gettimeofday(&timeval_end, NULL); + f64 game_ms_elapsed = (timeval_end.tv_sec - timeval_start.tv_sec) * 1000.0; // sec to ms + game_ms_elapsed += (timeval_end.tv_usec - timeval_start.tv_usec) / 1000.0; // us to ms + // printf("Game: %.3f ms, %.1f fps\n", game_ms_elapsed, 1000.0f / max(1.0f, game_ms_elapsed)); + + SDL_UpdateTexture(state.texture, NULL, state.pixels, SCREEN_SIZE_X * 4); + SDL_RenderCopyEx( + state.renderer, + state.texture, + NULL, + NULL, + 0.0, + NULL, + SDL_FLIP_VERTICAL); + + // SDL_RENDERER_PRESENTVSYNC means this is syncronized with the monitor refresh rate. (30Hz) + SDL_RenderPresent(state.renderer); + + const u32 time_end = SDL_GetTicks(); + const u32 ms_elapsed = time_end - time_start; + const f32 fps = 1000.0f / max(1, ms_elapsed); + // printf("FPS: %.1f\n", fps); + } + + SDL_DestroyWindow(state.window); + return 0; +} diff --git a/wraps/flac.wrap b/wraps/flac.wrap new file mode 100644 index 0000000..ee36479 --- /dev/null +++ b/wraps/flac.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = flac-1.4.3 +source_url = https://github.com/xiph/flac/releases/download/1.4.3/flac-1.4.3.tar.xz +source_filename = flac-1.4.3.tar.xz +source_hash = 6c58e69cd22348f441b861092b825e591d0b822e106de6eb0ee4d05d27205b70 +patch_filename = flac_1.4.3-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/flac_1.4.3-2/get_patch +patch_hash = 3eace1bd0769d3e0d4ff099960160766a5185d391c8f583293b087a1f96c2a9c +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/flac_1.4.3-2/flac-1.4.3.tar.xz +wrapdb_version = 1.4.3-2 + +[provide] +flac = flac_dep diff --git a/wraps/ogg.wrap b/wraps/ogg.wrap new file mode 100644 index 0000000..e7f23eb --- /dev/null +++ b/wraps/ogg.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = libogg-1.3.5 +source_url = https://downloads.xiph.org/releases/ogg/libogg-1.3.5.tar.xz +source_filename = libogg-1.3.5.tar.xz +source_hash = c4d91be36fc8e54deae7575241e03f4211eb102afb3fc0775fbbc1b740016705 +patch_filename = ogg_1.3.5-6_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/ogg_1.3.5-6/get_patch +patch_hash = 8be6dcd5f93bbf9c0b9c8ec1fa29810226a60f846383074ca05b313a248e78b2 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/ogg_1.3.5-6/libogg-1.3.5.tar.xz +wrapdb_version = 1.3.5-6 + +[provide] +ogg = libogg_dep diff --git a/wraps/openal-soft.wrap b/wraps/openal-soft.wrap new file mode 100644 index 0000000..a46c525 --- /dev/null +++ b/wraps/openal-soft.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = openal-soft-1.23.1 +source_url = https://github.com/kcat/openal-soft/archive/refs/tags/1.23.1.tar.gz +source_filename = openal-soft-1.23.1.tar.gz +source_hash = dfddf3a1f61059853c625b7bb03de8433b455f2f79f89548cbcbd5edca3d4a4a +patch_filename = openal-soft_1.23.1-2_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/openal-soft_1.23.1-2/get_patch +patch_hash = e03c3afe0bb40a931d25d41d92a08b90e3c33b217d1b47210b26ca6627eb3aa3 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/openal-soft_1.23.1-2/openal-soft-1.23.1.tar.gz +wrapdb_version = 1.23.1-2 + +[provide] +openal = openal_dep diff --git a/wraps/sfml.wrap b/wraps/sfml.wrap new file mode 100644 index 0000000..9c591a1 --- /dev/null +++ b/wraps/sfml.wrap @@ -0,0 +1,13 @@ +[wrap-file] +directory = SFML-2.6.2 +source_url = https://github.com/SFML/SFML/archive/refs/tags/2.6.2.tar.gz +source_filename = 2.6.2.tar.gz +source_hash = 15ff4d608a018f287c6a885db0a2da86ea389e516d2323629e4d4407a7ce047f +patch_filename = sfml_2.6.2-1_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/sfml_2.6.2-1/get_patch +patch_hash = 36737f7fc6d616be791c6901b15414315b3a77df82dabc80b151d628e5d48386 +source_fallback_url = https://github.com/mesonbuild/wrapdb/releases/download/sfml_2.6.2-1/2.6.2.tar.gz +wrapdb_version = 2.6.2-1 + +[provide] +sfml = sfml_dep diff --git a/wraps/vorbis.wrap b/wraps/vorbis.wrap new file mode 100644 index 0000000..7425c11 --- /dev/null +++ b/wraps/vorbis.wrap @@ -0,0 +1,14 @@ +[wrap-file] +directory = libvorbis-1.3.7 +source_url = https://downloads.xiph.org/releases/vorbis/libvorbis-1.3.7.tar.xz +source_filename = libvorbis-1.3.7.tar.xz +source_hash = b33cc4934322bcbf6efcbacf49e3ca01aadbea4114ec9589d1b1e9d20f72954b +patch_filename = vorbis_1.3.7-4_patch.zip +patch_url = https://wrapdb.mesonbuild.com/v2/vorbis_1.3.7-4/get_patch +patch_hash = 979e22b24b16c927040700dfd8319cd6ba29bf52a14dbc66b1cb4ea60504f14a +wrapdb_version = 1.3.7-4 + +[provide] +vorbis = vorbis_dep +vorbisfile = vorbisfile_dep +vorbisenc = vorbisenc_dep