Skip to main content

oxiphysics_collision/recast/
compact.rs

1// Copyright 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Compact heightfield: dense array of walkable spans with precomputed connectivity.
5
6use super::RecastConfig;
7use super::rasterize::Heightfield;
8
9/// Shared direction table: 4 cardinal neighbours (dx, dz).
10/// Dir 0 = +X, Dir 1 = +Z, Dir 2 = -X, Dir 3 = -Z.
11pub(crate) const DIRS: [(i32, i32); 4] = [(1, 0), (0, 1), (-1, 0), (0, -1)];
12
13#[derive(Debug, Clone, Default)]
14pub struct CompactCell {
15    /// Index into `CompactHeightfield::spans` for the first span in this cell.
16    pub index: u32,
17    /// Number of walkable spans in this cell.
18    pub count: u16,
19}
20
21#[derive(Debug, Clone, Default)]
22pub struct CompactSpan {
23    /// Floor height (top of the walkable span) in voxel units.
24    pub y: u16,
25    /// Region ID (0 = unassigned).
26    pub reg: u16,
27    /// Connectivity info: 4 directions × 8 bits each.
28    /// For direction `d`: bits `d*8 .. d*8+7`.
29    /// Value 0xFF in those bits = no connection (border or no matching span).
30    pub con: u32,
31    /// Clearance above this span (in voxels), capped at walkable_height.
32    pub h: u16,
33}
34
35pub struct CompactHeightfield {
36    pub width: usize,
37    pub height: usize, // Z dimension
38    pub span_count: usize,
39    pub walkable_height: u16,
40    pub walkable_climb: u16,
41    pub bmin: [f64; 3],
42    pub bmax: [f64; 3],
43    pub cs: f64,
44    pub ch: f64,
45    pub cells: Vec<CompactCell>, // width * height cells
46    pub spans: Vec<CompactSpan>, // all walkable spans
47    pub dist: Vec<u16>,          // distance-to-border field per span
48    pub max_dist: u16,
49}
50
51pub fn build_compact_heightfield(hf: &Heightfield, cfg: &RecastConfig) -> CompactHeightfield {
52    let walkable_height = (cfg.agent_height / cfg.cell_height).ceil() as u16;
53    let walkable_climb = (cfg.agent_max_climb / cfg.cell_height) as u16;
54    let w = hf.width;
55    let h = hf.height;
56    let n = w * h;
57
58    let mut cells = vec![CompactCell::default(); n];
59    let mut spans: Vec<CompactSpan> = Vec::new();
60
61    // Build compact spans from walkable open-heightfield spans
62    for iz in 0..h {
63        for ix in 0..w {
64            let col = &hf.spans[hf.col_idx(ix, iz)];
65            let cell_idx = iz * w + ix;
66            cells[cell_idx].index = spans.len() as u32;
67            let start = spans.len();
68            for (si, s) in col.iter().enumerate() {
69                if !s.walkable {
70                    continue;
71                }
72                let y = s.smax;
73                let clearance = if si + 1 < col.len() {
74                    col[si + 1].smin.saturating_sub(s.smax)
75                } else {
76                    u16::MAX
77                };
78                spans.push(CompactSpan {
79                    y,
80                    reg: 0,
81                    con: 0xFFFFFFFF, // all directions = no connection initially
82                    h: clearance.min(walkable_height),
83                });
84            }
85            cells[cell_idx].count = (spans.len() - start) as u16;
86        }
87    }
88
89    let span_count = spans.len();
90
91    // Build connectivity using the shared DIRS table
92    for iz in 0..h {
93        for ix in 0..w {
94            let cell_idx = iz * w + ix;
95            let ci = cells[cell_idx].index as usize;
96            let cc = cells[cell_idx].count as usize;
97            for i in 0..cc {
98                let si = ci + i;
99                let s_y = spans[si].y as i32;
100                let mut con = 0xFFFFFFFFu32;
101                for (dir, &(dx, dz)) in DIRS.iter().enumerate() {
102                    let nx = ix as i32 + dx;
103                    let nz = iz as i32 + dz;
104                    if nx < 0 || nz < 0 || nx >= w as i32 || nz >= h as i32 {
105                        // Border: keep 0xFF in this direction slot (already set)
106                        continue;
107                    }
108                    let ncell_idx = nz as usize * w + nx as usize;
109                    let nci = cells[ncell_idx].index as usize;
110                    let ncc = cells[ncell_idx].count as usize;
111                    // Find the best neighbour span (closest floor height within climb)
112                    let mut best = 0xFFu32;
113                    for ni in 0..ncc {
114                        let n_y = spans[nci + ni].y as i32;
115                        if (n_y - s_y).abs() <= walkable_climb as i32 {
116                            best = ni as u32;
117                            break;
118                        }
119                    }
120                    // Clear the 8-bit slot for this direction, then set new value
121                    con &= !(0xFFu32 << (dir * 8));
122                    con |= (best & 0xFF) << (dir * 8);
123                }
124                spans[si].con = con;
125            }
126        }
127    }
128
129    // Distance transform (two-pass, Chebyshev approximation)
130    let mut dist = vec![u16::MAX; span_count];
131
132    // Mark border spans: any span with at least one direction having no connection (0xFF)
133    for si in 0..span_count {
134        let con = spans[si].con;
135        let is_border = (0..4).any(|dir| ((con >> (dir * 8)) & 0xFF) == 0xFF);
136        if is_border {
137            dist[si] = 0;
138        }
139    }
140
141    // Forward pass (+X, +Z directions)
142    for iz in 0..h {
143        for ix in 0..w {
144            let cell_idx = iz * w + ix;
145            let ci = cells[cell_idx].index as usize;
146            let cc = cells[cell_idx].count as usize;
147            for i in 0..cc {
148                let si = ci + i;
149                for (dir, &(dx, dz)) in DIRS.iter().enumerate() {
150                    let nc_raw = (spans[si].con >> (dir * 8)) & 0xFF;
151                    if nc_raw == 0xFF {
152                        continue;
153                    }
154                    let nx = ix as i32 + dx;
155                    let nz = iz as i32 + dz;
156                    if nx < 0 || nz < 0 || nx >= w as i32 || nz >= h as i32 {
157                        continue;
158                    }
159                    let ncell_idx = nz as usize * w + nx as usize;
160                    let nci = cells[ncell_idx].index as usize;
161                    let nsi = nci + nc_raw as usize;
162                    if dist[nsi] != u16::MAX {
163                        let new_dist = dist[nsi].saturating_add(2);
164                        if new_dist < dist[si] {
165                            dist[si] = new_dist;
166                        }
167                    }
168                }
169            }
170        }
171    }
172
173    // Backward pass (-X, -Z directions)
174    for iz in (0..h).rev() {
175        for ix in (0..w).rev() {
176            let cell_idx = iz * w + ix;
177            let ci = cells[cell_idx].index as usize;
178            let cc = cells[cell_idx].count as usize;
179            for i in 0..cc {
180                let si = ci + i;
181                for (dir, &(dx, dz)) in DIRS.iter().enumerate() {
182                    let nc_raw = (spans[si].con >> (dir * 8)) & 0xFF;
183                    if nc_raw == 0xFF {
184                        continue;
185                    }
186                    let nx = ix as i32 + dx;
187                    let nz = iz as i32 + dz;
188                    if nx < 0 || nz < 0 || nx >= w as i32 || nz >= h as i32 {
189                        continue;
190                    }
191                    let ncell_idx = nz as usize * w + nx as usize;
192                    let nci = cells[ncell_idx].index as usize;
193                    let nsi = nci + nc_raw as usize;
194                    if dist[nsi] != u16::MAX {
195                        let new_dist = dist[nsi].saturating_add(2);
196                        if new_dist < dist[si] {
197                            dist[si] = new_dist;
198                        }
199                    }
200                }
201            }
202        }
203    }
204
205    let max_dist = dist
206        .iter()
207        .cloned()
208        .filter(|&d| d != u16::MAX)
209        .max()
210        .unwrap_or(0);
211
212    CompactHeightfield {
213        width: w,
214        height: h,
215        span_count,
216        walkable_height,
217        walkable_climb,
218        bmin: hf.bmin,
219        bmax: hf.bmax,
220        cs: hf.cs,
221        ch: hf.ch,
222        cells,
223        spans,
224        dist,
225        max_dist,
226    }
227}