Skip to content

Commit 0832441

Browse files
authored
Merge pull request #1054 from louis-e/feat/water-depth
feat(water): per-cell water depth carving
2 parents 6172ffb + a7d4610 commit 0832441

7 files changed

Lines changed: 584 additions & 29 deletions

File tree

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,7 @@ Thumbs.db
3030
# Generated files
3131
/export.json
3232
/parsed_osm_data.txt
33-
/elevation_debug.png
34-
/landcover_debug*.png
33+
/*debug*.png
3534
/terrain-tile-cache
3635
/arnis-tile-cache
3736

src/data_processing.rs

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ pub fn generate_world_with_options(
6868
};
6969
let ground = Arc::new(ground);
7070

71+
// Per-cell water depth field from the LC_WATER mask; empty without land cover.
72+
let big_water_field = crate::water_depth::compute_big_water_field(&ground, &xzbbox);
73+
7174
println!("{} Processing data...", "[4/7]".bold());
7275

7376
// Build highway connectivity map once before processing
@@ -250,7 +253,13 @@ pub fn generate_world_with_options(
250253
} else if let Some(val) = way.tags.get("waterway") {
251254
if val == "dock" {
252255
// docks count as water areas
253-
water_areas::generate_water_area_from_way(&mut editor, way, &xzbbox);
256+
water_areas::generate_water_area_from_way(
257+
&mut editor,
258+
way,
259+
&xzbbox,
260+
&big_water_field,
261+
&road_mask,
262+
);
254263
} else {
255264
waterways::generate_waterways(&mut editor, way);
256265
}
@@ -349,7 +358,13 @@ pub fn generate_world_with_options(
349358
.map(|val| val == "water" || val == "bay")
350359
.unwrap_or(false)
351360
{
352-
water_areas::generate_water_areas_from_relation(&mut editor, rel, &xzbbox);
361+
water_areas::generate_water_areas_from_relation(
362+
&mut editor,
363+
rel,
364+
&xzbbox,
365+
&big_water_field,
366+
&road_mask,
367+
);
353368
} else if rel.tags.contains_key("natural") {
354369
natural::generate_natural_from_relation(
355370
&mut editor,
@@ -388,10 +403,9 @@ pub fn generate_world_with_options(
388403
process_pb.inc(element_counter % pb_batch_size);
389404
process_pb.finish();
390405

391-
// Drop remaining caches
406+
// Keep road_mask alive for the LC_WATER carve below.
392407
drop(highway_connectivity);
393408
drop(flood_fill_cache);
394-
drop(road_mask);
395409

396410
// Generate ground layer (surface blocks, vegetation, shorelines, underground fill)
397411
ground_generation::generate_ground_layer(
@@ -402,6 +416,17 @@ pub fn generate_world_with_options(
402416
&building_footprints,
403417
)?;
404418

419+
// Carve depth into ESA water cells (water_areas.rs only covers OSM polygons).
420+
crate::water_depth::carve_lc_water_pass(
421+
&mut editor,
422+
ground.as_ref(),
423+
&xzbbox,
424+
&big_water_field,
425+
&road_mask,
426+
);
427+
428+
drop(road_mask);
429+
405430
// Carve subway tunnel interiors now that underground is filled with stone.
406431
// This must happen after ground generation so AIR blocks are not overwritten.
407432
if !subway_points.is_empty() {

src/element_processing/water_areas.rs

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
use crate::block_definitions::WATER;
21
use crate::clipping::clip_water_ring_to_bbox;
2+
use crate::floodfill_cache::RoadMaskBitmap;
3+
use crate::water_depth::{carve_water_column, BigWaterField};
34
use crate::{
45
coordinate_system::cartesian::{XZBBox, XZPoint},
56
osm_parser::{ProcessedMemberRole, ProcessedNode, ProcessedRelation, ProcessedWay},
@@ -10,20 +11,24 @@ pub fn generate_water_area_from_way(
1011
editor: &mut WorldEditor,
1112
element: &ProcessedWay,
1213
_xzbbox: &XZBBox,
14+
bwf: &BigWaterField,
15+
road_mask: &RoadMaskBitmap,
1316
) {
1417
let outers = [element.nodes.clone()];
1518
if !verify_closed_rings(&outers) {
1619
println!("Skipping way {} due to invalid polygon", element.id);
1720
return;
1821
}
1922

20-
generate_water_areas(editor, &outers, &[]);
23+
generate_water_areas(editor, &outers, &[], bwf, road_mask);
2124
}
2225

2326
pub fn generate_water_areas_from_relation(
2427
editor: &mut WorldEditor,
2528
element: &ProcessedRelation,
2629
xzbbox: &XZBBox,
30+
bwf: &BigWaterField,
31+
road_mask: &RoadMaskBitmap,
2732
) {
2833
// Check if this is a water relation (either with water tag or natural=water)
2934
let is_water = element.tags.contains_key("water")
@@ -116,13 +121,15 @@ pub fn generate_water_areas_from_relation(
116121
return;
117122
}
118123

119-
generate_water_areas(editor, &outers, &inners);
124+
generate_water_areas(editor, &outers, &inners, bwf, road_mask);
120125
}
121126

122127
fn generate_water_areas(
123128
editor: &mut WorldEditor,
124129
outers: &[Vec<ProcessedNode>],
125130
inners: &[Vec<ProcessedNode>],
131+
bwf: &BigWaterField,
132+
road_mask: &RoadMaskBitmap,
126133
) {
127134
// Calculate polygon bounding box to limit fill area
128135
let mut poly_min_x = i32::MAX;
@@ -161,7 +168,9 @@ fn generate_water_areas(
161168
.map(|x| x.iter().map(|y| y.xz()).collect::<Vec<_>>())
162169
.collect();
163170

164-
scanline_fill_water(min_x, min_z, max_x, max_z, &outers_xz, &inners_xz, editor);
171+
scanline_fill_water(
172+
min_x, min_z, max_x, max_z, &outers_xz, &inners_xz, editor, bwf, road_mask,
173+
);
165174
}
166175

167176
/// Verifies all rings are properly closed (first node matches last).
@@ -391,6 +400,8 @@ fn scanline_fill_water(
391400
outers: &[Vec<XZPoint>],
392401
inners: &[Vec<XZPoint>],
393402
editor: &mut WorldEditor,
403+
bwf: &BigWaterField,
404+
road_mask: &RoadMaskBitmap,
394405
) {
395406
// Collect edges per outer ring so we can union their spans correctly,
396407
// even if multiple outer rings happen to overlap (invalid OSM, but
@@ -427,14 +438,18 @@ fn scanline_fill_water(
427438

428439
for (start, end) in fill_spans {
429440
for x in start..=end {
441+
// Keep road/bridge surfaces (carve would overwrite them).
442+
if road_mask.contains(x, z) {
443+
continue;
444+
}
430445
let water_y = editor.get_water_level(x, z);
431446
let ground_y = editor.get_ground_level(x, z);
432-
// Only place water where terrain is at or below the water
433-
// surface — skip hillside blocks where the polygon extends
434-
// above the actual waterline.
435-
if ground_y <= water_y {
436-
editor.set_block_absolute(WATER, x, water_y, z, None, None);
447+
// Skip hillside blocks the polygon claims above the waterline.
448+
if ground_y > water_y {
449+
continue;
437450
}
451+
// depth_at gives the carved depth (0 without land-cover water data).
452+
carve_water_column(editor, x, z, water_y, bwf.depth_at(x, z));
438453
}
439454
}
440455
}

src/ground.rs

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ impl Ground {
6565
// post-processing pipeline for land-cover-aware artifact repair.
6666
// The elevation grid is built from the same (bbox, scale) so both
6767
// grids share dimensions (both use compute_grid_dims).
68-
let (_, _, grid_w, grid_h) = compute_grid_dims(bbox, scale);
68+
let (world_w, world_h, grid_w, grid_h) = compute_grid_dims(bbox, scale);
6969
let mut land_cover = if fetch_land_cover {
7070
let lc = land_cover::fetch_land_cover_data(bbox, grid_w, grid_h);
7171
if lc.is_some() {
@@ -78,18 +78,28 @@ impl Ground {
7878
None
7979
};
8080

81+
// Raise the floor for the deepest water carve (elevation path only).
82+
let water_floor = match &land_cover {
83+
Some(lc) => {
84+
let max_depth =
85+
crate::water_depth::estimate_max_carve_depth(&lc.grid, world_w, world_h);
86+
ground_level.max(crate::world_editor::MIN_Y + max_depth + 2)
87+
}
88+
None => ground_level,
89+
};
90+
8191
match fetch_elevation_data(
8292
bbox,
8393
scale,
84-
ground_level,
94+
water_floor,
8595
disable_height_limit,
8696
extended_max_y,
8797
land_cover.as_mut(),
8898
aws_only_elevation,
8999
) {
90100
Ok(elevation_data) => Self {
91101
elevation_enabled: true,
92-
ground_level,
102+
ground_level: water_floor,
93103
elevation_data: Some(elevation_data),
94104
land_cover,
95105
rotation_mask: None,
@@ -165,6 +175,36 @@ impl Ground {
165175
);
166176
}
167177

178+
/// Local block bbox (min_x, min_z, max_x, max_z) covering all LC_WATER cells,
179+
/// derived from the land-cover grid; None if no land cover or no water.
180+
pub fn lc_water_block_bounds(&self) -> Option<(i32, i32, i32, i32)> {
181+
let (lc, data) = match (&self.land_cover, &self.elevation_data) {
182+
(Some(lc), Some(data)) => (lc, data),
183+
_ => return None,
184+
};
185+
let (mut gx0, mut gz0, mut gx1, mut gz1) = (usize::MAX, usize::MAX, 0usize, 0usize);
186+
let mut any = false;
187+
for (z, row) in lc.grid.iter().enumerate() {
188+
for (x, &c) in row.iter().enumerate() {
189+
if c == land_cover::LC_WATER {
190+
gx0 = gx0.min(x);
191+
gx1 = gx1.max(x);
192+
gz0 = gz0.min(z);
193+
gz1 = gz1.max(z);
194+
any = true;
195+
}
196+
}
197+
}
198+
if !any {
199+
return None;
200+
}
201+
let (x0, x1) =
202+
crate::water_depth::grid_span_to_block_span(gx0, gx1, data.world_width, lc.width);
203+
let (z0, z1) =
204+
crate::water_depth::grid_span_to_block_span(gz0, gz1, data.world_height, lc.height);
205+
Some((x0, z0, x1, z1))
206+
}
207+
168208
/// Returns the ESA WorldCover land cover class at the given coordinates.
169209
/// Returns 0 if land cover data is not available.
170210
#[inline(always)]

src/ground_generation.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -318,18 +318,10 @@ pub fn generate_ground_layer(
318318
}
319319

320320
if place_esa_water {
321-
// Single block of water at snapped level
321+
// Pre-paint; carve_lc_water_pass later overwrites with depth.
322322
editor.set_block_if_absent_absolute(WATER, x, water_y, z);
323-
324-
// Floor: sand/gravel/clay + sandstone below
325-
let h = land_cover::coord_hash(x, z);
326-
let floor_block = match h % 5 {
327-
0 => GRAVEL,
328-
1 => CLAY,
329-
_ => SAND,
330-
};
331323
if water_y - 1 > MIN_Y {
332-
editor.set_block_if_absent_absolute(floor_block, x, water_y - 1, z);
324+
editor.set_block_if_absent_absolute(SAND, x, water_y - 1, z);
333325
}
334326
if water_y - 2 > MIN_Y {
335327
editor.set_block_if_absent_absolute(SANDSTONE, x, water_y - 2, z);
@@ -1132,7 +1124,7 @@ pub fn generate_ground_layer(
11321124
///
11331125
/// Cost: 4 hash calls + a few f64 ops per block — still well under 100 ns,
11341126
/// negligible over the whole ground pass.
1135-
fn value_noise_01(x: i32, z: i32, scale: i32) -> f64 {
1127+
pub(crate) fn value_noise_01(x: i32, z: i32, scale: i32) -> f64 {
11361128
let s = scale.max(1);
11371129
// Integer lattice cell containing (x, z). div_euclid gives floor
11381130
// division for negative coordinates too, so patches tile uniformly

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ mod telemetry;
3333
#[cfg(test)]
3434
mod test_utilities;
3535
mod version_check;
36+
mod water_depth;
3637
mod world_editor;
3738
mod world_utils;
3839

0 commit comments

Comments
 (0)