quilted chunk optimisations
This commit is contained in:
+239
-139
@@ -1,5 +1,5 @@
|
||||
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::time::Instant;
|
||||
use rayon::prelude::*;
|
||||
@@ -9,6 +9,11 @@ use crate::{
|
||||
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)]
|
||||
pub enum TerrainSpriteState {
|
||||
Inactive,
|
||||
@@ -24,20 +29,52 @@ pub struct CurrentWorldSpriteState {
|
||||
|
||||
#[derive(Component)]
|
||||
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)]
|
||||
pub struct QuiltCache {
|
||||
pub dimensions: HashMap<usize, (u32, u32)>,
|
||||
pub dirty_indices: Vec<usize>,
|
||||
/// Pixel dimensions (w, h) of the last-baked texture for each chunk×z.
|
||||
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 {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
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>,
|
||||
textures: Res<Textures>,
|
||||
texture_ids: Res<TextureIDs>,
|
||||
query_terrain_sprites: Query<Entity, With<TerrainSprite>>,
|
||||
query_terrain_sprites: Query<(Entity, &TerrainSprite)>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut quilt_cache: ResMut<QuiltCache>,
|
||||
) {
|
||||
@@ -59,127 +96,196 @@ pub fn build_quilted_terrain_sprites(
|
||||
let now = Instant::now();
|
||||
cwss.state = TerrainSpriteState::InProgress;
|
||||
|
||||
// Despawn existing terrain sprites
|
||||
let despawn_entities: Vec<Entity> = query_terrain_sprites.iter().collect();
|
||||
for entity in despawn_entities {
|
||||
commands.command_scope(|mut cmd| {
|
||||
cmd.entity(entity).despawn();
|
||||
});
|
||||
// --- Determine whether this is a full or partial (dirty-only) bake ---
|
||||
//
|
||||
// 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),
|
||||
);
|
||||
}
|
||||
|
||||
// Collect tiles by z-index first
|
||||
let mut tiles_by_z: HashMap<usize, Vec<(Vec2, &FloorTile)>> = HashMap::new();
|
||||
// --- 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();
|
||||
|
||||
// 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));
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Thread-safe collections to store results
|
||||
let dimensions_mutex = Mutex::new(HashMap::new());
|
||||
let texture_handles_mutex = Mutex::new(HashMap::new());
|
||||
// --- 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()
|
||||
};
|
||||
|
||||
// Process each z-level in parallel
|
||||
let z_indices: Vec<usize> = tiles_by_z.keys().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| {
|
||||
cmd.entity(entity).despawn();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
z_indices.into_par_iter().for_each(|z_index| {
|
||||
let tiles = if let Some(tiles) = tiles_by_z.get(&z_index) {
|
||||
tiles
|
||||
} else {
|
||||
return; // Skip empty z-levels
|
||||
// Move pixel buffers out of the cache so rayon threads can borrow from the pool.
|
||||
let buffer_pool: Mutex<HashMap<ChunkZKey, Vec<u8>>> =
|
||||
Mutex::new(std::mem::take(&mut quilt_cache.pixel_buffers));
|
||||
|
||||
let texture_results: Mutex<Vec<(ChunkZKey, Image, f32, f32)>> =
|
||||
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()));
|
||||
|
||||
let tile_texture_data = &tile_texture_data;
|
||||
|
||||
// --- Parallel bake over chunk×z keys ---
|
||||
keys_to_bake.into_par_iter().for_each(|key| {
|
||||
let tiles = match tiles_by_chunk_z.get(&key) {
|
||||
Some(t) if !t.is_empty() => t,
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if tiles.is_empty() {
|
||||
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;
|
||||
|
||||
// Calculate bounds
|
||||
let min_x_aligned: f32 = ((tiles.iter().map(|(pos, _)| pos.x).reduce(f32::min).unwrap()
|
||||
// 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()
|
||||
- TILE_SIZE / 2.0)
|
||||
/ TILE_SIZE)
|
||||
.floor()
|
||||
* 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)
|
||||
.floor()
|
||||
* 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)
|
||||
.ceil()
|
||||
* 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)
|
||||
.ceil()
|
||||
* TILE_SIZE;
|
||||
|
||||
// Calculate terrain texture dimensions in pixels
|
||||
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;
|
||||
let required_len = (width_px * height_px * 4) as usize;
|
||||
|
||||
// Store dimensions in our thread-safe map
|
||||
dimensions_mutex
|
||||
dimensions_results
|
||||
.lock()
|
||||
.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];
|
||||
|
||||
// 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 {
|
||||
if let Some(texture_id) = texture_ids.refs.get(&floortile.id) {
|
||||
if let Some(texture) = textures.handles.get(texture_id) {
|
||||
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![];
|
||||
|
||||
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(
|
||||
data, // tile
|
||||
&mut texture_data, // terrain
|
||||
base_texture.size().x as u32,
|
||||
base_texture.size().y as u32,
|
||||
width_px,
|
||||
height_px,
|
||||
target_x,
|
||||
target_y,
|
||||
);
|
||||
// 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],
|
||||
}
|
||||
};
|
||||
|
||||
for (pos, floortile) in tiles {
|
||||
let Some((source, src_width, src_height)) = tile_texture_data.get(&floortile.id) else {
|
||||
continue;
|
||||
};
|
||||
let src_width = *src_width;
|
||||
let src_height = *src_height;
|
||||
|
||||
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;
|
||||
|
||||
blit_texture_with_alpha(
|
||||
source,
|
||||
&mut texture_data,
|
||||
src_width,
|
||||
src_height,
|
||||
width_px,
|
||||
height_px,
|
||||
tile_x * TILE_PIXELS,
|
||||
tile_y * TILE_PIXELS,
|
||||
);
|
||||
}
|
||||
|
||||
// Create the quilted texture
|
||||
let quilted_texture = Image::new_fill(
|
||||
render_resource::Extent3d {
|
||||
width: width_px,
|
||||
@@ -192,29 +298,27 @@ pub fn build_quilted_terrain_sprites(
|
||||
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_y = min_y_aligned + (max_y_aligned - min_y_aligned) / 2.0;
|
||||
|
||||
// Store texture and position data for later spawning
|
||||
texture_handles_mutex
|
||||
// Return buffer to pool for the next bake.
|
||||
buffer_pool.lock().unwrap().insert(key, texture_data);
|
||||
|
||||
texture_results
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(z_index, (quilted_texture, center_x, center_y));
|
||||
.push((key, quilted_texture, center_x, center_y));
|
||||
});
|
||||
|
||||
// Process collected textures and spawn entities
|
||||
let collected_dimensions = dimensions_mutex.into_inner().unwrap();
|
||||
let collected_textures = texture_handles_mutex.into_inner().unwrap();
|
||||
// --- Drain and store results back on the main thread ---
|
||||
|
||||
// Update quilt_cache dimensions
|
||||
for (z_index, dimensions) in collected_dimensions {
|
||||
quilt_cache.dimensions.insert(z_index, dimensions);
|
||||
quilt_cache.pixel_buffers = buffer_pool.into_inner().unwrap();
|
||||
|
||||
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 (z_index, (quilted_texture, center_x, center_y)) in collected_textures {
|
||||
for (key, quilted_texture, center_x, center_y) in texture_results.into_inner().unwrap() {
|
||||
let texture_handle = images.add(quilted_texture);
|
||||
|
||||
commands.command_scope(|mut cmd| {
|
||||
@@ -230,19 +334,24 @@ pub fn build_quilted_terrain_sprites(
|
||||
)
|
||||
.with_scale(Vec3::splat(PIXEL_RATIO)),
|
||||
Visibility::Hidden,
|
||||
TerrainSprite { z_index },
|
||||
TerrainSprite { key },
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
quilt_cache.dirty_indices.clear();
|
||||
quilt_cache.dirty_keys.clear();
|
||||
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(
|
||||
sources: Vec<&[u8]>,
|
||||
source: &[u8],
|
||||
target: &mut [u8],
|
||||
source_width: u32,
|
||||
source_height: u32,
|
||||
@@ -251,51 +360,42 @@ fn blit_texture_with_alpha(
|
||||
offset_x: u32,
|
||||
offset_y: u32,
|
||||
) {
|
||||
for (_, source_data) in sources.iter().rev().enumerate() {
|
||||
for y in 0..source_height {
|
||||
if y + offset_y >= target_height {
|
||||
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;
|
||||
}
|
||||
for x in 0..source_width {
|
||||
if x + offset_x >= target_width {
|
||||
continue;
|
||||
}
|
||||
|
||||
let source_pixel_idx = ((y * source_width) + x) as usize * 4;
|
||||
let target_pixel_idx =
|
||||
(((y + offset_y) * target_width) + (x + offset_x)) as usize * 4;
|
||||
let src_idx = ((y * source_width) + x) as usize * 4;
|
||||
let dst_idx = (((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 {
|
||||
continue;
|
||||
}
|
||||
if src_a == 0 {
|
||||
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 {
|
||||
target[dst_idx] = source[src_idx];
|
||||
target[dst_idx + 1] = source[src_idx + 1];
|
||||
target[dst_idx + 2] = source[src_idx + 2];
|
||||
target[dst_idx + 3] = 255;
|
||||
} else {
|
||||
let alpha = src_a as f32 / 255.0;
|
||||
let inv_alpha = 1.0 - alpha;
|
||||
|
||||
if src_a == 255 {
|
||||
target[target_pixel_idx] = src_r;
|
||||
target[target_pixel_idx + 1] = src_g;
|
||||
target[target_pixel_idx + 2] = src_b;
|
||||
target[target_pixel_idx + 3] = 255;
|
||||
} else {
|
||||
let dst_r = target[target_pixel_idx];
|
||||
let dst_g = target[target_pixel_idx + 1];
|
||||
let dst_b = target[target_pixel_idx + 2];
|
||||
|
||||
let alpha_factor = src_a as f32 / 255.0;
|
||||
let inv_alpha = 1.0 - alpha_factor;
|
||||
|
||||
target[target_pixel_idx] =
|
||||
(src_r as f32 * alpha_factor + dst_r as f32 * inv_alpha) as u8;
|
||||
target[target_pixel_idx + 1] =
|
||||
(src_g as f32 * alpha_factor + dst_g as f32 * inv_alpha) as u8;
|
||||
target[target_pixel_idx + 2] =
|
||||
(src_b as f32 * alpha_factor + dst_b as f32 * inv_alpha) as u8;
|
||||
target[target_pixel_idx + 3] = 255;
|
||||
}
|
||||
target[dst_idx] =
|
||||
(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[dst_idx + 1] as f32 * inv_alpha)
|
||||
as u8;
|
||||
target[dst_idx + 2] = (source[src_idx + 2] as f32 * alpha
|
||||
+ target[dst_idx + 2] as f32 * inv_alpha)
|
||||
as u8;
|
||||
target[dst_idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ pub fn update_tile_visibility(
|
||||
let now = Instant::now();
|
||||
|
||||
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
|
||||
} else {
|
||||
Visibility::Hidden
|
||||
|
||||
Reference in New Issue
Block a user