diff --git a/README.md b/README.md index cec4f55..95eedad 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,9 @@ My take on making a raycasting pseudo-3D engine in C++, also with my own tiny template linear algebra types. One of the goals was to make raycasting computation equations more explicit and readable. -## Controls +![alt tag](https://raw.githubusercontent.com/balintkissdev/raycaster-engine/master/demo2.png) + +## Features - Up/Down to move and Left/Right to turn around - WASD to move and strafe diff --git a/demo.gif b/demo.gif index 79b9a79..c4c7ac2 100644 Binary files a/demo.gif and b/demo.gif differ diff --git a/demo2.png b/demo2.png new file mode 100644 index 0000000..530b7b7 Binary files /dev/null and b/demo2.png differ diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index fb59a8d..2cb3a50 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,5 +16,4 @@ target_sources(${PROJECT_NAME} Texture.h Vector2.h Vector2.inl - WallTypes.h ) diff --git a/src/Camera.cpp b/src/Camera.cpp index df90077..c90f77c 100644 --- a/src/Camera.cpp +++ b/src/Camera.cpp @@ -1,7 +1,6 @@ #include "Camera.h" #include "Matrix2.h" -#include "WallTypes.h" Camera::Camera(const Vector2& position, const Vector2& direction, const float fieldOfView, Map& map) : map_(map) @@ -17,13 +16,13 @@ void Camera::move(const float moveDirection) { if (map_.position( static_cast(position_.x + moveDirection * (direction_.x * movementSpeed_)), - static_cast(position_.y)) == EMPTY_SPACE) + static_cast(position_.y)) == Map::EMPTY_SQUARE_INDEX) { position_.x += moveDirection * (direction_.x * movementSpeed_); } if (map_.position( static_cast(position_.x), - static_cast(position_.y + moveDirection * (direction_.y * movementSpeed_))) == EMPTY_SPACE) + static_cast(position_.y + moveDirection * (direction_.y * movementSpeed_))) == Map::EMPTY_SQUARE_INDEX) { position_.y += moveDirection * (direction_.y * movementSpeed_); } @@ -39,13 +38,13 @@ void Camera::strafe(const float strafeDirection) { if (map_.position( static_cast(position_.x - strafeDirection * (plane_.x * movementSpeed_)), - static_cast(position_.y)) == EMPTY_SPACE) + static_cast(position_.y)) == Map::EMPTY_SQUARE_INDEX) { position_.x -= strafeDirection * (plane_.x * movementSpeed_); } if (map_.position( static_cast(position_.x), - static_cast(position_.y - strafeDirection * (plane_.y * movementSpeed_))) == EMPTY_SPACE) + static_cast(position_.y - strafeDirection * (plane_.y * movementSpeed_))) == Map::EMPTY_SQUARE_INDEX) { position_.y -= strafeDirection * (plane_.y * movementSpeed_); } @@ -77,3 +76,13 @@ Camera& Camera::rotationSpeed(const float rotationSpeed) rotationSpeed_ = rotationSpeed; return *this; } + +Vector2 Camera::planeLeftEdgeDirection() const +{ + return direction_ - plane_; +} + +Vector2 Camera::planeRightEdgeDirection() const +{ + return direction_ + plane_; +} \ No newline at end of file diff --git a/src/Camera.h b/src/Camera.h index c6e7090..7f5e1d6 100644 --- a/src/Camera.h +++ b/src/Camera.h @@ -36,6 +36,9 @@ class Camera Camera& movementSpeed(const float movementSpeed); Camera& rotationSpeed(const float rotationSpeed); + [[nodiscard]] Vector2 planeLeftEdgeDirection() const; + [[nodiscard]] Vector2 planeRightEdgeDirection() const; + private: Map& map_; Vector2 position_; diff --git a/src/Game.cpp b/src/Game.cpp index 16c0c72..ad5a932 100644 --- a/src/Game.cpp +++ b/src/Game.cpp @@ -1,7 +1,6 @@ #include "Game.h" #include "SDLRenderer.h" -#include "WallTypes.h" #include @@ -167,7 +166,7 @@ void Game::event() running_ = false; break; case SDLK_m: - overviewMapOn_ = !overviewMapOn_; + raycaster_.toggleMapDraw(); break; } } @@ -182,98 +181,10 @@ void Game::update() void Game::render() { renderer_->clearScreen(); - raycaster_.drawEverything(*renderer_); - - if (overviewMapOn_) - { - drawMap(); - } - renderer_->refreshScreen(); } -// TODO: this is a very basic map -// TODO: Move this into RayCaster -void Game::drawMap() -{ - static const WallColor grey = {160, 160, 160}; - static const WallColor red = {255, 0, 0}; - static const WallColor green = {0, 255, 0}; - static const WallColor blue = {0, 0, 255}; - static const WallColor yellow = {255, 255, 0}; - - struct Rectangle - { - size_t x; - size_t y; - size_t width; - size_t height; - }; - - Rectangle rect; - - // Draw blocks - const size_t squareSize = 32; - for (size_t row = 0; row < map_.rowCount(); ++row) - { - for (size_t column = 0; column < map_.columnCount(); ++column) - { - WallColor wallColor = grey; - switch (map_.position(row, column)) - { - case RED_WALL: - wallColor = red; - break; - case GREEN_WALL: - wallColor = green; - break; - case BLUE_WALL: - wallColor = blue; - break; - case YELLOW_WALL: - wallColor = yellow; - break; - } - renderer_->setDrawColor(wallColor.red, wallColor.green, wallColor.blue); - - // Watch out: row/column is not the same as x/y. This was a source of a nasty bug. - rect = {0 + squareSize * column, 0 + squareSize * row, squareSize, squareSize}; - renderer_->fillRectangle( - static_cast(rect.x), - static_cast(rect.y), - static_cast(rect.width), - static_cast(rect.height)); - renderer_->setDrawColor(0, 0, 0); - renderer_->drawRectangle( - static_cast(rect.x), - static_cast(rect.y), - static_cast(rect.width), - static_cast(rect.height)); - } - } - - // Draw player - renderer_->setDrawColor(255, 255, 255); - // HACK: need to change internal representation of the map instead switching x/y here - rect = { - squareSize * static_cast(camera_.position().y) + squareSize / 4, - squareSize * static_cast(camera_.position().x) + squareSize / 4, - squareSize / 2, - squareSize / 2}; - renderer_->fillRectangle( - static_cast(rect.x), - static_cast(rect.y), - static_cast(rect.width), - static_cast(rect.height)); - renderer_->setDrawColor(0, 0, 0, 255); - renderer_->drawRectangle( - static_cast(rect.x), - static_cast(rect.y), - static_cast(rect.width), - static_cast(rect.height)); -} - int main(int /*argc*/, char** /*argv*/) { Game game; diff --git a/src/Game.h b/src/Game.h index 5ef60f9..8534148 100644 --- a/src/Game.h +++ b/src/Game.h @@ -41,13 +41,10 @@ class Game std::unique_ptr renderer_; float movementSpeed_; bool running_{false}; - bool overviewMapOn_{false}; void event(); void update(); void render(); - - void drawMap(); }; #endif diff --git a/src/Map.h b/src/Map.h index 78b04cf..0de6a24 100644 --- a/src/Map.h +++ b/src/Map.h @@ -1,7 +1,7 @@ #ifndef MAP_H #define MAP_H -#include +#include "Vector2.h" #include #include @@ -10,6 +10,8 @@ class Map { public: + static constexpr size_t EMPTY_SQUARE_INDEX = 0; + static std::optional create(const std::string& mapFilePath); Map(); diff --git a/src/RayCaster.cpp b/src/RayCaster.cpp index 75abd24..6dc1c7c 100644 --- a/src/RayCaster.cpp +++ b/src/RayCaster.cpp @@ -32,10 +32,24 @@ void RayCaster::drawEverything(IRenderer& renderer) drawTop(); drawBottom(); drawWalls(); + if (overviewMapOn_) + { + drawMap(); + } renderer.drawBuffer(drawBuffer_.data()); } +void RayCaster::toggleMapDraw() +{ + overviewMapOn_ = !overviewMapOn_; +} + +constexpr uint32_t RayCaster::rgbToUint32(const uint8_t r, const uint8_t g, const uint8_t b) +{ + return (255 << 24) + (r << 16) + (g << 8) + b; +} + void RayCaster::drawTop() { // FIXME: Use full sky texture, not just half. It bugs out if walls are too far. @@ -46,8 +60,8 @@ void RayCaster::drawBottom() { for (size_t y = screenHeight_ / 2 + 1; y < screenHeight_; ++y) { - const Vector2 leftmostRayDirection = camera_.direction() - camera_.plane(); - const Vector2 rightmostRayDirection = camera_.direction() + camera_.plane(); + const Vector2 leftmostRayDirection = camera_.planeLeftEdgeDirection(); + const Vector2 rightmostRayDirection = camera_.planeRightEdgeDirection(); const size_t screenCenterDistance = y - screenHeight_ / 2; const float cameraVerticalPosition = 0.5f * static_cast(screenHeight_); @@ -103,7 +117,20 @@ void RayCaster::drawWalls() const auto [drawStart, drawEnd] = calculateDrawLocations(wallColumnHeight); - if (mapSquareIndex != EMPTY_SPACE) + if (x == 0) + { + planeLeftDistance_ = wallDistance; + } + if (x == screenWidth_ / 2) + { + cameraLineDistance_ = wallDistance; + } + if (x == static_cast(screenWidth_ - 1)) + { + planeRightDistance_ = wallDistance; + } + + if (mapSquareIndex != Map::EMPTY_SQUARE_INDEX) { drawTexturedColumn( x, mapSquareIndex, side, wallDistance, wallColumnHeight, rayDirection, drawStart, drawEnd); @@ -111,6 +138,97 @@ void RayCaster::drawWalls() } } +void RayCaster::drawMap() +{ + drawMapSquares(); + drawMapPlayer(); +} + +void RayCaster::drawMapSquares() +{ + for (size_t column = 0; column < map_.columnCount(); ++column) + { + for (size_t row = 0; row < map_.rowCount(); ++row) + { + size_t mapSquareIndex = map_.position(row, column); + Texture* wallTexture = &(*bottomTexture_); + if (mapSquareIndex != Map::EMPTY_SQUARE_INDEX) + { + wallTexture = mapIndexToWallTexture(mapSquareIndex); + } + + // TODO: It's ugly as opposed to a normal scaling algorithm like Lanczos resampling, but it's not noticable + // at smaller sizes + // TODO: Optimization: generate mipmaps and store them instead of calculating every time + for (size_t i = 0; i < wallTexture->width / 2; ++i) + { + for (size_t j = 0; j < wallTexture->height / 2; ++j) + { + plotPixel( + MAP_SQUARE_SIZE * column + i, + MAP_SQUARE_SIZE * row + j, + wallTexture->texels[j * 2 * wallTexture->height + i * 2]); + } + } + } + } +} + +void RayCaster::drawMapPlayer() +{ + static constexpr uint32_t positionColor = rgbToUint32(255, 0, 0); + + const Vector2 mapPlayerPosition{ + MAP_SQUARE_SIZE * camera_.position().x, + MAP_SQUARE_SIZE * camera_.position().y, + }; + // Draw a thicker point + plotPixel(static_cast(mapPlayerPosition.y), static_cast(mapPlayerPosition.x), positionColor); + plotPixel( + static_cast(mapPlayerPosition.y - 1), static_cast(mapPlayerPosition.x), positionColor); + plotPixel( + static_cast(mapPlayerPosition.y + 1), static_cast(mapPlayerPosition.x), positionColor); + plotPixel( + static_cast(mapPlayerPosition.y), static_cast(mapPlayerPosition.x - 1), positionColor); + plotPixel( + static_cast(mapPlayerPosition.y), static_cast(mapPlayerPosition.x + 1), positionColor); + + drawMapDebugLines(mapPlayerPosition); +} + +void RayCaster::drawMapDebugLines(const Vector2& mapPlayerPosition) +{ + static constexpr uint32_t cameraLineColor = rgbToUint32(0, 255, 0); + static constexpr uint32_t planeColor = rgbToUint32(0, 0, 255); + + // Draw camera line + for (uint16_t i = 2; i < static_cast(cameraLineDistance_ * MAP_SQUARE_SIZE); ++i) + { + plotPixel( + static_cast(mapPlayerPosition.y + (static_cast(i) * camera_.direction().y)), + static_cast(mapPlayerPosition.x + (static_cast(i) * camera_.direction().x)), + cameraLineColor); + } + + // Draw leftmost plane line + for (uint16_t i = 2; i < planeLeftDistance_ * MAP_SQUARE_SIZE; ++i) + { + plotPixel( + static_cast(mapPlayerPosition.y + (static_cast(i) * camera_.planeLeftEdgeDirection().y)), + static_cast(mapPlayerPosition.x + (static_cast(i) * camera_.planeLeftEdgeDirection().x)), + planeColor); + } + + // Draw rightmost plane line + for (uint16_t i = 2; i < planeRightDistance_ * MAP_SQUARE_SIZE; ++i) + { + plotPixel( + static_cast(mapPlayerPosition.y + (static_cast(i) * camera_.planeRightEdgeDirection().y)), + static_cast(mapPlayerPosition.x + (static_cast(i) * camera_.planeRightEdgeDirection().x)), + planeColor); + } +} + std::pair, Vector2> RayCaster::calculateInitialStep( const Vector2& mapSquarePosition, const Vector2& rayDirection, const Vector2& rayStepDistance) { @@ -140,15 +258,15 @@ std::pair, Vector2> RayCaster::calculateInitialStep( return {stepDirection, sideDistance}; } -std::pair RayCaster::performDDA( +std::pair RayCaster::performDDA( const Vector2& stepDirection, const Vector2& rayStepDistance, Vector2& mapSquarePositionInOut, Vector2& sideDistanceInOut) const { // Scan where ray hits a wall - WallSide side = VERTICAL; - size_t mapSquareIndex = EMPTY_SPACE; + WallSide side = WallSide::VERTICAL; + size_t mapSquareIndex = Map::EMPTY_SQUARE_INDEX; while (true) { // Jump to next square @@ -156,18 +274,18 @@ std::pair RayCaster::performDDA( { sideDistanceInOut.x += rayStepDistance.x; mapSquarePositionInOut.x += stepDirection.x; - side = VERTICAL; + side = WallSide::VERTICAL; } else { sideDistanceInOut.y += rayStepDistance.y; mapSquarePositionInOut.y += stepDirection.y; - side = HORIZONTAL; + side = WallSide::HORIZONTAL; } // Check for hit mapSquareIndex = map_.position(mapSquarePositionInOut.x, mapSquarePositionInOut.y); - if (mapSquareIndex != EMPTY_SPACE) + if (mapSquareIndex != Map::EMPTY_SQUARE_INDEX) { break; } @@ -182,12 +300,12 @@ float RayCaster::calculateWallDistance( const Vector2& stepDirection, const Vector2& rayDirection) { - return (side == VERTICAL) ? ((static_cast(mapSquarePosition.x) - camera_.position().x + - static_cast(1 - stepDirection.x) / 2) / - rayDirection.x) - : ((static_cast(mapSquarePosition.y) - camera_.position().y + - static_cast(1 - stepDirection.y) / 2) / - rayDirection.y); + return (side == WallSide::VERTICAL) ? ((static_cast(mapSquarePosition.x) - camera_.position().x + + static_cast(1 - stepDirection.x) / 2) / + rayDirection.x) + : ((static_cast(mapSquarePosition.y) - camera_.position().y + + static_cast(1 - stepDirection.y) / 2) / + rayDirection.y); } std::pair RayCaster::calculateDrawLocations(const int wallColumnHeight) const @@ -218,10 +336,10 @@ void RayCaster::drawTexturedColumn( const int drawEnd) { // 1 subtracted from it so that texture 0 can be used - const Texture& wallTexture = *(wallTextures_[mapSquareIndex - 1]); + const Texture& wallTexture = *mapIndexToWallTexture(mapSquareIndex); float wallHitX; - if (side == VERTICAL) + if (side == WallSide::VERTICAL) { wallHitX = camera_.position().y + wallDistance * ray.y; } @@ -232,11 +350,11 @@ void RayCaster::drawTexturedColumn( wallHitX -= std::floor((wallHitX)); auto texCoordX = static_cast(wallHitX * static_cast(wallTexture.width)); - if (side == VERTICAL && ray.x > 0) + if (side == WallSide::VERTICAL && ray.x > 0) { texCoordX = wallTexture.width - texCoordX - 1; } - if (side == HORIZONTAL && ray.y < 0) + if (side == WallSide::HORIZONTAL && ray.y < 0) { texCoordX = wallTexture.width - texCoordX - 1; } @@ -257,7 +375,7 @@ void RayCaster::drawTexturedColumn( uint32_t texel = wallTexture.texels[texelIndex]; // Shade horizontal sides darker - if (side == HORIZONTAL) + if (side == WallSide::HORIZONTAL) { texel = (texel >> 1) & DARKEN_MASK; } @@ -269,4 +387,10 @@ void RayCaster::drawTexturedColumn( void RayCaster::plotPixel(const uint16_t x, const uint16_t y, const uint32_t pixel) { drawBuffer_[y * screenWidth_ + x] = pixel; -} \ No newline at end of file +} + +Texture* RayCaster::mapIndexToWallTexture(const size_t index) +{ + // 1 subtracted from it so that texture 0 can be used + return &(*(wallTextures_[index - 1])); +} diff --git a/src/RayCaster.h b/src/RayCaster.h index 79a91a5..271bfeb 100644 --- a/src/RayCaster.h +++ b/src/RayCaster.h @@ -4,7 +4,6 @@ #include "Camera.h" #include "Map.h" #include "Texture.h" -#include "WallTypes.h" #include #include @@ -19,13 +18,27 @@ class RayCaster bool init(IRenderer& renderer); void drawEverything(IRenderer& renderer); + void toggleMapDraw(); private: + enum class WallSide + { + VERTICAL, + HORIZONTAL + }; + + static constexpr size_t MAP_SQUARE_SIZE = 32; static constexpr uint32_t DARKEN_MASK = 8355711; + static constexpr uint32_t rgbToUint32(const uint8_t r, const uint8_t g, const uint8_t b); + void drawTop(); void drawBottom(); void drawWalls(); + void drawMap(); + void drawMapSquares(); + void drawMapPlayer(); + void drawMapDebugLines(const Vector2& mapPlayerPosition); std::pair, Vector2> calculateInitialStep( const Vector2& mapSquarePosition, @@ -63,6 +76,7 @@ class RayCaster const int drawEnd); void plotPixel(const uint16_t x, const uint16_t y, const uint32_t pixel); + Texture* mapIndexToWallTexture(const size_t index); Camera& camera_; Map& map_; @@ -71,6 +85,11 @@ class RayCaster std::array, 4> wallTextures_; std::vector drawBuffer_; uint16_t screenWidth_, screenHeight_; + + bool overviewMapOn_{false}; + float cameraLineDistance_; + float planeLeftDistance_; + float planeRightDistance_; }; #endif diff --git a/src/SDLRenderer.cpp b/src/SDLRenderer.cpp index 84783fc..fcedf29 100644 --- a/src/SDLRenderer.cpp +++ b/src/SDLRenderer.cpp @@ -30,13 +30,15 @@ bool SDLRenderer::init(const uint16_t screenWidth, const uint16_t screenHeight, return false; } - renderer_.reset(SDL_CreateRenderer(window_.get(), -1, + renderer_.reset(SDL_CreateRenderer( + window_.get(), + -1, #ifdef __EMSCRIPTEN__ - SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC // TODO: Not sure if makes difference in speed. + SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC // TODO: Not sure if makes difference in speed. #else 0 #endif - )); + )); if (!renderer_) { std::cerr << "Error creating renderer: " + std::string(SDL_GetError()); diff --git a/src/WallTypes.h b/src/WallTypes.h deleted file mode 100644 index db6f773..0000000 --- a/src/WallTypes.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef WALL_TYPES_H -#define WALL_TYPES_H - -#include - -// TODO: Not relevant since texturing, so these will be removed. -enum SquareType -{ - EMPTY_SPACE, - RED_WALL, - GREEN_WALL, - BLUE_WALL, - YELLOW_WALL -}; - -enum WallSide -{ - VERTICAL, - HORIZONTAL -}; - -// TODO: Not relevant since texturing, so these will be removed. -struct WallColor -{ - uint8_t red, green, blue; -}; - -#endif