optimize visibility calcs

This commit is contained in:
2026-02-28 11:27:50 +00:00
parent b1de9f95c2
commit b0b0ca0390
3 changed files with 93 additions and 68 deletions
+11 -1
View File
@@ -146,8 +146,18 @@ pub fn generate_chunk_terrain(
}); });
}); });
for (pos, data) in tilemap_updates.into_inner().unwrap() { let new_positions: Vec<IVec3> = tilemap_updates
.into_inner()
.unwrap()
.into_iter()
.map(|(pos, data)| {
tilemap.floor_tiles.insert(pos, data); tilemap.floor_tiles.insert(pos, data);
pos
})
.collect();
// All tiles now in tilemap — safe to calculate visibility
for pos in new_positions {
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos }); occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
} }
+22 -19
View File
@@ -209,39 +209,42 @@ pub fn build_quilted_terrain_sprites(
_ => return, _ => return,
}; };
// Bounds are clamped to the chunk grid so textures are always exactly // Actual tile extent within this chunk, padded by one tile on each side
// CHUNK_TILES wide/tall (or smaller at world edges), keeping sizes // so the rendered quad always shows one tile of context beyond the edge
// predictable and buffer reuse rates high. // (prevents the hard black cutoff at chunk/world boundaries).
let chunk_world_min_x = key.chunk_x as f32 * TILE_SIZE * CHUNK_TILES as f32;
let chunk_world_min_y = key.chunk_y as f32 * TILE_SIZE * CHUNK_TILES as f32;
// Actual tile extent within this chunk (may be smaller than full chunk
// at world edges or sparse z-levels).
let min_x_aligned: f32 = ((tiles.iter().map(|(p, _)| p.x).reduce(f32::min).unwrap() let min_x_aligned: f32 = ((tiles.iter().map(|(p, _)| p.x).reduce(f32::min).unwrap()
- TILE_SIZE / 2.0) - TILE_SIZE / 2.0)
/ TILE_SIZE) / TILE_SIZE)
.floor() .floor()
* TILE_SIZE; * TILE_SIZE
- TILE_SIZE;
let min_y_aligned: f32 = ((tiles.iter().map(|(p, _)| p.y).reduce(f32::min).unwrap() let min_y_aligned: f32 = ((tiles.iter().map(|(p, _)| p.y).reduce(f32::min).unwrap()
- TILE_SIZE / 2.0) - TILE_SIZE / 2.0)
/ TILE_SIZE) / TILE_SIZE)
.floor() .floor()
* TILE_SIZE; * TILE_SIZE
- TILE_SIZE;
let max_x_aligned: f32 = ((tiles.iter().map(|(p, _)| p.x).reduce(f32::max).unwrap() let max_x_aligned: f32 = ((tiles.iter().map(|(p, _)| p.x).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0) + TILE_SIZE / 2.0)
/ TILE_SIZE) / TILE_SIZE)
.ceil() .ceil()
* TILE_SIZE; * TILE_SIZE
+ TILE_SIZE;
let max_y_aligned: f32 = ((tiles.iter().map(|(p, _)| p.y).reduce(f32::max).unwrap() let max_y_aligned: f32 = ((tiles.iter().map(|(p, _)| p.y).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0) + TILE_SIZE / 2.0)
/ TILE_SIZE) / TILE_SIZE)
.ceil() .ceil()
* TILE_SIZE; * TILE_SIZE
+ TILE_SIZE;
let width_tiles = ((max_x_aligned - min_x_aligned) / TILE_SIZE) as u32; let width_tiles = ((max_x_aligned - min_x_aligned) / TILE_SIZE) as u32;
let height_tiles = ((max_y_aligned - min_y_aligned) / TILE_SIZE) as u32; let height_tiles = ((max_y_aligned - min_y_aligned) / TILE_SIZE) as u32;
let width_px = width_tiles * TILE_PIXELS;
let height_px = height_tiles * TILE_PIXELS; // Add 2px bleed (1px each side) so adjacent chunk quads overlap by 1px
// at any zoom level. Without this, sub-pixel gaps appear between chunks
// at fractional orthographic scales.
let width_px = width_tiles * TILE_PIXELS + 2;
let height_px = height_tiles * TILE_PIXELS + 2;
let required_len = (width_px * height_px * 4) as usize; let required_len = (width_px * height_px * 4) as usize;
dimensions_results dimensions_results
@@ -281,8 +284,8 @@ pub fn build_quilted_terrain_sprites(
src_height, src_height,
width_px, width_px,
height_px, height_px,
tile_x * TILE_PIXELS, tile_x * TILE_PIXELS + 1, // +1 to account for 1px bleed border
tile_y * TILE_PIXELS, tile_y * TILE_PIXELS + 1,
); );
} }
@@ -328,13 +331,13 @@ pub fn build_quilted_terrain_sprites(
..Default::default() ..Default::default()
}, },
Transform::from_xyz( Transform::from_xyz(
center_x - TILE_SIZE / 2.0, center_x - TILE_SIZE / 2.0 - PIXEL_RATIO,
center_y - TILE_SIZE / 2.0, center_y - TILE_SIZE / 2.0 + PIXEL_RATIO,
-Z_BELOW * TILE_SIZE, -Z_BELOW * TILE_SIZE,
) )
.with_scale(Vec3::splat(PIXEL_RATIO)), .with_scale(Vec3::splat(PIXEL_RATIO)),
Visibility::Hidden, Visibility::Hidden,
TerrainSprite { key }, TerrainSprite { key }, // access z_index via .key.z_index
)); ));
}); });
} }
+50 -38
View File
@@ -118,34 +118,8 @@ pub fn handle_tile_occlusion_updates(
pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] { pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
let mut visible_range = [0u32; 8]; let mut visible_range = [0u32; 8];
for mut camera_z in -Z_BELOW as i32..=Z_ABOVE as i32 { let touches_air = 'neighbor_check: {
camera_z *= ITILE_SIZE; for x_offset in -1..=1 {
let mut is_visible = false;
if pos.z > camera_z {
continue;
}
let mut is_occluded = false;
let v_check_height: i32 = Z_TOTAL as i32 - (camera_z / ITILE_SIZE) + Z_BELOW as i32 + 1;
'vertical_check: for z_offset in 1..v_check_height {
let above_pos = IVec3::new(pos.x, pos.y, pos.z + (z_offset * ITILE_SIZE));
if above_pos.z <= camera_z {
if let Some(&(_, _, _, visibly_transparent, _, _)) =
tilemap.floor_tiles.get(&above_pos)
{
if !visibly_transparent {
is_occluded = true;
break 'vertical_check;
}
}
} else {
break 'vertical_check;
}
}
if !is_occluded {
'neighbor_check: for x_offset in -1..=1 {
for y_offset in -1..=1 { for y_offset in -1..=1 {
for z_offset in 0..=1 { for z_offset in 0..=1 {
if x_offset == 0 && y_offset == 0 && z_offset == 0 { if x_offset == 0 && y_offset == 0 && z_offset == 0 {
@@ -156,23 +130,61 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
pos.y + y_offset * ITILE_SIZE, pos.y + y_offset * ITILE_SIZE,
pos.z + z_offset * ITILE_SIZE, pos.z + z_offset * ITILE_SIZE,
); );
match tilemap.floor_tiles.get(&neighbor_pos) {
Some(&(id, _, _, _, _, _)) if id == 0 => break 'neighbor_check true,
None => {}
_ => {}
}
}
}
}
false
};
if let Some(&(id, _, _, _, _, _)) = tilemap.floor_tiles.get(&neighbor_pos) { if !touches_air {
if id == 0 { return visible_range;
// id 0 = air tile
is_visible = true;
break 'neighbor_check;
}
}
}
} }
let base_camera_index = (pos.z / ITILE_SIZE) + Z_BELOW as i32;
if base_camera_index >= 0 && base_camera_index <= Z_TOTAL as i32 {
if touches_air {
let z2 = base_camera_index as usize;
visible_range[z2 / 32] |= 1 << (z2 % 32);
} }
} }
if is_visible { let mut occluded = false;
let z2 = ((camera_z / ITILE_SIZE) + Z_BELOW as i32) as usize;
for z_offset in 1..=(Z_TOTAL as i32 + Z_BELOW as i32 + 1) {
let check_z = pos.z + z_offset * ITILE_SIZE;
let camera_z_index = (check_z / ITILE_SIZE) + Z_BELOW as i32;
if camera_z_index < 0 {
match tilemap.floor_tiles.get(&IVec3::new(pos.x, pos.y, check_z)) {
Some(&(_, _, _, visibly_transparent, _, _)) => occluded = !visibly_transparent,
None => occluded = false,
}
continue;
}
if camera_z_index > Z_TOTAL as i32 {
break;
}
if !occluded {
let z2 = camera_z_index as usize;
visible_range[z2 / 32] |= 1 << ((z2 % 32) as u32); visible_range[z2 / 32] |= 1 << ((z2 % 32) as u32);
} }
let tile = tilemap.floor_tiles.get(&IVec3::new(pos.x, pos.y, check_z));
match tile {
Some(&(_, _, _, visibly_transparent, _, _)) => {
occluded = !visibly_transparent;
}
None => occluded = false,
}
} }
visible_range visible_range