From e688de5204873329439c9d9232d633481af9a3ee Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 19 Jul 2026 19:45:44 -0400 Subject: [PATCH] Add image accumulation entry point I was hoping to implement the image filter/reduction step as well, but that seems to be meaningfully more complex --- enkou-shaders-tests/src/lib.rs | 5 +- enkou-shaders/examples/gasket.rs | 6 +- enkou-shaders/src/camera.rs | 49 ++---------- enkou-shaders/src/chaos_game.rs | 5 +- enkou-shaders/src/entry/image_accumulate.rs | 63 ++++++++++++++++ enkou-shaders/src/entry/mod.rs | 4 + enkou-shaders/src/image.rs | 84 ++++++++++++++++++++- enkou-shaders/src/lib.rs | 11 ++- enkou-shaders/src/transform.rs | 15 ++-- 9 files changed, 182 insertions(+), 60 deletions(-) create mode 100644 enkou-shaders/src/entry/image_accumulate.rs create mode 100644 enkou-shaders/src/entry/mod.rs diff --git a/enkou-shaders-tests/src/lib.rs b/enkou-shaders-tests/src/lib.rs index c17dbf6..02eafc6 100644 --- a/enkou-shaders-tests/src/lib.rs +++ b/enkou-shaders-tests/src/lib.rs @@ -54,6 +54,9 @@ mod test { #[test] fn has_entry_main_camera() { - assert!(has_entry_point(ExecutionModel::GLCompute, "main_camera")) + assert!(has_entry_point( + ExecutionModel::GLCompute, + "main_image_accumulate" + )) } } diff --git a/enkou-shaders/examples/gasket.rs b/enkou-shaders/examples/gasket.rs index c7c8041..9ac1790 100644 --- a/enkou-shaders/examples/gasket.rs +++ b/enkou-shaders/examples/gasket.rs @@ -2,6 +2,7 @@ use anyhow::{Context, Result}; use enkou_shaders::Coefficients2; use enkou_shaders::camera::Camera; use enkou_shaders::chaos_game::ChaosGame; +use enkou_shaders::image::{BlendMode, ImageSettings}; use enkou_shaders::transform::Transform; use enkou_shaders::variation::Variation; use glam::{Affine2, UVec2, Vec2, uvec2, vec2}; @@ -50,6 +51,8 @@ pub fn main() -> Result<()> { IMAGE_DIMENSION.as_vec2(), ); + let image_settings = ImageSettings::new(BlendMode::Linear, IMAGE_DIMENSION); + let mut image = GrayImage::new(IMAGE_DIMENSION.x, IMAGE_DIMENSION.y); let chaos_game = ChaosGame::new(&mut rng, &transforms, &weights, &variations); @@ -57,7 +60,8 @@ pub fn main() -> Result<()> { chaos_game .skip(ITERATIONS_DISCARD) .take(ITERATIONS) - .filter_map(|(point_ifs, _)| camera.transform_point_to_image(point_ifs)) + .map(|(point_ifs, _)| camera.transform_point(point_ifs)) + .filter_map(|point_pixel| image_settings.transform_point_to_image(point_pixel)) .for_each(|point_pixel| image.put_pixel(point_pixel.x, point_pixel.y, Luma([255]))); let temp = NamedTempFile::with_suffix(".png").context("Unable to create file for image")?; diff --git a/enkou-shaders/src/camera.rs b/enkou-shaders/src/camera.rs index fda29f1..83b82e9 100644 --- a/enkou-shaders/src/camera.rs +++ b/enkou-shaders/src/camera.rs @@ -12,7 +12,6 @@ use libm::powf; #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct Camera { - dimensions: UVec2, transform: Affine2, } @@ -23,7 +22,7 @@ impl Camera { /// to express the transform steps individually. /// /// # Arguments - /// + /// * `blend_mode` - Color blending mode for the output image /// * `dimensions` - Width and height of the output image (in pixels). /// * `center` - Location of the origin in IFS coordinates. Positive `x` shifts the image /// left, and positive `y` position shifts the image up. @@ -47,10 +46,7 @@ impl Camera { * zoom_transform * ifs_center_transform; - Camera { - dimensions, - transform, - } + Camera { transform } } /// Map a point from IFS coordinates to pixel coordinates. @@ -73,40 +69,6 @@ impl Camera { pub fn transform_point(&self, point: Vec2) -> IVec2 { self.transform.transform_point2(point).as_ivec2() } - - /// Map a point from IFS coordinates to pixel coordinates (like [`transform_point`](Camera::transform_point)), - /// and check that the result is within the provided image dimensions. - pub fn transform_point_to_image(&self, point: Vec2) -> Option { - let pixel_coordinates = self.transform_point(point); - if pixel_coordinates.x < 0 - || pixel_coordinates.y < 0 - || (pixel_coordinates.x as u32) >= self.dimensions.x - || (pixel_coordinates.y as u32) >= self.dimensions.y - { - None - } else { - Some(pixel_coordinates.as_uvec2()) - } - } -} - -/// Shader entry point for running the camera transformation over a list of IFS coordinates -pub mod entry { - use crate::camera::Camera; - use spirv_std::glam::{IVec2, Vec2}; - use spirv_std::spirv; - - /// Transform IFS coordinates to pixel coordinates - #[spirv(compute(entry_point_name = "main_camera", threads(1)))] - pub fn main_camera( - #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] camera: &Camera, - #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] coordinates_ifs: &[Vec2], - #[spirv(storage_buffer, descriptor_set = 1, binding = 0)] coordinates_pixel: &mut [IVec2], - ) { - for i in 0..coordinates_ifs.len() { - coordinates_pixel[i] = camera.transform_point(coordinates_ifs[i]) - } - } } #[cfg(test)] @@ -148,9 +110,10 @@ mod test { // The camera is implemented by composing affine transforms, // which ends up with a slightly different result because of rounding. - let error = camera.transform_point(starting_point) - point; - assert!(error.x.abs() <= 1); - assert!(error.y.abs() <= 1); + let error = (camera.transform_point(starting_point) - point) + .abs() + .as_uvec2(); + assert!(error.x <= 1 && error.y <= 1); } #[test] diff --git a/enkou-shaders/src/chaos_game.rs b/enkou-shaders/src/chaos_game.rs index 0a875d7..5f50e18 100644 --- a/enkou-shaders/src/chaos_game.rs +++ b/enkou-shaders/src/chaos_game.rs @@ -83,10 +83,7 @@ impl<'a, R: Rng> ChaosGame<'a, R> { weights: &'a [f32], variations: &'a [Variation], ) -> Self { - let current_point = vec2( - rng.sample(BiUnit), - rng.sample(BiUnit), - ); + let current_point = vec2(rng.sample(BiUnit), rng.sample(BiUnit)); let current_color = rng.sample(StandardUniform); ChaosGame { current_point, diff --git a/enkou-shaders/src/entry/image_accumulate.rs b/enkou-shaders/src/entry/image_accumulate.rs new file mode 100644 index 0000000..c38f538 --- /dev/null +++ b/enkou-shaders/src/entry/image_accumulate.rs @@ -0,0 +1,63 @@ +//! Image Accumulate + +use crate::camera::Camera; +use crate::chaos_game::ChaosGame; +use crate::image::ImageSettings; +use crate::rng::xoshiro256starstar_from_seed; +use crate::transform::Transform; +use crate::variation::Variation; +use glam::{UVec2, Vec4}; +use spirv_std::spirv; + +/// Run the chaos game and accumulate points into the output image buffer +/// +/// # Arguments +/// * `iterations` - Controls the iteration count; the first `x` iterations are discarded, +/// the next `y` iterations are accumulated into the output image +/// * `rng_seed` +/// * `transforms` +/// * `weights` +/// * `variations` +/// * `camera` - Camera transformation to map IFS coordinates to pixel coordinates +/// * `image_settings` - Settings to use for image accumulation +/// * `palette` - List of colors to use for the image palette; assumed to be RGB values scaled to `[0-255]`, with an alpha of 255 +/// * `image` - Output image buffer +#[spirv(compute(entry_point_name = "main_image_accumulate", threads(1)))] +pub fn main_image_accumulate( + #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] iterations: &UVec2, + #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] rng_seed: &[u8], + #[spirv(storage_buffer, descriptor_set = 0, binding = 2)] transforms: &[Transform], + #[spirv(storage_buffer, descriptor_set = 0, binding = 3)] weights: &[f32], + #[spirv(storage_buffer, descriptor_set = 0, binding = 4)] variations: &[Variation], + #[spirv(storage_buffer, descriptor_set = 0, binding = 5)] camera: &Camera, + #[spirv(storage_buffer, descriptor_set = 0, binding = 6)] image_settings: &ImageSettings, + #[spirv(storage_buffer, descriptor_set = 0, binding = 7)] palette: &[Vec4], + #[spirv(storage_buffer, descriptor_set = 1, binding = 0)] image: &mut [Vec4], +) { + let mut rng_seed_actual = [0u8; 32]; + for i in 0..rng_seed_actual.len() { + rng_seed_actual[i] = rng_seed[i]; + } + + let mut rng = xoshiro256starstar_from_seed(rng_seed_actual); + + let chaos_game = ChaosGame::new(&mut rng, transforms, weights, variations); + let (iterations_fuse, iterations_accumulate) = (iterations.x, iterations.y); + + let ifs_to_image = |(ifs_point, ifs_color)| { + let pixel_coordinates = camera.transform_point(ifs_point); + let pixel_color = image_settings.transform_color(ifs_color, palette); + image_settings + .transform_point_to_index(pixel_coordinates) + .map(|pixel_index| (pixel_index, pixel_color)) + }; + + for ifs_point in chaos_game + .skip(iterations_fuse as usize) + .take(iterations_accumulate as usize) + { + if let Some((pixel_index, pixel_color)) = ifs_to_image(ifs_point) { + image[pixel_index as usize] = pixel_color; + } + } +} diff --git a/enkou-shaders/src/entry/mod.rs b/enkou-shaders/src/entry/mod.rs new file mode 100644 index 0000000..3b1faa3 --- /dev/null +++ b/enkou-shaders/src/entry/mod.rs @@ -0,0 +1,4 @@ +//! # Entry +//! +//! Entry points for Enkou shaders +pub mod image_accumulate; diff --git a/enkou-shaders/src/image.rs b/enkou-shaders/src/image.rs index 62b49f9..98dbafc 100644 --- a/enkou-shaders/src/image.rs +++ b/enkou-shaders/src/image.rs @@ -1,5 +1,7 @@ //! Image -use glam::Vec4; + +use bytemuck::{Pod, Zeroable}; +use glam::{IVec2, UVec2, Vec4}; use libm::floorf; /// Blending modes for mapping IFS color values (which are on a scale `[0, 1]`) @@ -36,10 +38,53 @@ unsafe impl bytemuck::Zeroable for BlendMode {} // UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value unsafe impl bytemuck::Pod for BlendMode {} +/// Settings to use for mapping the IFS coordinates to an output image +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct ImageSettings { + blend_mode: BlendMode, + dimensions: UVec2, +} + +impl ImageSettings { + /// Create a new settings object + pub fn new(blend_mode: BlendMode, dimensions: UVec2) -> Self { + ImageSettings { + blend_mode, + dimensions, + } + } + + /// Map a point from camera coordinates to pixel coordinates, + /// and check that the result is within the provided image dimensions. + pub fn transform_point_to_image(&self, point: IVec2) -> Option { + if 0 <= point.x + && (point.x as u32) < self.dimensions.x + && 0 <= point.y + && (point.y as u32) < self.dimensions.y + { + Some(point.as_uvec2()) + } else { + None + } + } + + /// Map a point from camera coordinates to a final pixel index + pub fn transform_point_to_index(&self, point: IVec2) -> Option { + self.transform_point_to_image(point) + .map(|pixel| self.dimensions.with_x(1).dot(pixel)) + } + + /// Map an IFS color coordinate to the palette RGB value + pub fn transform_color(&self, color: f32, palette: &[Vec4]) -> Vec4 { + self.blend_mode.ifs_to_rgb(color, palette) + } +} + #[cfg(test)] mod test { - use glam::Vec4; - use crate::image::BlendMode; + use crate::image::{BlendMode, ImageSettings}; + use glam::{Vec4, ivec2, uvec2}; #[test] fn blend_linear() { @@ -55,7 +100,12 @@ mod test { assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(2.0)); assert_eq!(ifs_to_rgb(1.0, palette), Vec4::splat(3.0)); - let palette = &[Vec4::splat(1.0), Vec4::splat(2.0), Vec4::splat(3.0), Vec4::splat(4.0)]; + let palette = &[ + Vec4::splat(1.0), + Vec4::splat(2.0), + Vec4::splat(3.0), + Vec4::splat(4.0), + ]; assert_eq!(ifs_to_rgb(0.0, palette), Vec4::splat(1.0)); assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(2.5)); assert_eq!(ifs_to_rgb(1.0, palette), Vec4::splat(4.0)); @@ -76,4 +126,30 @@ mod test { assert_eq!(ifs_to_rgb(0.7, palette), palette[1]); assert_eq!(ifs_to_rgb(1.0, palette), palette[2]); } + + #[test] + fn image_bounds() { + let image_settings = ImageSettings::new(BlendMode::Linear, uvec2(100, 100)); + + assert!( + image_settings + .transform_point_to_image(ivec2(-1, -1)) + .is_none() + ); + assert!( + image_settings + .transform_point_to_image(ivec2(0, 0)) + .is_some() + ); + assert!( + image_settings + .transform_point_to_image(ivec2(99, 99)) + .is_some() + ); + assert!( + image_settings + .transform_point_to_image(ivec2(100, 100)) + .is_none() + ); + } } diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index b87ff85..ccea645 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -1,14 +1,21 @@ //! # Enkou #![no_std] #![warn(missing_docs)] -#![allow(clippy::needless_range_loop)] // SPIR-V backend has issues with iteration over items + +// SPIR-V backend has issues with iteration over items: +#![allow(clippy::needless_range_loop)] +#![allow(clippy::manual_memcpy)] + +// Shader entry points are expected to have a lot of arguments: +#![allow(clippy::too_many_arguments)] pub mod camera; pub mod chaos_game; +pub mod entry; +pub mod image; mod rng; pub mod transform; pub mod variation; -pub mod image; use glam::Affine2; diff --git a/enkou-shaders/src/transform.rs b/enkou-shaders/src/transform.rs index a5897d7..c07b34a 100644 --- a/enkou-shaders/src/transform.rs +++ b/enkou-shaders/src/transform.rs @@ -66,7 +66,7 @@ mod test { use crate::rng::xoshiro256starstar_from_seed; use crate::transform::Transform; use crate::variation::{Variation, VariationKind}; - use glam::{Affine2, uvec2, vec2, Vec2}; + use glam::{Affine2, Vec2, uvec2, vec2}; #[test] fn transform_scaling() { @@ -91,7 +91,12 @@ mod test { #[test] fn transform_scaling_post() { let scale_coefficients = vec2(2.0, 0.5); - let transform_pdj = Transform::new(Affine2::IDENTITY, Affine2::IDENTITY, uvec2(0, 1), Vec2::ZERO); + let transform_pdj = Transform::new( + Affine2::IDENTITY, + Affine2::IDENTITY, + uvec2(0, 1), + Vec2::ZERO, + ); let transform_pdj_post = Transform::new( Affine2::IDENTITY, Affine2::from_scale(scale_coefficients), @@ -108,7 +113,7 @@ mod test { assert_eq!(point_pdj * scale_coefficients, point_pdj_post); } - + #[test] fn transform_color() { // Color 0.5, color speed 1.0, so color value will always be 0.5 after transform @@ -118,11 +123,11 @@ mod test { assert_eq!(transform.transform_color(0.0), 0.5); assert_eq!(transform.transform_color(1.0), 0.5); assert_eq!(transform.transform_color(2.0), 0.5); - + // Color 1.0, color speed 0.5, so color value moves to halfway between current and 1.0 let color = vec2(1.0, 0.5); let transform = Transform::new(Affine2::IDENTITY, Affine2::IDENTITY, uvec2(0, 1), color); - + assert_eq!(transform.transform_color(0.0), 0.5); assert_eq!(transform.transform_color(1.0), 1.0); assert_eq!(transform.transform_color(2.0), 1.5);