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);
pos
})
.collect();
// All tiles now in tilemap — safe to calculate visibility
for pos in new_positions {
occlusion_event_writer.write(TileOcclusionEvent { tile_position: pos });
}
+22 -19
View File
@@ -209,39 +209,42 @@ pub fn build_quilted_terrain_sprites(
_ => return,
};
// Bounds are clamped to the chunk grid so textures are always exactly
// CHUNK_TILES wide/tall (or smaller at world edges), keeping sizes
// predictable and buffer reuse rates high.
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).
// Actual tile extent within this chunk, padded by one tile on each side
// so the rendered quad always shows one tile of context beyond the edge
// (prevents the hard black cutoff at chunk/world boundaries).
let min_x_aligned: f32 = ((tiles.iter().map(|(p, _)| p.x).reduce(f32::min).unwrap()
- TILE_SIZE / 2.0)
/ TILE_SIZE)
.floor()
* TILE_SIZE;
* TILE_SIZE
- TILE_SIZE;
let min_y_aligned: f32 = ((tiles.iter().map(|(p, _)| p.y).reduce(f32::min).unwrap()
- TILE_SIZE / 2.0)
/ TILE_SIZE)
.floor()
* TILE_SIZE;
* TILE_SIZE
- TILE_SIZE;
let max_x_aligned: f32 = ((tiles.iter().map(|(p, _)| p.x).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0)
/ TILE_SIZE)
.ceil()
* TILE_SIZE;
* TILE_SIZE
+ TILE_SIZE;
let max_y_aligned: f32 = ((tiles.iter().map(|(p, _)| p.y).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0)
/ TILE_SIZE)
.ceil()
* TILE_SIZE;
* TILE_SIZE
+ TILE_SIZE;
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 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;
dimensions_results
@@ -281,8 +284,8 @@ pub fn build_quilted_terrain_sprites(
src_height,
width_px,
height_px,
tile_x * TILE_PIXELS,
tile_y * TILE_PIXELS,
tile_x * TILE_PIXELS + 1, // +1 to account for 1px bleed border
tile_y * TILE_PIXELS + 1,
);
}
@@ -328,13 +331,13 @@ pub fn build_quilted_terrain_sprites(
..Default::default()
},
Transform::from_xyz(
center_x - TILE_SIZE / 2.0,
center_y - TILE_SIZE / 2.0,
center_x - TILE_SIZE / 2.0 - PIXEL_RATIO,
center_y - TILE_SIZE / 2.0 + PIXEL_RATIO,
-Z_BELOW * TILE_SIZE,
)
.with_scale(Vec3::splat(PIXEL_RATIO)),
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] {
let mut visible_range = [0u32; 8];
for mut camera_z in -Z_BELOW as i32..=Z_ABOVE as i32 {
camera_z *= ITILE_SIZE;
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 {
let touches_air = 'neighbor_check: {
for x_offset in -1..=1 {
for y_offset in -1..=1 {
for z_offset in 0..=1 {
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.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 id == 0 {
// id 0 = air tile
is_visible = true;
break 'neighbor_check;
}
}
}
if !touches_air {
return visible_range;
}
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 z2 = ((camera_z / ITILE_SIZE) + Z_BELOW as i32) as usize;
let mut occluded = false;
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);
}
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