diff options
| author | salmonik <raisersoft@proton.me> | 2026-07-09 23:27:55 +0200 |
|---|---|---|
| committer | salmonik <raisersoft@proton.me> | 2026-07-09 23:27:55 +0200 |
| commit | 3460d889aa2e7af1a60d218a1a8589027b4e4e63 (patch) | |
| tree | 89e66acd05fba086835337bfa95b36710dc8c4da /src/renderer.rs | |
| download | dungeon-3460d889aa2e7af1a60d218a1a8589027b4e4e63.tar.gz dungeon-3460d889aa2e7af1a60d218a1a8589027b4e4e63.zip | |
init
Diffstat (limited to 'src/renderer.rs')
| -rw-r--r-- | src/renderer.rs | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/src/renderer.rs b/src/renderer.rs new file mode 100644 index 0000000..0792537 --- /dev/null +++ b/src/renderer.rs | |||
| @@ -0,0 +1,196 @@ | |||
| 1 | use crate::game::Game; | ||
| 2 | use crate::texture::TEX_SIZE; | ||
| 3 | |||
| 4 | pub struct Renderer { | ||
| 5 | screen_w: usize, | ||
| 6 | screen_h: usize, | ||
| 7 | /// Precomputed: floor_dist_lut[row] = perpendicular distance to floor at that row. | ||
| 8 | floor_dist_lut: Vec<f64>, | ||
| 9 | /// Precomputed: floor_bright_lut[row] = brightness × 256 for that row (fixed-point). | ||
| 10 | floor_bright_lut: Vec<u32>, | ||
| 11 | } | ||
| 12 | |||
| 13 | impl Renderer { | ||
| 14 | pub fn new(screen_w: usize, screen_h: usize) -> Self { | ||
| 15 | let half_h = screen_h as f64 / 2.0; | ||
| 16 | let mut floor_dist_lut = vec![0.0f64; screen_h]; | ||
| 17 | let mut floor_bright_lut = vec![0u32; screen_h]; | ||
| 18 | |||
| 19 | // Only rows below the horizon have meaningful floor values. | ||
| 20 | for py in (screen_h / 2 + 1)..screen_h { | ||
| 21 | let dy = py as f64 - half_h; | ||
| 22 | let dist = 0.5 * screen_h as f64 / dy; | ||
| 23 | floor_dist_lut[py] = dist; | ||
| 24 | let bright = (1.0 / (1.0 + dist * 0.15)).max(0.05); | ||
| 25 | floor_bright_lut[py] = (bright * 256.0) as u32; | ||
| 26 | } | ||
| 27 | |||
| 28 | Self { | ||
| 29 | screen_w, | ||
| 30 | screen_h, | ||
| 31 | floor_dist_lut, | ||
| 32 | floor_bright_lut, | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | /// Produce one frame into `buffer` (packed `0xAARRGGBB`). | ||
| 37 | pub fn render(&self, game: &Game, buffer: &mut [u32]) { | ||
| 38 | buffer.fill(0xFF00_0000); // pure-black ceiling | ||
| 39 | |||
| 40 | let half_h = self.screen_h as f64 / 2.0; | ||
| 41 | let fov = std::f64::consts::PI / 3.0; // 60° | ||
| 42 | |||
| 43 | for sx in 0..self.screen_w { | ||
| 44 | let cam_x = 2.0 * sx as f64 / self.screen_w as f64 - 1.0; | ||
| 45 | let ray_angle = game.player.angle + (fov / 2.0) * cam_x; | ||
| 46 | let rdx = ray_angle.cos(); | ||
| 47 | let rdy = ray_angle.sin(); | ||
| 48 | |||
| 49 | // ---- DDA setup ------------------------------------------------- | ||
| 50 | let mut map_x = game.player.x as isize; | ||
| 51 | let mut map_y = game.player.y as isize; | ||
| 52 | |||
| 53 | let ddx = if rdx == 0.0 { | ||
| 54 | f64::MAX | ||
| 55 | } else { | ||
| 56 | (1.0 / rdx).abs() | ||
| 57 | }; | ||
| 58 | let ddy = if rdy == 0.0 { | ||
| 59 | f64::MAX | ||
| 60 | } else { | ||
| 61 | (1.0 / rdy).abs() | ||
| 62 | }; | ||
| 63 | |||
| 64 | let (step_x, mut side_x) = if rdx < 0.0 { | ||
| 65 | (-1isize, (game.player.x - map_x as f64) * ddx) | ||
| 66 | } else { | ||
| 67 | (1isize, (map_x as f64 + 1.0 - game.player.x) * ddx) | ||
| 68 | }; | ||
| 69 | |||
| 70 | let (step_y, mut side_y) = if rdy < 0.0 { | ||
| 71 | (-1isize, (game.player.y - map_y as f64) * ddy) | ||
| 72 | } else { | ||
| 73 | (1isize, (map_y as f64 + 1.0 - game.player.y) * ddy) | ||
| 74 | }; | ||
| 75 | |||
| 76 | let mut hit_side: bool; | ||
| 77 | let euclidean: f64; | ||
| 78 | |||
| 79 | // ---- DDA loop -------------------------------------------------- | ||
| 80 | loop { | ||
| 81 | if side_x < side_y { | ||
| 82 | side_x += ddx; | ||
| 83 | map_x += step_x; | ||
| 84 | hit_side = false; | ||
| 85 | } else { | ||
| 86 | side_y += ddy; | ||
| 87 | map_y += step_y; | ||
| 88 | hit_side = true; | ||
| 89 | } | ||
| 90 | |||
| 91 | if map_y < 0 | ||
| 92 | || map_y >= game.maze.height() as isize | ||
| 93 | || map_x < 0 | ||
| 94 | || map_x >= game.maze.width() as isize | ||
| 95 | { | ||
| 96 | euclidean = 1.0; | ||
| 97 | break; | ||
| 98 | } | ||
| 99 | |||
| 100 | if game.maze.is_wall(map_y as usize, map_x as usize) { | ||
| 101 | euclidean = if !hit_side { | ||
| 102 | (map_x as f64 - game.player.x + (1.0 - step_x as f64) / 2.0) / rdx | ||
| 103 | } else { | ||
| 104 | (map_y as f64 - game.player.y + (1.0 - step_y as f64) / 2.0) / rdy | ||
| 105 | }; | ||
| 106 | break; | ||
| 107 | } | ||
| 108 | } | ||
| 109 | |||
| 110 | // Fisheye correction | ||
| 111 | let perp_dist = (euclidean * (ray_angle - game.player.angle).cos().abs()).max(0.001); | ||
| 112 | |||
| 113 | // ---- wall slice ------------------------------------------------ | ||
| 114 | let wall_h = (self.screen_h as f64 / perp_dist) as isize; | ||
| 115 | let draw_start = ((-wall_h / 2) + self.screen_h as isize / 2).max(0) as usize; | ||
| 116 | let draw_end = ((wall_h / 2) + self.screen_h as isize / 2) | ||
| 117 | .min(self.screen_h as isize - 1) as usize; | ||
| 118 | |||
| 119 | // Texture x-coordinate (uses Euclidean, not perp) | ||
| 120 | let wall_x = if !hit_side { | ||
| 121 | game.player.y + euclidean * rdy | ||
| 122 | } else { | ||
| 123 | game.player.x + euclidean * rdx | ||
| 124 | }; | ||
| 125 | let wall_x = wall_x - wall_x.floor(); | ||
| 126 | let tex_x = ((wall_x * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); | ||
| 127 | |||
| 128 | // Hoist: brightness is identical for every pixel in this column | ||
| 129 | let wall_bright_fx = { | ||
| 130 | let b = (1.0 / (1.0 + perp_dist * 0.12)).max(0.08); | ||
| 131 | (b * 256.0) as u32 | ||
| 132 | }; | ||
| 133 | |||
| 134 | for py in draw_start..=draw_end { | ||
| 135 | let tex_y = ((py as f64 - half_h + wall_h as f64 / 2.0) * TEX_SIZE as f64 | ||
| 136 | / wall_h as f64) as usize; | ||
| 137 | let tex_y = tex_y.min(TEX_SIZE - 1); | ||
| 138 | |||
| 139 | let color = game.wall_texture[tex_y * TEX_SIZE + tex_x]; | ||
| 140 | buffer[py * self.screen_w + sx] = darken_fx(color, wall_bright_fx); | ||
| 141 | } | ||
| 142 | |||
| 143 | // ---- floor below the wall -------------------------------------- | ||
| 144 | let floor_start = draw_end + 1; | ||
| 145 | if floor_start < self.screen_h { | ||
| 146 | for py in floor_start..self.screen_h { | ||
| 147 | let floor_dist = self.floor_dist_lut[py]; | ||
| 148 | |||
| 149 | if floor_dist > 15.0 { | ||
| 150 | // Far floor → just a dark pixel, skip texture & world calc. | ||
| 151 | let gray = darken_fx(0xFF80_8080, self.floor_bright_lut[py]); | ||
| 152 | buffer[py * self.screen_w + sx] = gray; | ||
| 153 | } else { | ||
| 154 | let fx = game.player.x + floor_dist * rdx; | ||
| 155 | let fy = game.player.y + floor_dist * rdy; | ||
| 156 | |||
| 157 | let ftx = ((fx.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); | ||
| 158 | let fty = ((fy.fract().abs() * TEX_SIZE as f64) as usize).min(TEX_SIZE - 1); | ||
| 159 | |||
| 160 | let color = game.floor_texture[fty * TEX_SIZE + ftx]; | ||
| 161 | buffer[py * self.screen_w + sx] = | ||
| 162 | darken_fx(color, self.floor_bright_lut[py]); | ||
| 163 | } | ||
| 164 | } | ||
| 165 | } | ||
| 166 | } | ||
| 167 | |||
| 168 | // ---- crosshair ----------------------------------------------------- | ||
| 169 | let cx = self.screen_w / 2; | ||
| 170 | let cy = self.screen_h / 2; | ||
| 171 | let cross_color = 0xFF00_FF00; | ||
| 172 | for i in 0..=4isize { | ||
| 173 | if cy as isize - i >= 0 { | ||
| 174 | buffer[(cy - i as usize) * self.screen_w + cx] = cross_color; | ||
| 175 | } | ||
| 176 | if (cy + i as usize) < self.screen_h { | ||
| 177 | buffer[(cy + i as usize) * self.screen_w + cx] = cross_color; | ||
| 178 | } | ||
| 179 | if cx as isize - i >= 0 { | ||
| 180 | buffer[cy * self.screen_w + (cx - i as usize)] = cross_color; | ||
| 181 | } | ||
| 182 | if (cx + i as usize) < self.screen_w { | ||
| 183 | buffer[cy * self.screen_w + (cx + i as usize)] = cross_color; | ||
| 184 | } | ||
| 185 | } | ||
| 186 | } | ||
| 187 | } | ||
| 188 | |||
| 189 | /// Integer darken: `bright_fx` is brightness × 256 in fixed-point. | ||
| 190 | #[inline(always)] | ||
| 191 | fn darken_fx(color: u32, bright_fx: u32) -> u32 { | ||
| 192 | let r = (((color >> 16) & 0xFF) * bright_fx) >> 8; | ||
| 193 | let g = (((color >> 8) & 0xFF) * bright_fx) >> 8; | ||
| 194 | let b = ((color & 0xFF) * bright_fx) >> 8; | ||
| 195 | 0xFF00_0000 | (r << 16) | (g << 8) | b | ||
| 196 | } | ||
