average composite on terrain sprites

This commit is contained in:
2025-05-06 22:31:31 +01:00
parent 6f82b904fe
commit 6f4de57f1b
6 changed files with 93 additions and 115 deletions
+2 -30
View File
@@ -315,9 +315,6 @@ fn calculate_path(tilemap: &TileMap, start: IVec3, goal: IVec3) -> Vec<Vec3> {
open_set.push(neighbor_node); open_set.push(neighbor_node);
in_open_set.insert(neighbor_pos); in_open_set.insert(neighbor_pos);
} else { } else {
// For nodes already in the open set, we need to update their priority
// This is tricky with a binary heap - one approach is to add a new entry
// The old one will still be there but will be ignored when we check the closed set
let neighbor_node = PathNode { let neighbor_node = PathNode {
position: neighbor_pos, position: neighbor_pos,
f_score: f, f_score: f,
@@ -342,8 +339,6 @@ fn octile_distance_3d(a: IVec3, b: IVec3) -> i32 {
let cost2 = 14; // 2D Diagonal let cost2 = 14; // 2D Diagonal
let cost3 = 17; // 3D Diagonal let cost3 = 17; // 3D Diagonal
// The heuristic calculation sorts the dimensions implicitly
// It calculates the minimum number of diagonal steps needed
let mut diffs = [dx, dy, dz]; let mut diffs = [dx, dy, dz];
diffs.sort_unstable(); // Sorts ascending: [dmin, dmid, dmax] diffs.sort_unstable(); // Sorts ascending: [dmin, dmid, dmax]
@@ -375,6 +370,8 @@ fn reconstruct_path(came_from: HashMap<IVec3, IVec3>, mut current: IVec3) -> Vec
} }
pub fn spawn_citizens(mut commands: Commands, asset_server: Res<AssetServer>) { pub fn spawn_citizens(mut commands: Commands, asset_server: Res<AssetServer>) {
println!("spawning citizens");
let mut rng = rand::rng(); let mut rng = rand::rng();
// Spawn a handful of citizens // Spawn a handful of citizens
@@ -387,28 +384,3 @@ pub fn spawn_citizens(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Citizen::new(&asset_server, position)); commands.spawn(Citizen::new(&asset_server, position));
} }
} }
// pub fn check_citizen_positions_for_chunks(
// query: Query<&Transform, With<Ambulatory>>,
// chunk_map: Res<tilemap::ChunkMap>,
// mut event_writer: EventWriter<tilemap::LoadChunkEvent>,
// ) {
// for transform in query.iter() {
// let position = transform.translation;
// // Convert citizen position to chunk coordinates
// let chunk_x =
// (position.x / (tilemap::CHUNK_SIZE as f32 * constants::TILE_SIZE)).floor() as i32;
// let chunk_y =
// (position.y / (tilemap::CHUNK_SIZE as f32 * constants::TILE_SIZE)).floor() as i32;
// let chunk_pos = IVec2::new(chunk_x, chunk_y);
// // // Load chunk if not already loaded
// // if !chunk_map.loaded_chunks.contains_key(&chunk_pos) {
// // event_writer.send(tilemap::LoadChunkEvent {
// // chunk_position: chunk_pos,
// // });
// // }
// }
// }
+2 -3
View File
@@ -1,5 +1,4 @@
pub const PIXEL_RATIO: f32 = 2.0; pub const PIXEL_RATIO: f32 = 2.0;
pub const TILE_PIXELS: f32 = 16.0; pub const TILE_PIXELS: u32 = 16;
pub const TILE_SIZE: f32 = TILE_PIXELS * PIXEL_RATIO; pub const TILE_SIZE: f32 = TILE_PIXELS as f32 * PIXEL_RATIO;
pub const ITILE_SIZE: i32 = TILE_SIZE as i32; pub const ITILE_SIZE: i32 = TILE_SIZE as i32;
pub const UTILE_SIZE: u32 = TILE_SIZE as u32;
+1 -1
View File
@@ -29,7 +29,7 @@ fn main() {
}) })
.set(ImagePlugin::default_nearest()), .set(ImagePlugin::default_nearest()),
) )
.insert_resource(ClearColor(Color::srgb(0.0, 0.0, 0.0))) .insert_resource(ClearColor(Color::srgb(0., 0., 0.)))
.add_plugins(tilemap::TilemapPlugin) .add_plugins(tilemap::TilemapPlugin)
.add_plugins(citizen::PathfindingPlugin) .add_plugins(citizen::PathfindingPlugin)
.add_systems( .add_systems(
+1 -3
View File
@@ -76,7 +76,6 @@ pub struct TileMap {
pub fixture_tiles: HashMap<IVec3, (u32, bool, [u32; 8])>, pub fixture_tiles: HashMap<IVec3, (u32, bool, [u32; 8])>,
} }
// This system updates sprite visibility based on current z-index
pub fn update_tile_visibility( pub fn update_tile_visibility(
z_index: Res<game::ZIndex>, z_index: Res<game::ZIndex>,
mut query: Query<(&tiles::TerrainSprite, &mut Visibility)>, mut query: Query<(&tiles::TerrainSprite, &mut Visibility)>,
@@ -85,10 +84,9 @@ pub fn update_tile_visibility(
let current_z = z_index.0 as isize; let current_z = z_index.0 as isize;
// Update visibility for all terrain sprites
for (terrain_sprite, mut visibility) in query.iter_mut() { for (terrain_sprite, mut visibility) in query.iter_mut() {
*visibility = *visibility =
if terrain_sprite.z_index <= ((current_z + tilemap::Z_BELOW as isize) as usize) { if terrain_sprite.z_index == ((current_z + tilemap::Z_BELOW as isize) as usize) {
Visibility::Visible Visibility::Visible
} else { } else {
Visibility::Hidden Visibility::Hidden
+7 -5
View File
@@ -46,6 +46,7 @@ pub fn handle_tile_occlusion_updates(
)>, )>,
mut cwss: ResMut<CurrentWorldSpriteState>, mut cwss: ResMut<CurrentWorldSpriteState>,
) { ) {
println!("updating tile occlusion");
query_set query_set
.p0() .p0()
.par_iter_mut() .par_iter_mut()
@@ -91,7 +92,8 @@ pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] {
let mut is_occluded = false; let mut is_occluded = false;
'vertical_check: for z_offset in 1..15 { 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)); let above_pos = IVec3::new(pos.x, pos.y, pos.z + (z_offset * ITILE_SIZE));
if above_pos.z <= camera_z { if above_pos.z <= camera_z {
if let Some(&(_, opaque, _, _, _)) = tilemap.floor_tiles.get(&above_pos) { if let Some(&(_, opaque, _, _, _)) = tilemap.floor_tiles.get(&above_pos) {
@@ -194,13 +196,13 @@ fn generate_chunks_from_algo(
); );
let pos_ivec = position.as_ivec3(); let pos_ivec = position.as_ivec3();
if z < -8 { if z < -5 {
let noise_value = cave_noise.get([ let noise_value = cave_noise.get([
world_x as f64 * 0.05, world_x as f64 * 0.05,
world_y as f64 * 0.05, world_y as f64 * 0.05,
z as f64 * 0.05, z as f64 * (0.15 * -z as f64),
]); ]);
if noise_value < -0.5 { if noise_value < -0.65 {
FloorTilePrefab::air(position).spawn(&mut commands); FloorTilePrefab::air(position).spawn(&mut commands);
tilemap tilemap
.floor_tiles .floor_tiles
@@ -275,7 +277,7 @@ fn generate_chunks_from_algo(
fn setup_initial_chunks(mut event_writer: EventWriter<LoadChunkEvent>) { fn setup_initial_chunks(mut event_writer: EventWriter<LoadChunkEvent>) {
println!("setup_initial_chunks"); println!("setup_initial_chunks");
for x in -2..=2 { for x in -3..=3 {
for y in -2..=2 { for y in -2..=2 {
event_writer.write(LoadChunkEvent { event_writer.write(LoadChunkEvent {
chunk_position: IVec2::new(x, y), chunk_position: IVec2::new(x, y),
+80 -73
View File
@@ -74,7 +74,6 @@ pub fn initialize_textures(mut commands: Commands, asset_server: Res<AssetServer
asset_server.load(BEDROCK_WALL_PATH), asset_server.load(BEDROCK_WALL_PATH),
); );
// Insert the textures resource into the world
commands.insert_resource(Textures { handles: textures }); commands.insert_resource(Textures { handles: textures });
commands.insert_resource(TextureIDs { refs: texture_ids }); commands.insert_resource(TextureIDs { refs: texture_ids });
} }
@@ -101,9 +100,7 @@ pub struct TerrainSprite {
#[derive(Resource)] #[derive(Resource)]
pub struct QuiltCache { pub struct QuiltCache {
// Tracks the dimensions of the quilted texture for each z-index
pub dimensions: HashMap<usize, (u32, u32)>, pub dimensions: HashMap<usize, (u32, u32)>,
// Tracks if any z-index needs to be requilt
pub dirty_indices: Vec<usize>, pub dirty_indices: Vec<usize>,
} }
@@ -116,7 +113,6 @@ impl Default for QuiltCache {
} }
} }
// This system generates a quilted sprite for each z-index
pub fn build_quilted_terrain_sprites( pub fn build_quilted_terrain_sprites(
query: Query<(&FloorTile, &Transform)>, query: Query<(&FloorTile, &Transform)>,
mut commands: Commands, mut commands: Commands,
@@ -130,19 +126,15 @@ pub fn build_quilted_terrain_sprites(
if cwss.state != TerrainSpriteState::WaitingForRender { if cwss.state != TerrainSpriteState::WaitingForRender {
return; return;
} }
let now = Instant::now(); let now = Instant::now();
cwss.state = TerrainSpriteState::InProgress; cwss.state = TerrainSpriteState::InProgress;
// despawn all existing quilted terrain sprites
for entity in query_sprites.iter() { for entity in query_sprites.iter() {
commands.entity(entity).despawn(); commands.entity(entity).despawn();
} }
// Map to collect all visible tiles per z-index
let mut tiles_by_z: HashMap<usize, Vec<(Vec2, &FloorTile)>> = HashMap::new(); let mut tiles_by_z: HashMap<usize, Vec<(Vec2, &FloorTile)>> = HashMap::new();
// Calculate bounds for all visible tiles
// First pass: Collect all visible tiles by z_index and compute bounds
for z_index in 0..=tilemap::Z_TOTAL as usize { for z_index in 0..=tilemap::Z_TOTAL as usize {
for (floortile, transform) in query.iter() { for (floortile, transform) in query.iter() {
let is_visible = let is_visible =
@@ -157,75 +149,83 @@ pub fn build_quilted_terrain_sprites(
} }
} }
// For each z-index, create a quilted texture // For each z-index, create a new quilted texture, baked and composited.
for (z_index, tiles) in tiles_by_z.iter() { for (z_index, tiles) in tiles_by_z.iter() {
if tiles.is_empty() { if tiles.is_empty() {
continue; continue;
} }
// Find bounds for the quilted texture // Snap min/max values to tile grid - accounting for half tile on each edge
let min_x = tiles.iter().map(|(pos, _)| pos.x).reduce(f32::min).unwrap(); // We need to extend the boundaries by half a tile in each direction to ensure full coverage
let max_x = tiles.iter().map(|(pos, _)| pos.x).reduce(f32::max).unwrap(); // Don't ask, dragons.
let min_y = tiles.iter().map(|(pos, _)| pos.y).reduce(f32::min).unwrap(); let min_x_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.x).reduce(f32::min).unwrap()
let max_y = tiles.iter().map(|(pos, _)| pos.y).reduce(f32::max).unwrap(); - TILE_SIZE / 2.0)
/ TILE_SIZE)
.floor()
* TILE_SIZE;
let min_y_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.y).reduce(f32::min).unwrap()
- TILE_SIZE / 2.0)
/ TILE_SIZE)
.floor()
* TILE_SIZE;
let max_x_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.x).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0)
/ TILE_SIZE)
.ceil()
* TILE_SIZE;
let max_y_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.y).reduce(f32::max).unwrap()
+ TILE_SIZE / 2.0)
/ TILE_SIZE)
.ceil()
* TILE_SIZE;
// Snap min/max values to tile grid - ensure we're centered on tile centers // Calculate terrain texture dimensions in pixels
// Our tiles are positioned at their centers (not corners)
let min_x_aligned = (min_x / TILE_SIZE).floor() * TILE_SIZE;
let min_y_aligned = (min_y / TILE_SIZE).floor() * TILE_SIZE;
let max_x_aligned = (max_x / TILE_SIZE).ceil() * TILE_SIZE;
let max_y_aligned = (max_y / TILE_SIZE).ceil() * TILE_SIZE;
// Calculate texture dimensions in pixels
// We use TILE_PIXELS (16.0) as the base unit for the texture 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 as u32; let width_px = width_tiles * TILE_PIXELS;
let height_px = height_tiles * TILE_PIXELS as u32; let height_px = height_tiles * TILE_PIXELS;
// Store the dimensions for later use (especially for adjusting the transform)
quilt_cache quilt_cache
.dimensions .dimensions
.insert(*z_index, (width_px, height_px)); .insert(*z_index, (width_px, height_px));
// Create a new texture
let mut texture_data = vec![0u8; (width_px * height_px * 4) as usize]; let mut texture_data = vec![0u8; (width_px * height_px * 4) as usize];
// Draw each tile into the texture
for (pos, floortile) in tiles { for (pos, floortile) in tiles {
let id = floortile.id; let texture = textures
let texture_id = texture_ids.refs.get(&id).unwrap(); .handles
let texture = textures.handles.get(texture_id).unwrap(); .get(texture_ids.refs.get(&floortile.id).unwrap())
.unwrap();
let rel_x = pos.x - min_x_aligned;
let rel_y = pos.y - min_y_aligned;
let tile_x = (rel_x / TILE_SIZE).round() as u32;
let tile_y = (height_tiles as f32 - 1.0 - (rel_y / TILE_SIZE).round()) as u32;
let target_x = tile_x * TILE_PIXELS;
let target_y = tile_y * TILE_PIXELS;
let mut data: Vec<&[u8]> = vec![];
// Fetch the source texture data
if let Some(source_texture) = images.get(texture) { if let Some(source_texture) = images.get(texture) {
// Calculate position within the quilted texture if let Some(_data) = &source_texture.data {
// We need to be precise with the mapping from world coordinates to texture pixels // replace this one texture push with loop
// if the texture contains any transparent or semi-transparent pixels we should also add the tile below it (z-1)
// Calculate tile position relative to the quilted texture origin data.push(_data);
// This gives us the tile index (whole number of tiles from origin)
let tile_x = ((pos.x - min_x_aligned) / TILE_SIZE).round() as u32;
let tile_y =
(height_tiles as f32 - 1.0 - ((pos.y - min_y_aligned) / TILE_SIZE).round())
as u32;
// Copy the tile texture into the quilted texture
// Convert from tile indices to pixel coordinates in the output texture
if let Some(data) = &source_texture.data {
blit_texture(
data,
&mut texture_data,
source_texture.size().x as u32,
source_texture.size().y as u32,
width_px,
height_px,
(tile_x * TILE_PIXELS as u32) + (TILE_PIXELS / 2.0) as u32,
(tile_y * TILE_PIXELS as u32) + (TILE_PIXELS / 2.0) as u32,
);
} }
blit_texture(
data, // tile
&mut texture_data, // terrain
source_texture.size().x as u32,
source_texture.size().y as u32,
width_px,
height_px,
target_x,
target_y,
);
} }
} }
// Create a new image asset
let quilted_texture = Image::new_fill( let quilted_texture = Image::new_fill(
render_resource::Extent3d { render_resource::Extent3d {
width: width_px, width: width_px,
@@ -237,30 +237,28 @@ pub fn build_quilted_terrain_sprites(
render_resource::TextureFormat::Rgba8UnormSrgb, render_resource::TextureFormat::Rgba8UnormSrgb,
RenderAssetUsages::RENDER_WORLD, RenderAssetUsages::RENDER_WORLD,
); );
let texture_handle = images.add(quilted_texture); let texture_handle = images.add(quilted_texture);
// Calculate the center position of the quilted sprite
// Using the aligned coordinates to ensure proper centering
let center_x = min_x_aligned + (max_x_aligned - min_x_aligned) / 2.0; let center_x = min_x_aligned + (max_x_aligned - min_x_aligned) / 2.0;
let center_y = min_y_aligned + (max_y_aligned - min_y_aligned) / 2.0; let center_y = min_y_aligned + (max_y_aligned - min_y_aligned) / 2.0;
// Spawn the quilted sprite
commands.spawn(( commands.spawn((
Sprite { Sprite {
image: texture_handle, image: texture_handle,
..Default::default() ..Default::default()
}, },
Transform::from_xyz(center_x, center_y, (*z_index as f32 - Z_BELOW) * TILE_SIZE) Transform::from_xyz(
.with_scale(Vec3::splat(PIXEL_RATIO)), center_x + TILE_SIZE / 2.0,
center_y + TILE_SIZE / 2.0,
(*z_index as f32 - Z_BELOW) * TILE_SIZE,
)
.with_scale(Vec3::splat(PIXEL_RATIO)),
Visibility::Hidden, Visibility::Hidden,
TerrainSprite { z_index: *z_index }, TerrainSprite { z_index: *z_index },
)); ));
} }
// Clear any dirty flags
quilt_cache.dirty_indices.clear(); quilt_cache.dirty_indices.clear();
cwss.state = TerrainSpriteState::RenderReady; cwss.state = TerrainSpriteState::RenderReady;
println!( println!(
"Quilted world sprites built. Elapsed: {:.2?}", "Quilted world sprites built. Elapsed: {:.2?}",
@@ -270,7 +268,7 @@ pub fn build_quilted_terrain_sprites(
// Helper function to blit a texture onto another texture // Helper function to blit a texture onto another texture
fn blit_texture( fn blit_texture(
source: &[u8], sources: Vec<&[u8]>,
target: &mut [u8], target: &mut [u8],
source_width: u32, source_width: u32,
source_height: u32, source_height: u32,
@@ -283,22 +281,31 @@ fn blit_texture(
if y + offset_y >= target_height { if y + offset_y >= target_height {
continue; continue;
} }
for x in 0..source_width { for x in 0..source_width {
if x + offset_x >= target_width { if x + offset_x >= target_width {
continue; continue;
} }
let source_idx = ((y * source_width) + x) as usize * 4; let source_idx = ((y * source_width) + x) as usize * 4;
let target_idx = (((y + offset_y) * target_width) + (x + offset_x)) as usize * 4; let target_idx = (((y + offset_y) * target_width) + (x + offset_x)) as usize * 4;
// Only copy non-transparent pixels // New colour
if source.len() > source_idx + 3 && source[source_idx + 3] > 0 { let mut r: u64 = 0;
target[target_idx] = source[source_idx]; // R let mut g: u64 = 0;
target[target_idx + 1] = source[source_idx + 1]; // G let mut b: u64 = 0;
target[target_idx + 2] = source[source_idx + 2]; // B
target[target_idx + 3] = source[source_idx + 3]; // A for source in sources.iter() {
r += source[source_idx] as u64;
g += source[source_idx + 1] as u64;
b += source[source_idx + 2] as u64;
} }
r /= sources.len() as u64;
g /= sources.len() as u64;
b /= sources.len() as u64;
target[target_idx] = r as u8;
target[target_idx + 1] = g as u8;
target[target_idx + 2] = b as u8;
target[target_idx + 3] = 255;
} }
} }
} }