From d230b152cfab6fe1d7a61ba6096b5841478eea2f Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Wed, 15 Jan 2025 12:25:09 -0500 Subject: [PATCH] BREAKING: This code does NOT work, but has a segv on startup on windows due to ...magic? --- Makefile | 13 +- main.cpp | 44 +++++ meson.build | 5 +- raycaster.cpp | 0 raycaster.hpp | 477 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 532 insertions(+), 7 deletions(-) create mode 100644 main.cpp create mode 100644 raycaster.cpp create mode 100644 raycaster.hpp diff --git a/Makefile b/Makefile index 247d28f..b5dadbe 100644 --- a/Makefile +++ b/Makefile @@ -13,14 +13,17 @@ test: build ./builddir/runtests run: build test - powershell "cp ./builddir/lodecaster.exe ." - ./lodecaster + powershell "cp ./builddir/zedcaster.exe ." + ./zedcaster + +debug: build + gdb --nx -x .gdbinit --ex run --args builddir/zedcaster.exe + +debug_run: build + gdb --nx -x .gdbinit --batch --ex run --ex bt --ex q --args builddir/zedcaster.exe clean: meson compile --clean -C builddir debug_test: build gdb --nx -x .gdbinit --ex run --args builddir/runtests.exe - -cover: - gcovr --html coverage/index.html --gcov-ignore-errors=no_working_dir_found --exclude "scratchpad.*" --exclude "subprojects.*" --html-nested coverage/ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..6880d39 --- /dev/null +++ b/main.cpp @@ -0,0 +1,44 @@ +#include "raycaster.hpp" + +static const int SCREEN_HEIGHT=960; +static const int SCREEN_WIDTH=1280; + +int main() { + sf::RenderWindow window(sf::VideoMode(SCREEN_WIDTH, SCREEN_HEIGHT), "Zed's Ray Caster Game Thing"); + + Raycaster rayview(window); + + /* + using KB = sf::Keyboard; + rayview.load_textures(); + + double moveSpeed = 0.1; + double rotSpeed = 0.1; + + while(window.isOpen()) { + rayview.render(); + + if(KB::isKeyPressed(KB::W)) { + rayview.move_forward(moveSpeed); + } else if(KB::isKeyPressed(KB::S)) { + rayview.move_backward(moveSpeed); + } + + if(KB::isKeyPressed(KB::D)) { + rayview.rotate_right(rotSpeed); + } else if(KB::isKeyPressed(KB::A)) { + rayview.rotate_left(rotSpeed); + } + + sf::Event event; + while(window.pollEvent(event)) { + if(event.type == sf::Event::Closed) { + window.close(); + } + } + + } + */ + + return 0; +} diff --git a/meson.build b/meson.build index e4b80ea..aa89fa6 100644 --- a/meson.build +++ b/meson.build @@ -15,9 +15,10 @@ executable('runtests', [ ], dependencies: dependencies + [catch2]) -executable('sfmlcaster', [ +executable('zedcaster', [ 'dbc.cpp', 'matrix.cpp', - 'sfmlcaster.cpp', + 'raycaster.cpp', + 'main.cpp' ], dependencies: dependencies) diff --git a/raycaster.cpp b/raycaster.cpp new file mode 100644 index 0000000..e69de29 diff --git a/raycaster.hpp b/raycaster.hpp new file mode 100644 index 0000000..18a88b2 --- /dev/null +++ b/raycaster.hpp @@ -0,0 +1,477 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include "matrix.hpp" +#include +#include +#include "dbc.hpp" + +using matrix::Matrix; +using namespace fmt; + +#define texWidth 256 // must be power of two +#define texHeight 256 // must be power of two + +#define numSprites 1 +#define numTextures 11 + +struct Sprite { + double x; + double y; + double elevation; + int texture; +}; + +//parameters for scaling and moving the sprites +#define uDiv 1 +#define vDiv 1 + +#define RAY_VIEW_WIDTH 960 +#define RAY_VIEW_HEIGHT 720 +#define RAY_VIEW_X (1280 - RAY_VIEW_WIDTH) +#define RAY_VIEW_Y 0 + +#define rgba_color(r,g,b,a) (r<<(0*8))|(g<<(1*8))|(b<<(2*8))|(a<<(3*8)) +#define gray_color(c) rgba_color(c, c, c, 255) + +union RGBA { + struct { + uint8_t r; + uint8_t g; + uint8_t b; + uint8_t a; + } color; + + uint32_t out; +}; + +inline void RGBA_brightness(RGBA& pixel, double distance) { + pixel.color.r /= distance; + pixel.color.g /= distance; + pixel.color.b /= distance; +} + +struct Raycaster { + std::vector SPRITE; + std::vector texture[numTextures]; + Matrix MAP; + int PITCH=0; + // I chose fixed textures for this instead + const int floorTexture = 3; + const int ceilingTexture = 6; + + float player_x = RAY_VIEW_HEIGHT / 2; + float player_y = RAY_VIEW_HEIGHT / 2; + + // x and y start position + double posX = player_x / TILE_SIZE; + double posY = player_y / TILE_SIZE; + + // initial direction vector + double dirX = -1; + double dirY = 0; + + // the 2d raycaster version of camera plane + double planeX = 0; + double planeY = 0.66; + + std::array ZBuffer; + int spriteOrder[numSprites]; + double spriteDistance[numSprites]; + + RGBA pixels[RAY_VIEW_WIDTH * RAY_VIEW_HEIGHT]; + + sf::Texture view_texture; + sf::Sprite view_sprite; + + sf::RenderWindow& $window; + int TILE_SIZE; + + Raycaster(sf::RenderWindow& window) : + $window(window) + { + $window.setVerticalSyncEnabled(true); + view_texture.create(RAY_VIEW_WIDTH, RAY_VIEW_HEIGHT); + view_sprite.setTexture(view_texture); + view_sprite.setPosition(RAY_VIEW_X, 0); + + SPRITE = {{4.0, 3.55, 0, 8}}; + MAP = {{8,8,8,8,8,8,8,8,8}, + {8,0,2,0,0,0,0,0,8}, + {8,0,7,0,0,5,6,0,8}, + {8,0,0,0,0,0,0,0,8}, + {8,8,0,0,0,0,0,8,8}, + {8,0,0,1,3,4,0,0,8}, + {8,0,0,0,0,0,8,8,8}, + {8,0,0,0,0,0,0,0,8}, + {8,8,8,8,8,8,8,8,8} + }; + + TILE_SIZE = RAY_VIEW_HEIGHT / matrix::width(MAP); + } + + inline size_t pixcoord(int x, int y) { + return ((y) * RAY_VIEW_WIDTH) + (x); + } + + void load_image(std::vector& texture, const char *filename) { + sf::Image img; + bool good = img.loadFromFile(filename); + dbc::check(good, format("failed to load {}", filename)); + uint32_t *pixbuf = (uint32_t *)img.getPixelsPtr(); + std::copy_n(pixbuf, texture.size(), texture.begin()); + } + + void load_textures() { + for(int i = 0; i < numTextures; i++) { + texture[i].resize(texWidth * texHeight); + } + + load_image(texture[0], "assets/tile16.png"); + load_image(texture[1], "assets/tile02.png"); + load_image(texture[2], "assets/tile03.png"); + load_image(texture[3], "assets/tile32.png"); + load_image(texture[4], "assets/tile05.png"); + load_image(texture[5], "assets/tile17.png"); + load_image(texture[6], "assets/tile10.png"); + load_image(texture[7], "assets/tile01.png"); + load_image(texture[8], "assets/portal.png"); + } + + void draw_pixel_buffer() { + view_texture.update((uint8_t *)pixels, RAY_VIEW_WIDTH, RAY_VIEW_HEIGHT, 0, 0); + // BUG: can I do this once and just update it? + $window.draw(view_sprite); + } + + void clear() { + std::fill_n((uint32_t *)pixels, RAY_VIEW_WIDTH * RAY_VIEW_HEIGHT, 0); + $window.clear(); + } + + void cast_rays(Matrix& map) { + int w = RAY_VIEW_WIDTH; + int h = RAY_VIEW_HEIGHT; + double perpWallDist; + + // WALL CASTING + for(int x = 0; x < w; x++) { + // calculate ray position and direction + double cameraX = 2 * x / double(w) - 1; // x-coord 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 pos 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 = std::abs(1.0 / rayDirX); + double deltaDistY = std::abs(1.0 / rayDirY); + + int stepX = 0; + int stepY = 0; + int hit = 0; + int side = 0; + + // 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) { + if(sideDistX < sideDistY) { + sideDistX += deltaDistX; + mapX += stepX; + side = 0; + } else { + sideDistY += deltaDistY; + mapY += stepY; + side = 1; + } + + if(map[mapY][mapX] > 0) hit = 1; + } + + if(side == 0) { + perpWallDist = (sideDistX - deltaDistX); + } else { + perpWallDist = (sideDistY - deltaDistY); + } + + int lineHeight = int(h / perpWallDist); + + 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; + + int texNum = MAP[mapY][mapX] - 1; + + // 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 coorindate 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; + + // LODE: 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++) { + int texY = (int)texPos & (texHeight - 1); + texPos += step; + RGBA pixel{.out=texture[texNum][texHeight * texY + texX]}; + RGBA_brightness(pixel, perpWallDist); + pixels[pixcoord(x, y)] = pixel; + } + + // SET THE ZBUFFER FOR THE SPRITE CASTING + ZBuffer[x] = perpWallDist; + } + + // SPRITE CASTING + // sort sprites from far to close + for(int i = 0; i < numSprites; i++) { + spriteOrder[i] = i; + // this is just the distance calculation + spriteDistance[i] = ((posX - SPRITE[i].x) * + (posX - SPRITE[i].x) + + (posY - SPRITE[i].y) * + (posY - SPRITE[i].y)); + } + + sort_sprites(spriteOrder, spriteDistance, numSprites); + + // after sorting the sprites, do the projection + for(int i = 0; i < numSprites; i++) { + int sprite_index = spriteOrder[i]; + Sprite& sprite_rec = SPRITE[sprite_index]; + double spriteX = sprite_rec.x - posX; + double spriteY = sprite_rec.y - posY; + int sprite_texture_number = sprite_rec.texture; + auto sprite_texture = texture[sprite_texture_number]; + + //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); + //this is actually the depth inside the screen, that what Z is in 3D, the distance of sprite to player, matching sqrt(spriteDistance[i]) + + double transformY = invDet * (-planeY * spriteX + planeX * spriteY); + + int spriteScreenX = int((w / 2) * (1 + transformX / transformY)); + + int vMoveScreen = int(sprite_rec.elevation * -1 / transformY); + + // calculate the height of the sprite on screen + //using "transformY" instead of the real distance prevents fisheye + int spriteHeight = abs(int(h / transformY)) / vDiv; + + //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 the the sprite + // same as height of sprite, given that it's square + int spriteWidth = abs(int(h / transformY)) / uDiv; + 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 the 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++) { + //256 and 128 factors to avoid floats + int d = (y - vMoveScreen) * 256 - h * 128 + spriteHeight * 128; + int texY = ((d * texHeight) / spriteHeight) / 256; + //get current color from the texture + uint32_t color = sprite_texture[texWidth * texY + texX]; + // poor person's transparency, get current color from the texture + if((color & 0x00FFFFFF) != 0) { + RGBA pixel{.out=color}; + RGBA_brightness(pixel, perpWallDist); + pixels[pixcoord(stripe, y)] = pixel; + } + } + } + } + } + } + + void draw_ceiling_floor() { + int screenHeight = RAY_VIEW_HEIGHT; + int screenWidth = RAY_VIEW_WIDTH; + + for(int y = screenHeight / 2 + 1; y < screenHeight; ++y) { + // rayDir for leftmost ray (x=0) and rightmost (x = w) + float rayDirX0 = dirX - planeX; + float rayDirY0 = dirY - planeY; + float rayDirX1 = dirX + planeX; + float rayDirY1 = dirY + planeY; + + // current y position compared to the horizon + int p = y - screenHeight / 2; + + // vertical position of the camera + // 0.5 will the camera at the center horizon. For a + // different value you need a separate loop for ceiling + // and floor since they're no longer symmetrical. + 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 + // See NOTE in Lode's code for more. + 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 wight 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 taken from the int parts of + // floorX and floorY. + int cellX = int(floorX); + int cellY = int(floorY); + + // get the texture coordinat from the fractional part + int tx = int(texWidth * (floorX - cellX)) & (texWidth - 1); + int ty = int(texWidth * (floorY - cellY)) & (texHeight - 1); + + floorX += floorStepX; + floorY += floorStepY; + + // now get the pixel from the texture + uint32_t color; + // this uses the previous ty/tx fractional parts of + // floorX cellX to find the texture x/y. How? + + // FLOOR + color = texture[floorTexture][texWidth * ty + tx]; + pixels[pixcoord(x, y)].out = color; + + // CEILING + color = texture[ceilingTexture][texWidth * ty + tx]; + pixels[pixcoord(x, screenHeight - y - 1)].out = color; + } + } + } + + void render() { + draw_ceiling_floor(); + cast_rays(MAP); + draw_pixel_buffer(); + $window.display(); + } + + bool empty_space(int new_x, int new_y) { + dbc::check((size_t)new_x < matrix::width(MAP), + format("x={} too wide={}", new_x, matrix::width(MAP))); + dbc::check((size_t)new_y < matrix::height(MAP), + format("y={} too high={}", new_y, matrix::height(MAP))); + + return MAP[new_y][new_x] == 0; + } + + + void sort_sprites(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 + for(int i = 0; i < amount; i++) { + dist[i] = sprites[amount - i - 1].first; + order[i] = sprites[amount - i - 1].second; + } + } + + void move_forward(double moveSpeed) { + if(empty_space(int(posX + dirX * moveSpeed), int(posY))) posX += dirX * moveSpeed; + if(empty_space(int(posX), int(posY + dirY * moveSpeed))) posY += dirY * moveSpeed; + } + + void move_backward(double moveSpeed) { + if(empty_space(int(posX - dirX * moveSpeed), int(posY))) posX -= dirX * moveSpeed; + if(empty_space(int(posX), int(posY - dirY * moveSpeed))) posY -= dirY * moveSpeed; + } + + void rotate_right(double rotSpeed) { + 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); + } + + void rotate_left(double rotSpeed) { + 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); + } +};