1use embedded_graphics_core::pixelcolor::IntoStorage;
8use embedded_graphics_core::pixelcolor::Rgb565;
9use embedded_graphics_core::pixelcolor::RgbColor;
10#[allow(unused_imports)]
11use micromath::F32Ext;
12
13#[derive(Debug, Clone, Copy)]
15pub struct RaycastTexture {
16 pub pixels: [Rgb565; 256],
17}
18
19impl RaycastTexture {
20 pub const fn new(pixels: [Rgb565; 256]) -> Self {
22 Self { pixels }
23 }
24
25 #[inline(always)]
27 pub fn sample(&self, u: usize, v: usize) -> Rgb565 {
28 self.pixels[(v & 15) * 16 + (u & 15)]
29 }
30}
31
32#[derive(Debug, Clone, Copy)]
34pub struct RaycastSprite {
35 pub x: f32,
36 pub y: f32,
37 pub texture_id: u8,
38 pub active: bool,
39}
40
41#[derive(Debug, Clone)]
43pub struct Mode7Renderer {
44 width: usize,
45 height: usize,
46 fov_scale: f32,
47}
48
49impl Mode7Renderer {
50 pub fn new(width: usize, height: usize) -> Self {
51 Self {
52 width,
53 height,
54 fov_scale: 0.66,
55 }
56 }
57
58 pub fn set_fov_scale(&mut self, fov_scale: f32) {
60 self.fov_scale = fov_scale;
61 }
62
63 pub fn render_floor_and_ceiling(
65 &self,
66 pos_x: f32,
67 pos_y: f32,
68 angle: f32,
69 head_bob: i32,
70 floor_color_a: Rgb565,
71 floor_color_b: Rgb565,
72 ceil_color_a: Rgb565,
73 ceil_color_b: Rgb565,
74 framebuf_u32: &mut [u32],
75 ) {
76 let dir_x = angle.cos();
77 let dir_y = angle.sin();
78
79 let plane_x = -dir_y * self.fov_scale;
80 let plane_y = dir_x * self.fov_scale;
81
82 let ray_dir_x0 = dir_x + plane_x;
84 let ray_dir_y0 = dir_y + plane_y;
85 let ray_dir_x1 = dir_x - plane_x;
86 let ray_dir_y1 = dir_y - plane_y;
87
88 let center_y = (self.height / 2) as i32 + head_bob;
89 let stride_u32 = self.width / 2;
90
91 let floor_start = center_y.clamp(0, self.height as i32) as usize;
93 for y in floor_start..self.height {
94 let p = (y as i32 - center_y).max(1);
95 let row_dist = (0.5 * self.height as f32) / (p as f32);
96
97 let floor_step_x = row_dist * (ray_dir_x1 - ray_dir_x0) / (self.width as f32);
98 let floor_step_y = row_dist * (ray_dir_y1 - ray_dir_y0) / (self.width as f32);
99
100 let mut floor_x = pos_x + row_dist * ray_dir_x0;
101 let mut floor_y = pos_y + row_dist * ray_dir_y0;
102
103 let f_shade = (1.0 / (1.0 + row_dist * 0.18)).clamp(0.08, 0.85);
104 let row_u32 = y * stride_u32;
105
106 for x_u32 in 0..stride_u32 {
107 let cell_x = floor_x as i32;
108 let cell_y = floor_y as i32;
109
110 let tx = ((floor_x - cell_x as f32) * 16.0) as usize & 15;
111 let ty = ((floor_y - cell_y as f32) * 16.0) as usize & 15;
112
113 let is_grout = (tx == 0) || (ty == 0);
114 let f_base = if is_grout {
115 Rgb565::new(4, 3, 2)
116 } else if (cell_x + cell_y) % 2 == 0 {
117 floor_color_a
118 } else {
119 floor_color_b
120 };
121
122 let shaded = apply_shade(f_base, f_shade);
123 framebuf_u32[row_u32 + x_u32] = pack_rgb565_u32(shaded);
124
125 floor_x += floor_step_x * 2.0;
126 floor_y += floor_step_y * 2.0;
127 }
128 }
129
130 let ceil_end = center_y.clamp(0, self.height as i32) as usize;
132 for y in 0..ceil_end {
133 let p = (center_y - y as i32).max(1);
134 let row_dist = (0.5 * self.height as f32) / (p as f32);
135
136 let ceil_step_x = row_dist * (ray_dir_x1 - ray_dir_x0) / (self.width as f32);
137 let ceil_step_y = row_dist * (ray_dir_y1 - ray_dir_y0) / (self.width as f32);
138
139 let mut ceil_x = pos_x + row_dist * ray_dir_x0;
140 let mut ceil_y = pos_y + row_dist * ray_dir_y0;
141
142 let c_shade = (1.0 / (1.0 + row_dist * 0.22)).clamp(0.08, 0.7);
143 let row_u32 = y * stride_u32;
144
145 for x_u32 in 0..stride_u32 {
146 let cell_x = ceil_x as i32;
147 let cell_y = ceil_y as i32;
148
149 let tx = ((ceil_x - cell_x as f32) * 16.0) as usize & 15;
150 let ty = ((ceil_y - cell_y as f32) * 16.0) as usize & 15;
151
152 let is_beam = (tx == 0) || (ty == 0);
153 let c_base = if is_beam {
154 Rgb565::new(2, 4, 8)
155 } else if (cell_x + cell_y) % 2 == 0 {
156 ceil_color_a
157 } else {
158 ceil_color_b
159 };
160
161 let shaded = apply_shade(c_base, c_shade);
162 framebuf_u32[row_u32 + x_u32] = pack_rgb565_u32(shaded);
163
164 ceil_x += ceil_step_x * 2.0;
165 ceil_y += ceil_step_y * 2.0;
166 }
167 }
168 }
169
170 pub fn render_floor_and_ceiling_fast(
172 &self,
173 pos_x: f32,
174 pos_y: f32,
175 angle: f32,
176 head_bob: i32,
177 floor_color_a: Rgb565,
178 floor_color_b: Rgb565,
179 ceil_color_a: Rgb565,
180 ceil_color_b: Rgb565,
181 framebuf_u32: &mut [u32],
182 ) {
183 let dir_x = angle.cos();
184 let dir_y = angle.sin();
185 let plane_x = -dir_y * self.fov_scale;
186 let plane_y = dir_x * self.fov_scale;
187
188 let horizon = (self.height / 2) as i32 + head_bob;
189
190 let floor_a_u32 = pack_rgb565_u32(floor_color_a);
191 let floor_b_u32 = pack_rgb565_u32(floor_color_b);
192 let ceiling_a_u32 = pack_rgb565_u32(ceil_color_a);
193 let ceiling_b_u32 = pack_rgb565_u32(ceil_color_b);
194
195 let fov_inv = 2.0 / (self.width as f32);
196 let stride_u32 = self.width / 2;
197
198 for y in 0..self.height {
199 let p = (y as i32 - horizon) as f32;
200 if p == 0.0 {
201 continue;
202 }
203
204 let is_floor = p > 0.0;
205 let row_distance = if is_floor {
206 (self.height as f32 * 0.625) / p
207 } else {
208 (self.height as f32 * 0.625) / -p
209 };
210
211 let step_x = -row_distance * (plane_x * fov_inv) * 2.0;
212 let step_y = -row_distance * (plane_y * fov_inv) * 2.0;
213
214 let mut curr_x = pos_x + row_distance * (dir_x + plane_x);
215 let mut curr_y = pos_y + row_distance * (dir_y + plane_y);
216
217 let row_u32 = y * stride_u32;
218 let (col_a, col_b) = if is_floor {
219 (floor_a_u32, floor_b_u32)
220 } else {
221 (ceiling_a_u32, ceiling_b_u32)
222 };
223
224 for x2 in 0..stride_u32 {
225 let tx = (curr_x as usize) & 1;
226 let ty = (curr_y as usize) & 1;
227 let pixel_u32 = if (tx ^ ty) == 0 { col_a } else { col_b };
228
229 if row_u32 + x2 < framebuf_u32.len() {
230 framebuf_u32[row_u32 + x2] = pixel_u32;
231 }
232 curr_x += step_x;
233 curr_y += step_y;
234 }
235 }
236 }
237}
238
239#[derive(Debug, Clone)]
241pub struct Raycaster2D {
242 width: usize,
243 height: usize,
244 fov_scale: f32,
245}
246
247impl Raycaster2D {
248 pub fn new(width: usize, height: usize) -> Self {
249 Self {
250 width,
251 height,
252 fov_scale: 0.66,
253 }
254 }
255
256 pub fn render_walls(
258 &self,
259 pos_x: f32,
260 pos_y: f32,
261 angle: f32,
262 head_bob: i32,
263 map: &[u8],
264 map_size: usize,
265 wall_colors: &[Rgb565],
266 z_buffer: &mut [f32],
267 framebuf_u32: &mut [u32],
268 ) {
269 let dir_x = angle.cos();
270 let dir_y = angle.sin();
271
272 let plane_x = -dir_y * self.fov_scale;
273 let plane_y = dir_x * self.fov_scale;
274
275 let stride_u32 = self.width / 2;
276
277 for x in (0..self.width).step_by(4) {
278 let camera_x = -(2.0 * (x as f32) / (self.width as f32) - 1.0);
279 let ray_dir_x = dir_x + plane_x * camera_x;
280 let ray_dir_y = dir_y + plane_y * camera_x;
281
282 let mut map_x = pos_x as i32;
283 let mut map_y = pos_y as i32;
284
285 let delta_dist_x = if ray_dir_x == 0.0 {
286 1e30
287 } else {
288 (1.0 / ray_dir_x).abs()
289 };
290 let delta_dist_y = if ray_dir_y == 0.0 {
291 1e30
292 } else {
293 (1.0 / ray_dir_y).abs()
294 };
295
296 let (step_x, mut side_dist_x) = if ray_dir_x < 0.0 {
297 (-1, (pos_x - map_x as f32) * delta_dist_x)
298 } else {
299 (1, (map_x as f32 + 1.0 - pos_x) * delta_dist_x)
300 };
301
302 let (step_y, mut side_dist_y) = if ray_dir_y < 0.0 {
303 (-1, (pos_y - map_y as f32) * delta_dist_y)
304 } else {
305 (1, (map_y as f32 + 1.0 - pos_y) * delta_dist_y)
306 };
307
308 let mut hit_wall = 0u8;
309 let mut side = 0u8;
310 let mut steps = 0;
311
312 while hit_wall == 0 && steps < 24 {
313 if side_dist_x < side_dist_y {
314 side_dist_x += delta_dist_x;
315 map_x += step_x;
316 side = 0;
317 } else {
318 side_dist_y += delta_dist_y;
319 map_y += step_y;
320 side = 1;
321 }
322
323 if map_x >= 0 && map_x < map_size as i32 && map_y >= 0 && map_y < map_size as i32 {
324 let tile = map[(map_y as usize) * map_size + (map_x as usize)];
325 if tile > 0 {
326 hit_wall = tile;
327 }
328 } else {
329 hit_wall = 1;
330 }
331 steps += 1;
332 }
333
334 let perp_wall_dist = if side == 0 {
335 side_dist_x - delta_dist_x
336 } else {
337 side_dist_y - delta_dist_y
338 }
339 .max(0.1);
340
341 for i in 0..4 {
342 if x + i < self.width {
343 z_buffer[x + i] = perp_wall_dist;
344 }
345 }
346
347 let line_height = (self.height as f32 / perp_wall_dist) as i32;
348 let center_y = (self.height / 2) as i32 + head_bob;
349
350 let draw_start = (center_y - line_height / 2).clamp(0, self.height as i32 - 1) as usize;
351 let draw_end = (center_y + line_height / 2).clamp(0, self.height as i32 - 1) as usize;
352
353 let mut wall_x = if side == 0 {
354 pos_y + perp_wall_dist * ray_dir_y
355 } else {
356 pos_x + perp_wall_dist * ray_dir_x
357 };
358 wall_x -= wall_x.floor();
359 let tex_x = ((wall_x * 16.0) as usize).clamp(0, 15);
360
361 let shade_factor = 1.0 / (1.0 + perp_wall_dist * 0.18);
362 let color_idx = (hit_wall as usize).saturating_sub(1) % wall_colors.len().max(1);
363 let base_color = if wall_colors.is_empty() {
364 Rgb565::RED
365 } else {
366 wall_colors[color_idx]
367 };
368 let base_color = if side == 1 {
369 apply_shade(base_color, 0.7)
370 } else {
371 base_color
372 };
373
374 let x_u32 = x / 2;
375 let tex_step = 16.0 / (line_height as f32).max(1.0);
376 let mut tex_pos = ((draw_start as i32 - center_y + line_height / 2) as f32) * tex_step;
377
378 for y in draw_start..=draw_end {
379 let tex_y = (tex_pos as usize) & 15;
380 tex_pos += tex_step;
381
382 let is_pattern = (tex_x == 0) || (tex_y == 0);
383 let pixel = if is_pattern {
384 apply_shade(base_color, 0.5)
385 } else {
386 base_color
387 };
388 let shaded = apply_shade(pixel, shade_factor);
389 let pixel_u32 = pack_rgb565_u32(shaded);
390
391 let idx = y * stride_u32 + x_u32;
392 if idx < framebuf_u32.len() {
393 framebuf_u32[idx] = pixel_u32;
394 if idx + 1 < framebuf_u32.len() {
395 framebuf_u32[idx + 1] = pixel_u32;
396 }
397 }
398 }
399 }
400 }
401
402 pub fn render_walls_textured<F>(
412 &self,
413 pos_x: f32,
414 pos_y: f32,
415 angle: f32,
416 head_bob: i32,
417 map: &[u8],
418 map_size: usize,
419 z_buffer: &mut [f32],
420 framebuf_u32: &mut [u32],
421 get_pixel: F,
422 ) where
423 F: Fn(u8, usize, usize) -> Rgb565,
424 {
425 let dir_x = angle.cos();
426 let dir_y = angle.sin();
427
428 let plane_x = -dir_y * self.fov_scale;
429 let plane_y = dir_x * self.fov_scale;
430
431 let stride_u32 = self.width / 2;
432
433 for x in (0..self.width).step_by(4) {
434 let camera_x = -(2.0 * (x as f32) / (self.width as f32) - 1.0);
435 let ray_dir_x = dir_x + plane_x * camera_x;
436 let ray_dir_y = dir_y + plane_y * camera_x;
437
438 let mut map_x = pos_x as i32;
439 let mut map_y = pos_y as i32;
440
441 let delta_dist_x = if ray_dir_x == 0.0 {
442 1e30
443 } else {
444 (1.0 / ray_dir_x).abs()
445 };
446 let delta_dist_y = if ray_dir_y == 0.0 {
447 1e30
448 } else {
449 (1.0 / ray_dir_y).abs()
450 };
451
452 let (step_x, mut side_dist_x) = if ray_dir_x < 0.0 {
453 (-1, (pos_x - map_x as f32) * delta_dist_x)
454 } else {
455 (1, (map_x as f32 + 1.0 - pos_x) * delta_dist_x)
456 };
457
458 let (step_y, mut side_dist_y) = if ray_dir_y < 0.0 {
459 (-1, (pos_y - map_y as f32) * delta_dist_y)
460 } else {
461 (1, (map_y as f32 + 1.0 - pos_y) * delta_dist_y)
462 };
463
464 let mut hit_wall = 0u8;
465 let mut side = 0u8;
466 let mut steps = 0;
467
468 while hit_wall == 0 && steps < 24 {
469 if side_dist_x < side_dist_y {
470 side_dist_x += delta_dist_x;
471 map_x += step_x;
472 side = 0;
473 } else {
474 side_dist_y += delta_dist_y;
475 map_y += step_y;
476 side = 1;
477 }
478
479 if map_x >= 0 && map_x < map_size as i32 && map_y >= 0 && map_y < map_size as i32 {
480 let tile = map[(map_y as usize) * map_size + (map_x as usize)];
481 if tile > 0 {
482 hit_wall = tile;
483 }
484 } else {
485 hit_wall = 1;
486 }
487 steps += 1;
488 }
489
490 let perp_wall_dist = if side == 0 {
491 side_dist_x - delta_dist_x
492 } else {
493 side_dist_y - delta_dist_y
494 }
495 .max(0.1);
496
497 for i in 0..4 {
498 if x + i < self.width {
499 z_buffer[x + i] = perp_wall_dist;
500 }
501 }
502
503 let line_height = (self.height as f32 / perp_wall_dist) as i32;
504 let center_y = (self.height / 2) as i32 + head_bob;
505
506 let draw_start = (center_y - line_height / 2).clamp(0, self.height as i32 - 1) as usize;
507 let draw_end = (center_y + line_height / 2).clamp(0, self.height as i32 - 1) as usize;
508
509 let mut wall_x = if side == 0 {
511 pos_y + perp_wall_dist * ray_dir_y
512 } else {
513 pos_x + perp_wall_dist * ray_dir_x
514 };
515 wall_x -= wall_x.floor();
516 let tex_x = ((wall_x * 16.0) as usize).clamp(0, 15);
517
518 let base_shade = 1.0 / (1.0 + perp_wall_dist * 0.18);
520 let shade = if side == 1 {
521 base_shade * 0.7
522 } else {
523 base_shade
524 };
525 let shade_q8 = (shade.clamp(0.05, 1.0) * 256.0) as u32;
526
527 let x_u32 = x / 2;
528 let tex_step = 16.0 / (line_height as f32).max(1.0);
529 let mut tex_pos = ((draw_start as i32 - center_y + line_height / 2) as f32) * tex_step;
530
531 for y in draw_start..=draw_end {
532 let tex_y = (tex_pos as usize) & 15;
533 tex_pos += tex_step;
534
535 let raw_color = get_pixel(hit_wall, tex_x, tex_y);
536 let shaded = apply_shade_q8(raw_color, shade_q8);
537 let pixel_u32 = pack_rgb565_u32(shaded);
538
539 let idx = y * stride_u32 + x_u32;
540 if idx < framebuf_u32.len() {
541 framebuf_u32[idx] = pixel_u32;
542 if idx + 1 < framebuf_u32.len() {
543 framebuf_u32[idx + 1] = pixel_u32;
544 }
545 }
546 }
547 }
548 }
549
550 pub fn render_sprites_fast<P, F, T>(
556 &self,
557 pos_x: f32,
558 pos_y: f32,
559 angle: f32,
560 head_bob: i32,
561 sprites: &[RaycastSprite],
562 z_buffer: &[f32],
563 framebuf: &mut [Rgb565],
564 prepare_sprite: P,
565 get_pixel: F,
566 ) where
567 P: Fn(&RaycastSprite, f32) -> Option<T>,
568 F: Fn(&T, usize, usize, usize, usize) -> Option<Rgb565>,
569 {
570 let dir_x = angle.cos();
571 let dir_y = angle.sin();
572 let plane_x = -dir_y * self.fov_scale;
573 let plane_y = dir_x * self.fov_scale;
574 let inv_det = 1.0 / (plane_x * dir_y - dir_x * plane_y);
575 let center_y = (self.height / 2) as i32 + head_bob;
576
577 for sprite in sprites {
578 if !sprite.active {
579 continue;
580 }
581
582 let sx = sprite.x - pos_x;
583 let sy = sprite.y - pos_y;
584
585 let transform_x = inv_det * (dir_y * sx - dir_x * sy);
586 let transform_y = inv_det * (-plane_y * sx + plane_x * sy);
587
588 if transform_y <= 0.3 {
590 continue;
591 }
592
593 let sprite_data = match prepare_sprite(sprite, transform_y) {
594 Some(d) => d,
595 None => continue,
596 };
597
598 let sprite_screen_x =
599 ((self.width as f32 / 2.0) * (1.0 - transform_x / transform_y)) as i32;
600 let sprite_height = ((self.height as f32 / transform_y).abs()) as i32;
601 let sprite_width = sprite_height;
602
603 let draw_start_y =
604 (center_y - sprite_height / 2).clamp(0, self.height as i32 - 1) as usize;
605 let draw_end_y =
606 (center_y + sprite_height / 2).clamp(0, self.height as i32 - 1) as usize;
607
608 let draw_start_x =
609 (sprite_screen_x - sprite_width / 2).clamp(0, self.width as i32 - 1) as usize;
610 let draw_end_x =
611 (sprite_screen_x + sprite_width / 2).clamp(0, self.width as i32 - 1) as usize;
612
613 for stripe_x in draw_start_x..draw_end_x {
614 if stripe_x >= z_buffer.len() || transform_y >= z_buffer[stripe_x] {
616 continue;
617 }
618
619 for y in draw_start_y..draw_end_y {
620 if let Some(color) =
621 get_pixel(&sprite_data, stripe_x, y, draw_start_y, draw_end_y)
622 {
623 let idx = y * self.width + stripe_x;
624 if idx < framebuf.len() {
625 framebuf[idx] = color;
626 }
627 }
628 }
629 }
630 }
631 }
632
633 pub fn render_sprites<F>(
636 &self,
637 pos_x: f32,
638 pos_y: f32,
639 angle: f32,
640 head_bob: i32,
641 sprites: &[RaycastSprite],
642 z_buffer: &[f32],
643 framebuf: &mut [Rgb565],
644 get_color: F,
645 ) where
646 F: Fn(&RaycastSprite, f32) -> Option<Rgb565>,
647 {
648 self.render_sprites_fast(
649 pos_x,
650 pos_y,
651 angle,
652 head_bob,
653 sprites,
654 z_buffer,
655 framebuf,
656 |sprite, _dist| Some(*sprite),
657 |sprite, _stripe_x, y, draw_start_y, draw_end_y| {
658 let norm_y = if draw_end_y > draw_start_y {
659 (y - draw_start_y) as f32 / (draw_end_y - draw_start_y) as f32
660 } else {
661 0.0
662 };
663 get_color(sprite, norm_y)
664 },
665 );
666 }
667}
668
669#[inline(always)]
671pub fn apply_shade_q8(color: Rgb565, shade_q8: u32) -> Rgb565 {
672 let raw = color.into_storage() as u32;
673 let r = ((((raw >> 11) & 0x1F) * shade_q8) >> 8).min(31);
674 let g = ((((raw >> 5) & 0x3F) * shade_q8) >> 8).min(63);
675 let b = (((raw & 0x1F) * shade_q8) >> 8).min(31);
676 Rgb565::new(r as u8, g as u8, b as u8)
677}
678
679#[inline(always)]
681pub fn apply_shade(color: Rgb565, factor: f32) -> Rgb565 {
682 let shade_q8 = (factor.clamp(0.05, 1.0) * 256.0) as u32;
683 apply_shade_q8(color, shade_q8)
684}
685
686#[inline(always)]
691pub fn pack_rgb565_u32(color: Rgb565) -> u32 {
692 let raw = color.into_storage() as u32;
693 (raw << 16) | raw
694}
695
696#[cfg(test)]
697mod tests {
698 use super::*;
699
700 #[test]
701 fn test_mode7_renderer_execution() {
702 let renderer = Mode7Renderer::new(240, 256);
703 let mut framebuf_u32 = [0u32; (240 * 256) / 2];
704
705 renderer.render_floor_and_ceiling(
706 2.5,
707 2.5,
708 0.0,
709 0,
710 Rgb565::RED,
711 Rgb565::GREEN,
712 Rgb565::BLUE,
713 Rgb565::YELLOW,
714 &mut framebuf_u32,
715 );
716
717 let non_zero_pixels = framebuf_u32.iter().filter(|&&p| p != 0).count();
718 assert!(
719 non_zero_pixels > 0,
720 "Mode7Renderer should render non-zero pixels"
721 );
722 }
723
724 #[test]
725 fn test_raycaster2d_execution() {
726 let raycaster = Raycaster2D::new(240, 256);
727 let map = [1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1];
728 let wall_colors = [Rgb565::RED, Rgb565::GREEN];
729 let mut z_buffer = [0.0f32; 240];
730 let mut framebuf_u32 = [0u32; (240 * 256) / 2];
731
732 raycaster.render_walls(
733 1.5,
734 1.5,
735 0.0,
736 0,
737 &map,
738 4,
739 &wall_colors,
740 &mut z_buffer,
741 &mut framebuf_u32,
742 );
743
744 assert!(z_buffer[0] > 0.0, "Z-buffer should record wall distances");
745 }
746
747 #[test]
748 fn test_render_walls_textured_writes_pixels_and_z_buffer() {
749 let raycaster = Raycaster2D::new(240, 256);
750 let map = [1u8, 1, 1, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1];
752 let mut z_buffer = [0.0f32; 240];
753 let mut framebuf_u32 = [0u32; (240 * 256) / 2];
754
755 raycaster.render_walls_textured(
756 1.5,
757 1.5,
758 0.0,
759 0,
760 &map,
761 4,
762 &mut z_buffer,
763 &mut framebuf_u32,
764 |tile, _tex_x, _tex_y| {
765 if tile == 1 {
767 Rgb565::GREEN
768 } else {
769 Rgb565::RED
770 }
771 },
772 );
773
774 assert!(
775 z_buffer[0] > 0.0,
776 "render_walls_textured must write the z-buffer"
777 );
778 let written = framebuf_u32.iter().any(|&p| p != 0);
779 assert!(
780 written,
781 "render_walls_textured must write at least one pixel"
782 );
783 }
784
785 #[test]
786 fn test_render_sprites_draws_behind_z_buffer() {
787 let raycaster = Raycaster2D::new(240, 256);
788
789 let sprites = [RaycastSprite {
792 x: 5.0,
793 y: 1.5,
794 texture_id: 0,
795 active: true,
796 }];
797 let z_buffer = [100.0f32; 240];
798 let mut framebuf = [Rgb565::BLACK; 240 * 256];
799
800 raycaster.render_sprites(
801 1.5,
802 1.5,
803 0.0, 0,
805 &sprites,
806 &z_buffer,
807 &mut framebuf,
808 |_sprite, _norm_y| Some(Rgb565::YELLOW),
809 );
810
811 let yellow_pixels = framebuf.iter().filter(|&&p| p == Rgb565::YELLOW).count();
813 assert!(
814 yellow_pixels > 0,
815 "render_sprites should draw the sprite when unoccluded"
816 );
817 }
818
819 #[test]
820 fn test_render_sprites_occluded_by_z_buffer() {
821 let raycaster = Raycaster2D::new(240, 256);
822
823 let sprites = [RaycastSprite {
825 x: 5.0,
826 y: 1.5,
827 texture_id: 0,
828 active: true,
829 }];
830 let z_buffer = [0.1f32; 240]; let mut framebuf = [Rgb565::BLACK; 240 * 256];
832
833 raycaster.render_sprites(
834 1.5,
835 1.5,
836 0.0,
837 0,
838 &sprites,
839 &z_buffer,
840 &mut framebuf,
841 |_sprite, _norm_y| Some(Rgb565::YELLOW),
842 );
843
844 let yellow_pixels = framebuf.iter().filter(|&&p| p == Rgb565::YELLOW).count();
845 assert_eq!(
846 yellow_pixels, 0,
847 "Occluded sprite should not write any pixels"
848 );
849 }
850
851 #[test]
852 fn test_apply_shade_public() {
853 let c = Rgb565::new(20, 40, 15);
855 let out = apply_shade(c, 1.0);
856 assert_eq!(out.r(), 20);
857 assert_eq!(out.g(), 40);
858 assert_eq!(out.b(), 15);
859
860 let dark = apply_shade(Rgb565::new(31, 63, 31), 0.0);
862 assert!(dark.r() > 0);
863 }
864
865 #[test]
866 fn test_pack_rgb565_u32_public() {
867 let c = Rgb565::new(10, 20, 10);
868 let raw = c.into_storage() as u32;
869 let packed = pack_rgb565_u32(c);
870 assert_eq!(packed, (raw << 16) | raw);
871 }
872}