diff --git a/src/constants.rs b/src/constants.rs index eb38dd5..42cdd04 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -1,4 +1,4 @@ pub const PIXEL_RATIO: f32 = 2.0; -pub const TILE_SIZE: f32 = 16.0 * PIXEL_RATIO; +pub const TILE_SIZE: f32 = 16.0; pub const ITILE_SIZE: i32 = TILE_SIZE as i32; pub const UTILE_SIZE: u32 = TILE_SIZE as u32; diff --git a/src/main.rs b/src/main.rs index 9fd2d04..40fbe97 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,9 @@ fn main() { App::new() .insert_resource(game::ZIndex(0.0)) .insert_resource(tiles::CurrentWorldSpriteState { - state: tiles::WorldSpriteState::Inactive, + state: tiles::TerrainSpriteState::Inactive, }) + .insert_resource(tiles::QuiltCache::default()) .add_systems(PreStartup, tiles::initialize_textures) .add_plugins( DefaultPlugins @@ -44,7 +45,7 @@ fn main() { ( camera::camera_movement, cursor::move_cursor, - tiles::build_world_sprites, + tiles::build_quilted_terrain_sprites, ), ) .init_resource::() diff --git a/src/tile.rs b/src/tile.rs index c72403f..c1f4a01 100644 --- a/src/tile.rs +++ b/src/tile.rs @@ -88,7 +88,7 @@ pub fn update_tile_visibility( // Update visibility for all terrain sprites for (terrain_sprite, mut visibility) in query.iter_mut() { *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 } else { Visibility::Hidden diff --git a/src/tilemap.rs b/src/tilemap.rs index 947a279..41a318a 100644 --- a/src/tilemap.rs +++ b/src/tilemap.rs @@ -1,6 +1,8 @@ use crate::constants::{ITILE_SIZE, TILE_SIZE}; use crate::tile::{FixtureTile, FloorTile, NeedsOccluded, TileMap}; -use crate::tiles::{CurrentWorldSpriteState, FixtureTilePrefab, FloorTilePrefab, WorldSpriteState}; +use crate::tiles::{ + CurrentWorldSpriteState, FixtureTilePrefab, FloorTilePrefab, TerrainSpriteState, +}; use bevy::prelude::*; use bevy_platform::collections::hash_map::HashMap; use noise::{NoiseFn, Perlin}; @@ -73,7 +75,7 @@ pub fn handle_tile_occlusion_updates( commands.entity(entity).remove::(); } } - cwss.state = WorldSpriteState::WaitingForRender; + cwss.state = TerrainSpriteState::WaitingForRender; } pub fn calculate_visibility(pos: IVec3, tilemap: &TileMap) -> [u32; 8] { diff --git a/src/tiles.rs b/src/tiles.rs index df0607f..0bb4c75 100644 --- a/src/tiles.rs +++ b/src/tiles.rs @@ -1,6 +1,8 @@ use crate::tile::{FixtureTile, FloorTile, NeedsOccluded, TileState}; use crate::{constants::*, game, tilemap}; +use bevy::asset::RenderAssetUsages; use bevy::prelude::*; +use bevy::render::render_resource; use bevy_platform::collections::hash_map::HashMap; #[derive(Resource)] @@ -77,7 +79,7 @@ pub fn initialize_textures(mut commands: Commands, asset_server: Res, + // Tracks if any z-index needs to be requilt + pub dirty_indices: Vec, +} + +impl Default for QuiltCache { + fn default() -> Self { + Self { + dimensions: HashMap::new(), + dirty_indices: Vec::new(), + } + } +} + +// This system generates a quilted sprite for each z-index +pub fn build_quilted_terrain_sprites( query: Query<(&FloorTile, &Transform)>, mut commands: Commands, mut cwss: ResMut, textures: Res, texture_ids: Res, - query_1: Query>, + query_sprites: Query>, + mut images: ResMut>, + mut quilt_cache: ResMut, ) { - if cwss.state != WorldSpriteState::WaitingForRender { + if cwss.state != TerrainSpriteState::WaitingForRender { return; } let now = Instant::now(); - cwss.state = WorldSpriteState::InProgress; + cwss.state = TerrainSpriteState::InProgress; - // despawn all existing terrain sprites - for entity in query_1.iter() { + // despawn all existing quilted terrain sprites + for entity in query_sprites.iter() { commands.entity(entity).despawn(); } - // Generate sprites for all z-indices (0 to 150) + // Map to collect all visible tiles per z-index + let mut tiles_by_z: HashMap> = HashMap::new(); + + // First pass: Collect all visible tiles by z_index and compute bounds for z_index in 0..=tilemap::Z_TOTAL as usize { for (floortile, transform) in query.iter() { let is_visible = (floortile.visible_range[z_index / 32] & (1 << (z_index % 32) as u32)) != 0; if is_visible { - let id = floortile.id; - let texture_id = texture_ids.refs.get(&id).unwrap(); - let texture = textures.handles.get(texture_id).unwrap(); - let sprite = Sprite { - image: texture.clone(), - ..Default::default() - }; - - // Spawn with hidden visibility initially - update_tile_visibility will handle showing/hiding - commands.spawn(( - sprite, - *transform, - TerrainSprite { z_index }, - Visibility::Hidden, - )); + let position = Vec2::new(transform.translation.x, transform.translation.y); + tiles_by_z + .entry(z_index) + .or_default() + .push((position, floortile)); } } } - cwss.state = WorldSpriteState::RenderReady; + // For each z-index, create a quilted texture + for (z_index, tiles) in tiles_by_z.iter() { + if tiles.is_empty() { + continue; + } - let elapsed = now.elapsed(); + // Find bounds for the quilted texture + let min_x = tiles.iter().map(|(pos, _)| pos.x).reduce(f32::min).unwrap(); + let max_x = tiles.iter().map(|(pos, _)| pos.x).reduce(f32::max).unwrap(); + let min_y = tiles.iter().map(|(pos, _)| pos.y).reduce(f32::min).unwrap(); + let max_y = tiles.iter().map(|(pos, _)| pos.y).reduce(f32::max).unwrap(); - println!("World sprites built. Elapsed: {:.2?}", elapsed); + // Calculate texture dimensions in pixels + let width_tiles = ((max_x - min_x) / TILE_SIZE).ceil() as u32 + 1; + let height_tiles = ((max_y - min_y) / TILE_SIZE).ceil() as u32 + 1; + let width_px = width_tiles * UTILE_SIZE; + let height_px = height_tiles * UTILE_SIZE; + + // Store the dimensions for later use (especially for adjusting the transform) + quilt_cache + .dimensions + .insert(*z_index, (width_px, height_px)); + + // Create a new texture + let mut texture_data = vec![0u8; (width_px * height_px * 4) as usize]; + + // Draw each tile into the texture + for (pos, floortile) in tiles { + let id = floortile.id; + let texture_id = texture_ids.refs.get(&id).unwrap(); + let texture = textures.handles.get(texture_id).unwrap(); + + // Fetch the source texture data + if let Some(source_texture) = images.get(texture) { + // Calculate position within the quilted texture + let tile_x = ((pos.x - min_x) / TILE_SIZE).floor() as u32; + let tile_y = ((pos.y - min_y) / TILE_SIZE).floor() as u32; + + // Copy the tile texture into the quilted 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 * UTILE_SIZE, + tile_y * UTILE_SIZE, + ); + } + } + } + + // Create a new image asset + let quilted_texture = Image::new_fill( + render_resource::Extent3d { + width: width_px, + height: height_px, + depth_or_array_layers: 1, + }, + render_resource::TextureDimension::D2, + &texture_data, + render_resource::TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::RENDER_WORLD, + ); + + let texture_handle = images.add(quilted_texture); + + // Calculate the center position of the quilted sprite + let center_x = min_x + (max_x - min_x) / 2.0; + let center_y = min_y + (max_y - min_y) / 2.0; + + // Spawn the quilted sprite + commands.spawn(( + Sprite { + image: texture_handle, + ..Default::default() + }, + Transform::from_xyz(center_x, center_y, *z_index as f32) + .with_scale(Vec3::splat(PIXEL_RATIO)), + Visibility::Hidden, + TerrainSprite { z_index: *z_index }, + )); + } + + // Clear any dirty flags + quilt_cache.dirty_indices.clear(); + + cwss.state = TerrainSpriteState::RenderReady; + println!( + "Quilted world sprites built. Elapsed: {:.2?}", + now.elapsed() + ); +} + +// Helper function to blit a texture onto another texture +fn blit_texture( + source: &[u8], + target: &mut [u8], + source_width: u32, + source_height: u32, + target_width: u32, + target_height: u32, + offset_x: u32, + offset_y: u32, +) { + for y in 0..source_height { + if y + offset_y >= target_height { + continue; + } + + for x in 0..source_width { + if x + offset_x >= target_width { + continue; + } + + let source_idx = ((y * source_width) + x) as usize * 4; + let target_idx = (((y + offset_y) * target_width) + (x + offset_x)) as usize * 4; + + // Only copy non-transparent pixels + if source.len() > source_idx + 3 && source[source_idx + 3] > 0 { + target[target_idx] = source[source_idx]; // R + target[target_idx + 1] = source[source_idx + 1]; // G + target[target_idx + 2] = source[source_idx + 2]; // B + target[target_idx + 3] = source[source_idx + 3]; // A + } + } + } +} + +// This system handles updating quilted sprites when the world changes +pub fn update_quilts_on_world_change( + mut commands: Commands, + mut quilt_cache: ResMut, + mut cwss: ResMut, + // Add any resources or queries that indicate world changes + // For example, if you have a WorldChangeEvent: + // mut world_changes: EventReader, +) { + // Example: Check for world changes + // if !world_changes.is_empty() { + // for event in world_changes.iter() { + // quilt_cache.dirty_indices.push(event.z_index); + // } + // cwss.state = WorldSpriteState::WaitingForRender; + // } + + // Alternatively, if you have specific systems that modify the world, + // you could have them set cwss.state = WorldSpriteState::WaitingForRender + // and add affected z-indices to quilt_cache.dirty_indices } #[derive(Bundle)] @@ -161,7 +321,7 @@ pub struct FloorTilePrefab { impl FloorTilePrefab { pub fn grass(position: Vec3) -> Self { FloorTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FloorTile { id: 1, astar_weight: 100, @@ -179,7 +339,7 @@ impl FloorTilePrefab { pub fn dirt(position: Vec3) -> Self { FloorTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FloorTile { id: 2, astar_weight: 85, @@ -197,7 +357,7 @@ impl FloorTilePrefab { pub fn rock(position: Vec3) -> Self { FloorTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FloorTile { id: 3, astar_weight: 50, @@ -215,7 +375,7 @@ impl FloorTilePrefab { pub fn air(position: Vec3) -> Self { FloorTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FloorTile { id: 0, opaque: false, @@ -234,7 +394,7 @@ impl FloorTilePrefab { pub fn bedrock(position: Vec3) -> Self { FloorTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FloorTile { id: 4, astar_weight: 150, @@ -272,7 +432,7 @@ pub struct FixtureTilePrefab { impl FixtureTilePrefab { pub fn dirt_wall(position: Vec3) -> Self { FixtureTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FixtureTile { id: FIXTURE_ID_OFFSET + 1, ..Default::default() @@ -286,7 +446,7 @@ impl FixtureTilePrefab { pub fn rock_wall(position: Vec3) -> Self { FixtureTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FixtureTile { id: 2, ..Default::default() @@ -300,7 +460,7 @@ impl FixtureTilePrefab { pub fn bedrock_wall(position: Vec3) -> Self { FixtureTilePrefab { - transform: Transform::from_translation(position).with_scale(Vec3::splat(PIXEL_RATIO)), + transform: Transform::from_translation(position), tile: FixtureTile { id: 0, ..Default::default()