quilted chunk optimisations
This commit is contained in:
+222
-122
@@ -1,5 +1,5 @@
|
|||||||
use bevy::{asset::RenderAssetUsages, prelude::*, render::render_resource};
|
use bevy::{asset::RenderAssetUsages, prelude::*, render::render_resource};
|
||||||
use bevy_platform::collections::HashMap;
|
use bevy_platform::collections::{HashMap, HashSet};
|
||||||
use bevy_platform::sync::Mutex;
|
use bevy_platform::sync::Mutex;
|
||||||
use bevy_platform::time::Instant;
|
use bevy_platform::time::Instant;
|
||||||
use rayon::prelude::*;
|
use rayon::prelude::*;
|
||||||
@@ -9,6 +9,11 @@ use crate::{
|
|||||||
world::{tiles::FloorTile, TextureIDs, Textures, Z_BELOW, Z_TOTAL},
|
world::{tiles::FloorTile, TextureIDs, Textures, Z_BELOW, Z_TOTAL},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Side length of a spatial chunk in tiles.
|
||||||
|
/// 32 tiles × TILE_PIXELS px = 512 px texture (GPU-friendly power-of-two).
|
||||||
|
/// Tune this constant to balance draw calls vs. rebake granularity.
|
||||||
|
pub const CHUNK_TILES: i32 = 32;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum TerrainSpriteState {
|
pub enum TerrainSpriteState {
|
||||||
Inactive,
|
Inactive,
|
||||||
@@ -24,20 +29,52 @@ pub struct CurrentWorldSpriteState {
|
|||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct TerrainSprite {
|
pub struct TerrainSprite {
|
||||||
pub z_index: usize, // Store the z-index this sprite belongs to (as world_Z)
|
/// Chunk grid coordinate and z-level this sprite covers.
|
||||||
|
pub key: ChunkZKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uniquely identifies one spatial chunk at one z-level.
|
||||||
|
/// Used as a HashMap / HashSet key throughout the bake pipeline.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct ChunkZKey {
|
||||||
|
pub chunk_x: i32,
|
||||||
|
pub chunk_y: i32,
|
||||||
|
pub z_index: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChunkZKey {
|
||||||
|
#[inline]
|
||||||
|
fn from_world(world_x: f32, world_y: f32, z_index: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
chunk_x: (world_x / (TILE_SIZE * CHUNK_TILES as f32)).floor() as i32,
|
||||||
|
chunk_y: (world_y / (TILE_SIZE * CHUNK_TILES as f32)).floor() as i32,
|
||||||
|
z_index,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct QuiltCache {
|
pub struct QuiltCache {
|
||||||
pub dimensions: HashMap<usize, (u32, u32)>,
|
/// Pixel dimensions (w, h) of the last-baked texture for each chunk×z.
|
||||||
pub dirty_indices: Vec<usize>,
|
pub dimensions: HashMap<ChunkZKey, (u32, u32)>,
|
||||||
|
|
||||||
|
/// Which chunk×z combinations need to be rebaked on the next bake pass.
|
||||||
|
/// Insert here whenever a tile edit occurs.
|
||||||
|
/// Cleared after every full bake; ignored during partial (dirty-only) bakes.
|
||||||
|
pub dirty_keys: HashSet<ChunkZKey>,
|
||||||
|
|
||||||
|
/// Reusable pixel buffers keyed by ChunkZKey.
|
||||||
|
/// Zeroed and reused when dimensions are unchanged; reallocated on resize.
|
||||||
|
/// Buffers for chunks with no remaining tiles are dropped automatically.
|
||||||
|
pub pixel_buffers: HashMap<ChunkZKey, Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for QuiltCache {
|
impl Default for QuiltCache {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
dimensions: HashMap::new(),
|
dimensions: HashMap::new(),
|
||||||
dirty_indices: Vec::new(),
|
dirty_keys: HashSet::new(),
|
||||||
|
pixel_buffers: HashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +86,7 @@ pub fn build_quilted_terrain_sprites(
|
|||||||
mut cwss: ResMut<CurrentWorldSpriteState>,
|
mut cwss: ResMut<CurrentWorldSpriteState>,
|
||||||
textures: Res<Textures>,
|
textures: Res<Textures>,
|
||||||
texture_ids: Res<TextureIDs>,
|
texture_ids: Res<TextureIDs>,
|
||||||
query_terrain_sprites: Query<Entity, With<TerrainSprite>>,
|
query_terrain_sprites: Query<(Entity, &TerrainSprite)>,
|
||||||
mut images: ResMut<Assets<Image>>,
|
mut images: ResMut<Assets<Image>>,
|
||||||
mut quilt_cache: ResMut<QuiltCache>,
|
mut quilt_cache: ResMut<QuiltCache>,
|
||||||
) {
|
) {
|
||||||
@@ -59,127 +96,196 @@ pub fn build_quilted_terrain_sprites(
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
cwss.state = TerrainSpriteState::InProgress;
|
cwss.state = TerrainSpriteState::InProgress;
|
||||||
|
|
||||||
// Despawn existing terrain sprites
|
// --- Determine whether this is a full or partial (dirty-only) bake ---
|
||||||
let despawn_entities: Vec<Entity> = query_terrain_sprites.iter().collect();
|
//
|
||||||
for entity in despawn_entities {
|
// A full bake (dirty_keys is empty) rebuilds every chunk×z from scratch —
|
||||||
|
// used on initial load or after a world reload.
|
||||||
|
// A partial bake only processes the chunk×z entries listed in dirty_keys,
|
||||||
|
// leaving all other sprites untouched. This makes incremental tile edits
|
||||||
|
// proportional to the number of changed chunks, not total world size.
|
||||||
|
let full_bake = quilt_cache.dirty_keys.is_empty();
|
||||||
|
|
||||||
|
// --- Pre-extract texture data on the main thread ---
|
||||||
|
//
|
||||||
|
// Reads Assets<Image> once here, safely, before the rayon parallel section.
|
||||||
|
// Deduplicates lookups (many tiles share textures) and produces an owned
|
||||||
|
// HashMap that is freely shareable across threads without any locking.
|
||||||
|
let mut tile_texture_data: HashMap<u32, (Vec<u8>, u32, u32)> = HashMap::new();
|
||||||
|
for (floortile, _) in query_tiles.iter() {
|
||||||
|
if tile_texture_data.contains_key(&floortile.id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(texture_id) = texture_ids.refs.get(&floortile.id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(texture) = textures.handles.get(texture_id) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(img) = images.get(texture) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(data) = &img.data else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
tile_texture_data.insert(
|
||||||
|
floortile.id,
|
||||||
|
(data.clone(), img.size().x as u32, img.size().y as u32),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Bucket tiles into (chunk_x, chunk_y, z_index) keys ---
|
||||||
|
//
|
||||||
|
// Set-bit iteration means each tile only touches as many buckets as z-levels
|
||||||
|
// it is actually visible at — not Z_TOTAL iterations every time.
|
||||||
|
let mut tiles_by_chunk_z: HashMap<ChunkZKey, Vec<(Vec2, &FloorTile)>> = HashMap::new();
|
||||||
|
|
||||||
|
for (floortile, transform) in query_tiles.iter() {
|
||||||
|
let position = Vec2::new(transform.translation.x, transform.translation.y);
|
||||||
|
|
||||||
|
for (word_idx, &word) in floortile.visible_range.iter().enumerate() {
|
||||||
|
let mut bits = word;
|
||||||
|
while bits != 0 {
|
||||||
|
let bit_pos = bits.trailing_zeros() as usize;
|
||||||
|
let z_index = word_idx * 32 + bit_pos;
|
||||||
|
|
||||||
|
if z_index <= Z_TOTAL as usize {
|
||||||
|
let key = ChunkZKey::from_world(position.x, position.y, z_index);
|
||||||
|
tiles_by_chunk_z
|
||||||
|
.entry(key)
|
||||||
|
.or_default()
|
||||||
|
.push((position, floortile));
|
||||||
|
}
|
||||||
|
|
||||||
|
bits &= bits - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Decide which keys to actually bake this frame ---
|
||||||
|
//
|
||||||
|
// Full bake → all keys that have tiles.
|
||||||
|
// Dirty bake → only the intersection of dirty_keys and keys that have tiles.
|
||||||
|
// Keys in dirty_keys that have no tiles are stale deletions;
|
||||||
|
// their sprites will be despawned below.
|
||||||
|
let keys_to_bake: Vec<ChunkZKey> = if full_bake {
|
||||||
|
tiles_by_chunk_z.keys().cloned().collect()
|
||||||
|
} else {
|
||||||
|
tiles_by_chunk_z
|
||||||
|
.keys()
|
||||||
|
.filter(|k| quilt_cache.dirty_keys.contains(*k))
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Despawn sprites that need rebuilding ---
|
||||||
|
//
|
||||||
|
// On a full bake, despawn everything.
|
||||||
|
// On a partial bake, despawn only the sprites for dirty chunk×z keys so the
|
||||||
|
// rest of the world remains visible without flickering.
|
||||||
|
for (entity, terrain_sprite) in query_terrain_sprites.iter() {
|
||||||
|
let should_despawn = full_bake || quilt_cache.dirty_keys.contains(&terrain_sprite.key);
|
||||||
|
if should_despawn {
|
||||||
commands.command_scope(|mut cmd| {
|
commands.command_scope(|mut cmd| {
|
||||||
cmd.entity(entity).despawn();
|
cmd.entity(entity).despawn();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect tiles by z-index first
|
|
||||||
let mut tiles_by_z: HashMap<usize, Vec<(Vec2, &FloorTile)>> = HashMap::new();
|
|
||||||
|
|
||||||
// Calculate bounds for all visible tiles
|
|
||||||
for (floortile, transform) in query_tiles.iter() {
|
|
||||||
let position = Vec2::new(transform.translation.x, transform.translation.y);
|
|
||||||
|
|
||||||
for z_index in 0..=Z_TOTAL as usize {
|
|
||||||
if (floortile.visible_range[z_index / 32] & (1 << (z_index % 32) as u32)) != 0 {
|
|
||||||
tiles_by_z
|
|
||||||
.entry(z_index)
|
|
||||||
.or_default()
|
|
||||||
.push((position, floortile));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread-safe collections to store results
|
// Move pixel buffers out of the cache so rayon threads can borrow from the pool.
|
||||||
let dimensions_mutex = Mutex::new(HashMap::new());
|
let buffer_pool: Mutex<HashMap<ChunkZKey, Vec<u8>>> =
|
||||||
let texture_handles_mutex = Mutex::new(HashMap::new());
|
Mutex::new(std::mem::take(&mut quilt_cache.pixel_buffers));
|
||||||
|
|
||||||
// Process each z-level in parallel
|
let texture_results: Mutex<Vec<(ChunkZKey, Image, f32, f32)>> =
|
||||||
let z_indices: Vec<usize> = tiles_by_z.keys().cloned().collect();
|
Mutex::new(Vec::with_capacity(keys_to_bake.len()));
|
||||||
|
let dimensions_results: Mutex<Vec<(ChunkZKey, (u32, u32))>> =
|
||||||
|
Mutex::new(Vec::with_capacity(keys_to_bake.len()));
|
||||||
|
|
||||||
z_indices.into_par_iter().for_each(|z_index| {
|
let tile_texture_data = &tile_texture_data;
|
||||||
let tiles = if let Some(tiles) = tiles_by_z.get(&z_index) {
|
|
||||||
tiles
|
// --- Parallel bake over chunk×z keys ---
|
||||||
} else {
|
keys_to_bake.into_par_iter().for_each(|key| {
|
||||||
return; // Skip empty z-levels
|
let tiles = match tiles_by_chunk_z.get(&key) {
|
||||||
|
Some(t) if !t.is_empty() => t,
|
||||||
|
_ => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
if tiles.is_empty() {
|
// Bounds are clamped to the chunk grid so textures are always exactly
|
||||||
return;
|
// 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;
|
||||||
|
|
||||||
// Calculate bounds
|
// Actual tile extent within this chunk (may be smaller than full chunk
|
||||||
let min_x_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.x).reduce(f32::min).unwrap()
|
// at world edges or sparse z-levels).
|
||||||
|
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;
|
||||||
let min_y_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.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;
|
||||||
let max_x_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.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;
|
||||||
let max_y_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.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;
|
||||||
|
|
||||||
// Calculate terrain texture dimensions in pixels
|
|
||||||
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 width_px = width_tiles * TILE_PIXELS;
|
||||||
let height_px = height_tiles * TILE_PIXELS;
|
let height_px = height_tiles * TILE_PIXELS;
|
||||||
|
let required_len = (width_px * height_px * 4) as usize;
|
||||||
|
|
||||||
// Store dimensions in our thread-safe map
|
dimensions_results
|
||||||
dimensions_mutex
|
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(z_index, (width_px, height_px));
|
.push((key, (width_px, height_px)));
|
||||||
|
|
||||||
let mut texture_data = vec![0u8; (width_px * height_px * 4) as usize];
|
// Reuse buffer if dimensions match; reallocate only on resize.
|
||||||
|
let mut texture_data = {
|
||||||
|
let mut pool = buffer_pool.lock().unwrap();
|
||||||
|
match pool.remove(&key) {
|
||||||
|
Some(mut buf) if buf.len() == required_len => {
|
||||||
|
buf.fill(0);
|
||||||
|
buf
|
||||||
|
}
|
||||||
|
_ => vec![0u8; required_len],
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Process tiles for this z-level
|
|
||||||
// We can't parallelize this inner loop without more complex locking on texture_data
|
|
||||||
for (pos, floortile) in tiles {
|
for (pos, floortile) in tiles {
|
||||||
if let Some(texture_id) = texture_ids.refs.get(&floortile.id) {
|
let Some((source, src_width, src_height)) = tile_texture_data.get(&floortile.id) else {
|
||||||
if let Some(texture) = textures.handles.get(texture_id) {
|
continue;
|
||||||
|
};
|
||||||
|
let src_width = *src_width;
|
||||||
|
let src_height = *src_height;
|
||||||
|
|
||||||
let rel_x = pos.x - min_x_aligned;
|
let rel_x = pos.x - min_x_aligned;
|
||||||
let rel_y = pos.y - min_y_aligned;
|
let rel_y = pos.y - min_y_aligned;
|
||||||
|
|
||||||
let tile_x = (rel_x / TILE_SIZE).round() as u32;
|
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 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![];
|
|
||||||
|
|
||||||
let mut base_texture: &Image = &Default::default();
|
|
||||||
|
|
||||||
// Use a thread-safe approach to access images
|
|
||||||
// In a full implementation, this would require a more sophisticated
|
|
||||||
// thread-safe access pattern to Assets<Image>
|
|
||||||
if let Some(_base_texture) = images.get(texture) {
|
|
||||||
if let Some(_data) = &_base_texture.data {
|
|
||||||
data.push(_data);
|
|
||||||
base_texture = _base_texture;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
blit_texture_with_alpha(
|
blit_texture_with_alpha(
|
||||||
data, // tile
|
source,
|
||||||
&mut texture_data, // terrain
|
&mut texture_data,
|
||||||
base_texture.size().x as u32,
|
src_width,
|
||||||
base_texture.size().y as u32,
|
src_height,
|
||||||
width_px,
|
width_px,
|
||||||
height_px,
|
height_px,
|
||||||
target_x,
|
tile_x * TILE_PIXELS,
|
||||||
target_y,
|
tile_y * TILE_PIXELS,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the quilted texture
|
|
||||||
let quilted_texture = Image::new_fill(
|
let quilted_texture = Image::new_fill(
|
||||||
render_resource::Extent3d {
|
render_resource::Extent3d {
|
||||||
width: width_px,
|
width: width_px,
|
||||||
@@ -192,29 +298,27 @@ pub fn build_quilted_terrain_sprites(
|
|||||||
RenderAssetUsages::RENDER_WORLD,
|
RenderAssetUsages::RENDER_WORLD,
|
||||||
);
|
);
|
||||||
|
|
||||||
// In a real implementation, we would need thread-safe access to images
|
|
||||||
// For now, we'll collect the textures and add them after parallel processing
|
|
||||||
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;
|
||||||
|
|
||||||
// Store texture and position data for later spawning
|
// Return buffer to pool for the next bake.
|
||||||
texture_handles_mutex
|
buffer_pool.lock().unwrap().insert(key, texture_data);
|
||||||
|
|
||||||
|
texture_results
|
||||||
.lock()
|
.lock()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.insert(z_index, (quilted_texture, center_x, center_y));
|
.push((key, quilted_texture, center_x, center_y));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process collected textures and spawn entities
|
// --- Drain and store results back on the main thread ---
|
||||||
let collected_dimensions = dimensions_mutex.into_inner().unwrap();
|
|
||||||
let collected_textures = texture_handles_mutex.into_inner().unwrap();
|
|
||||||
|
|
||||||
// Update quilt_cache dimensions
|
quilt_cache.pixel_buffers = buffer_pool.into_inner().unwrap();
|
||||||
for (z_index, dimensions) in collected_dimensions {
|
|
||||||
quilt_cache.dimensions.insert(z_index, dimensions);
|
for (key, dimensions) in dimensions_results.into_inner().unwrap() {
|
||||||
|
quilt_cache.dimensions.insert(key, dimensions);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add images and spawn entities with the collected data
|
for (key, quilted_texture, center_x, center_y) in texture_results.into_inner().unwrap() {
|
||||||
for (z_index, (quilted_texture, center_x, center_y)) in collected_textures {
|
|
||||||
let texture_handle = images.add(quilted_texture);
|
let texture_handle = images.add(quilted_texture);
|
||||||
|
|
||||||
commands.command_scope(|mut cmd| {
|
commands.command_scope(|mut cmd| {
|
||||||
@@ -230,19 +334,24 @@ pub fn build_quilted_terrain_sprites(
|
|||||||
)
|
)
|
||||||
.with_scale(Vec3::splat(PIXEL_RATIO)),
|
.with_scale(Vec3::splat(PIXEL_RATIO)),
|
||||||
Visibility::Hidden,
|
Visibility::Hidden,
|
||||||
TerrainSprite { z_index },
|
TerrainSprite { key },
|
||||||
));
|
));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
quilt_cache.dirty_indices.clear();
|
quilt_cache.dirty_keys.clear();
|
||||||
cwss.state = TerrainSpriteState::RenderReady;
|
cwss.state = TerrainSpriteState::RenderReady;
|
||||||
println!("Terrain sprites baked in: {:.2?}", now.elapsed());
|
println!(
|
||||||
|
"Terrain sprites baked in: {:.2?} ({} chunks×z, {} full)",
|
||||||
|
now.elapsed(),
|
||||||
|
tiles_by_chunk_z.len(),
|
||||||
|
if full_bake { "yes" } else { "no" },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to blit a texture onto another texture
|
/// Blits `source` onto `target` at (`offset_x`, `offset_y`) with per-pixel alpha compositing.
|
||||||
fn blit_texture_with_alpha(
|
fn blit_texture_with_alpha(
|
||||||
sources: Vec<&[u8]>,
|
source: &[u8],
|
||||||
target: &mut [u8],
|
target: &mut [u8],
|
||||||
source_width: u32,
|
source_width: u32,
|
||||||
source_height: u32,
|
source_height: u32,
|
||||||
@@ -251,7 +360,6 @@ fn blit_texture_with_alpha(
|
|||||||
offset_x: u32,
|
offset_x: u32,
|
||||||
offset_y: u32,
|
offset_y: u32,
|
||||||
) {
|
) {
|
||||||
for (_, source_data) in sources.iter().rev().enumerate() {
|
|
||||||
for y in 0..source_height {
|
for y in 0..source_height {
|
||||||
if y + offset_y >= target_height {
|
if y + offset_y >= target_height {
|
||||||
continue;
|
continue;
|
||||||
@@ -261,41 +369,33 @@ fn blit_texture_with_alpha(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let source_pixel_idx = ((y * source_width) + x) as usize * 4;
|
let src_idx = ((y * source_width) + x) as usize * 4;
|
||||||
let target_pixel_idx =
|
let dst_idx = (((y + offset_y) * target_width) + (x + offset_x)) as usize * 4;
|
||||||
(((y + offset_y) * target_width) + (x + offset_x)) as usize * 4;
|
|
||||||
|
|
||||||
let src_a = source_data[source_pixel_idx + 3];
|
let src_a = source[src_idx + 3];
|
||||||
|
|
||||||
if src_a == 0 {
|
if src_a == 0 {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let src_r = source_data[source_pixel_idx];
|
|
||||||
let src_g = source_data[source_pixel_idx + 1];
|
|
||||||
let src_b = source_data[source_pixel_idx + 2];
|
|
||||||
|
|
||||||
if src_a == 255 {
|
if src_a == 255 {
|
||||||
target[target_pixel_idx] = src_r;
|
target[dst_idx] = source[src_idx];
|
||||||
target[target_pixel_idx + 1] = src_g;
|
target[dst_idx + 1] = source[src_idx + 1];
|
||||||
target[target_pixel_idx + 2] = src_b;
|
target[dst_idx + 2] = source[src_idx + 2];
|
||||||
target[target_pixel_idx + 3] = 255;
|
target[dst_idx + 3] = 255;
|
||||||
} else {
|
} else {
|
||||||
let dst_r = target[target_pixel_idx];
|
let alpha = src_a as f32 / 255.0;
|
||||||
let dst_g = target[target_pixel_idx + 1];
|
let inv_alpha = 1.0 - alpha;
|
||||||
let dst_b = target[target_pixel_idx + 2];
|
|
||||||
|
|
||||||
let alpha_factor = src_a as f32 / 255.0;
|
target[dst_idx] =
|
||||||
let inv_alpha = 1.0 - alpha_factor;
|
(source[src_idx] as f32 * alpha + target[dst_idx] as f32 * inv_alpha) as u8;
|
||||||
|
target[dst_idx + 1] = (source[src_idx + 1] as f32 * alpha
|
||||||
target[target_pixel_idx] =
|
+ target[dst_idx + 1] as f32 * inv_alpha)
|
||||||
(src_r as f32 * alpha_factor + dst_r as f32 * inv_alpha) as u8;
|
as u8;
|
||||||
target[target_pixel_idx + 1] =
|
target[dst_idx + 2] = (source[src_idx + 2] as f32 * alpha
|
||||||
(src_g as f32 * alpha_factor + dst_g as f32 * inv_alpha) as u8;
|
+ target[dst_idx + 2] as f32 * inv_alpha)
|
||||||
target[target_pixel_idx + 2] =
|
as u8;
|
||||||
(src_b as f32 * alpha_factor + dst_b as f32 * inv_alpha) as u8;
|
target[dst_idx + 3] = 255;
|
||||||
target[target_pixel_idx + 3] = 255;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ pub fn update_tile_visibility(
|
|||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
|
|
||||||
for (terrain_sprite, mut visibility) in query.iter_mut() {
|
for (terrain_sprite, mut visibility) in query.iter_mut() {
|
||||||
*visibility = if terrain_sprite.z_index == ((z_index.0 + Z_BELOW) as usize) {
|
*visibility = if terrain_sprite.key.z_index == ((z_index.0 + Z_BELOW) as usize) {
|
||||||
Visibility::Visible
|
Visibility::Visible
|
||||||
} else {
|
} else {
|
||||||
Visibility::Hidden
|
Visibility::Hidden
|
||||||
|
|||||||
Reference in New Issue
Block a user