#include "raycaster.hpp" #include "texture.hpp" #include #include "constants.hpp" using namespace fmt; using std::make_unique; union ColorConv { struct { uint8_t r; uint8_t g; uint8_t b; uint8_t a; } as_color; uint32_t as_int; }; inline uint32_t dumb_lighting(uint32_t pixel, double distance) { if(distance < 1.0) return pixel; ColorConv conv{.as_int=pixel}; conv.as_color.r /= distance; conv.as_color.g /= distance; conv.as_color.b /= distance; return conv.as_int; } Raycaster::Raycaster(sf::RenderWindow& window, TexturePack &textures, Matrix &map, int width, int height) : $textures(textures), $view_texture({(unsigned int)width, (unsigned int)height}), $view_sprite($view_texture), $width(width), $height(height), $window(window), $map(map), spriteOrder(NUM_SPRITES), spriteDistance(NUM_SPRITES), ZBuffer(width), $anim(256, 256, 10, "assets/monster-1.ogg") { $view_sprite.setPosition({0, 0}); $pixels = make_unique($width * $height); $view_texture.setSmooth(false); } void Raycaster::init_shaders() { bool good = $paused.loadFromFile("shaders/modal.frag", sf::Shader::Type::Fragment); dbc::check(good, "shader could not be loaded"); $paused.setUniform("offsetFactor", sf::Glsl::Vec2{0.01f, 0.01f}); $paused.setUniform("darkness", 0.01f); } void Raycaster::set_position(int x, int y) { $view_sprite.setPosition({(float)x, (float)y}); } void Raycaster::position_camera(float player_x, float player_y) { // x and y start position $posX = player_x; $posY = player_y; } void Raycaster::draw_pixel_buffer() { $view_texture.update((uint8_t *)$pixels.get(), {(unsigned int)$width, (unsigned int)$height}, {0, 0}); $window.draw($view_sprite, $active_shader); } void Raycaster::clear() { std::fill_n($pixels.get(), $width * $height, 0); $window.clear(); } void Raycaster::sprite_casting() { const int textureWidth = TEXTURE_WIDTH; const int textureHeight = TEXTURE_HEIGHT; const int halfHeight = TEXTURE_HEIGHT / 2; // sort sprites from far to close for(int i = 0; i < NUM_SPRITES; i++) { auto& sprite = $textures.get_sprite(i); spriteOrder[i] = i; // this is just the distance calculation spriteDistance[i] = (($posX - sprite.x) * ($posX - sprite.x) + ($posY - sprite.y) * ($posY - sprite.y)); } sort_sprites(spriteOrder, spriteDistance, NUM_SPRITES); // after sorting the sprites, do the projection for(int i = 0; i < NUM_SPRITES; i++) { int sprite_index = spriteOrder[i]; Sprite& sprite_rec = $textures.get_sprite(sprite_index); // TODO: this must die auto sf_sprite = sprite_rec.sprite.sprite; double spriteX = sprite_rec.x - $posX; double spriteY = sprite_rec.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); //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(($width / 2) * (1 + transformX / transformY)); // calculate the height of the sprite on screen //using "transformY" instead of the real distance prevents fisheye int spriteHeight = abs(int($height / transformY)); // calculate width the the sprite // same as height of sprite, given that it's square int spriteWidth = abs(int($height / transformY)); int drawStartX = -spriteWidth / 2 + spriteScreenX; if(drawStartX < 0) drawStartX = 0; int drawEndX = spriteWidth / 2 + spriteScreenX; if(drawEndX > $width) drawEndX = $width; int stripe = drawStartX; for(; stripe < drawEndX; stripe++) { //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])) break; } int texX_end = int(textureWidth * (stripe - (-spriteWidth / 2 + spriteScreenX)) * textureWidth / spriteWidth) / textureWidth; if(drawStartX < drawEndX && transformY > 0 && transformY < ZBuffer[drawStartX]) { //calculate lowest and highest pixel to fill in current stripe int drawStartY = -spriteHeight / 2 + $height / 2; if(drawStartY < 0) drawStartY = 0; int drawEndY = spriteHeight / 2 + $height / 2; if(drawEndY >= $height) drawEndY = $height - 1; int texX = int(textureWidth * (drawStartX - (-spriteWidth / 2 + spriteScreenX)) * textureWidth / spriteWidth) / textureWidth; float x = float(drawStartX + RAY_VIEW_X); float y = float(drawStartY + RAY_VIEW_Y); float sprite_w = float(spriteWidth) / float(textureWidth); float sprite_h = float(spriteHeight) / float(textureHeight); int d = y * textureHeight - $height * halfHeight + spriteHeight * halfHeight; int texY = ((d * textureHeight) / spriteHeight) / textureHeight; sf_sprite->setScale({sprite_w, sprite_h}); $anim.step(*sf_sprite, texX, texY, texX_end - texX, textureHeight); sf_sprite->setPosition({x, y}); $window.draw(*sf_sprite, $active_shader); } } } void Raycaster::cast_rays() { constexpr static const int textureWidth = TEXTURE_WIDTH; constexpr static const int textureHeight = TEXTURE_HEIGHT; double perpWallDist; // WALL CASTING for(int x = 0; x < $width; x++) { // calculate ray position and direction double cameraX = 2 * x / double($width) - 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($height / perpWallDist); int drawStart = -lineHeight / 2 + $height / 2 + $pitch; if(drawStart < 0) drawStart = 0; int drawEnd = lineHeight / 2 + $height / 2 + $pitch; if(drawEnd >= $height) drawEnd = $height - 1; auto texture = $textures.get_surface($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(textureWidth)); if(side == 0 && rayDirX > 0) texX = textureWidth - texX - 1; if(side == 1 && rayDirY < 0) texX = textureWidth - 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 * textureHeight / lineHeight; // Starting texture coordinate double texPos = (drawStart - $pitch - $height / 2 + lineHeight / 2) * step; for(int y = drawStart; y < drawEnd; y++) { int texY = (int)texPos & (textureHeight - 1); texPos += step; RGBA pixel = texture[textureHeight * texY + texX]; $pixels[pixcoord(x, y)] = dumb_lighting(pixel, perpWallDist); } // SET THE ZBUFFER FOR THE SPRITE CASTING ZBuffer[x] = perpWallDist; } } void Raycaster::draw_ceiling_floor() { constexpr static const int textureWidth = TEXTURE_WIDTH; constexpr static const int textureHeight = TEXTURE_HEIGHT; for(int y = $height / 2 + 1; y < $height; ++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 - $height / 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 * $height; // 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) / $width; float floorStepY = rowDistance * (rayDirY1 - rayDirY0) / $width; // 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; auto floor_texture = (const uint32_t *)$textures.floor.getPixelsPtr(); auto ceiling_texture = (const uint32_t *)$textures.ceiling.getPixelsPtr(); for(int x = 0; x < $width; ++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(textureWidth * (floorX - cellX)) & (textureWidth - 1); int ty = int(textureWidth * (floorY - cellY)) & (textureHeight - 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 = floor_texture[textureWidth * ty + tx]; $pixels[pixcoord(x, y)] = color; // CEILING color = ceiling_texture[textureWidth * ty + tx]; $pixels[pixcoord(x, $height - y - 1)] = color; } } } void Raycaster::render() { draw_ceiling_floor(); cast_rays(); draw_pixel_buffer(); sprite_casting(); } bool Raycaster::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 Raycaster::sort_sprites(std::vector& order, std::vector& 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 Raycaster::run(double speed, int dir) { double speed_and_dir = speed * dir; if(empty_space(int($posX + $dirX * speed_and_dir), int($posY))) { $posX += $dirX * speed_and_dir; } if(empty_space(int($posX), int($posY + $dirY * speed_and_dir))) { $posY += $dirY * speed_and_dir; } } void Raycaster::rotate(double speed, int dir) { double speed_and_dir = speed * dir; double oldDirX = $dirX; $dirX = $dirX * cos(speed_and_dir) - $dirY * sin(speed_and_dir); $dirY = oldDirX * sin(speed_and_dir) + $dirY * cos(speed_and_dir); double oldPlaneX = $planeX; $planeX = $planeX * cos(speed_and_dir) - $planeY * sin(speed_and_dir); $planeY = oldPlaneX * sin(speed_and_dir) + $planeY * cos(speed_and_dir); }