Skip to main content

roxlap_core/
opticast.rs

1//! Per-frame render settings shared by the CPU renderer.
2//!
3//! Historically this module also held voxlap's `opticast` orchestrator
4//! (the four-quadrant 2.5D scan loops). That renderer was replaced by
5//! the per-pixel 3D-DDA renderer in [`crate::dda`] and removed; what
6//! remains is [`OpticastSettings`], the framebuffer + projection +
7//! scan-distance bundle both the DDA renderer and the sprite raycaster
8//! still consume. (The name is kept for API stability; "opticast"
9//! survives only as a label for the settings struct.)
10
11/// Per-frame settings the renderer forwards through its passes. Most
12/// fields map onto a classic raycaster control: `(hx, hy, hz)` are the
13/// pinhole projection centre + focal length (the voxlap `setcamera`
14/// `dahx`/`dahy`/`dahz`), `mip_*` drive the distance-mip ladder, and
15/// `max_scan_dist` bounds the ray.
16///
17/// `y_start..y_end` is the strip-render iteration bound. Default is the
18/// full framebuffer (`0..yres`). Tile / strip callers set a sub-range
19/// to render only that horizontal strip; the projection centre stays in
20/// absolute screen coords, only the viewport edges shrink.
21#[derive(Debug, Clone, Copy)]
22pub struct OpticastSettings {
23    /// Framebuffer width in pixels. The projection centre and the
24    /// strip / band bounds below are all in this full-frame space.
25    pub xres: u32,
26    /// Framebuffer height in pixels.
27    pub yres: u32,
28    /// First y-row this render call covers (inclusive). `0` for
29    /// full-frame.
30    pub y_start: u32,
31    /// One past the last y-row (exclusive). `yres` for full-frame.
32    pub y_end: u32,
33    /// PF.13 (C7) — first x-column covered (inclusive). `0` for
34    /// full-frame. The DDA renderer's pixels are fully independent, so
35    /// an x sub-range is as safe as the y strip (the historical
36    /// full-width-only constraint came from the deleted voxlap radar's
37    /// column-indexed `angstart`).
38    pub x_start: u32,
39    /// One past the last x-column (exclusive). `xres` for full-frame.
40    pub x_end: u32,
41    /// Projection-centre x in pixels (the voxlap `setcamera` `dahx`).
42    /// `xres / 2` for a centred view; stays in absolute screen coords
43    /// even when a strip sub-range is set.
44    pub hx: f32,
45    /// Projection-centre y in pixels (the voxlap `dahy`). `yres / 2`
46    /// for a centred view.
47    pub hy: f32,
48    /// Focal length in pixels (the voxlap `dahz`): pixel `(px, py)`
49    /// casts along `(px−hx)·right + (py−hy)·down + hz·forward`, so the
50    /// vertical field of view is `2·atan(yres/2 / hz)` and
51    /// `hz = xres/2` gives 90° horizontal.
52    pub hz: f32,
53    /// Voxlap's `anginc` — the deleted opticast's per-column angle
54    /// step (`1` = one ray per pixel column). The per-pixel DDA casts
55    /// every pixel regardless; today the value survives only as the
56    /// `anginc + 1` viewport padding in `roxlap-scene`'s screen-rect
57    /// cull. Leave at the default `1`.
58    pub anginc: f32,
59    /// Depth of the distance-mip ladder: how many mip levels the
60    /// renderer may sample, `1` = mip-0 only (level `n` is `2ⁿ`×
61    /// coarser per axis). Also the clamp ceiling for the per-grid LOD
62    /// overrides (`LodThresholds::mid_mip_levels`,
63    /// `Grid::mip_levels_override` in `roxlap-scene`). The demos run 6.
64    pub mip_levels: u32,
65    /// Mip-transition distance in voxels: rays step through
66    /// progressively coarser mips beyond it, so scan distance costs
67    /// roughly logarithmically instead of linearly. Smaller ⇒ coarser
68    /// sooner. The demos run 64; per-grid Mid-LOD overrides may only
69    /// shrink it (`min`, never extend).
70    pub mip_scan_dist: i32,
71    /// Hard ray-length cap in voxels (voxlap's `vx5.maxscandist`): the
72    /// march gives up (sky / fog) past it, and `roxlap-scene` skips
73    /// whole grids entirely outside this radius. Floored at 1
74    /// downstream; pair with the fog distance so the cutoff reads as
75    /// atmosphere instead of a wall.
76    pub max_scan_dist: i32,
77}
78
79impl OpticastSettings {
80    /// Default settings for a `width × height` framebuffer with the
81    /// convention `(hx, hy, hz) = (w/2, h/2, w/2)` and `anginc = 1`.
82    /// Renders the full frame (`y_start = 0, y_end = height`).
83    //
84    // `width` / `height` cast to f32 is bounded by realistic screen
85    // sizes (≤ 16M, well within f32's 24-bit mantissa).
86    #[allow(clippy::cast_precision_loss)]
87    #[must_use]
88    pub fn for_oracle_framebuffer(width: u32, height: u32) -> Self {
89        let half_w = (width as f32) * 0.5;
90        let half_h = (height as f32) * 0.5;
91        Self {
92            xres: width,
93            yres: height,
94            y_start: 0,
95            y_end: height,
96            x_start: 0,
97            x_end: width,
98            hx: half_w,
99            hy: half_h,
100            hz: half_w,
101            anginc: 1.0,
102            mip_levels: 1,
103            mip_scan_dist: 4,
104            max_scan_dist: 1024,
105        }
106    }
107
108    /// Pick an explicit vertical field of view (radians): sets the
109    /// focal length `hz = (yres/2) / tan(fov_y/2)` so both renderer
110    /// backends show exactly this FOV (QE.2a — the facade derives the
111    /// GPU projection from these settings too). The projection centre
112    /// (`hx`, `hy`) is untouched. The default
113    /// [`Self::for_oracle_framebuffer`] focal (`hz = w/2`) equals
114    /// `with_fov_y(2·atan(h/w))` — ≈ 73.7° for 4:3, ≈ 58.7° for 16:9.
115    #[must_use]
116    pub fn with_fov_y(mut self, fov_y_rad: f32) -> Self {
117        // yres → f32 is exact for realistic screen sizes.
118        #[allow(clippy::cast_precision_loss)]
119        let half_h = (self.yres as f32) * 0.5;
120        self.hz = half_h / (fov_y_rad * 0.5).tan();
121        self
122    }
123
124    /// Restrict this settings struct to the `[y_start, y_end)`
125    /// horizontal strip. Used by the per-strip parallel dispatch — each
126    /// strip clones the base settings and clamps the y-range. Caller is
127    /// responsible for ensuring `y_start < y_end <= yres`.
128    #[must_use]
129    pub fn with_y_range(mut self, y_start: u32, y_end: u32) -> Self {
130        self.y_start = y_start;
131        self.y_end = y_end;
132        self
133    }
134
135    /// PF.13 (C7) — restrict to the `[x_start, x_end)` vertical band;
136    /// the per-grid screen scissor pairs this with
137    /// [`Self::with_y_range`] so a small grid renders only its true
138    /// screen rect instead of full-width rows. Caller ensures
139    /// `x_start < x_end <= xres`.
140    #[must_use]
141    pub fn with_x_range(mut self, x_start: u32, x_end: u32) -> Self {
142        self.x_start = x_start;
143        self.x_end = x_end;
144        self
145    }
146}