# include "raycaster.hpp"
# include "dbc.hpp"
# include "matrix.hpp"
# include <algorithm>
# include <cmath>
# include <cstdlib>
# include <fmt/core.h>
# include <memory>
# include <numbers>
# include "components.hpp"
# include "textures.hpp"
# include "systems.hpp"
# include "shaders.hpp"
using namespace fmt ;
using std : : make_unique , std : : shared_ptr ;
union ColorConv {
struct {
uint8_t r ;
uint8_t g ;
uint8_t b ;
uint8_t a ;
} as_color ;
RGBA as_int ;
} ;
// from: https://permadi.com/1996/05/ray-casting-tutorial-19/
// Intensity = (kI/(d+do))*(N*L)
// rcr says: kI = intensity coefficient, d = distance, d0 = fudge term to prevent division by zero, N is surface, L is direction to light from surface
//
// That formula is just "Inverse-square law" (except they don't square, which is physically dubious), and "Lambertian reflectance" ("Diffuse reflection") which sounds fancy but is super standard. All the quoted terms have wikipedia articles
//
// Distance means distance to surface from light.
//
// Intensity = Object Intensity/Distance * Multiplier
//
/* 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 RGBA lighting_calc ( RGBA pixel , float dist , int level ) {
ColorConv conv { . as_int = pixel } ;
if ( conv . as_color . b < GLOW_LIMIT
& & conv . as_color . r < GLOW_LIMIT
& & conv . as_color . g < GLOW_LIMIT )
{
float intensity = ( float ( level ) * PERCENT ) / ( dist + 1 ) * LIGHT_MULTIPLIER ;
conv . as_color . r * = intensity ;
conv . as_color . g * = intensity ;
conv . as_color . b * = intensity ;
}
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 < RGBA [ ] > ( $ width * $ height ) ;
$ view_texture . setSmooth ( false ) ;
$ camera . target_x = $ pos_x ;
$ camera . target_y = $ pos_y ;
update_camera_aiming ( ) ;
}
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 ;
update_camera_aiming ( ) ;
}
void Raycaster : : draw_pixel_buffer ( ) {
$ view_texture . update ( ( uint8_t * ) $ pixels . get ( ) , { ( unsigned int ) $ width , ( unsigned int ) $ height } , { 0 , 0 } ) ;
}
void Raycaster : : apply_sprite_effect ( shared_ptr < sf : : Shader > effect , float width , float height ) {
effect - > setUniform ( " u_time " , $ clock . getElapsedTime ( ) . asSeconds ( ) ) ;
sf : : Vector2f u_resolution { width , height } ;
effect - > setUniform ( " u_resolution " , u_resolution ) ;
}
void Raycaster : : sprite_casting ( sf : : RenderTarget & target ) {
auto & lights = $ level . lights - > lighting ( ) ;
$ level . collision - > distance_sorted ( $ sprite_order , { ( size_t ) $ pos_x , ( size_t ) $ pos_y } , RENDER_DISTANCE ) ;
// after sorting the sprites, do the projection
for ( auto & rec : $ sprite_order ) {
if ( ! $ sprites . contains ( rec . entity ) ) continue ;
auto & sprite_texture = $ sprites . at ( rec . entity ) ;
int texture_width = ( float ) sprite_texture . frame_size . x ;
int texture_height = ( float ) sprite_texture . frame_size . y ;
int half_height = texture_height / 2 ;
auto & sf_sprite = sprite_texture . sprite ;
auto sprite_pos = $ level . world - > get < components : : Position > ( rec . entity ) ;
double sprite_x = double ( sprite_pos . location . x ) - rec . wiggle - $ pos_x + 0.5 ;
double sprite_y = double ( sprite_pos . location . y ) - rec . wiggle - $ 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 ) dbc : : log ( " X < rayview left bounds " ) ;
if ( y < $ screen_pos_y ) dbc : : log ( " Y < rayview top bounds " ) ;
if ( x > = SCREEN_WIDTH ) dbc : : log ( " OUT OF BOUNDS X " ) ;
if ( y > = $ height ) dbc : : log ( " 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.0f , texture_height / 2.0f } ;
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 < components : : Animation > ( rec . entity ) ) {
auto & animation = $ level . world - > get < components : : Animation > ( rec . entity ) ;
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 ) ;
float level = lights [ sprite_pos . location . y ] [ sprite_pos . location . x ] * PERCENT ;
shared_ptr < sf : : Shader > effect = System : : sprite_effect ( rec . entity ) ;
if ( effect ) {
apply_sprite_effect ( effect , sprite_width , sprite_height ) ;
} else {
effect = $ brightness ;
level + = ( aiming_at = = sprite_pos . location ) * AIMED_AT_BRIGHTNESS ;
effect - > setUniform ( " darkness " , level ) ;
}
target . draw ( * sf_sprite , effect . get ( ) ) ;
}
}
}
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 ( $ walls [ map_y ] [ map_x ] = = 1 ) 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 ( $ tiles [ map_y ] [ map_x ] ) ;
// 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 ) ] = lighting_calc ( pixel , perp_wall_dist , light_level ) ;
}
// SET THE ZBUFFER FOR THE SPRITE CASTING
$ zbuffer [ x ] = perp_wall_dist ;
}
}
void Raycaster : : draw_ceiling_floor ( ) {
constexpr const int texture_width = TEXTURE_WIDTH ;
constexpr const int texture_height = TEXTURE_HEIGHT ;
auto & lights = $ level . lights - > lighting ( ) ;
size_t surface_i = 0 ;
const RGBA * floor_texture = textures : : get_surface ( surface_i ) ;
const RGBA * ceiling_texture = textures : : get_ceiling ( surface_i ) ;
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
RGBA 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 ) ;
if ( ! matrix : : inbounds ( lights , map_x , map_y ) ) continue ;
int light_level = lights [ map_y ] [ map_x ] ;
size_t new_surface_i = $ tiles [ map_y ] [ map_x ] ;
if ( new_surface_i ! = surface_i ) {
surface_i = new_surface_i ;
floor_texture = textures : : get_surface ( surface_i ) ;
ceiling_texture = textures : : get_ceiling ( surface_i ) ;
}
// NOTE: use map_x/y to get the floor, ceiling texture.
// FLOOR
color = floor_texture [ texture_width * ty + tx ] ;
$ pixels [ pixcoord ( x , y ) ] = lighting_calc ( color , row_distance , light_level ) ;
// CEILING
color = ceiling_texture [ texture_width * ty + tx ] ;
$ pixels [ pixcoord ( x , $ height - y - 1 ) ] = lighting_calc ( color , row_distance , 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 ) {
auto sprite_txt = textures : : get_sprite ( sprite . name ) ;
$ sprites . insert_or_assign ( ent , sprite_txt ) ;
}
void Raycaster : : update_level ( GameDB : : Level & level ) {
$ sprites . clear ( ) ;
$ sprite_order . clear ( ) ;
$ level = level ;
$ tiles = $ level . map - > tiles ( ) ;
$ walls = $ level . map - > walls ( ) ;
$ level . world - > query < components : : Sprite > ( [ & ] ( const auto ent , auto & sprite ) {
// player doesn't need a sprite
if ( $ level . player ! = ent ) {
update_sprite ( ent , sprite ) ;
}
} ) ;
}
void Raycaster : : init_shaders ( ) {
$ brightness = shaders : : get ( " rayview_sprites " ) ;
}
Point Raycaster : : plan_move ( int dir , bool strafe ) {
$ camera . t = 0.0 ;
if ( strafe ) {
$ camera . target_x = $ pos_x + int ( - $ dir_y * 1.5 * dir ) ;
$ camera . target_y = $ pos_y + int ( $ dir_x * 1.5 * dir ) ;
} else {
$ camera . target_x = $ pos_x + int ( $ dir_x * 1.5 * dir ) ;
$ camera . target_y = $ pos_y + int ( $ dir_y * 1.5 * dir ) ;
}
return { size_t ( $ camera . target_x ) , size_t ( $ camera . target_y ) } ;
}
void Raycaster : : plan_rotate ( int dir , float amount ) {
$ camera . t = 0.0 ;
double angle_dir = std : : numbers : : pi * amount * float ( dir ) ;
$ camera . target_dir_x = $ dir_x * cos ( angle_dir ) - $ dir_y * sin ( angle_dir ) ;
$ camera . target_dir_y = $ dir_x * sin ( angle_dir ) + $ dir_y * cos ( angle_dir ) ;
$ camera . target_plane_x = $ plane_x * cos ( angle_dir ) - $ plane_y * sin ( angle_dir ) ;
$ camera . target_plane_y = $ plane_x * sin ( angle_dir ) + $ plane_y * cos ( angle_dir ) ;
}
bool Raycaster : : play_rotate ( ) {
$ camera . t + = $ camera . rot_speed ;
$ dir_x = std : : lerp ( $ dir_x , $ camera . target_dir_x , $ camera . t ) ;
$ dir_y = std : : lerp ( $ dir_y , $ camera . target_dir_y , $ camera . t ) ;
$ plane_x = std : : lerp ( $ plane_x , $ camera . target_plane_x , $ camera . t ) ;
$ plane_y = std : : lerp ( $ plane_y , $ camera . target_plane_y , $ camera . t ) ;
update_camera_aiming ( ) ;
return $ camera . t > = 1.0 ;
}
bool Raycaster : : play_move ( ) {
$ camera . t + = $ camera . move_speed ;
$ pos_x = std : : lerp ( $ pos_x , $ camera . target_x , $ camera . t ) ;
$ pos_y = std : : lerp ( $ pos_y , $ camera . target_y , $ camera . t ) ;
update_camera_aiming ( ) ;
return $ camera . t > = 1.0 ;
}
void Raycaster : : abort_plan ( ) {
$ camera . target_x = $ pos_x ;
$ camera . target_y = $ pos_y ;
update_camera_aiming ( ) ;
}
bool Raycaster : : is_target ( DinkyECS : : Entity entity ) {
( void ) entity ;
return false ;
}
void Raycaster : : update_camera_aiming ( ) {
aiming_at = { size_t ( $ pos_x + $ dir_x ) , size_t ( $ pos_y + $ dir_y ) } ;
camera_at = { size_t ( $ camera . target_x ) , size_t ( $ camera . target_y ) } ;
}