Add TilemapChunk rendering (#18866)
# Objective An attempt to start building a base for first-party tilemaps (#13782). The objective is to create a very simple tilemap chunk rendering plugin that can be used as a building block for 3rd-party tilemap crates, and eventually a first-party tilemap implementation. ## Solution - Introduces two user-facing components, `TilemapChunk` and `TilemapChunkIndices`, and a new material `TilemapChunkMaterial`. - `TilemapChunk` holds the chunk and tile sizes, and the tileset image - The tileset image is expected to be a layered image for use with `texture_2d_array`, with the assumption that atlases or multiple images would go through an asset loader/processor. Not sure if that should be part of this PR or not.. - `TilemapChunkIndices` holds a 1d representation of all of the tile's Option<u32> index into the tileset image. - Indices are fixed to the size of tiles in a chunk (though maybe this should just be an assertion instead?) - Indices are cloned and sent to the shader through a u32 texture. ## Testing - Initial testing done with the `tilemap_chunk` example, though I need to include some way to update indices as part of it. - Tested wasm with webgl2 and webgpu - I'm thinking it would probably be good to do some basic perf testing. --- ## Showcase ```rust let chunk_size = UVec2::splat(64); let tile_size = UVec2::splat(16); let indices: Vec<Option<u32>> = (0..chunk_size.x * chunk_size.y) .map(|_| rng.gen_range(0..5)) .map(|i| if i == 0 { None } else { Some(i - 1) }) .collect(); commands.spawn(( TilemapChunk { chunk_size, tile_size, tileset, }, TilemapChunkIndices(indices), )); ``` 
This commit is contained in:
parent
d3ad66f033
commit
3f187cf752
11
Cargo.toml
11
Cargo.toml
@ -845,6 +845,17 @@ description = "Generates a texture atlas (sprite sheet) from individual sprites"
|
|||||||
category = "2D Rendering"
|
category = "2D Rendering"
|
||||||
wasm = false
|
wasm = false
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "tilemap_chunk"
|
||||||
|
path = "examples/2d/tilemap_chunk.rs"
|
||||||
|
doc-scrape-examples = true
|
||||||
|
|
||||||
|
[package.metadata.example.tilemap_chunk]
|
||||||
|
name = "Tilemap Chunk"
|
||||||
|
description = "Renders a tilemap chunk"
|
||||||
|
category = "2D Rendering"
|
||||||
|
wasm = true
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "transparency_2d"
|
name = "transparency_2d"
|
||||||
path = "examples/2d/transparency_2d.rs"
|
path = "examples/2d/transparency_2d.rs"
|
||||||
|
@ -16,6 +16,7 @@ mod picking_backend;
|
|||||||
mod render;
|
mod render;
|
||||||
mod sprite;
|
mod sprite;
|
||||||
mod texture_slice;
|
mod texture_slice;
|
||||||
|
mod tilemap_chunk;
|
||||||
|
|
||||||
/// The sprite prelude.
|
/// The sprite prelude.
|
||||||
///
|
///
|
||||||
@ -40,6 +41,7 @@ pub use picking_backend::*;
|
|||||||
pub use render::*;
|
pub use render::*;
|
||||||
pub use sprite::*;
|
pub use sprite::*;
|
||||||
pub use texture_slice::*;
|
pub use texture_slice::*;
|
||||||
|
pub use tilemap_chunk::*;
|
||||||
|
|
||||||
use bevy_app::prelude::*;
|
use bevy_app::prelude::*;
|
||||||
use bevy_asset::{embedded_asset, AssetEventSystems, Assets};
|
use bevy_asset::{embedded_asset, AssetEventSystems, Assets};
|
||||||
@ -87,7 +89,12 @@ impl Plugin for SpritePlugin {
|
|||||||
.register_type::<TextureSlicer>()
|
.register_type::<TextureSlicer>()
|
||||||
.register_type::<Anchor>()
|
.register_type::<Anchor>()
|
||||||
.register_type::<Mesh2d>()
|
.register_type::<Mesh2d>()
|
||||||
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
|
.add_plugins((
|
||||||
|
Mesh2dRenderPlugin,
|
||||||
|
ColorMaterialPlugin,
|
||||||
|
TilemapChunkPlugin,
|
||||||
|
TilemapChunkMaterialPlugin,
|
||||||
|
))
|
||||||
.add_systems(
|
.add_systems(
|
||||||
PostUpdate,
|
PostUpdate,
|
||||||
(
|
(
|
||||||
|
263
crates/bevy_sprite/src/tilemap_chunk/mod.rs
Normal file
263
crates/bevy_sprite/src/tilemap_chunk/mod.rs
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
use crate::{AlphaMode2d, Anchor, MeshMaterial2d};
|
||||||
|
use bevy_app::{App, Plugin, Update};
|
||||||
|
use bevy_asset::{Assets, Handle, RenderAssetUsages};
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::{
|
||||||
|
component::Component,
|
||||||
|
entity::Entity,
|
||||||
|
lifecycle::Add,
|
||||||
|
observer::On,
|
||||||
|
query::Changed,
|
||||||
|
resource::Resource,
|
||||||
|
system::{Commands, Query, ResMut},
|
||||||
|
};
|
||||||
|
use bevy_image::{Image, ImageSampler};
|
||||||
|
use bevy_math::{FloatOrd, UVec2, Vec2, Vec3};
|
||||||
|
use bevy_platform::collections::HashMap;
|
||||||
|
use bevy_render::{
|
||||||
|
mesh::{Indices, Mesh, Mesh2d, PrimitiveTopology},
|
||||||
|
render_resource::{
|
||||||
|
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use tracing::warn;
|
||||||
|
|
||||||
|
mod tilemap_chunk_material;
|
||||||
|
|
||||||
|
pub use tilemap_chunk_material::*;
|
||||||
|
|
||||||
|
/// Plugin that handles the initialization and updating of tilemap chunks.
|
||||||
|
/// Adds systems for processing newly added tilemap chunks and updating their indices.
|
||||||
|
pub struct TilemapChunkPlugin;
|
||||||
|
|
||||||
|
impl Plugin for TilemapChunkPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_resource::<TilemapChunkMeshCache>()
|
||||||
|
.add_observer(on_add_tilemap_chunk)
|
||||||
|
.add_systems(Update, update_tilemap_chunk_indices);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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>>);
|
||||||
|
|
||||||
|
/// 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(Mesh2d, MeshMaterial2d<TilemapChunkMaterial>, TilemapChunkIndices, Anchor)]
|
||||||
|
pub struct TilemapChunk {
|
||||||
|
/// The size of the chunk in tiles
|
||||||
|
pub chunk_size: UVec2,
|
||||||
|
/// The size to use for each tile, not to be confused with the size of a tile in the tileset image.
|
||||||
|
/// The size of the tile in the tileset image is determined by the tileset image's dimensions.
|
||||||
|
pub tile_display_size: UVec2,
|
||||||
|
/// Handle to the tileset image containing all tile textures
|
||||||
|
pub tileset: Handle<Image>,
|
||||||
|
/// The alpha mode to use for the tilemap chunk
|
||||||
|
pub alpha_mode: AlphaMode2d,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Component storing the indices of tiles within a chunk.
|
||||||
|
/// Each index corresponds to a specific tile in the tileset.
|
||||||
|
#[derive(Component, Clone, Debug, Default, Deref, DerefMut)]
|
||||||
|
pub struct TilemapChunkIndices(pub Vec<Option<u16>>);
|
||||||
|
|
||||||
|
fn on_add_tilemap_chunk(
|
||||||
|
trigger: On<Add, TilemapChunk>,
|
||||||
|
tilemap_chunk_query: Query<(&TilemapChunk, &TilemapChunkIndices, &Anchor)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<TilemapChunkMaterial>>,
|
||||||
|
mut images: ResMut<Assets<Image>>,
|
||||||
|
mut tilemap_chunk_mesh_cache: ResMut<TilemapChunkMeshCache>,
|
||||||
|
) {
|
||||||
|
let chunk_entity = trigger.target();
|
||||||
|
let Ok((
|
||||||
|
TilemapChunk {
|
||||||
|
chunk_size,
|
||||||
|
tile_display_size,
|
||||||
|
tileset,
|
||||||
|
alpha_mode,
|
||||||
|
},
|
||||||
|
indices,
|
||||||
|
anchor,
|
||||||
|
)) = tilemap_chunk_query.get(chunk_entity)
|
||||||
|
else {
|
||||||
|
warn!("Tilemap chunk {} not found", chunk_entity);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let expected_indices_length = chunk_size.element_product() as usize;
|
||||||
|
if indices.len() != expected_indices_length {
|
||||||
|
warn!(
|
||||||
|
"Invalid indices length for tilemap chunk {} of size {}. Expected {}, got {}",
|
||||||
|
chunk_entity,
|
||||||
|
chunk_size,
|
||||||
|
indices.len(),
|
||||||
|
expected_indices_length
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let indices_image = make_chunk_image(chunk_size, &indices.0);
|
||||||
|
|
||||||
|
let display_size = (chunk_size * 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 mesh = tilemap_chunk_mesh_cache
|
||||||
|
.entry(mesh_key)
|
||||||
|
.or_insert_with(|| meshes.add(make_chunk_mesh(chunk_size, &display_size, anchor)));
|
||||||
|
|
||||||
|
commands.entity(chunk_entity).insert((
|
||||||
|
Mesh2d(mesh.clone()),
|
||||||
|
MeshMaterial2d(materials.add(TilemapChunkMaterial {
|
||||||
|
tileset: tileset.clone(),
|
||||||
|
indices: images.add(indices_image),
|
||||||
|
alpha_mode: *alpha_mode,
|
||||||
|
})),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_tilemap_chunk_indices(
|
||||||
|
query: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&TilemapChunk,
|
||||||
|
&TilemapChunkIndices,
|
||||||
|
&MeshMaterial2d<TilemapChunkMaterial>,
|
||||||
|
),
|
||||||
|
Changed<TilemapChunkIndices>,
|
||||||
|
>,
|
||||||
|
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 {
|
||||||
|
warn!(
|
||||||
|
"Invalid TilemapChunkIndices length for tilemap chunk {} of size {}. Expected {}, got {}",
|
||||||
|
chunk_entity,
|
||||||
|
chunk_size,
|
||||||
|
indices.len(),
|
||||||
|
expected_indices_length
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(material) = materials.get_mut(material.id()) else {
|
||||||
|
warn!(
|
||||||
|
"TilemapChunkMaterial not found for tilemap chunk {}",
|
||||||
|
chunk_entity
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(indices_image) = images.get_mut(&material.indices) else {
|
||||||
|
warn!(
|
||||||
|
"TilemapChunkMaterial indices image not found for tilemap chunk {}",
|
||||||
|
chunk_entity
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(data) = indices_image.data.as_mut() else {
|
||||||
|
warn!(
|
||||||
|
"TilemapChunkMaterial indices 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(),
|
||||||
|
),
|
||||||
|
texture_descriptor: TextureDescriptor {
|
||||||
|
size: Extent3d {
|
||||||
|
width: size.x,
|
||||||
|
height: size.y,
|
||||||
|
depth_or_array_layers: 1,
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
use crate::{AlphaMode2d, Material2d, Material2dKey, Material2dPlugin};
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle};
|
||||||
|
use bevy_image::Image;
|
||||||
|
use bevy_reflect::prelude::*;
|
||||||
|
use bevy_render::{
|
||||||
|
mesh::{Mesh, MeshVertexBufferLayoutRef},
|
||||||
|
render_resource::*,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Plugin that adds support for tilemap chunk materials.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct TilemapChunkMaterialPlugin;
|
||||||
|
|
||||||
|
impl Plugin for TilemapChunkMaterialPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
embedded_asset!(app, "tilemap_chunk_material.wgsl");
|
||||||
|
|
||||||
|
app.add_plugins(Material2dPlugin::<TilemapChunkMaterial>::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Material used for rendering tilemap chunks.
|
||||||
|
///
|
||||||
|
/// This material is used internally by the tilemap system to render chunks of tiles
|
||||||
|
/// efficiently using a single draw call per chunk.
|
||||||
|
#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)]
|
||||||
|
pub struct TilemapChunkMaterial {
|
||||||
|
pub alpha_mode: AlphaMode2d,
|
||||||
|
|
||||||
|
#[texture(0, dimension = "2d_array")]
|
||||||
|
#[sampler(1)]
|
||||||
|
pub tileset: Handle<Image>,
|
||||||
|
|
||||||
|
#[texture(2, sample_type = "u_int")]
|
||||||
|
pub indices: Handle<Image>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Material2d for TilemapChunkMaterial {
|
||||||
|
fn fragment_shader() -> ShaderRef {
|
||||||
|
ShaderRef::Path(
|
||||||
|
AssetPath::from_path_buf(embedded_path!("tilemap_chunk_material.wgsl"))
|
||||||
|
.with_source("embedded"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
fn specialize(
|
||||||
|
descriptor: &mut RenderPipelineDescriptor,
|
||||||
|
layout: &MeshVertexBufferLayoutRef,
|
||||||
|
_key: Material2dKey<Self>,
|
||||||
|
) -> 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(())
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,58 @@
|
|||||||
|
#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<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(1) var tileset_sampler: sampler;
|
||||||
|
@group(2) @binding(2) var tile_indices: texture_2d<u32>;
|
||||||
|
|
||||||
|
@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<f32>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
@fragment
|
||||||
|
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
|
||||||
|
let chunk_size = textureDimensions(tile_indices, 0);
|
||||||
|
let tile_xy = vec2<u32>(
|
||||||
|
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 {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = textureSample(tileset, tileset_sampler, in.uv, tile_id);
|
||||||
|
if color.a < 0.001 {
|
||||||
|
discard;
|
||||||
|
}
|
||||||
|
return color;
|
||||||
|
}
|
70
examples/2d/tilemap_chunk.rs
Normal file
70
examples/2d/tilemap_chunk.rs
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
//! Shows a tilemap chunk rendered with a single draw call.
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
prelude::*,
|
||||||
|
sprite::{TilemapChunk, TilemapChunkIndices},
|
||||||
|
};
|
||||||
|
use rand::{Rng, SeedableRng};
|
||||||
|
use rand_chacha::ChaCha8Rng;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins((DefaultPlugins.set(ImagePlugin::default_nearest()),))
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.add_systems(Update, (update_tileset_image, update_tilemap))
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Deref, DerefMut)]
|
||||||
|
struct UpdateTimer(Timer);
|
||||||
|
|
||||||
|
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
||||||
|
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
||||||
|
let chunk_size = UVec2::splat(64);
|
||||||
|
let tile_display_size = UVec2::splat(8);
|
||||||
|
let indices: Vec<Option<u16>> = (0..chunk_size.element_product())
|
||||||
|
.map(|_| rng.gen_range(0..5))
|
||||||
|
.map(|i| if i == 0 { None } else { Some(i - 1) })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
TilemapChunk {
|
||||||
|
chunk_size,
|
||||||
|
tile_display_size,
|
||||||
|
tileset: assets.load("textures/array_texture.png"),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
TilemapChunkIndices(indices),
|
||||||
|
UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
|
||||||
|
));
|
||||||
|
|
||||||
|
commands.spawn(Camera2d);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_tileset_image(
|
||||||
|
chunk_query: Single<&TilemapChunk>,
|
||||||
|
mut events: EventReader<AssetEvent<Image>>,
|
||||||
|
mut images: ResMut<Assets<Image>>,
|
||||||
|
) {
|
||||||
|
let chunk = *chunk_query;
|
||||||
|
for event in events.read() {
|
||||||
|
if event.is_loaded_with_dependencies(chunk.tileset.id()) {
|
||||||
|
let image = images.get_mut(&chunk.tileset).unwrap();
|
||||||
|
image.reinterpret_stacked_2d_as_array(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_tilemap(time: Res<Time>, mut query: Query<(&mut TilemapChunkIndices, &mut UpdateTimer)>) {
|
||||||
|
for (mut indices, mut timer) in query.iter_mut() {
|
||||||
|
timer.tick(time.delta());
|
||||||
|
|
||||||
|
if timer.just_finished() {
|
||||||
|
let mut rng = ChaCha8Rng::from_entropy();
|
||||||
|
for _ in 0..50 {
|
||||||
|
let index = rng.gen_range(0..indices.len());
|
||||||
|
indices[index] = Some(rng.gen_range(0..5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -129,6 +129,7 @@ Example | Description
|
|||||||
[Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid
|
[Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid
|
||||||
[Text 2D](../examples/2d/text2d.rs) | Generates text in 2D
|
[Text 2D](../examples/2d/text2d.rs) | Generates text in 2D
|
||||||
[Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites
|
[Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites
|
||||||
|
[Tilemap Chunk](../examples/2d/tilemap_chunk.rs) | Renders a tilemap chunk
|
||||||
[Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d
|
[Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d
|
||||||
|
|
||||||
## 3D Rendering
|
## 3D Rendering
|
||||||
|
25
release-content/release-notes/tilemap-chunk-rendering.md
Normal file
25
release-content/release-notes/tilemap-chunk-rendering.md
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
title: Tilemap Chunk Rendering
|
||||||
|
authors: ["@ConnerPetzold", "@grind086", "@IceSentry"]
|
||||||
|
pull_requests: [18866]
|
||||||
|
---
|
||||||
|
|
||||||
|
A performant way to render tilemap chunks has been added as the first building block to Bevy's tilemap support. You can render a chunk by supplying a tileset texture to the `TilemapChunk` component and the indices into that tileset for each tile to `TilemapChunkIndices`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let chunk_size = UVec2::splat(64);
|
||||||
|
let tile_size = UVec2::splat(16);
|
||||||
|
let indices: Vec<Option<u32>> = (0..chunk_size.x * chunk_size.y)
|
||||||
|
.map(|_| rng.gen_range(0..5))
|
||||||
|
.map(|i| if i == 0 { None } else { Some(i - 1) })
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
commands.spawn((
|
||||||
|
TilemapChunk {
|
||||||
|
chunk_size,
|
||||||
|
tile_size,
|
||||||
|
tileset,
|
||||||
|
},
|
||||||
|
TilemapChunkIndices(indices),
|
||||||
|
));
|
||||||
|
```
|
Loading…
Reference in New Issue
Block a user