From 3928bc16c75d1928c5a893311771b7bc6ea041f4 Mon Sep 17 00:00:00 2001 From: Conner Petzold Date: Tue, 1 Jul 2025 23:32:31 -0400 Subject: [PATCH 1/6] TilemapChunk single quad; TileData (color/visibility) --- crates/bevy_sprite/src/tilemap_chunk/mod.rs | 200 ++++++------------ .../tilemap_chunk/tilemap_chunk_material.rs | 100 ++++++--- .../tilemap_chunk/tilemap_chunk_material.wgsl | 78 ++++--- examples/2d/tilemap_chunk.rs | 24 ++- 4 files changed, 193 insertions(+), 209 deletions(-) diff --git a/crates/bevy_sprite/src/tilemap_chunk/mod.rs b/crates/bevy_sprite/src/tilemap_chunk/mod.rs index 174816154bc6e..68065bcc6ea90 100644 --- a/crates/bevy_sprite/src/tilemap_chunk/mod.rs +++ b/crates/bevy_sprite/src/tilemap_chunk/mod.rs @@ -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, @@ -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; @@ -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>); +pub struct TilemapChunkMeshCache(HashMap>); /// 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 @@ -60,10 +54,36 @@ pub struct TilemapChunk { pub alpha_mode: AlphaMode2d, } +#[derive(Clone, Copy, Debug)] +pub struct TileData { + pub tileset_index: u16, + pub visible: bool, + pub color: Color, +} + +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, + visible: true, + color: Color::WHITE, + } + } +} + /// 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>); +pub struct TilemapChunkTileData(pub Vec>); fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) { let Some(tilemap_chunk) = world.get::(entity) else { @@ -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::(entity) else { + let Some(tile_data) = world.get::(entity) else { warn!("TilemapChunkIndices not found for tilemap chunk {}", entity); return; }; - let Some(&anchor) = world.get::(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 = + 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::(); - 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::>(); - 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::>(); - let indices = images.add(indices_image); + let tile_data = images.add(tile_data_image); let mut materials = world.resource_mut::>(); let material = materials.add(TilemapChunkMaterial { tileset, - indices, + tile_data, alpha_mode, }); @@ -138,27 +149,30 @@ fn update_tilemap_chunk_indices( ( Entity, &TilemapChunk, - &TilemapChunkIndices, + &TilemapChunkTileData, &MeshMaterial2d, ), - Changed, + Changed, >, mut materials: ResMut>, mut images: ResMut>, ) { - 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 = + 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!( @@ -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]) -> 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 -} diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs index 71af0244c8709..4d738f0537e62 100644 --- a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs +++ b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs @@ -1,12 +1,12 @@ -use crate::{AlphaMode2d, Material2d, Material2dKey, Material2dPlugin}; +use crate::{AlphaMode2d, Material2d, Material2dPlugin, TileData}; use bevy_app::{App, Plugin}; -use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle}; -use bevy_image::Image; +use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle, RenderAssetUsages}; +use bevy_color::ColorToPacked; +use bevy_image::{Image, ImageSampler, ToExtents}; +use bevy_math::UVec2; use bevy_reflect::prelude::*; -use bevy_render::{ - mesh::{Mesh, MeshVertexBufferLayoutRef}, - render_resource::*, -}; +use bevy_render::render_resource::*; +use bytemuck::{Pod, Zeroable}; /// Plugin that adds support for tilemap chunk materials. pub struct TilemapChunkMaterialPlugin; @@ -32,7 +32,7 @@ pub struct TilemapChunkMaterial { pub tileset: Handle, #[texture(2, sample_type = "u_int")] - pub indices: Handle, + pub tile_data: Handle, } impl Material2d for TilemapChunkMaterial { @@ -43,27 +43,75 @@ impl Material2d for TilemapChunkMaterial { ) } - fn vertex_shader() -> ShaderRef { - ShaderRef::Path( - AssetPath::from_path_buf(embedded_path!("tilemap_chunk_material.wgsl")) - .with_source("embedded"), - ) - } - fn alpha_mode(&self) -> AlphaMode2d { self.alpha_mode } +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Pod, Zeroable)] +pub struct PackedTileData { + tileset_index: u16, + flags: u16, // flags (visibility, etc.) + color_red_green: u16, // r in low 8 bits, g in high 8 bits + color_blue_alpha: u16, // b in low 8 bits, a in high 8 bits +} + +impl PackedTileData { + fn empty() -> Self { + Self { + tileset_index: u16::MAX, + flags: 0, + color_red_green: 0, + color_blue_alpha: 0, + } + } +} + +impl From for PackedTileData { + fn from( + TileData { + tileset_index, + visible, + color, + }: TileData, + ) -> Self { + let [r, g, b, a] = color.to_srgba().to_u8_array(); + + Self { + tileset_index, + flags: visible as u16, + color_red_green: (r as u16) | ((g as u16) << 8), + color_blue_alpha: (b as u16) | ((a as u16) << 8), + } + } +} + +impl From> for PackedTileData { + fn from(maybe_tile_data: Option) -> Self { + maybe_tile_data + .map(Into::into) + .unwrap_or(PackedTileData::empty()) + } +} - fn specialize( - descriptor: &mut RenderPipelineDescriptor, - layout: &MeshVertexBufferLayoutRef, - _key: Material2dKey, - ) -> Result<(), SpecializedMeshPipelineError> { - let vertex_layout = layout.0.get_layout(&[ - Mesh::ATTRIBUTE_POSITION.at_shader_location(0), - Mesh::ATTRIBUTE_UV_0.at_shader_location(1), - ])?; - descriptor.vertex.buffers = vec![vertex_layout]; - Ok(()) +pub fn make_chunk_tile_data_image(size: &UVec2, data: &[PackedTileData]) -> Image { + Image { + data: Some(bytemuck::cast_slice(data).to_vec()), + data_order: TextureDataOrder::default(), + texture_descriptor: TextureDescriptor { + size: size.to_extents(), + dimension: TextureDimension::D2, + format: TextureFormat::Rgba16Uint, + 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, } } diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl index 7424995e22954..b3daf2fa19e7b 100644 --- a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl +++ b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl @@ -1,58 +1,54 @@ #import bevy_sprite::{ mesh2d_functions as mesh_functions, mesh2d_view_bindings::view, -} - -struct Vertex { - @builtin(instance_index) instance_index: u32, - @builtin(vertex_index) vertex_index: u32, - @location(0) position: vec3, - @location(1) uv: vec2, -}; - -struct VertexOutput { - @builtin(position) position: vec4, - @location(0) uv: vec2, - @location(1) tile_index: u32, + mesh2d_vertex_output::VertexOutput, } @group(2) @binding(0) var tileset: texture_2d_array; @group(2) @binding(1) var tileset_sampler: sampler; -@group(2) @binding(2) var tile_indices: texture_2d; - -@vertex -fn vertex(vertex: Vertex) -> VertexOutput { - var out: VertexOutput; - - let world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); - let world_position = mesh_functions::mesh2d_position_local_to_world( - world_from_local, - vec4(vertex.position, 1.0) - ); - - out.position = mesh_functions::mesh2d_position_world_to_clip(world_position); - out.uv = vertex.uv; - out.tile_index = vertex.vertex_index / 4u; - - return out; +@group(2) @binding(2) var tile_data: texture_2d; + +struct TileData { + tileset_index: u32, + visible: bool, + color: vec4, +} + +fn getTileData(coord: vec2) -> TileData { + let data = textureLoad(tile_data, coord, 0); + + let tileset_index = data.r; + let visible = data.g != 0u; + + let color_r = f32(data.b & 0xFFu) / 255.0; + let color_g = f32((data.b >> 8u) & 0xFFu) / 255.0; + let color_b = f32(data.a & 0xFFu) / 255.0; + let color_a = f32((data.a >> 8u) & 0xFFu) / 255.0; + + let color = vec4(color_r, color_g, color_b, color_a); + + return TileData(tileset_index, visible, color); } @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { - let chunk_size = textureDimensions(tile_indices, 0); - let tile_xy = vec2( - in.tile_index % chunk_size.x, - in.tile_index / chunk_size.x - ); - let tile_id = textureLoad(tile_indices, tile_xy, 0).r; - - if tile_id == 0xffffu { + let chunk_size = textureDimensions(tile_data, 0); + let tile_uv = in.uv * vec2(chunk_size); + let tile_coord = clamp(vec2(floor(tile_uv)), vec2(0), chunk_size - 1); + + let tile = getTileData(tile_coord); + + if (tile.tileset_index == 0xffffu || !tile.visible) { discard; } - let color = textureSample(tileset, tileset_sampler, in.uv, tile_id); - if color.a < 0.001 { + let local_uv = fract(tile_uv); + let tex_color = textureSample(tileset, tileset_sampler, local_uv, tile.tileset_index); + let final_color = tex_color * tile.color; + + if (final_color.a < 0.001) { discard; } - return color; + + return final_color; } \ No newline at end of file diff --git a/examples/2d/tilemap_chunk.rs b/examples/2d/tilemap_chunk.rs index 8663b036b1574..d2f32e3bbad43 100644 --- a/examples/2d/tilemap_chunk.rs +++ b/examples/2d/tilemap_chunk.rs @@ -2,7 +2,7 @@ use bevy::{ prelude::*, - sprite::{TilemapChunk, TilemapChunkIndices}, + sprite::{TileData, TilemapChunk, TilemapChunkTileData}, }; use rand::{Rng, SeedableRng}; use rand_chacha::ChaCha8Rng; @@ -27,10 +27,16 @@ fn setup(mut commands: Commands, assets: Res) { let mut rng = ChaCha8Rng::seed_from_u64(42); let chunk_size = UVec2::splat(64); - let tile_display_size = UVec2::splat(8); - let indices: Vec> = (0..chunk_size.element_product()) + let tile_display_size = UVec2::splat(16); + let tile_data: Vec> = (0..chunk_size.element_product()) .map(|_| rng.gen_range(0..5)) - .map(|i| if i == 0 { None } else { Some(i - 1) }) + .map(|i| { + if i == 0 { + None + } else { + Some(TileData::from_index(i - 1)) + } + }) .collect(); commands.spawn(( @@ -40,7 +46,7 @@ fn setup(mut commands: Commands, assets: Res) { tileset: assets.load("textures/array_texture.png"), ..default() }, - TilemapChunkIndices(indices), + TilemapChunkTileData(tile_data), UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), )); @@ -65,16 +71,16 @@ fn update_tileset_image( fn update_tilemap( time: Res