From ca80736d7c2cc9f52e72d3f87de8713dadb50e81 Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Sat, 4 Jan 2025 12:20:41 -0500 Subject: [PATCH] First cut at a replica of the python raycaster. Left side almost works the same but have to sort out math differences. --- dbc.cpp | 40 ++ dbc.hpp | 29 + main.cpp | 34 -- matrix.cpp | 94 +++ matrix.hpp | 305 ++++++++++ meson.build | 34 +- point.hpp | 19 + pycaster.py | 126 ++++ raycaster.cpp | 102 ++++ quickcg.cpp => scratchpad/quickcg.cpp | 9 +- quickcg.h => scratchpad/quickcg.h | 0 .../raycaster_flat.cpp | 80 +-- scratchpad/raycaster_sprites.cpp | 471 +++++++++++++++ scratchpad/raycaster_textured.cpp | 293 ++++++++++ sdlprog.cpp => scratchpad/sdlprog.cpp | 0 scratchpad/timcaster.cpp | 553 ++++++++++++++++++ wraps/flac.wrap | 13 + wraps/ogg.wrap | 13 + wraps/openal-soft.wrap | 13 + wraps/sfml.wrap | 13 + wraps/vorbis.wrap | 14 + 21 files changed, 2165 insertions(+), 90 deletions(-) create mode 100644 dbc.cpp create mode 100644 dbc.hpp delete mode 100644 main.cpp create mode 100644 matrix.cpp create mode 100644 matrix.hpp create mode 100644 point.hpp create mode 100644 pycaster.py create mode 100644 raycaster.cpp rename quickcg.cpp => scratchpad/quickcg.cpp (99%) rename quickcg.h => scratchpad/quickcg.h (100%) rename raycaster_flat.cpp => scratchpad/raycaster_flat.cpp (82%) create mode 100644 scratchpad/raycaster_sprites.cpp create mode 100644 scratchpad/raycaster_textured.cpp rename sdlprog.cpp => scratchpad/sdlprog.cpp (100%) create mode 100644 scratchpad/timcaster.cpp create mode 100644 wraps/flac.wrap create mode 100644 wraps/ogg.wrap create mode 100644 wraps/openal-soft.wrap create mode 100644 wraps/sfml.wrap create mode 100644 wraps/vorbis.wrap 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