cpu_draw/
cpu_draw.rs

1//! Example of how to draw to a texture from the CPU.
2//!
3//! You can set the values of individual pixels to whatever you want.
4//! Bevy provides user-friendly APIs that work with [`Color`](bevy::color::Color)
5//! values and automatically perform any necessary conversions and encoding
6//! into the texture's native pixel format.
7
8use bevy::color::{color_difference::EuclideanDistance, palettes::css};
9use bevy::prelude::*;
10use bevy::render::{
11    render_asset::RenderAssetUsages,
12    render_resource::{Extent3d, TextureDimension, TextureFormat},
13};
14use rand::{Rng, SeedableRng};
15use rand_chacha::ChaCha8Rng;
16
17const IMAGE_WIDTH: u32 = 256;
18const IMAGE_HEIGHT: u32 = 256;
19
20fn main() {
21    App::new()
22        .add_plugins(DefaultPlugins)
23        // In this example, we will use a fixed timestep to draw a pattern on the screen
24        // one pixel at a time, so the pattern will gradually emerge over time, and
25        // the speed at which it appears is not tied to the framerate.
26        // Let's make the fixed update very fast, so it doesn't take too long. :)
27        .insert_resource(Time::<Fixed>::from_hz(1024.0))
28        .add_systems(Startup, setup)
29        .add_systems(FixedUpdate, draw)
30        .run();
31}
32
33/// Store the image handle that we will draw to, here.
34#[derive(Resource)]
35struct MyProcGenImage(Handle<Image>);
36
37#[derive(Resource)]
38struct SeededRng(ChaCha8Rng);
39
40fn setup(mut commands: Commands, mut images: ResMut<Assets<Image>>) {
41    commands.spawn(Camera2d);
42
43    // Create an image that we are going to draw into
44    let mut image = Image::new_fill(
45        // 2D image of size 256x256
46        Extent3d {
47            width: IMAGE_WIDTH,
48            height: IMAGE_HEIGHT,
49            depth_or_array_layers: 1,
50        },
51        TextureDimension::D2,
52        // Initialize it with a beige color
53        &(css::BEIGE.to_u8_array()),
54        // Use the same encoding as the color we set
55        TextureFormat::Rgba8UnormSrgb,
56        RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
57    );
58
59    // To make it extra fancy, we can set the Alpha of each pixel,
60    // so that it fades out in a circular fashion.
61    for y in 0..IMAGE_HEIGHT {
62        for x in 0..IMAGE_WIDTH {
63            let center = Vec2::new(IMAGE_WIDTH as f32 / 2.0, IMAGE_HEIGHT as f32 / 2.0);
64            let max_radius = IMAGE_HEIGHT.min(IMAGE_WIDTH) as f32 / 2.0;
65            let r = Vec2::new(x as f32, y as f32).distance(center);
66            let a = 1.0 - (r / max_radius).clamp(0.0, 1.0);
67
68            // Here we will set the A value by accessing the raw data bytes.
69            // (it is the 4th byte of each pixel, as per our `TextureFormat`)
70
71            // Find our pixel by its coordinates
72            let pixel_bytes = image.pixel_bytes_mut(UVec3::new(x, y, 0)).unwrap();
73            // Convert our f32 to u8
74            pixel_bytes[3] = (a * u8::MAX as f32) as u8;
75        }
76    }
77
78    // Add it to Bevy's assets, so it can be used for rendering
79    // this will give us a handle we can use
80    // (to display it in a sprite, or as part of UI, etc.)
81    let handle = images.add(image);
82
83    // Create a sprite entity using our image
84    commands.spawn(Sprite::from_image(handle.clone()));
85    commands.insert_resource(MyProcGenImage(handle));
86
87    // We're seeding the PRNG here to make this example deterministic for testing purposes.
88    // This isn't strictly required in practical use unless you need your app to be deterministic.
89    let seeded_rng = ChaCha8Rng::seed_from_u64(19878367467712);
90    commands.insert_resource(SeededRng(seeded_rng));
91}
92
93/// Every fixed update tick, draw one more pixel to make a spiral pattern
94fn draw(
95    my_handle: Res<MyProcGenImage>,
96    mut images: ResMut<Assets<Image>>,
97    // Used to keep track of where we are
98    mut i: Local<u32>,
99    mut draw_color: Local<Color>,
100    mut seeded_rng: ResMut<SeededRng>,
101) {
102    if *i == 0 {
103        // Generate a random color on first run.
104        *draw_color = Color::linear_rgb(
105            seeded_rng.0.r#gen(),
106            seeded_rng.0.r#gen(),
107            seeded_rng.0.r#gen(),
108        );
109    }
110
111    // Get the image from Bevy's asset storage.
112    let image = images.get_mut(&my_handle.0).expect("Image not found");
113
114    // Compute the position of the pixel to draw.
115
116    let center = Vec2::new(IMAGE_WIDTH as f32 / 2.0, IMAGE_HEIGHT as f32 / 2.0);
117    let max_radius = IMAGE_HEIGHT.min(IMAGE_WIDTH) as f32 / 2.0;
118    let rot_speed = 0.0123;
119    let period = 0.12345;
120
121    let r = ops::sin(*i as f32 * period) * max_radius;
122    let xy = Vec2::from_angle(*i as f32 * rot_speed) * r + center;
123    let (x, y) = (xy.x as u32, xy.y as u32);
124
125    // Get the old color of that pixel.
126    let old_color = image.get_color_at(x, y).unwrap();
127
128    // If the old color is our current color, change our drawing color.
129    let tolerance = 1.0 / 255.0;
130    if old_color.distance(&draw_color) <= tolerance {
131        *draw_color = Color::linear_rgb(
132            seeded_rng.0.r#gen(),
133            seeded_rng.0.r#gen(),
134            seeded_rng.0.r#gen(),
135        );
136    }
137
138    // Set the new color, but keep old alpha value from image.
139    image
140        .set_color_at(x, y, draw_color.with_alpha(old_color.alpha()))
141        .unwrap();
142
143    *i += 1;
144}