Skip to content

Commit 05b8fe2

Browse files
fix(recast): use chf.areas[] instead of stale spans[].area in contour building
After erode_walkable_area runs, chf.areas[i] is zeroed for eroded spans but chf.spans[i].area retains the pre-erosion value. The contour building code used the stale spans[i].area for area border detection, causing RC_AREA_BORDER (0x20000) to never be set on contour vertices. This affected simplify_contour anchor selection, changing vertex order, triangulation, and polygon merge decisions. Bug #27: polymesh.rs used contour.vertices[0].region instead of contour.region for polygon region assignment. Bug #28: merge_holes calculated winding for contours with < 3 vertices; C++ skips these entirely. Bug #29: All spans[i].area references in contour.rs replaced with chf.areas[i] in walk_contour, get_corner_height, build_contours, build_regions, and flood_fill_region. Results after fix: - nav_test: 537 polys, 2228 detail verts (exact C++ match, was 530/2207) - dungeon: 216 polys (C++ 217), 866 detail verts (C++ 868), 434 tris (exact) - bridge: exact match
1 parent 8d3f787 commit 05b8fe2

File tree

5 files changed

+105
-24
lines changed

5 files changed

+105
-24
lines changed

crates/detour/tests/integration.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,9 +143,9 @@ fn dungeon_find_nearest_poly_center() {
143143
let (poly_ref, snapped) = result.unwrap();
144144
assert!(poly_ref.is_valid());
145145

146-
// After box blur fix
146+
// After area border flag fix (Bug #29)
147147
assert!((snapped.x - 12.1450).abs() < 0.01);
148-
assert!((snapped.y - 10.3973).abs() < 0.01);
148+
assert!((snapped.y - 10.3074).abs() < 0.01);
149149
assert!((snapped.z - (-40.5750)).abs() < 0.01);
150150
}
151151

crates/recast/src/contour.rs

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -479,9 +479,9 @@ impl ContourSet {
479479
let mut current_region_id = 1u16;
480480
let mut regions_created = 0;
481481

482-
for (span_idx, span) in chf.spans.iter().enumerate() {
482+
for span_idx in 0..chf.spans.len() {
483483
// Skip non-walkable spans or already assigned spans
484-
if span.area == 0 || region_ids[span_idx] != 0 {
484+
if chf.areas[span_idx] == 0 || region_ids[span_idx] != 0 {
485485
continue;
486486
}
487487

@@ -557,10 +557,8 @@ impl ContourSet {
557557
continue;
558558
}
559559

560-
let span = &chf.spans[span_idx];
561-
562560
// Skip unwalkable spans
563-
if span.area == 0 {
561+
if chf.areas[span_idx] == 0 {
564562
continue;
565563
}
566564

@@ -571,10 +569,8 @@ impl ContourSet {
571569
// Using 8-direction constants: N=1, E=3, S=5, W=7
572570
for dir in [1u8, 3u8, 5u8, 7u8] {
573571
if let Some(neighbor_idx) = chf.get_neighbor(span_idx, dir) {
574-
let neighbor_span = &chf.spans[neighbor_idx];
575-
576572
// Add to stack if not assigned and walkable
577-
if region_ids[neighbor_idx] == 0 && neighbor_span.area != 0 {
573+
if region_ids[neighbor_idx] == 0 && chf.areas[neighbor_idx] != 0 {
578574
stack.push(neighbor_idx);
579575
}
580576
}
@@ -731,7 +727,7 @@ impl ContourSet {
731727
continue;
732728
}
733729

734-
let area = chf.spans[span_idx].area;
730+
let area = chf.areas[span_idx];
735731
contours_attempted += 1;
736732

737733
// Trace and simplify contour
@@ -834,7 +830,7 @@ impl ContourSet {
834830

835831
// Combine region and area codes to prevent border vertices between areas
836832
// from being removed
837-
regs[0] = region_ids[i] as u32 | ((span.area as u32) << 16);
833+
regs[0] = region_ids[i] as u32 | ((chf.areas[i] as u32) << 16);
838834

839835
// Check neighbor in dir direction
840836
if span.con[dir as usize] != RC_NOT_CONNECTED {
@@ -844,7 +840,7 @@ impl ContourSet {
844840
let ai = base_idx + span.con[dir as usize];
845841
let as_ = &chf.spans[ai];
846842
ch = ch.max(as_.y);
847-
regs[1] = region_ids[ai] as u32 | ((as_.area as u32) << 16);
843+
regs[1] = region_ids[ai] as u32 | ((chf.areas[ai] as u32) << 16);
848844

849845
// Check diagonal neighbor (from ai in dirp)
850846
if as_.con[dirp] != RC_NOT_CONNECTED {
@@ -854,7 +850,7 @@ impl ContourSet {
854850
let ai2 = base_idx2 + as_.con[dirp];
855851
let as2 = &chf.spans[ai2];
856852
ch = ch.max(as2.y);
857-
regs[2] = region_ids[ai2] as u32 | ((as2.area as u32) << 16);
853+
regs[2] = region_ids[ai2] as u32 | ((chf.areas[ai2] as u32) << 16);
858854
}
859855
}
860856
}
@@ -868,7 +864,7 @@ impl ContourSet {
868864
let ai = base_idx + span.con[dirp];
869865
let as_ = &chf.spans[ai];
870866
ch = ch.max(as_.y);
871-
regs[3] = region_ids[ai] as u32 | ((as_.area as u32) << 16);
867+
regs[3] = region_ids[ai] as u32 | ((chf.areas[ai] as u32) << 16);
872868

873869
// Check diagonal neighbor (from ai in dir)
874870
if as_.con[dir as usize] != RC_NOT_CONNECTED {
@@ -878,7 +874,7 @@ impl ContourSet {
878874
let ai2 = base_idx2 + as_.con[dir as usize];
879875
let as2 = &chf.spans[ai2];
880876
ch = ch.max(as2.y);
881-
regs[2] = region_ids[ai2] as u32 | ((as2.area as u32) << 16);
877+
regs[2] = region_ids[ai2] as u32 | ((chf.areas[ai2] as u32) << 16);
882878
}
883879
}
884880
}
@@ -931,7 +927,7 @@ impl ContourSet {
931927

932928
let start_dir = dir;
933929
let start_i = i;
934-
let area = chf.spans[i].area;
930+
let area = chf.areas[i];
935931

936932
let mut iter = 0;
937933
let mut cur_x = x;
@@ -974,7 +970,7 @@ impl ContourSet {
974970
let _span = &chf.spans[cur_i];
975971
if let Some(neighbor_idx) = chf.get_neighbor_connection(cur_i, dir as usize) {
976972
r = region_ids[neighbor_idx] as i32;
977-
if area != chf.spans[neighbor_idx].area {
973+
if area != chf.areas[neighbor_idx] {
978974
is_area_border = true;
979975
}
980976
}
@@ -1488,10 +1484,14 @@ impl ContourSet {
14881484
return;
14891485
}
14901486

1491-
// Calculate winding for each contour
1487+
// Calculate winding for each contour.
1488+
// C++ skips contours with nverts < 3 entirely (winding stays 0).
14921489
let windings: Vec<i32> = contours
14931490
.iter()
14941491
.map(|c| {
1492+
if c.vertices.len() < 3 {
1493+
return 0;
1494+
}
14951495
if Self::calc_area_of_polygon_2d(&c.vertices) < 0 {
14961496
-1
14971497
} else {
@@ -1511,12 +1511,16 @@ impl ContourSet {
15111511
contours.len()
15121512
);
15131513

1514-
// Group contours by region: outlines and holes
1514+
// Group contours by region: outlines and holes.
1515+
// Only process contours with non-zero winding (matches C++ nverts >= 3 check).
15151516
let nregions = max_regions as usize + 1;
15161517
let mut region_outlines: Vec<Option<usize>> = vec![None; nregions];
15171518
let mut region_holes: Vec<Vec<usize>> = vec![Vec::new(); nregions];
15181519

15191520
for (i, contour) in contours.iter().enumerate() {
1521+
if windings[i] == 0 {
1522+
continue;
1523+
}
15201524
let reg = contour.region as usize;
15211525
if reg >= nregions {
15221526
continue;

crates/recast/src/polymesh.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ impl PolyMesh {
300300

301301
p[..nvp].copy_from_slice(q);
302302

303-
mesh.regs[mesh.npolys] = (contour.vertices[0].region & 0xffff) as u16;
303+
mesh.regs[mesh.npolys] = contour.region;
304304
mesh.areas[mesh.npolys] = contour.area;
305305
mesh.npolys += 1;
306306

crates/recast/tests/integration.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,11 +112,11 @@ fn dungeon_generation_rust_output() {
112112
let (_, poly_mesh, detail_mesh, _) = build_mesh("dungeon.obj");
113113

114114
// Current Rust output (regression test)
115-
// After hole merging fix
115+
// After area border flag fix (Bug #29)
116116
assert_eq!(poly_mesh.poly_count(), 216); // C++ 217 (0.995x)
117117
assert_eq!(poly_mesh.vert_count(), 452); // C++ 452 (exact)
118-
assert_eq!(detail_mesh.vert_count(), 874); // C++ 868 (1.007x)
119-
assert_eq!(detail_mesh.tri_count(), 447); // C++ 434 (1.03x)
118+
assert_eq!(detail_mesh.vert_count(), 866); // C++ 868 (0.998x)
119+
assert_eq!(detail_mesh.tri_count(), 434); // C++ 434 (exact)
120120
}
121121

122122
#[test]

crates/recast/tests/pipeline_diagnostic.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,17 @@ fn run_pipeline_diagnostic(obj_name: &str) {
218218
sizes.iter().sum::<u32>()
219219
);
220220
}
221+
// Per-region span counts
222+
let mut reg_sizes: Vec<(u16, u32)> = region_span_counts
223+
.iter()
224+
.filter(|&(r, _)| *r > 0 && *r < 0x8000)
225+
.map(|(&r, &c)| (r, c))
226+
.collect();
227+
reg_sizes.sort_by_key(|&(r, _)| r);
228+
println!(" Per-region span counts:");
229+
for &(reg, count) in &reg_sizes {
230+
println!(" region {}: {} spans", reg, count);
231+
}
221232
println!();
222233

223234
// Stage 5: Contours
@@ -257,6 +268,20 @@ fn run_pipeline_diagnostic(obj_name: &str) {
257268
}
258269
);
259270

271+
// Dump all contour vertices for comparison with C++
272+
for (i, c) in contour_set.contours.iter().enumerate() {
273+
print!(
274+
"contour[{}] reg={} nverts={}: ",
275+
i,
276+
c.region,
277+
c.vertices.len()
278+
);
279+
for v in &c.vertices {
280+
print!("({},{},{},{}) ", v.x, v.y, v.z, v.region);
281+
}
282+
println!();
283+
}
284+
260285
// Per-contour vertex counts (sorted) for C++ comparison
261286
let mut vert_counts: Vec<usize> = contour_set
262287
.contours
@@ -279,6 +304,58 @@ fn run_pipeline_diagnostic(obj_name: &str) {
279304
println!(" nverts: {}", poly_mesh.vert_count());
280305
println!(" npolys: {}", poly_mesh.poly_count());
281306
println!(" maxpolys: {}", poly_mesh.max_polys());
307+
308+
// Per-contour info for C++ comparison
309+
println!("\n Per-contour data (index, region, nverts):");
310+
for (i, c) in contour_set.contours.iter().enumerate() {
311+
println!(
312+
" cont[{}]: reg={} nverts={}",
313+
i,
314+
c.region,
315+
c.vertices.len()
316+
);
317+
}
318+
319+
// Polys per region
320+
let nvp = config.max_vertices_per_polygon as usize;
321+
let mut polys_per_reg = std::collections::HashMap::new();
322+
let regs = poly_mesh.regs();
323+
for i in 0..poly_mesh.poly_count() {
324+
*polys_per_reg.entry(regs[i]).or_insert(0u32) += 1;
325+
}
326+
let mut reg_list: Vec<_> = polys_per_reg.iter().collect();
327+
reg_list.sort_by_key(|&(r, _)| *r);
328+
println!("\n Polys per region:");
329+
for &(reg, count) in &reg_list {
330+
println!(" region {}: {} polys", reg, count);
331+
}
332+
333+
// Poly vertex count distribution
334+
let polys_data = poly_mesh.polys();
335+
let mut count3 = 0u32;
336+
let mut count4 = 0u32;
337+
let mut count5 = 0u32;
338+
let mut count6 = 0u32;
339+
for i in 0..poly_mesh.poly_count() {
340+
let mut nv = 0;
341+
for j in 0..nvp {
342+
if polys_data[i * nvp * 2 + j] == 0xffff {
343+
break;
344+
}
345+
nv += 1;
346+
}
347+
match nv {
348+
3 => count3 += 1,
349+
4 => count4 += 1,
350+
5 => count5 += 1,
351+
6 => count6 += 1,
352+
_ => {}
353+
}
354+
}
355+
println!(
356+
"\n Poly vertex counts: 3-vert: {}, 4-vert: {}, 5-vert: {}, 6-vert: {}",
357+
count3, count4, count5, count6
358+
);
282359
println!();
283360

284361
// Stage 7: PolyMeshDetail

0 commit comments

Comments
 (0)