TilemapChunk single quad; TileData (color/visibility)

This commit is contained in:
Conner Petzold 2025-07-01 23:32:31 -04:00
parent 9edf538643
commit 3928bc16c7
4 changed files with 189 additions and 205 deletions

View File

@ -1,6 +1,7 @@
use crate::{AlphaMode2d, Anchor, MeshMaterial2d}; use crate::{AlphaMode2d, MeshMaterial2d};
use bevy_app::{App, Plugin, Update}; 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_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
component::Component, component::Component,
@ -11,15 +12,11 @@ use bevy_ecs::{
system::{Query, ResMut}, system::{Query, ResMut},
world::DeferredWorld, world::DeferredWorld,
}; };
use bevy_image::{Image, ImageSampler, ToExtents}; use bevy_image::Image;
use bevy_math::{FloatOrd, UVec2, Vec2, Vec3}; use bevy_math::{primitives::Rectangle, UVec2};
use bevy_platform::collections::HashMap; use bevy_platform::collections::HashMap;
use bevy_render::{ use bevy_render::mesh::{Mesh, Mesh2d};
mesh::{Indices, Mesh, Mesh2d, PrimitiveTopology}, use bevy_utils::default;
render_resource::{
TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
},
};
use tracing::warn; use tracing::warn;
mod tilemap_chunk_material; 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. /// A resource storing the meshes for each tilemap chunk size.
#[derive(Resource, Default, Deref, DerefMut)] #[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. /// A component representing a chunk of a tilemap.
/// Each chunk is a rectangular section of tiles that is rendered as a single mesh. /// Each chunk is a rectangular section of tiles that is rendered as a single mesh.
#[derive(Component, Clone, Debug, Default)] #[derive(Component, Clone, Debug, Default)]
#[require(Anchor)]
#[component(immutable, on_insert = on_insert_tilemap_chunk)] #[component(immutable, on_insert = on_insert_tilemap_chunk)]
pub struct TilemapChunk { pub struct TilemapChunk {
/// The size of the chunk in tiles /// The size of the chunk in tiles
@ -60,10 +54,36 @@ pub struct TilemapChunk {
pub alpha_mode: AlphaMode2d, 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. /// Component storing the indices of tiles within a chunk.
/// Each index corresponds to a specific tile in the tileset. /// Each index corresponds to a specific tile in the tileset.
#[derive(Component, Clone, Debug, Deref, DerefMut)] #[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) { fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let Some(tilemap_chunk) = world.get::<TilemapChunk>(entity) else { let Some(tilemap_chunk) = world.get::<TilemapChunk>(entity) else {
@ -75,55 +95,46 @@ fn on_insert_tilemap_chunk(mut world: DeferredWorld, HookContext { entity, .. }:
let alpha_mode = tilemap_chunk.alpha_mode; let alpha_mode = tilemap_chunk.alpha_mode;
let tileset = tilemap_chunk.tileset.clone(); 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); warn!("TilemapChunkIndices not found for tilemap chunk {}", entity);
return; return;
}; };
let Some(&anchor) = world.get::<Anchor>(entity) else { let expected_tile_data_length = chunk_size.element_product() as usize;
warn!("Anchor not found for tilemap chunk {}", entity); if tile_data.len() != expected_tile_data_length {
return;
};
let expected_indices_length = chunk_size.element_product() as usize;
if indices.len() != expected_indices_length {
warn!( warn!(
"Invalid indices length for tilemap chunk {} of size {}. Expected {}, got {}", "Invalid tile data length for tilemap chunk {} of size {}. Expected {}, got {}",
entity, entity,
chunk_size, chunk_size,
indices.len(), expected_tile_data_length,
expected_indices_length tile_data.len(),
); );
return; 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 tile_data_image = make_chunk_tile_data_image(&chunk_size, &packed_tile_data);
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 tilemap_chunk_mesh_cache = world.resource::<TilemapChunkMeshCache>(); 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() mesh.clone()
} else { } else {
let mut meshes = world.resource_mut::<Assets<Mesh>>(); 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 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 mut materials = world.resource_mut::<Assets<TilemapChunkMaterial>>();
let material = materials.add(TilemapChunkMaterial { let material = materials.add(TilemapChunkMaterial {
tileset, tileset,
indices, tile_data,
alpha_mode, alpha_mode,
}); });
@ -138,27 +149,30 @@ fn update_tilemap_chunk_indices(
( (
Entity, Entity,
&TilemapChunk, &TilemapChunk,
&TilemapChunkIndices, &TilemapChunkTileData,
&MeshMaterial2d<TilemapChunkMaterial>, &MeshMaterial2d<TilemapChunkMaterial>,
), ),
Changed<TilemapChunkIndices>, Changed<TilemapChunkTileData>,
>, >,
mut materials: ResMut<Assets<TilemapChunkMaterial>>, mut materials: ResMut<Assets<TilemapChunkMaterial>>,
mut images: ResMut<Assets<Image>>, mut images: ResMut<Assets<Image>>,
) { ) {
for (chunk_entity, TilemapChunk { chunk_size, .. }, indices, material) in query { for (chunk_entity, TilemapChunk { chunk_size, .. }, tile_data, material) in query {
let expected_indices_length = chunk_size.element_product() as usize; let expected_tile_data_length = chunk_size.element_product() as usize;
if indices.len() != expected_indices_length { if tile_data.len() != expected_tile_data_length {
warn!( warn!(
"Invalid TilemapChunkIndices length for tilemap chunk {} of size {}. Expected {}, got {}", "Invalid TilemapChunkTileData length for tilemap chunk {} of size {}. Expected {}, got {}",
chunk_entity, chunk_entity,
chunk_size, chunk_size,
indices.len(), tile_data.len(),
expected_indices_length expected_tile_data_length
); );
continue; continue;
} }
let packed_tile_data: Vec<PackedTileData> =
tile_data.0.iter().map(|&tile| tile.into()).collect();
// Getting the material mutably to trigger change detection // Getting the material mutably to trigger change detection
let Some(material) = materials.get_mut(material.id()) else { let Some(material) = materials.get_mut(material.id()) else {
warn!( warn!(
@ -167,101 +181,21 @@ fn update_tilemap_chunk_indices(
); );
continue; continue;
}; };
let Some(indices_image) = images.get_mut(&material.indices) else { let Some(tile_data_image) = images.get_mut(&material.tile_data) else {
warn!( warn!(
"TilemapChunkMaterial indices image not found for tilemap chunk {}", "TilemapChunkMaterial tile data image not found for tilemap chunk {}",
chunk_entity chunk_entity
); );
continue; continue;
}; };
let Some(data) = indices_image.data.as_mut() else { let Some(data) = tile_data_image.data.as_mut() else {
warn!( warn!(
"TilemapChunkMaterial indices image data not found for tilemap chunk {}", "TilemapChunkMaterial tile data image data not found for tilemap chunk {}",
chunk_entity chunk_entity
); );
continue; continue;
}; };
data.clear(); data.clear();
data.extend( data.extend_from_slice(bytemuck::cast_slice(&packed_tile_data));
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,
}
}
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
}

View File

@ -1,12 +1,12 @@
use crate::{AlphaMode2d, Material2d, Material2dKey, Material2dPlugin}; use crate::{AlphaMode2d, Material2d, Material2dPlugin, TileData};
use bevy_app::{App, Plugin}; use bevy_app::{App, Plugin};
use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle}; use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle, RenderAssetUsages};
use bevy_image::Image; use bevy_color::ColorToPacked;
use bevy_image::{Image, ImageSampler, ToExtents};
use bevy_math::UVec2;
use bevy_reflect::prelude::*; use bevy_reflect::prelude::*;
use bevy_render::{ use bevy_render::render_resource::*;
mesh::{Mesh, MeshVertexBufferLayoutRef}, use bytemuck::{Pod, Zeroable};
render_resource::*,
};
/// Plugin that adds support for tilemap chunk materials. /// Plugin that adds support for tilemap chunk materials.
pub struct TilemapChunkMaterialPlugin; pub struct TilemapChunkMaterialPlugin;
@ -32,7 +32,7 @@ pub struct TilemapChunkMaterial {
pub tileset: Handle<Image>, pub tileset: Handle<Image>,
#[texture(2, sample_type = "u_int")] #[texture(2, sample_type = "u_int")]
pub indices: Handle<Image>, pub tile_data: Handle<Image>,
} }
impl Material2d for TilemapChunkMaterial { 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 { fn alpha_mode(&self) -> AlphaMode2d {
self.alpha_mode self.alpha_mode
} }
}
fn specialize( #[repr(C)]
descriptor: &mut RenderPipelineDescriptor, #[derive(Clone, Copy, Debug, Pod, Zeroable)]
layout: &MeshVertexBufferLayoutRef, pub struct PackedTileData {
_key: Material2dKey<Self>, tileset_index: u16,
) -> Result<(), SpecializedMeshPipelineError> { flags: u16, // flags (visibility, etc.)
let vertex_layout = layout.0.get_layout(&[ color_red_green: u16, // r in low 8 bits, g in high 8 bits
Mesh::ATTRIBUTE_POSITION.at_shader_location(0), color_blue_alpha: u16, // b in low 8 bits, a in high 8 bits
Mesh::ATTRIBUTE_UV_0.at_shader_location(1), }
])?;
descriptor.vertex.buffers = vec![vertex_layout]; impl PackedTileData {
Ok(()) fn empty() -> Self {
Self {
tileset_index: u16::MAX,
flags: 0,
color_red_green: 0,
color_blue_alpha: 0,
}
}
}
impl From<TileData> 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<Option<TileData>> for PackedTileData {
fn from(maybe_tile_data: Option<TileData>) -> Self {
maybe_tile_data
.map(Into::into)
.unwrap_or(PackedTileData::empty())
}
}
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,
} }
} }

View File

@ -1,58 +1,54 @@
#import bevy_sprite::{ #import bevy_sprite::{
mesh2d_functions as mesh_functions, mesh2d_functions as mesh_functions,
mesh2d_view_bindings::view, mesh2d_view_bindings::view,
} mesh2d_vertex_output::VertexOutput,
struct Vertex {
@builtin(instance_index) instance_index: u32,
@builtin(vertex_index) vertex_index: u32,
@location(0) position: vec3<f32>,
@location(1) uv: vec2<f32>,
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) uv: vec2<f32>,
@location(1) tile_index: u32,
} }
@group(2) @binding(0) var tileset: texture_2d_array<f32>; @group(2) @binding(0) var tileset: texture_2d_array<f32>;
@group(2) @binding(1) var tileset_sampler: sampler; @group(2) @binding(1) var tileset_sampler: sampler;
@group(2) @binding(2) var tile_indices: texture_2d<u32>; @group(2) @binding(2) var tile_data: texture_2d<u32>;
@vertex struct TileData {
fn vertex(vertex: Vertex) -> VertexOutput { tileset_index: u32,
var out: VertexOutput; visible: bool,
color: vec4<f32>,
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<f32>(vertex.position, 1.0)
);
out.position = mesh_functions::mesh2d_position_world_to_clip(world_position); fn getTileData(coord: vec2<u32>) -> TileData {
out.uv = vertex.uv; let data = textureLoad(tile_data, coord, 0);
out.tile_index = vertex.vertex_index / 4u;
return out; 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<f32>(color_r, color_g, color_b, color_a);
return TileData(tileset_index, visible, color);
} }
@fragment @fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> { fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
let chunk_size = textureDimensions(tile_indices, 0); let chunk_size = textureDimensions(tile_data, 0);
let tile_xy = vec2<u32>( let tile_uv = in.uv * vec2<f32>(chunk_size);
in.tile_index % chunk_size.x, let tile_coord = clamp(vec2<u32>(floor(tile_uv)), vec2<u32>(0), chunk_size - 1);
in.tile_index / chunk_size.x
);
let tile_id = textureLoad(tile_indices, tile_xy, 0).r;
if tile_id == 0xffffu { let tile = getTileData(tile_coord);
if (tile.tileset_index == 0xffffu || !tile.visible) {
discard; discard;
} }
let color = textureSample(tileset, tileset_sampler, in.uv, tile_id); let local_uv = fract(tile_uv);
if color.a < 0.001 { 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; discard;
} }
return color;
return final_color;
} }

View File

@ -2,7 +2,7 @@
use bevy::{ use bevy::{
prelude::*, prelude::*,
sprite::{TilemapChunk, TilemapChunkIndices}, sprite::{TileData, TilemapChunk, TilemapChunkTileData},
}; };
use rand::{Rng, SeedableRng}; use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng; use rand_chacha::ChaCha8Rng;
@ -27,10 +27,16 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
let mut rng = ChaCha8Rng::seed_from_u64(42); let mut rng = ChaCha8Rng::seed_from_u64(42);
let chunk_size = UVec2::splat(64); let chunk_size = UVec2::splat(64);
let tile_display_size = UVec2::splat(8); let tile_display_size = UVec2::splat(16);
let indices: Vec<Option<u16>> = (0..chunk_size.element_product()) let tile_data: Vec<Option<TileData>> = (0..chunk_size.element_product())
.map(|_| rng.gen_range(0..5)) .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(); .collect();
commands.spawn(( commands.spawn((
@ -40,7 +46,7 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
tileset: assets.load("textures/array_texture.png"), tileset: assets.load("textures/array_texture.png"),
..default() ..default()
}, },
TilemapChunkIndices(indices), TilemapChunkTileData(tile_data),
UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
)); ));
@ -65,16 +71,16 @@ fn update_tileset_image(
fn update_tilemap( fn update_tilemap(
time: Res<Time>, time: Res<Time>,
mut query: Query<(&mut TilemapChunkIndices, &mut UpdateTimer)>, mut query: Query<(&mut TilemapChunkTileData, &mut UpdateTimer)>,
mut rng: ResMut<SeededRng>, mut rng: ResMut<SeededRng>,
) { ) {
for (mut indices, mut timer) in query.iter_mut() { for (mut tile_data, mut timer) in query.iter_mut() {
timer.tick(time.delta()); timer.tick(time.delta());
if timer.just_finished() { if timer.just_finished() {
for _ in 0..50 { for _ in 0..50 {
let index = rng.gen_range(0..indices.len()); let index = rng.gen_range(0..tile_data.len());
indices[index] = Some(rng.gen_range(0..5)); tile_data[index] = Some(TileData::from_index(rng.gen_range(0..5)));
} }
} }
} }