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