diff --git a/Cargo.lock b/Cargo.lock index 27ec758d..c5f41451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + [[package]] name = "autocfg" version = "1.1.0" @@ -80,19 +89,53 @@ dependencies = [ [[package]] name = "crc32fast" -version = "1.3.2" +version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "either" version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.9" @@ -151,6 +194,12 @@ dependencies = [ "serde", ] +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "home" version = "0.5.9" @@ -172,6 +221,16 @@ dependencies = [ "png", ] +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "khronos_api" version = "3.1.0" @@ -202,12 +261,9 @@ checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.17" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "lua-src" @@ -230,9 +286,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "miniz_oxide" @@ -389,9 +445,23 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.159" +version = "1.0.204" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.204" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] [[package]] name = "simd-adler32" @@ -449,6 +519,7 @@ dependencies = [ "surreal-macros", "uuid", "winreg", + "zip", ] [[package]] @@ -485,7 +556,7 @@ version = "0.0.0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.107", ] [[package]] @@ -508,6 +579,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "unicode-ident" version = "1.0.0" @@ -720,3 +822,19 @@ name = "xml-rs" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b07db065a5cf61a7e4ba64f29e67db906fb1787316516c4e6e5ff0fea1efcd8a" + +[[package]] +name = "zip" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b895748a3ebcb69b9d38dcfdf21760859a4b0d0b0015277640c2ef4c69640e6f" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap", + "memchr", + "thiserror", +] diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 10510480..6d7e4023 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -12,6 +12,7 @@ rustc-hash = "1.1.0" smallvec = "1.8" uuid = { version = "1.8.0", features = ["serde"] } flate2 = "1.0.30" +zip = { version = "2.1.5", features = ["flate2"], default-features = false } # platform dependencies [target.'cfg(windows)'.dependencies] diff --git a/crates/graphics/src/sprites.rs b/crates/graphics/src/sprites.rs index 7ffea61f..f7625ed8 100644 --- a/crates/graphics/src/sprites.rs +++ b/crates/graphics/src/sprites.rs @@ -1,19 +1,18 @@ //! Sprite management and rendering. pub use aseprite::*; +pub use atlas::*; +pub use batch::*; pub use pyxel::*; use super::*; mod aseprite; +mod atlas; +mod batch; mod pyxel; -use common::{vec2, Angle, Color32, Mat2, Vec2}; - -/// The default number of sprites to allocate in a new batch. -const DEFAULT_SPRITE_COUNT: usize = 1024; - -/// A region of a texture that can be used to draw a sprite. +/// Represents something that can be drawn as a sprite. pub trait Sprite { /// Returns the texture region for this sprite. fn to_region(&self) -> TextureRegion; @@ -32,192 +31,3 @@ impl Sprite for TextureRegion { self.clone() } } - -/// Options for drawing a sprite. -pub struct SpriteOptions { - pub position: Vec2, - pub rotation: Angle, - pub scale: Vec2, - pub color: Color32, -} - -impl Default for SpriteOptions { - fn default() -> Self { - Self { - position: Vec2::ZERO, - rotation: Angle::ZERO, - scale: Vec2::ONE, - color: Color32::WHITE, - } - } -} - -/// A fast and lightweight sprite batch renderer. -/// -/// This batch pre-allocates an array of vertices and indices and re-uses them -/// for as many sprites as possible. -/// -/// Batching is possible over 1 material and for sprites of the same texture. -/// If you need to render sprites from multiple textures, you should use a -/// [`MultiSpriteBatch`] instead, otherwise the batch will flush prior to -/// rendering the sprites from the new texture. -pub struct SpriteBatch { - mesh: Mesh, - material: Option, - vertices: Vec, - last_texture: Option, -} - -/// A specialized vertex for use in our sprite batch. -#[repr(C)] -#[derive(Clone, Debug, Vertex)] -struct SpriteVertex { - #[vertex(2, F32)] - pub position: Vec2, - #[vertex(2, F32)] - pub uv: Vec2, - #[vertex(4, U8, normalize)] - pub color: Color32, -} - -impl SpriteBatch { - /// Constructs a new [`SpriteBatch`] with a default capacity. - pub fn new() -> Result { - Self::with_capacity(DEFAULT_SPRITE_COUNT) - } - - /// Creates a new [`SpriteBatch`] with the given expected capacity. - /// - /// This will pre-allocate buffers to minimize reallocation costs. - pub fn with_capacity(sprite_count: usize) -> Result { - // build standard quad indices ahead-of-time - let vertices = Vec::with_capacity(sprite_count * 4); - let indices = build_quad_indices(sprite_count); - - // create mesh, upload quad indices immediately - let mut mesh = Mesh::new(BufferUsage::Dynamic)?; - - mesh.with_buffers(|_, buffer| { - buffer.write_data(&indices); - }); - - Ok(Self { - mesh, - vertices, - material: None, - last_texture: None, - }) - } - - /// Starts a new batch run with the given `Material`. - pub fn begin(&mut self, material: &Material) { - self.material = Some(material.clone()); - self.vertices.clear(); - } - - /// Draws a single sprite texture to the batch with the given options. - pub fn draw_sprite(&mut self, sprite: &impl Sprite, options: &SpriteOptions) { - // flush if we've reached capacity - if self.vertices.len() + 4 >= self.vertices.capacity() { - self.flush(); - } - - // flush if the texture has changed - let region = sprite.to_region(); - if let Some(texture) = &self.last_texture { - if texture.id() != region.texture.id() { - self.flush(); - self.last_texture = Some(region.texture.clone()); - } - } else if self.last_texture.is_none() { - self.last_texture = Some(region.texture.clone()); - } - - let scale = vec2( - region.size.x as f32 * options.scale.x, - region.size.y as f32 * options.scale.y, - ); - - let angle = options.rotation; - let translation = options.position; - let transform = Mat2::from_scale_angle(scale, angle.into()); - let uv = region.calculate_uv(); - - // add vertices - self.vertices.push(SpriteVertex { - position: translation + transform * vec2(-0.5, -0.5), - color: options.color, - uv: uv.top_left(), - }); - - self.vertices.push(SpriteVertex { - position: translation + transform * vec2(-0.5, 0.5), - color: options.color, - uv: uv.bottom_left(), - }); - - self.vertices.push(SpriteVertex { - position: translation + transform * vec2(0.5, 0.5), - color: options.color, - uv: uv.bottom_right(), - }); - - self.vertices.push(SpriteVertex { - position: translation + transform * vec2(0.5, -0.5), - color: options.color, - uv: uv.top_right(), - }); - } - - /// Flushes the batch to the GPU. - pub fn flush(&mut self) { - if self.vertices.is_empty() { - return; // no vertices? no problem - } - - // fetch the material out - let material = &mut self.material; - if material.is_none() { - return; - } - let material = material.as_mut().unwrap(); - - // prepare to draw - let vertex_count = self.vertices.len(); - let sprite_count = vertex_count / 4; - let index_count = sprite_count * 6; - let mesh = &mut self.mesh; - - if let Some(texture) = &self.last_texture { - material.set_texture("u_texture", texture, None); - } - - // write vertices to mesh - mesh.with_buffers(|vertices, _| { - vertices.write_data(&self.vertices); - }); - - mesh.draw_sub(material, PrimitiveTopology::Triangles, vertex_count, index_count); - - self.vertices.clear(); - } -} - -/// Fills a new buffer with standard quad indices. -fn build_quad_indices(sprite_count: usize) -> Vec { - let mut indices = Vec::with_capacity(sprite_count * 6); - let mut index = 0; - - for _ in 0..sprite_count { - indices.push(index); - indices.push(index + 1); - indices.push(index + 2); - indices.push(index + 2); - indices.push(index + 3); - indices.push(index); - - index += 4; - } - - indices -} diff --git a/crates/graphics/src/sprites/atlas.rs b/crates/graphics/src/sprites/atlas.rs new file mode 100644 index 00000000..ac82725b --- /dev/null +++ b/crates/graphics/src/sprites/atlas.rs @@ -0,0 +1,8 @@ +//! A sprite atlas utility + +use super::*; + +/// An atlas of sprites. +pub struct SpriteAtlas { + texture: Texture, +} diff --git a/crates/graphics/src/sprites/batch.rs b/crates/graphics/src/sprites/batch.rs new file mode 100644 index 00000000..9fca6c79 --- /dev/null +++ b/crates/graphics/src/sprites/batch.rs @@ -0,0 +1,191 @@ +use common::{vec2, Angle, Color32, Mat2, Vec2}; + +use super::*; + +/// The default number of sprites to allocate in a new batch. +const DEFAULT_SPRITE_COUNT: usize = 1024; + +/// A fast and lightweight sprite batch renderer. +/// +/// This batch pre-allocates an array of vertices and indices and re-uses them +/// for as many sprites as possible. +/// +/// Batching is possible over 1 material and for sprites of the same texture. +pub struct SpriteBatch { + mesh: Mesh, + material: Option, + vertices: Vec, + last_texture: Option, +} + +/// A specialized vertex for use in our sprite batch. +#[repr(C)] +#[derive(Clone, Debug, Vertex)] +struct SpriteVertex { + #[vertex(2, F32)] + pub position: Vec2, + #[vertex(2, F32)] + pub uv: Vec2, + #[vertex(4, U8, normalize)] + pub color: Color32, +} + +/// Options for drawing a sprite. +pub struct SpriteOptions { + pub position: Vec2, + pub rotation: Angle, + pub scale: Vec2, + pub color: Color32, +} + +impl Default for SpriteOptions { + fn default() -> Self { + Self { + position: Vec2::ZERO, + rotation: Angle::ZERO, + scale: Vec2::ONE, + color: Color32::WHITE, + } + } +} + +impl SpriteBatch { + /// Constructs a new [`SpriteBatch`] with a default capacity. + pub fn new() -> Result { + Self::with_capacity(DEFAULT_SPRITE_COUNT) + } + + /// Creates a new [`SpriteBatch`] with the given expected capacity. + pub fn with_capacity(sprite_count: usize) -> Result { + // build standard quad indices ahead-of-time + let vertices = Vec::with_capacity(sprite_count * 4); + let indices = build_quad_indices(sprite_count); + + // create mesh, upload quad indices immediately + let mut mesh = Mesh::new(BufferUsage::Dynamic)?; + + mesh.with_buffers(|_, buffer| { + buffer.write_data(&indices); + }); + + Ok(Self { + mesh, + vertices, + material: None, + last_texture: None, + }) + } + + /// Starts a new batch run with the given [`Material`]. + pub fn begin(&mut self, material: &Material) { + self.material = Some(material.clone()); + self.vertices.clear(); + } + + /// Draws a single [`Sprite`] texture to the batch with the given + /// [`SpriteOptions`]. + pub fn draw_sprite(&mut self, sprite: &impl Sprite, options: &SpriteOptions) { + // flush if we've reached capacity + if self.vertices.len() + 4 >= self.vertices.capacity() { + self.flush(); + } + + // flush if the texture has changed + let region = sprite.to_region(); + if let Some(texture) = &self.last_texture { + if texture.id() != region.texture.id() { + self.flush(); + self.last_texture = Some(region.texture.clone()); + } + } else if self.last_texture.is_none() { + self.last_texture = Some(region.texture.clone()); + } + + let scale = vec2( + region.size.x as f32 * options.scale.x, + region.size.y as f32 * options.scale.y, + ); + + let angle = options.rotation; + let translation = options.position; + let transform = Mat2::from_scale_angle(scale, angle.into()); + let uv = region.calculate_uv(); + + // add vertices + self.vertices.push(SpriteVertex { + position: translation + transform * vec2(-0.5, -0.5), + color: options.color, + uv: uv.top_left(), + }); + + self.vertices.push(SpriteVertex { + position: translation + transform * vec2(-0.5, 0.5), + color: options.color, + uv: uv.bottom_left(), + }); + + self.vertices.push(SpriteVertex { + position: translation + transform * vec2(0.5, 0.5), + color: options.color, + uv: uv.bottom_right(), + }); + + self.vertices.push(SpriteVertex { + position: translation + transform * vec2(0.5, -0.5), + color: options.color, + uv: uv.top_right(), + }); + } + + /// Flushes the batch to the GPU. + pub fn flush(&mut self) { + if self.vertices.is_empty() { + return; // no vertices? no problem + } + + // fetch the material out + let material = &mut self.material; + if material.is_none() { + return; + } + let material = material.as_mut().unwrap(); + + // prepare to draw + let vertex_count = self.vertices.len(); + let sprite_count = vertex_count / 4; + let index_count = sprite_count * 6; + let mesh = &mut self.mesh; + + if let Some(texture) = &self.last_texture { + material.set_texture("u_texture", texture, None); + } + + // write vertices to mesh + mesh.with_buffers(|vertices, _| { + vertices.write_data(&self.vertices); + }); + + mesh.draw_sub(material, PrimitiveTopology::Triangles, vertex_count, index_count); + + self.vertices.clear(); + } +} + +/// Fills a new buffer with standard quad indices. +fn build_quad_indices(sprite_count: usize) -> Vec { + let mut indices = Vec::with_capacity(sprite_count * 6); + let mut index = 0; + + for _ in 0..sprite_count { + indices.push(index); + indices.push(index + 1); + indices.push(index + 2); + indices.push(index + 2); + indices.push(index + 3); + indices.push(index); + + index += 4; + } + + indices +}