#include "raycaster.hpp" #include "dbc.hpp" #include "matrix.hpp" #include #include #include #include #include #include #include "components.hpp" #include "textures.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; }; /* It's hard to believe, but this is faster than any bitfiddling * I could devise. Just use a union with a struct, do the math * and I guess the compiler can handle it better than shifting * bits around. */ inline uint32_t new_lighting(uint32_t pixel, int level) { float factor = level * PERCENT; ColorConv conv{.as_int=pixel}; conv.as_color.r *= factor; conv.as_color.g *= factor; conv.as_color.b *= factor; return conv.as_int; } Raycaster::Raycaster(int width, int height) : $view_texture(sf::Vector2u{(unsigned int)width, (unsigned int)height}), $view_sprite($view_texture), $width(width), $height(height), $zbuffer(width) { $view_sprite.setPosition({0, 0}); $pixels = make_unique($width * $height); $view_texture.setSmooth(false); $floor_texture = textures::get_floor(); $ceiling_texture = textures::get_ceiling(); } void Raycaster::set_position(int x, int y) { $screen_pos_x = x; $screen_pos_y = y; $view_sprite.setPosition({(float)x, (float)y}); } void Raycaster::position_camera(float player_x, float player_y) { // x and y start position $pos_x = player_x; $pos_y = player_y; $dir_x = 1; $dir_y = 0; $plane_x = 0; $plane_y = 0.66; } 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) { constexpr const int texture_width = TEXTURE_WIDTH; constexpr const int texture_height = TEXTURE_HEIGHT; constexpr const int half_height = TEXTURE_HEIGHT / 2; auto& lights = $level.lights->lighting(); // sort sprites from far to close auto sprite_order = $level.collision->distance_sorted({(size_t)$pos_x, (size_t)$pos_y}, 500); // after sorting the sprites, do the projection for(auto& rec : sprite_order) { if(!$sprites.contains(rec.second)) continue; auto& sprite_texture = $sprites.at(rec.second); auto& sf_sprite = sprite_texture.sprite; auto sprite_pos = $level.world->get(rec.second); double sprite_x = double(sprite_pos.location.x) - $pos_x + 0.5; double sprite_y = double(sprite_pos.location.y) - $pos_y + 0.5; double inv_det = 1.0 / ($plane_x * $dir_y - $dir_x * $plane_y); // required for correct matrix multiplication double transform_x = inv_det * ($dir_y * sprite_x - $dir_x * sprite_y); //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 transform_y = inv_det * (-$plane_y * sprite_x + $plane_x * sprite_y); int sprite_screen_x = int(($width / 2) * (1 + transform_x / transform_y)); // calculate the height of the sprite on screen //using "transform_y" instead of the real distance prevents fisheye int sprite_height = abs(int($height / transform_y)); if(sprite_height == 0) continue; // calculate width the the sprite // same as height of sprite, given that it's square int sprite_width = abs(int($height / transform_y)); if(sprite_width == 0) continue; int draw_start_x = -sprite_width / 2 + sprite_screen_x; if(draw_start_x < 0) draw_start_x = 0; int draw_end_x = sprite_width / 2 + sprite_screen_x; if(draw_end_x > $width) draw_end_x = $width; int stripe = draw_start_x; for(; stripe < draw_end_x; 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(!(transform_y > 0 && transform_y < $zbuffer[stripe])) break; } int tex_x_end = int(texture_width * (stripe - (-sprite_width / 2 + sprite_screen_x)) * texture_width / sprite_width) / texture_width; if(draw_start_x < draw_end_x && transform_y > 0 && transform_y < $zbuffer[draw_start_x]) { //calculate lowest and highest pixel to fill in current stripe int draw_start_y = -sprite_height / 2 + $height / 2; if(draw_start_y < 0) draw_start_y = 0; int tex_x = int(texture_width * (draw_start_x - (-sprite_width / 2 + sprite_screen_x)) * texture_width / sprite_width) / texture_width; int tex_render_width = tex_x_end - tex_x; // avoid drawing sprites that are not visible (width < 0) if(tex_render_width <= 0) continue; float x = float(draw_start_x + $screen_pos_x); float y = float(draw_start_y + $screen_pos_y); if(x < $screen_pos_x) fmt::println("X < rayview left bounds"); if(y < $screen_pos_y) fmt::println("Y < rayview top bounds"); if(x >= SCREEN_WIDTH) fmt::println("OUT OF BOUNDS X"); if(y >= $height) fmt::println("OUT OF BOUNDS Y"); float sprite_scale_w = float(sprite_width) / float(texture_width); float sprite_scale_h = float(sprite_height) / float(texture_height); int d = y * texture_height - $height * half_height + sprite_height * half_height; int tex_y = ((d * texture_height) / sprite_height) / texture_height; sf::Vector2f origin{texture_width / 2, texture_height / 2}; sf::Vector2f scale{sprite_scale_w, sprite_scale_h}; sf::Vector2f position{x + origin.x * scale.x, y + origin.y * scale.y}; sf::IntRect in_texture{ {tex_x, tex_y}, {tex_render_width, texture_height}}; if($level.world->has(rec.second)) { auto& animation = $level.world->get(rec.second); if(animation.playing) animation.step(scale, position, in_texture); } sf_sprite->setOrigin(origin); sf_sprite->setScale(scale); sf_sprite->setTextureRect(in_texture); sf_sprite->setPosition(position); $brightness.setUniform("offsetFactor", sf::Glsl::Vec2{0.0f, 0.0f}); // the SpatialMap.distance_sorted only calculates the // (x1-x2)^2 + (y1-y2)^2 portion of distance, so to get // the actual distance we need to sqrt that. // float level = sqrt(rec.first); float level = lights[sprite_pos.location.y][sprite_pos.location.x] * PERCENT; $brightness.setUniform("darkness", level); target.draw(*sf_sprite, &$brightness); } } } void Raycaster::cast_rays() { constexpr static const int texture_width = TEXTURE_WIDTH; constexpr static const int texture_height = TEXTURE_HEIGHT; double perp_wall_dist; auto& lights = $level.lights->lighting(); // 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 ray_dir_x = $dir_x + $plane_x * cameraX; double ray_dir_y = $dir_y + $plane_y * cameraX; // which box of the map we're in int map_x = int($pos_x); int map_y = int($pos_y); // length of ray from one x or y-side to next x or y-side double delta_dist_x = std::abs(1.0 / ray_dir_x); double delta_dist_y = std::abs(1.0 / ray_dir_y); int step_x = 0; int step_y = 0; int hit = 0; int side = 0; // length of ray from current pos to next x or y-side double side_dist_x; double side_dist_y; if(ray_dir_x < 0) { step_x = -1; side_dist_x = ($pos_x - map_x) * delta_dist_x; } else { step_x = 1; side_dist_x = (map_x + 1.0 - $pos_x) * delta_dist_x; } if(ray_dir_y < 0) { step_y = -1; side_dist_y = ($pos_y - map_y) * delta_dist_y; } else { step_y = 1; side_dist_y = (map_y + 1.0 - $pos_y) * delta_dist_y; } // perform DDA while(hit == 0) { if(side_dist_x < side_dist_y) { side_dist_x += delta_dist_x; map_x += step_x; side = 0; } else { side_dist_y += delta_dist_y; map_y += step_y; side = 1; } if($map[map_y][map_x] > 0) hit = 1; } if(side == 0) { perp_wall_dist = (side_dist_x - delta_dist_x); } else { perp_wall_dist = (side_dist_y - delta_dist_y); } int line_height = int($height / perp_wall_dist); int draw_start = -line_height / 2 + $height / 2 + $pitch; if(draw_start < 0) draw_start = 0; int draw_end = line_height / 2 + $height / 2 + $pitch; if(draw_end >= $height) draw_end = $height - 1; auto texture = textures::get_surface($map[map_y][map_x] - 1); // calculate value of wall_x double wall_x; // where exactly the wall was hit if(side == 0) { wall_x = $pos_y + perp_wall_dist * ray_dir_y; } else { wall_x = $pos_x + perp_wall_dist * ray_dir_x; } wall_x -= floor(wall_x); // x coorindate on the texture int tex_x = int(wall_x * double(texture_width)); if(side == 0 && ray_dir_x > 0) tex_x = texture_width - tex_x - 1; if(side == 1 && ray_dir_y < 0) tex_x = texture_width - tex_x - 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 * texture_height / line_height; // Starting texture coordinate double tex_pos = (draw_start - $pitch - $height / 2 + line_height / 2) * step; for(int y = draw_start; y < draw_end; y++) { int tex_y = (int)tex_pos & (texture_height - 1); tex_pos += step; RGBA pixel = texture[texture_height * tex_y + tex_x]; int light_level = lights[map_y][map_x]; $pixels[pixcoord(x, y)] = new_lighting(pixel, light_level); } // SET THE ZBUFFER FOR THE SPRITE CASTING $zbuffer[x] = perp_wall_dist; } } void Raycaster::draw_ceiling_floor() { constexpr static const int texture_width = TEXTURE_WIDTH; constexpr static const int texture_height = TEXTURE_HEIGHT; auto &lights = $level.lights->lighting(); for(int y = $height / 2 + 1; y < $height; ++y) { // rayDir for leftmost ray (x=0) and rightmost (x = w) float ray_dir_x0 = $dir_x - $plane_x; float ray_dir_y0 = $dir_y - $plane_y; float ray_dir_x1 = $dir_x + $plane_x; float ray_dir_y1 = $dir_y + $plane_y; // 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 pos_z = 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 row_distance = pos_z / 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 floor_step_x = row_distance * (ray_dir_x1 - ray_dir_x0) / $width; float floor_step_y = row_distance * (ray_dir_y1 - ray_dir_y0) / $width; // real world coordinates of the leftmost column. // This will be updated as we step to the right float floor_x = $pos_x + row_distance * ray_dir_x0; float floor_y = $pos_y + row_distance * ray_dir_y0; for(int x = 0; x < $width; ++x) { // the cell coord is simply taken from the int parts of // floor_x and floor_y. int cell_x = int(floor_x); int cell_y = int(floor_y); // get the texture coordinate from the fractional part int tx = int(texture_width * (floor_x - cell_x)) & (texture_width - 1); int ty = int(texture_width * (floor_y - cell_y)) & (texture_height - 1); floor_x += floor_step_x; floor_y += floor_step_y; // now get the pixel from the texture uint32_t color; // this uses the previous ty/tx fractional parts of // floor_x cell_x to find the texture x/y. How? int map_x = int(floor_x); int map_y = int(floor_y); int light_level = matrix::inbounds(lights, map_x, map_y) ? lights[map_y][map_x] : 30; // FLOOR color = $floor_texture[texture_width * ty + tx]; $pixels[pixcoord(x, y)] = new_lighting(color, light_level); // CEILING color = $ceiling_texture[texture_width * ty + tx]; $pixels[pixcoord(x, $height - y - 1)] = new_lighting(color, light_level); } } } void Raycaster::render() { draw_ceiling_floor(); cast_rays(); draw_pixel_buffer(); } void Raycaster::draw(sf::RenderTarget& target) { target.draw($view_sprite); sprite_casting(target); } void Raycaster::update_sprite(DinkyECS::Entity ent, components::Sprite& sprite) { fmt::println("entity UPDATE SPRITE {} will have sprite named {}", ent, sprite.name); auto sprite_txt = textures::get(sprite.name); $sprites.insert_or_assign(ent, sprite_txt); } void Raycaster::update_level(GameLevel level) { $sprites.clear(); $level = level; auto& tiles = $level.map->tiles(); $map = textures::convert_char_to_texture(tiles.$tile_ids); $level.world->query([&](const auto ent, auto& sprite) { // player doesn't need a sprite if($level.player == ent) return; fmt::println("entity {} will have sprite named {}", ent, sprite.name); auto sprite_txt = textures::get(sprite.name); $sprites.insert_or_assign(ent, sprite_txt); }); } void Raycaster::init_shaders() { dbc::check(sf::Shader::isAvailable(), "no shaders?!"); bool good = $brightness.loadFromFile("shaders/modal.frag", sf::Shader::Type::Fragment); dbc::check(good, "shader could not be loaded"); $brightness.setUniform("source", sf::Shader::CurrentTexture); }