use crate::game::Game; use crate::texture::TEX_SIZE; pub struct Renderer { screen_w: usize, screen_h: usize, /// Precomputed: floor_dist_lut[row] = perpendicular distance to floor at that row. floor_dist_lut: Vec, /// Precomputed: floor_bright_lut[row] = brightness × 256 for that row (fixed-point). floor_bright_lut: Vec, } impl Renderer { pub fn new(screen_w: usize, screen_h: usize) -> Self { let half_h = screen_h as f64 / 2.0; let mut floor_dist_lut = vec![0.0f64; screen_h]; let mut floor_bright_lut = vec![0u32; screen_h]; // Only rows below the horizon have meaningful floor values. for py in (screen_h / 2 + 1)..screen_h { let dy = py as f64 - half_h; let dist = 0.5 * screen_h as f64 / dy; floor_dist_lut[py] = dist; let bright = (1.0 / (1.0 + dist * 0.15)).max(0.05); floor_bright_lut[py] = (bright * 256.0) as u32; } Self { screen_w, screen_h, floor_dist_lut, floor_bright_lut, } } /// Produce one frame into `buffer` (packed `0xAARRGGBB`). pub fn render(&self, game: &Game, buffer: &mut [u32]) { buffer.fill(0xFF00_0000); // pure-black ceiling let half_h = self.screen_h as f64 / 2.0; let fov = std::f64::consts::PI / 3.0; // 60° for sx in 0..self.screen_w { let cam_x = 2.0 * sx as f64 / self.screen_w as f64 - 1.0; let ray_angle = game.player.angle + (fov / 2.0) * cam_x; let rdx = ray_angle.cos(); let rdy = ray_angle.sin(); // ---- DDA setup ------------------------------------------------- let mut map_x = game.player.x as isize; let mut map_y = game.player.y as isize; let ddx = if rdx == 0.0 { f64::MAX } else { (1.0 / rdx).abs() }; let ddy = if rdy == 0.0 { f64::MAX } else { (1.0 / rdy).abs() }; let (step_x, mut side_x) = if rdx < 0.0 { (-1isize, (game.player.x - map_x as f64) * ddx) } else { (1isize, (map_x as f64 + 1.0 - game.player.x) * ddx) }; let (step_y, mut side_y) = if rdy < 0.0 { (-1isize, (game.player.y - map_y as f64) * ddy) } else { (1isize, (map_y as f64 + 1.0 - game.player.y) * ddy) }; let mut hit_side: bool; let euclidean: f64; // ---- DDA loop -------------------------------------------------- loop { if side_x < side_y { side_x += ddx; map_x += step_x; hit_side = false; } else { side_y += ddy; map_y += step_y; hit_side = true; } if map_y < 0 || map_y >= game.maze.height() as isize || map_x < 0 || map_x >= game.maze.width() as isize { euclidean = 1.0; break; } if game.maze.is_wall(map_y as usize, map_x as usize) { euclidean = if !hit_side { (map_x as f64 - game.player.x + (1.0 - step_x as f64) / 2.0) / rdx } else { (map_y as f64 - game.player.y + (1.0 - step_y as f64) / 2.0) / rdy }; break; } } // Fisheye correction let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); // ---- wall slice ------------------------------------------------ let wall_h = (self.screen_h as f64 / perp_dist) as isize; let draw_start = ((-wall_h / 2) + self.screen_h as isize / 2).max(0) as usize; let draw_end = ((wall_h / 2) + self.screen_h as isize / 2) .min(self.screen_h as isize - 1) as usize; // Texture x-coordinate (uses Euclidean, not perp) let wall_x = if !hit_side { game.player.y + euclidean * rdy } else { game.player.x + euclidean * rdx }; let wall_x = wall_x - wall_x.floor(); let tex_x = ((wall_x * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); // Hoist: brightness is identical for every pixel in this column let wall_bright_fx = { let b = (1.0 / (1.0 + perp_dist * 0.12)).max(0.08); (b * 256.0) as u32 }; for py in draw_start..=draw_end { let tex_y = ((py as f64 - half_h + wall_h as f64 / 2.0) * TEX_SIZE as f64 / wall_h as f64) as usize; let tex_y = tex_y.min(TEX_SIZE - 1); let color = game.wall_texture[tex_y * TEX_SIZE + tex_x]; buffer[py * self.screen_w + sx] = darken_fx(color, wall_bright_fx); } // ---- floor below the wall -------------------------------------- let floor_start = draw_end + 1; if floor_start < self.screen_h { for py in floor_start..self.screen_h { let floor_dist = self.floor_dist_lut[py]; if floor_dist > 15.0 { // Far floor → just a dark pixel, skip texture & world calc. let gray = darken_fx(0xFF80_8080, self.floor_bright_lut[py]); buffer[py * self.screen_w + sx] = gray; } else { let fx = game.player.x + floor_dist * rdx; let fy = game.player.y + floor_dist * rdy; let ftx = ((fx.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); let fty = ((fy.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); let color = game.floor_texture[fty * TEX_SIZE + ftx]; buffer[py * self.screen_w + sx] = darken_fx(color, self.floor_bright_lut[py]); } } } } } } /// Integer darken: `bright_fx` is brightness × 256 in fixed-point. #[inline(always)] fn darken_fx(color: u32, bright_fx: u32) -> u32 { let r = (((color >> 16) & 0xFF) * bright_fx) >> 8; let g = (((color >> 8) & 0xFF) * bright_fx) >> 8; let b = ((color & 0xFF) * bright_fx) >> 8; 0xFF00_0000 | (r << 16) | (g << 8) | b }