#include "raycaster.hpp" #include "dbc.hpp" #include "matrix.hpp" #include #include #include #include #include #include #include "components.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(TexturePack &textures, int width, int height) : $textures(textures), $view_texture({(unsigned int)width, (unsigned int)height}), $view_sprite($view_texture), $width(width), $height(height), 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::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}); } void Raycaster::sprite_casting(sf::RenderTarget &target) { const int textureWidth = TEXTURE_WIDTH; const int textureHeight = TEXTURE_HEIGHT; const int halfHeight = TEXTURE_HEIGHT / 2; // sort sprites from far to close auto sprite_order = $level.collision->distance_sorted({(size_t)$posX, (size_t)$posY}); // after sorting the sprites, do the projection for(auto& rec : sprite_order) { if(!$sprites.contains(rec.second)) continue; // BUG: eventually this needs to go away too auto& sf_sprite = $sprites.at(rec.second).sprite; auto sprite_pos = $level.world->get(rec.second); double spriteX = double(sprite_pos.location.x) - $posX + 0.5; double spriteY = double(sprite_pos.location.y) - $posY + 0.5; //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}); target.draw(*sf_sprite); } } } 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 coordinate from the fractional part int tx = int(textureWidth * (floorX - cellX)) & (textureWidth - 1); int ty = int(textureWidth * (floorY - cellY)) & (textureHeight - 1); floorX += floorStepX; floorY += floorStepY; double d = std::sqrt(($posX - floorX) * ($posX - floorX) + ($posY - floorY) * ($posY - floorY)); // 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)] = dumb_lighting(color, d); // CEILING color = ceiling_texture[textureWidth * ty + tx]; $pixels[pixcoord(x, $height - y - 1)] = dumb_lighting(color, d); } } } void Raycaster::draw(sf::RenderTarget& target) { draw_ceiling_floor(); cast_rays(); draw_pixel_buffer(); target.draw($view_sprite); sprite_casting(target); } void Raycaster::set_level(GameLevel level) { $level = level; auto& tiles = $level.map->tiles(); auto world = $level.world; $map = $textures.convert_char_to_texture(tiles.$tile_ids); world->query([&](const auto ent, auto &pos) { if(world->has(ent)) { fmt::println("enemy: {}, pos={},{}", ent, pos.location.x, pos.location.y); auto sprite_txt = $textures.sprite_textures.at("evil_eye"); $sprites.try_emplace(ent, sprite_txt); } else { fmt::println("item or device: {}, pos={},{}", ent, pos.location.x, pos.location.y); auto sprite_txt = $textures.sprite_textures.at("barrel"); $sprites.try_emplace(ent, sprite_txt); } }); }