From 31409ebc61f1d03680822c33d68ee98954b76375 Mon Sep 17 00:00:00 2001 From: "Ida \"Iyes" <40234599+inodentry@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:38:41 +0300 Subject: [PATCH] Add `Image` methods for easy access to a pixel's color (#10392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective If you want to draw / generate images from the CPU, such as: - to create procedurally-generated assets - for games whose artstyle is best implemented by poking pixels directly from the CPU, instead of using shaders It is currently very unergonomic to do in Bevy, because you have to deal with the raw bytes inside `image.data`, take care of the pixel format, etc. ## Solution This PR adds some helper methods to `Image` for pixel manipulation. These methods allow you to use Bevy's user-friendly `Color` struct to read and write the colors of pixels, at arbitrary coordinates (specified as `UVec3` to support any texture dimension). They handle encoding/decoding to the `Image`s `TextureFormat`, incl. any sRGB conversion. While we are at it, also add methods to help with direct access to the raw bytes. It is now easy to compute the offset where the bytes of a specific pixel coordinate are found, or to just get a Rust slice to access them. Caveat: `Color` roundtrips are obviously going to be lossy for non-float `TextureFormat`s. Using `set_color_at` followed by `get_color_at` will return a different value, due to the data conversions involved (such as `f32` -> `u8` -> `f32` for the common `Rgba8UnormSrgb` texture format). Be careful when comparing colors (such as checking for a color you wrote before)! Also adding a new example: `cpu_draw` (under `2d`), to showcase these new APIs. --- ## Changelog ### Added - `Image` APIs for easy access to the colors of specific pixels. --------- Co-authored-by: Pascal Hertleif Co-authored-by: François Co-authored-by: ltdk --- Cargo.toml | 11 + crates/bevy_image/src/image.rs | 453 ++++++++++++++++++++++++++++++++- examples/2d/cpu_draw.rs | 133 ++++++++++ examples/README.md | 1 + 4 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 examples/2d/cpu_draw.rs diff --git a/Cargo.toml b/Cargo.toml index 563cc9bb20644..15bc3e8b7c0de 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -578,6 +578,17 @@ description = "Renders a glTF mesh in 2D with a custom vertex attribute" category = "2D Rendering" wasm = true +[[example]] +name = "cpu_draw" +path = "examples/2d/cpu_draw.rs" +doc-scrape-examples = true + +[package.metadata.example.cpu_draw] +name = "CPU Drawing" +description = "Manually read/write the pixels of a texture" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite" path = "examples/2d/sprite.rs" diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index ad0b046e28cb4..6965f55ddec53 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -6,9 +6,11 @@ use super::dds::*; use super::ktx2::*; use bevy_asset::{Asset, RenderAssetUsages}; -use bevy_math::{AspectRatio, UVec2, Vec2}; -use bevy_reflect::prelude::*; +use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza}; +use bevy_math::{AspectRatio, UVec2, UVec3, Vec2}; +use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; +use core::hash::Hash; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor}; @@ -817,6 +819,442 @@ impl Image { .required_features() .contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) } + + /// Compute the byte offset where the data of a specific pixel is stored + /// + /// Returns None if the provided coordinates are out of bounds. + /// + /// For 2D textures, Z is ignored. For 1D textures, Y and Z are ignored. + #[inline(always)] + pub fn pixel_data_offset(&self, coords: UVec3) -> Option { + let width = self.texture_descriptor.size.width; + let height = self.texture_descriptor.size.height; + let depth = self.texture_descriptor.size.depth_or_array_layers; + + let pixel_size = self.texture_descriptor.format.pixel_size(); + let pixel_offset = match self.texture_descriptor.dimension { + TextureDimension::D3 => { + if coords.x > width || coords.y > height || coords.z > depth { + return None; + } + coords.z * height * width + coords.y * width + coords.x + } + TextureDimension::D2 => { + if coords.x > width || coords.y > height { + return None; + } + coords.y * width + coords.x + } + TextureDimension::D1 => { + if coords.x > width { + return None; + } + coords.x + } + }; + + Some(pixel_offset as usize * pixel_size) + } + + /// Get a reference to the data bytes where a specific pixel's value is stored + #[inline(always)] + pub fn pixel_bytes(&self, coords: UVec3) -> Option<&[u8]> { + let len = self.texture_descriptor.format.pixel_size(); + self.pixel_data_offset(coords) + .map(|start| &self.data[start..(start + len)]) + } + + /// Get a mutable reference to the data bytes where a specific pixel's value is stored + #[inline(always)] + pub fn pixel_bytes_mut(&mut self, coords: UVec3) -> Option<&mut [u8]> { + let len = self.texture_descriptor.format.pixel_size(); + self.pixel_data_offset(coords) + .map(|start| &mut self.data[start..(start + len)]) + } + + /// Read the color of a specific pixel (1D texture). + /// + /// See [`get_color_at`](Self::get_color_at) for more details. + #[inline(always)] + pub fn get_color_at_1d(&self, x: u32) -> Result { + if self.texture_descriptor.dimension != TextureDimension::D1 { + return Err(TextureAccessError::WrongDimension); + } + self.get_color_at_internal(UVec3::new(x, 0, 0)) + } + + /// Read the color of a specific pixel (2D texture). + /// + /// This function will find the raw byte data of a specific pixel and + /// decode it into a user-friendly [`Color`] struct for you. + /// + /// Supports many of the common [`TextureFormat`]s: + /// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear + /// - 16-bit and 32-bit unsigned integer + /// - 32-bit float + /// + /// Be careful: as the data is converted to [`Color`] (which uses `f32` internally), + /// there may be issues with precision when using non-float [`TextureFormat`]s. + /// If you read a value you previously wrote using `set_color_at`, it will not match. + /// If you are working with a 32-bit integer [`TextureFormat`], the value will be + /// inaccurate (as `f32` does not have enough bits to represent it exactly). + /// + /// Single channel (R) formats are assumed to represent greyscale, so the value + /// will be copied to all three RGB channels in the resulting [`Color`]. + /// + /// Other [`TextureFormat`]s are unsupported, such as: + /// - block-compressed formats + /// - non-byte-aligned formats like 10-bit + /// - 16-bit float formats + /// - signed integer formats + #[inline(always)] + pub fn get_color_at(&self, x: u32, y: u32) -> Result { + if self.texture_descriptor.dimension != TextureDimension::D2 { + return Err(TextureAccessError::WrongDimension); + } + self.get_color_at_internal(UVec3::new(x, y, 0)) + } + + /// Read the color of a specific pixel (3D texture). + /// + /// See [`get_color_at`](Self::get_color_at) for more details. + #[inline(always)] + pub fn get_color_at_3d(&self, x: u32, y: u32, z: u32) -> Result { + if self.texture_descriptor.dimension != TextureDimension::D3 { + return Err(TextureAccessError::WrongDimension); + } + self.get_color_at_internal(UVec3::new(x, y, z)) + } + + /// Change the color of a specific pixel (1D texture). + /// + /// See [`set_color_at`](Self::set_color_at) for more details. + #[inline(always)] + pub fn set_color_at_1d(&mut self, x: u32, color: Color) -> Result<(), TextureAccessError> { + if self.texture_descriptor.dimension != TextureDimension::D1 { + return Err(TextureAccessError::WrongDimension); + } + self.set_color_at_internal(UVec3::new(x, 0, 0), color) + } + + /// Change the color of a specific pixel (2D texture). + /// + /// This function will find the raw byte data of a specific pixel and + /// change it according to a [`Color`] you provide. The [`Color`] struct + /// will be encoded into the [`Image`]'s [`TextureFormat`]. + /// + /// Supports many of the common [`TextureFormat`]s: + /// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear + /// - 16-bit and 32-bit unsigned integer (with possibly-limited precision, as [`Color`] uses `f32`) + /// - 32-bit float + /// + /// Be careful: writing to non-float [`TextureFormat`]s is lossy! The data has to be converted, + /// so if you read it back using `get_color_at`, the `Color` you get will not equal the value + /// you used when writing it using this function. + /// + /// For R and RG formats, only the respective values from the linear RGB [`Color`] will be used. + /// + /// Other [`TextureFormat`]s are unsupported, such as: + /// - block-compressed formats + /// - non-byte-aligned formats like 10-bit + /// - 16-bit float formats + /// - signed integer formats + #[inline(always)] + pub fn set_color_at(&mut self, x: u32, y: u32, color: Color) -> Result<(), TextureAccessError> { + if self.texture_descriptor.dimension != TextureDimension::D2 { + return Err(TextureAccessError::WrongDimension); + } + self.set_color_at_internal(UVec3::new(x, y, 0), color) + } + + /// Change the color of a specific pixel (3D texture). + /// + /// See [`set_color_at`](Self::set_color_at) for more details. + #[inline(always)] + pub fn set_color_at_3d( + &mut self, + x: u32, + y: u32, + z: u32, + color: Color, + ) -> Result<(), TextureAccessError> { + if self.texture_descriptor.dimension != TextureDimension::D3 { + return Err(TextureAccessError::WrongDimension); + } + self.set_color_at_internal(UVec3::new(x, y, z), color) + } + + #[inline(always)] + fn get_color_at_internal(&self, coords: UVec3) -> Result { + let Some(bytes) = self.pixel_bytes(coords) else { + return Err(TextureAccessError::OutOfBounds { + x: coords.x, + y: coords.y, + z: coords.z, + }); + }; + + // NOTE: GPUs are always Little Endian. + // Make sure to respect that when we create color values from bytes. + match self.texture_descriptor.format { + TextureFormat::Rgba8UnormSrgb => Ok(Color::srgba( + bytes[0] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[2] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => Ok(Color::linear_rgba( + bytes[0] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[2] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Bgra8UnormSrgb => Ok(Color::srgba( + bytes[2] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[0] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Bgra8Unorm => Ok(Color::linear_rgba( + bytes[2] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[0] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Rgba32Float => Ok(Color::linear_rgba( + f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), + f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), + )), + TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => { + let (r, g, b, a) = ( + u16::from_le_bytes([bytes[0], bytes[1]]), + u16::from_le_bytes([bytes[2], bytes[3]]), + u16::from_le_bytes([bytes[4], bytes[5]]), + u16::from_le_bytes([bytes[6], bytes[7]]), + ); + Ok(Color::linear_rgba( + // going via f64 to avoid rounding errors with large numbers and division + (r as f64 / u16::MAX as f64) as f32, + (g as f64 / u16::MAX as f64) as f32, + (b as f64 / u16::MAX as f64) as f32, + (a as f64 / u16::MAX as f64) as f32, + )) + } + TextureFormat::Rgba32Uint => { + let (r, g, b, a) = ( + u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), + u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), + ); + Ok(Color::linear_rgba( + // going via f64 to avoid rounding errors with large numbers and division + (r as f64 / u32::MAX as f64) as f32, + (g as f64 / u32::MAX as f64) as f32, + (b as f64 / u32::MAX as f64) as f32, + (a as f64 / u32::MAX as f64) as f32, + )) + } + // assume R-only texture format means grayscale (linear) + // copy value to all of RGB in Color + TextureFormat::R8Unorm | TextureFormat::R8Uint => { + let x = bytes[0] as f32 / u8::MAX as f32; + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::R16Unorm | TextureFormat::R16Uint => { + let x = u16::from_le_bytes([bytes[0], bytes[1]]); + // going via f64 to avoid rounding errors with large numbers and division + let x = (x as f64 / u16::MAX as f64) as f32; + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::R32Uint => { + let x = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + // going via f64 to avoid rounding errors with large numbers and division + let x = (x as f64 / u32::MAX as f64) as f32; + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::R32Float => { + let x = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => { + let r = bytes[0] as f32 / u8::MAX as f32; + let g = bytes[1] as f32 / u8::MAX as f32; + Ok(Color::linear_rgb(r, g, 0.0)) + } + TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => { + let r = u16::from_le_bytes([bytes[0], bytes[1]]); + let g = u16::from_le_bytes([bytes[2], bytes[3]]); + // going via f64 to avoid rounding errors with large numbers and division + let r = (r as f64 / u16::MAX as f64) as f32; + let g = (g as f64 / u16::MAX as f64) as f32; + Ok(Color::linear_rgb(r, g, 0.0)) + } + TextureFormat::Rg32Uint => { + let r = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let g = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + // going via f64 to avoid rounding errors with large numbers and division + let r = (r as f64 / u32::MAX as f64) as f32; + let g = (g as f64 / u32::MAX as f64) as f32; + Ok(Color::linear_rgb(r, g, 0.0)) + } + TextureFormat::Rg32Float => { + let r = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let g = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + Ok(Color::linear_rgb(r, g, 0.0)) + } + _ => Err(TextureAccessError::UnsupportedTextureFormat( + self.texture_descriptor.format, + )), + } + } + + #[inline(always)] + fn set_color_at_internal( + &mut self, + coords: UVec3, + color: Color, + ) -> Result<(), TextureAccessError> { + let format = self.texture_descriptor.format; + + let Some(bytes) = self.pixel_bytes_mut(coords) else { + return Err(TextureAccessError::OutOfBounds { + x: coords.x, + y: coords.y, + z: coords.z, + }); + }; + + // NOTE: GPUs are always Little Endian. + // Make sure to respect that when we convert color values to bytes. + match format { + TextureFormat::Rgba8UnormSrgb => { + let [r, g, b, a] = Srgba::from(color).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (b * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (b * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Bgra8UnormSrgb => { + let [r, g, b, a] = Srgba::from(color).to_f32_array(); + bytes[0] = (b * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (r * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Bgra8Unorm => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + bytes[0] = (b * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (r * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Rgba32Float => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + bytes[0..4].copy_from_slice(&f32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&f32::to_le_bytes(g)); + bytes[8..12].copy_from_slice(&f32::to_le_bytes(b)); + bytes[12..16].copy_from_slice(&f32::to_le_bytes(a)); + } + TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + let [r, g, b, a] = [ + (r * u16::MAX as f32) as u16, + (g * u16::MAX as f32) as u16, + (b * u16::MAX as f32) as u16, + (a * u16::MAX as f32) as u16, + ]; + bytes[0..2].copy_from_slice(&u16::to_le_bytes(r)); + bytes[2..4].copy_from_slice(&u16::to_le_bytes(g)); + bytes[4..6].copy_from_slice(&u16::to_le_bytes(b)); + bytes[6..8].copy_from_slice(&u16::to_le_bytes(a)); + } + TextureFormat::Rgba32Uint => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + let [r, g, b, a] = [ + (r * u32::MAX as f32) as u32, + (g * u32::MAX as f32) as u32, + (b * u32::MAX as f32) as u32, + (a * u32::MAX as f32) as u32, + ]; + bytes[0..4].copy_from_slice(&u32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&u32::to_le_bytes(g)); + bytes[8..12].copy_from_slice(&u32::to_le_bytes(b)); + bytes[12..16].copy_from_slice(&u32::to_le_bytes(a)); + } + TextureFormat::R8Unorm | TextureFormat::R8Uint => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + } + TextureFormat::R16Unorm | TextureFormat::R16Uint => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + let r = (r * u16::MAX as f32) as u16; + bytes[0..2].copy_from_slice(&u16::to_le_bytes(r)); + } + TextureFormat::R32Uint => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + // go via f64 to avoid imprecision + let r = (r as f64 * u32::MAX as f64) as u32; + bytes[0..4].copy_from_slice(&u32::to_le_bytes(r)); + } + TextureFormat::R32Float => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + bytes[0..4].copy_from_slice(&f32::to_le_bytes(r)); + } + TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + } + TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + let r = (r * u16::MAX as f32) as u16; + let g = (g * u16::MAX as f32) as u16; + bytes[0..2].copy_from_slice(&u16::to_le_bytes(r)); + bytes[2..4].copy_from_slice(&u16::to_le_bytes(g)); + } + TextureFormat::Rg32Uint => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + // go via f64 to avoid imprecision + let r = (r as f64 * u32::MAX as f64) as u32; + let g = (g as f64 * u32::MAX as f64) as u32; + bytes[0..4].copy_from_slice(&u32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&u32::to_le_bytes(g)); + } + TextureFormat::Rg32Float => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + bytes[0..4].copy_from_slice(&f32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&f32::to_le_bytes(g)); + } + _ => { + return Err(TextureAccessError::UnsupportedTextureFormat( + self.texture_descriptor.format, + )); + } + } + Ok(()) + } } #[derive(Clone, Copy, Debug)] @@ -840,6 +1278,17 @@ pub enum TranscodeFormat { Rgb8, } +/// An error that occurs when accessing specific pixels in a texture +#[derive(Error, Debug)] +pub enum TextureAccessError { + #[error("out of bounds (x: {x}, y: {y}, z: {z})")] + OutOfBounds { x: u32, y: u32, z: u32 }, + #[error("unsupported texture format: {0:?}")] + UnsupportedTextureFormat(TextureFormat), + #[error("attempt to access texture with different dimension")] + WrongDimension, +} + /// An error that occurs when loading a texture #[derive(Error, Debug)] pub enum TextureError { diff --git a/examples/2d/cpu_draw.rs b/examples/2d/cpu_draw.rs new file mode 100644 index 0000000000000..0fa6e81a62bcf --- /dev/null +++ b/examples/2d/cpu_draw.rs @@ -0,0 +1,133 @@ +//! Example of how to draw to a texture from the CPU. +//! +//! You can set the values of individual pixels to whatever you want. +//! Bevy provides user-friendly APIs that work with [`Color`](bevy::color::Color) +//! values and automatically perform any necessary conversions and encoding +//! into the texture's native pixel format. + +use bevy::color::{color_difference::EuclideanDistance, palettes::css}; +use bevy::prelude::*; +use bevy::render::{ + render_asset::RenderAssetUsages, + render_resource::{Extent3d, TextureDimension, TextureFormat}, +}; +use rand::Rng; + +const IMAGE_WIDTH: u32 = 256; +const IMAGE_HEIGHT: u32 = 256; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // In this example, we will use a fixed timestep to draw a pattern on the screen + // one pixel at a time, so the pattern will gradually emerge over time, and + // the speed at which it appears is not tied to the framerate. + // Let's make the fixed update very fast, so it doesn't take too long. :) + .insert_resource(Time::::from_hz(1024.0)) + .add_systems(Startup, setup) + .add_systems(FixedUpdate, draw) + .run(); +} + +/// Store the image handle that we will draw to, here. +#[derive(Resource)] +struct MyProcGenImage(Handle); + +fn setup(mut commands: Commands, mut images: ResMut>) { + // spawn a camera + commands.spawn(Camera2d); + + // create an image that we are going to draw into + let mut image = Image::new_fill( + // 2D image of size 256x256 + Extent3d { + width: IMAGE_WIDTH, + height: IMAGE_HEIGHT, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + // Initialize it with a beige color + &(css::BEIGE.to_u8_array()), + // Use the same encoding as the color we set + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, + ); + + // to make it extra fancy, we can set the Alpha of each pixel + // so that it fades out in a circular fashion + for y in 0..IMAGE_HEIGHT { + for x in 0..IMAGE_WIDTH { + let center = Vec2::new(IMAGE_WIDTH as f32 / 2.0, IMAGE_HEIGHT as f32 / 2.0); + let max_radius = IMAGE_HEIGHT.min(IMAGE_WIDTH) as f32 / 2.0; + let r = Vec2::new(x as f32, y as f32).distance(center); + let a = 1.0 - (r / max_radius).clamp(0.0, 1.0); + + // here we will set the A value by accessing the raw data bytes + // (it is the 4th byte of each pixel, as per our `TextureFormat`) + + // find our pixel by its coordinates + let pixel_bytes = image.pixel_bytes_mut(UVec3::new(x, y, 0)).unwrap(); + // convert our f32 to u8 + pixel_bytes[3] = (a * u8::MAX as f32) as u8; + } + } + + // add it to Bevy's assets, so it can be used for rendering + // this will give us a handle we can use + // (to display it in a sprite, or as part of UI, etc.) + let handle = images.add(image); + + // create a sprite entity using our image + commands.spawn(SpriteBundle { + texture: handle.clone(), + ..Default::default() + }); + + commands.insert_resource(MyProcGenImage(handle)); +} + +/// Every fixed update tick, draw one more pixel to make a spiral pattern +fn draw( + my_handle: Res, + mut images: ResMut>, + // used to keep track of where we are + mut i: Local, + mut draw_color: Local, +) { + let mut rng = rand::thread_rng(); + + if *i == 0 { + // Generate a random color on first run. + *draw_color = Color::linear_rgb(rng.gen(), rng.gen(), rng.gen()); + } + + // Get the image from Bevy's asset storage. + let image = images.get_mut(&my_handle.0).expect("Image not found"); + + // Compute the position of the pixel to draw. + + let center = Vec2::new(IMAGE_WIDTH as f32 / 2.0, IMAGE_HEIGHT as f32 / 2.0); + let max_radius = IMAGE_HEIGHT.min(IMAGE_WIDTH) as f32 / 2.0; + let rot_speed = 0.0123; + let period = 0.12345; + + let r = ops::sin(*i as f32 * period) * max_radius; + let xy = Vec2::from_angle(*i as f32 * rot_speed) * r + center; + let (x, y) = (xy.x as u32, xy.y as u32); + + // Get the old color of that pixel. + let old_color = image.get_color_at(x, y).unwrap(); + + // If the old color is our current color, change our drawing color. + let tolerance = 1.0 / 255.0; + if old_color.distance(&draw_color) <= tolerance { + *draw_color = Color::linear_rgb(rng.gen(), rng.gen(), rng.gen()); + } + + // Set the new color, but keep old alpha value from image. + image + .set_color_at(x, y, draw_color.with_alpha(old_color.alpha())) + .unwrap(); + + *i += 1; +} diff --git a/examples/README.md b/examples/README.md index 7754978c9307d..a195a0cf44bbc 100644 --- a/examples/README.md +++ b/examples/README.md @@ -109,6 +109,7 @@ Example | Description [2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method [2D Wireframe](../examples/2d/wireframe_2d.rs) | Showcases wireframes for 2d meshes [Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of the circular segment and sector primitives +[CPU Drawing](../examples/2d/cpu_draw.rs) | Manually read/write the pixels of a texture [Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh