Skip to content

TilemapChunk single quad; TileData (color/visibility) #19924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
200 changes: 67 additions & 133 deletions crates/bevy_sprite/src/tilemap_chunk/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::{AlphaMode2d, Anchor, MeshMaterial2d};
use crate::{AlphaMode2d, MeshMaterial2d};
use bevy_app::{App, Plugin, Update};
use bevy_asset::{Assets, Handle, RenderAssetUsages};
use bevy_asset::{Assets, Handle};
use bevy_color::Color;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
Expand All @@ -11,15 +12,11 @@ use bevy_ecs::{
system::{Query, ResMut},
world::DeferredWorld,
};
use bevy_image::{Image, ImageSampler, ToExtents};
use bevy_math::{FloatOrd, UVec2, Vec2, Vec3};
use bevy_image::Image;
use bevy_math::{primitives::Rectangle, UVec2};
use bevy_platform::collections::HashMap;
use bevy_render::{
mesh::{Indices, Mesh, Mesh2d, PrimitiveTopology},
render_resource::{
TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
},
};
use bevy_render::mesh::{Mesh, Mesh2d};
use bevy_utils::default;
use tracing::warn;

mod tilemap_chunk_material;
Expand All @@ -37,16 +34,13 @@ impl Plugin for TilemapChunkPlugin {
}
}

type TilemapChunkMeshCacheKey = (UVec2, FloatOrd, FloatOrd, FloatOrd, FloatOrd);

/// A resource storing the meshes for each tilemap chunk size.
#[derive(Resource, Default, Deref, DerefMut)]
pub struct TilemapChunkMeshCache(HashMap<TilemapChunkMeshCacheKey, Handle<Mesh>>);
pub struct TilemapChunkMeshCache(HashMap<UVec2, Handle<Mesh>>);

/// A component representing a chunk of a tilemap.
/// Each chunk is a rectangular section of tiles that is rendered as a single mesh.
#[derive(Component, Clone, Debug, Default)]
#[require(Anchor)]
#[component(immutable, on_insert = on_insert_tilemap_chunk)]
pub struct TilemapChunk {
/// The size of the chunk in tiles
Expand All @@ -60,10 +54,36 @@ pub struct TilemapChunk {
pub alpha_mode: AlphaMode2d,
}

#[derive(Clone, Copy, Debug)]
pub struct TileData {
pub tileset_index: u16,
pub color: Color,
pub visible: bool,
}

impl TileData {
pub fn from_index(index: u16) -> Self {
Self {
tileset_index: index,
..default()
}
}
}

impl Default for TileData {
fn default() -> Self {
Self {
tileset_index: 0,
color: Color::WHITE,
visible: true,
}
}
}

/// Component storing the indices of tiles within a chunk.
/// Each index corresponds to a specific tile in the tileset.
#[derive(Component, Clone, Debug, Deref, DerefMut)]
pub struct TilemapChunkIndices(pub Vec<Option<u16>>);
pub struct TilemapChunkTileData(pub Vec<Option<TileData>>);

fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let Some(tilemap_chunk) = world.get::<TilemapChunk>(entity) else {
Expand All @@ -75,55 +95,46 @@ fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }:
let alpha_mode = tilemap_chunk.alpha_mode;
let tileset = tilemap_chunk.tileset.clone();

let Some(indices) = world.get::<TilemapChunkIndices>(entity) else {
let Some(tile_data) = world.get::<TilemapChunkTileData>(entity) else {
warn!("TilemapChunkIndices not found for tilemap chunk {}", entity);
return;
};

let Some(&anchor) = world.get::<Anchor>(entity) else {
warn!("Anchor not found for tilemap chunk {}", entity);
return;
};

let expected_indices_length = chunk_size.element_product() as usize;
if indices.len() != expected_indices_length {
let expected_tile_data_length = chunk_size.element_product() as usize;
if tile_data.len() != expected_tile_data_length {
warn!(
"Invalid indices length for tilemap chunk {} of size {}. Expected {}, got {}",
"Invalid tile data length for tilemap chunk {} of size {}. Expected {}, got {}",
entity,
chunk_size,
indices.len(),
expected_indices_length
expected_tile_data_length,
tile_data.len(),
);
return;
}

let indices_image = make_chunk_image(&chunk_size, &indices.0);
let packed_tile_data: Vec<PackedTileData> =
tile_data.0.iter().map(|&tile| tile.into()).collect();

let display_size = (chunk_size * tilemap_chunk.tile_display_size).as_vec2();

let mesh_key: TilemapChunkMeshCacheKey = (
chunk_size,
FloatOrd(display_size.x),
FloatOrd(display_size.y),
FloatOrd(anchor.as_vec().x),
FloatOrd(anchor.as_vec().y),
);
let tile_data_image = make_chunk_tile_data_image(&chunk_size, &packed_tile_data);

let tilemap_chunk_mesh_cache = world.resource::<TilemapChunkMeshCache>();
let mesh = if let Some(mesh) = tilemap_chunk_mesh_cache.get(&mesh_key) {

let mesh_size = chunk_size * tilemap_chunk.tile_display_size;

let mesh = if let Some(mesh) = tilemap_chunk_mesh_cache.get(&mesh_size) {
mesh.clone()
} else {
let mut meshes = world.resource_mut::<Assets<Mesh>>();
meshes.add(make_chunk_mesh(&chunk_size, &display_size, &anchor))
meshes.add(Rectangle::from_size(mesh_size.as_vec2()))
};

let mut images = world.resource_mut::<Assets<Image>>();
let indices = images.add(indices_image);
let tile_data = images.add(tile_data_image);

let mut materials = world.resource_mut::<Assets<TilemapChunkMaterial>>();
let material = materials.add(TilemapChunkMaterial {
tileset,
indices,
tile_data,
alpha_mode,
});

Expand All @@ -138,27 +149,30 @@ fn update_tilemap_chunk_indices(
(
Entity,
&TilemapChunk,
&TilemapChunkIndices,
&TilemapChunkTileData,
&MeshMaterial2d<TilemapChunkMaterial>,
),
Changed<TilemapChunkIndices>,
Changed<TilemapChunkTileData>,
>,
mut materials: ResMut<Assets<TilemapChunkMaterial>>,
mut images: ResMut<Assets<Image>>,
) {
for (chunk_entity, TilemapChunk { chunk_size, .. }, indices, material) in query {
let expected_indices_length = chunk_size.element_product() as usize;
if indices.len() != expected_indices_length {
for (chunk_entity, TilemapChunk { chunk_size, .. }, tile_data, material) in query {
let expected_tile_data_length = chunk_size.element_product() as usize;
if tile_data.len() != expected_tile_data_length {
warn!(
"Invalid TilemapChunkIndices length for tilemap chunk {} of size {}. Expected {}, got {}",
"Invalid TilemapChunkTileData length for tilemap chunk {} of size {}. Expected {}, got {}",
chunk_entity,
chunk_size,
indices.len(),
expected_indices_length
tile_data.len(),
expected_tile_data_length
);
continue;
}

let packed_tile_data: Vec<PackedTileData> =
tile_data.0.iter().map(|&tile| tile.into()).collect();

// Getting the material mutably to trigger change detection
let Some(material) = materials.get_mut(material.id()) else {
warn!(
Expand All @@ -167,101 +181,21 @@ fn update_tilemap_chunk_indices(
);
continue;
};
let Some(indices_image) = images.get_mut(&material.indices) else {
let Some(tile_data_image) = images.get_mut(&material.tile_data) else {
warn!(
"TilemapChunkMaterial indices image not found for tilemap chunk {}",
"TilemapChunkMaterial tile data image not found for tilemap chunk {}",
chunk_entity
);
continue;
};
let Some(data) = indices_image.data.as_mut() else {
let Some(data) = tile_data_image.data.as_mut() else {
warn!(
"TilemapChunkMaterial indices image data not found for tilemap chunk {}",
"TilemapChunkMaterial tile data image data not found for tilemap chunk {}",
chunk_entity
);
continue;
};
data.clear();
data.extend(
indices
.iter()
.copied()
.flat_map(|i| u16::to_ne_bytes(i.unwrap_or(u16::MAX))),
);
}
}

fn make_chunk_image(size: &UVec2, indices: &[Option<u16>]) -> Image {
Image {
data: Some(
indices
.iter()
.copied()
.flat_map(|i| u16::to_ne_bytes(i.unwrap_or(u16::MAX)))
.collect(),
),
data_order: TextureDataOrder::default(),
texture_descriptor: TextureDescriptor {
size: size.to_extents(),
dimension: TextureDimension::D2,
format: TextureFormat::R16Uint,
label: None,
mip_level_count: 1,
sample_count: 1,
usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST,
view_formats: &[],
},
sampler: ImageSampler::nearest(),
texture_view_descriptor: None,
asset_usage: RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
copy_on_resize: false,
data.extend_from_slice(bytemuck::cast_slice(&packed_tile_data));
}
}

fn make_chunk_mesh(size: &UVec2, display_size: &Vec2, anchor: &Anchor) -> Mesh {
let mut mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD,
);

let offset = display_size * (Vec2::splat(-0.5) - anchor.as_vec());

let num_quads = size.element_product() as usize;
let quad_size = display_size / size.as_vec2();

let mut positions = Vec::with_capacity(4 * num_quads);
let mut uvs = Vec::with_capacity(4 * num_quads);
let mut indices = Vec::with_capacity(6 * num_quads);

for y in 0..size.y {
for x in 0..size.x {
let i = positions.len() as u32;

let p0 = offset + quad_size * UVec2::new(x, y).as_vec2();
let p1 = p0 + quad_size;

positions.extend([
Vec3::new(p0.x, p0.y, 0.0),
Vec3::new(p1.x, p0.y, 0.0),
Vec3::new(p0.x, p1.y, 0.0),
Vec3::new(p1.x, p1.y, 0.0),
]);

uvs.extend([
Vec2::new(0.0, 1.0),
Vec2::new(1.0, 1.0),
Vec2::new(0.0, 0.0),
Vec2::new(1.0, 0.0),
]);

indices.extend([i, i + 2, i + 1]);
indices.extend([i + 3, i + 1, i + 2]);
}
}

mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions);
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs);
mesh.insert_indices(Indices::U32(indices));

mesh
}
Loading
Loading