#![allow(clippy::indexing_slicing)]
use alloc::vec;
use alloc::vec::Vec;
use crate::map::MapData;
use crate::math::*;
use crate::texture::TextureData;
#[doc(hidden)]
pub mod bsp;
pub mod hud;
#[doc(hidden)]
pub mod planes;
#[doc(hidden)]
pub mod sprites;
#[doc(hidden)]
pub mod tables;
#[doc(hidden)]
pub mod walls;
pub use hud::HudFrame;
pub type ViewParams = crate::world::Pose;
#[repr(u8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticClass {
Void = 0,
Floor = 1,
Ceiling = 2,
Wall = 3,
Door = 4,
Projectile = 5,
Enemy = 6,
Player = 7,
Item = 8,
}
impl SemanticClass {
#[inline]
pub fn from_u8(v: u8) -> Self {
match v {
1 => Self::Floor,
2 => Self::Ceiling,
3 => Self::Wall,
4 => Self::Door,
5 => Self::Projectile,
6 => Self::Enemy,
7 => Self::Player,
8 => Self::Item,
_ => Self::Void,
}
}
#[inline]
pub fn to_rgb(self) -> u32 {
match self {
Self::Void => 0x00_00_00_00,
Self::Floor => 0x00_3C_3C_3C,
Self::Ceiling => 0x00_28_28_50,
Self::Wall => 0x00_78_64_50,
Self::Door => 0x00_C8_C8_32,
Self::Projectile => 0x00_FF_64_00,
Self::Enemy => 0x00_FF_00_00,
Self::Player => 0x00_00_FF_00,
Self::Item => 0x00_00_C8_FF,
}
}
}
#[derive(Clone)]
pub struct DrawSeg {
pub x1: i32,
pub x2: i32,
pub scale1: Fixed,
pub scale2: Fixed,
pub scalestep: Fixed,
pub silhouette: i32,
pub bsilheight: Fixed,
pub tsilheight: Fixed,
pub sprite_top_clip: [i16; SCREENWIDTH],
pub sprite_bottom_clip: [i16; SCREENWIDTH],
}
#[derive(Clone)]
pub struct MaskedSeg {
pub seg_idx: usize,
pub x1: i32,
pub x2: i32,
pub scale1: Fixed,
pub scalestep: Fixed,
pub rw_offset: Fixed,
pub rw_distance: Fixed,
pub rw_center_angle: Angle,
pub light_num: usize,
pub top_clip: [i16; SCREENWIDTH],
pub bot_clip: [i16; SCREENWIDTH],
}
#[derive(Clone)]
pub struct VisSprite {
pub x1: i32,
pub x2: i32,
pub scale: Fixed,
pub x_iscale: Fixed,
pub start_frac: Fixed,
pub texture_mid: Fixed,
pub patch_lump: usize,
pub colormap_idx: usize,
pub mobj_flags: u32,
pub semantic: SemanticClass,
pub gz: Fixed,
pub gzt: Fixed,
pub tz: Fixed,
}
#[derive(Clone)]
pub struct VisPlane {
pub height: Fixed,
pub pic_num: i16,
pub light_level: i16,
pub min_x: i32,
pub max_x: i32,
pub top: [u8; SCREENWIDTH],
pub bottom: [u8; SCREENWIDTH],
}
impl VisPlane {
pub fn new(height: Fixed, pic_num: i16, light_level: i16) -> Self {
Self {
height,
pic_num,
light_level,
min_x: SCREENWIDTH as i32,
max_x: -1,
top: [0xFF; SCREENWIDTH],
bottom: [0; SCREENWIDTH],
}
}
}
#[derive(Clone, Copy)]
pub struct ClipRange {
pub first: i32,
pub last: i32,
}
const MAXSEGS: usize = 32;
pub const LIGHTLEVELS: usize = 16;
pub const LIGHTSEGSHIFT: i32 = 4;
pub const MAXLIGHTSCALE: usize = 48;
pub const LIGHTSCALESHIFT: i32 = 12;
pub const NUMCOLORMAPS: usize = 32;
pub const HEIGHTBITS: i32 = 12;
pub const HEIGHTUNIT: i32 = 1 << HEIGHTBITS;
pub const ML_DONTPEGBOTTOM: i16 = 16;
pub const ML_DONTPEGTOP: i16 = 8;
pub const SIL_NONE: i32 = 0;
pub const SIL_BOTTOM: i32 = 1;
pub const SIL_TOP: i32 = 2;
pub const SIL_BOTH: i32 = 3;
pub struct Renderer {
pub screen: Vec<u8>,
pub rgba: Vec<u8>,
pub semantic: Vec<u8>,
pub depth: Vec<Fixed>,
pub floor_clip: [i16; SCREENWIDTH],
pub ceiling_clip: [i16; SCREENWIDTH],
pub solidsegs: [ClipRange; MAXSEGS],
pub solidsegs_end: usize,
pub view_x: Fixed,
pub view_y: Fixed,
pub view_z: Fixed,
pub view_angle: Angle,
pub view_cos: Fixed,
pub view_sin: Fixed,
pub extra_light: i32,
pub view_width: i32,
pub view_height: i32,
pub center_x: Fixed,
pub center_y: Fixed,
pub center_x_frac: Fixed,
pub center_y_frac: Fixed,
pub projection: Fixed,
pub viewangletox: Vec<i32>,
pub xtoviewangle: Vec<Angle>,
pub clip_angle: Angle,
pub drawsegs: Vec<DrawSeg>,
pub visplanes: Vec<VisPlane>,
pub masked_segs: Vec<MaskedSeg>,
pub valid_count: i32,
pub scalelight: [[usize; MAXLIGHTSCALE]; LIGHTLEVELS],
pub yslope: [Fixed; SCREENHEIGHT],
pub distscale: [Fixed; SCREENWIDTH],
pub basexscale: Fixed,
pub baseyscale: Fixed,
pub zlight: [[usize; planes::MAXLIGHTZ]; LIGHTLEVELS],
pub sky_flat_num: i16,
pub sky_texture_num: i16,
pub vissprites: Vec<VisSprite>,
pub cur_floor_plane: Option<usize>,
pub cur_ceiling_plane: Option<usize>,
}
impl Default for Renderer {
fn default() -> Self {
Self::new()
}
}
impl Renderer {
pub fn new() -> Self {
let npix = SCREENWIDTH * SCREENHEIGHT;
let mut r = Self {
screen: vec![0u8; npix],
rgba: vec![0u8; npix * 4],
semantic: vec![0u8; npix],
depth: vec![0i32; npix],
floor_clip: [0i16; SCREENWIDTH],
ceiling_clip: [0i16; SCREENWIDTH],
solidsegs: [ClipRange { first: 0, last: 0 }; MAXSEGS],
solidsegs_end: 0,
view_x: 0,
view_y: 0,
view_z: 0,
view_angle: 0,
view_cos: 0,
view_sin: 0,
extra_light: 0,
view_width: SCREENWIDTH as i32,
view_height: SCREENHEIGHT as i32,
center_x: (SCREENWIDTH / 2) as Fixed,
center_y: (SCREENHEIGHT / 2) as Fixed,
center_x_frac: ((SCREENWIDTH / 2) as Fixed) << FRACBITS,
center_y_frac: ((SCREENHEIGHT / 2) as Fixed) << FRACBITS,
projection: ((SCREENWIDTH / 2) as Fixed) << FRACBITS,
viewangletox: vec![0i32; FINEANGLES / 2],
xtoviewangle: vec![0u32; SCREENWIDTH + 1],
clip_angle: 0,
drawsegs: Vec::with_capacity(128),
masked_segs: Vec::with_capacity(32),
visplanes: Vec::with_capacity(64),
valid_count: 0,
scalelight: [[0usize; MAXLIGHTSCALE]; LIGHTLEVELS],
yslope: [0; SCREENHEIGHT],
distscale: [0; SCREENWIDTH],
basexscale: 0,
baseyscale: 0,
zlight: [[0usize; planes::MAXLIGHTZ]; LIGHTLEVELS],
vissprites: Vec::with_capacity(128),
sky_flat_num: -1,
sky_texture_num: -1,
cur_floor_plane: None,
cur_ceiling_plane: None,
};
r.init_texture_mapping();
r.init_light_tables();
r.init_planes();
r.init_zlight();
r
}
fn init_light_tables(&mut self) {
const DISTMAP: usize = 2;
for i in 0..LIGHTLEVELS {
let startmap =
((LIGHTLEVELS - 1 - i) * 2 * NUMCOLORMAPS / LIGHTLEVELS) as i32;
for j in 0..MAXLIGHTSCALE {
let level = (startmap - (j as i32) / DISTMAP as i32)
.clamp(0, NUMCOLORMAPS as i32 - 1);
self.scalelight[i][j] = level as usize;
}
}
}
pub fn setup_frame(&mut self, x: Fixed, y: Fixed, z: Fixed, angle: Angle) {
self.view_x = x;
self.view_y = y;
self.view_z = z;
self.view_angle = angle;
self.view_sin = crate::tables::FINESINE[(angle >> ANGLETOFINESHIFT) as usize];
self.view_cos = finecosine((angle >> ANGLETOFINESHIFT) as usize);
self.valid_count += 1;
}
pub fn clear_frame(&mut self) {
self.masked_segs.clear();
self.screen.fill(0);
self.semantic.fill(SemanticClass::Void as u8);
self.depth.fill(0);
self.floor_clip.fill(self.view_height as i16);
self.ceiling_clip.fill(-1);
self.solidsegs[0] = ClipRange { first: -0x7FFF_FFFF, last: -1 };
self.solidsegs[1] = ClipRange {
first: self.view_width,
last: 0x7FFF_FFFF,
};
self.solidsegs_end = 2;
self.drawsegs.clear();
self.visplanes.clear();
self.cur_floor_plane = None;
self.cur_ceiling_plane = None;
}
pub fn render_player_view(
&mut self,
map: &MapData,
textures: &TextureData,
view: &ViewParams,
) {
self.setup_frame(view.x, view.y, view.z, view.angle);
self.clear_frame();
self.clear_planes();
let root = map.root_node();
self.render_bsp_node(map, textures, root);
self.draw_planes(textures);
}
pub fn draw_masked_walls(&mut self, map: &MapData, textures: &TextureData) {
self.draw_masked_segs(map, textures);
}
fn draw_masked_segs(&mut self, map: &MapData, textures: &TextureData) {
let mut segs: Vec<MaskedSeg> = core::mem::take(&mut self.masked_segs);
segs.sort_by(|a, b| b.rw_distance.cmp(&a.rw_distance));
for ms in &segs {
let seg = &map.segs[ms.seg_idx];
let line = &map.lines[seg.line as usize];
let side = &map.sides[seg.side as usize];
let front_sector = &map.sectors[seg.front_sector as usize];
let back_sector = match seg.back_sector {
Some(bs) => &map.sectors[bs as usize],
None => continue,
};
let masked_tex = if side.mid_texture > 0 {
side.mid_texture
} else {
let other = if line.sidenum[0] == Some(seg.side) { line.sidenum[1] } else { line.sidenum[0] };
other.map(|s| map.sides[s as usize].mid_texture).unwrap_or(0)
};
if masked_tex <= 0 || masked_tex as usize >= textures.textures.len() { continue; }
let tex = &textures.textures[masked_tex as usize];
let tex_h = (tex.height as Fixed) << FRACBITS;
let texmid = if line.flags & ML_DONTPEGBOTTOM != 0 {
let base = front_sector.floor_height.max(back_sector.floor_height);
base + tex_h - self.view_z
} else {
let base = front_sector.ceiling_height.min(back_sector.ceiling_height);
base - self.view_z
} + side.rowoffset;
let mut cur_scale = ms.scale1;
for x in ms.x1..=ms.x2 {
let angle_idx = ms.rw_center_angle
.wrapping_add(self.xtoviewangle[x as usize])
>> ANGLETOFINESHIFT;
let tex_col = (ms.rw_offset
- fixed_mul(
crate::tables::FINETANGENT[(angle_idx as usize) % (FINEANGLES / 2)],
ms.rw_distance,
)) >> FRACBITS;
let light_idx = ((cur_scale >> LIGHTSCALESHIFT) as usize).min(MAXLIGHTSCALE - 1);
let colormap_idx = self.scalelight[ms.light_num][light_idx];
let iscale = if cur_scale > 0 {
(0xFFFFFFFFu32 / cur_scale as u32) as Fixed
} else {
i32::MAX
};
let col_depth = if cur_scale > 0 {
fixed_div(self.projection, cur_scale)
} else {
i32::MAX
};
let mt_top = ms.top_clip[x as usize] as i32 + 1;
let mt_bot = ms.bot_clip[x as usize] as i32 - 1;
if mt_top <= mt_bot {
self.draw_masked_column(
x, mt_top, mt_bot,
tex, tex_col, colormap_idx, iscale, texmid,
SemanticClass::Wall, col_depth, &textures.colormaps,
);
}
cur_scale += ms.scalestep;
}
}
self.masked_segs = segs;
}
pub fn indexed_to_rgba(&mut self, textures: &TextureData) {
let Some(pal) = textures.palettes.first() else {
return;
};
let (rgb_triples, _tail) = pal.as_chunks::<3>();
for (&pixel, rgba) in self.screen.iter().zip(self.rgba.chunks_exact_mut(4)) {
let rgb = match rgb_triples.get(pixel as usize) {
Some(t) => *t,
None => [0, 0, 0],
};
rgba[0] = rgb[0];
rgba[1] = rgb[1];
rgba[2] = rgb[2];
rgba[3] = 255;
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_column(
&mut self,
x: i32,
yl: i32,
yh: i32,
texture: &crate::texture::WallTexture,
tex_col: i32,
colormap_idx: usize,
iscale: Fixed,
texturemid: Fixed,
class: SemanticClass,
distance: Fixed,
colormaps: &[[u8; 256]],
) {
if yl > yh || x < 0 || x >= SCREENWIDTH as i32 {
return;
}
let yl = yl.max(0);
let yh = yh.min(self.view_height - 1);
let col_idx = (tex_col as usize) % (texture.width as usize);
let col_data = texture.column(col_idx);
let tex_height = texture.height as i32;
if tex_height == 0 {
return;
}
let cmap = if colormap_idx < colormaps.len() {
&colormaps[colormap_idx]
} else if !colormaps.is_empty() {
&colormaps[0]
} else {
return;
};
let frac_start = texturemid
.wrapping_add((yl - self.center_y).wrapping_mul(iscale));
let mut frac = frac_start as i64;
for y in yl..=yh {
let idx = (y * SCREENWIDTH as i32 + x) as usize;
let mut tex_y = ((frac >> FRACBITS) as i32) % tex_height;
if tex_y < 0 {
tex_y += tex_height;
}
let tex_y = tex_y as usize;
let pixel = if tex_y < col_data.len() {
col_data[tex_y]
} else {
0
};
self.screen[idx] = cmap[pixel as usize];
self.semantic[idx] = class as u8;
self.depth[idx] = distance;
frac += iscale as i64;
}
}
#[allow(clippy::too_many_arguments)] pub fn draw_masked_column(
&mut self,
x: i32,
yl: i32,
yh: i32,
texture: &crate::texture::WallTexture,
tex_col: i32,
colormap_idx: usize,
iscale: Fixed,
texturemid: Fixed,
class: SemanticClass,
distance: Fixed,
colormaps: &[[u8; 256]],
) {
if yl > yh || x < 0 || x >= SCREENWIDTH as i32 { return; }
let yl = yl.max(0);
let yh = yh.min(self.view_height - 1);
let col_idx = (tex_col as usize) % (texture.width as usize);
let col_data = texture.column(col_idx);
let tex_height = texture.height as i32;
if tex_height == 0 { return; }
let cmap = if colormap_idx < colormaps.len() {
&colormaps[colormap_idx]
} else if !colormaps.is_empty() {
&colormaps[0]
} else {
return;
};
let frac_start = texturemid
.wrapping_add((yl - self.center_y).wrapping_mul(iscale));
let mut frac = frac_start as i64;
for y in yl..=yh {
let tex_y = (frac >> FRACBITS) as i32;
if tex_y >= 0 && tex_y < tex_height {
let pixel = col_data[tex_y as usize];
if pixel != 0 {
let idx = (y * SCREENWIDTH as i32 + x) as usize;
let sem = self.semantic[idx];
let is_sprite = sem == SemanticClass::Enemy as u8
|| sem == SemanticClass::Player as u8
|| sem == SemanticClass::Item as u8
|| sem == SemanticClass::Projectile as u8;
if is_sprite && distance > self.depth[idx] {
} else {
self.screen[idx] = cmap[pixel as usize];
self.semantic[idx] = class as u8;
self.depth[idx] = distance;
}
}
}
frac += iscale as i64;
}
}
}