diff --git a/enkou-shaders-tests/src/lib.rs b/enkou-shaders-tests/src/lib.rs index 7aa1873..c17dbf6 100644 --- a/enkou-shaders-tests/src/lib.rs +++ b/enkou-shaders-tests/src/lib.rs @@ -53,15 +53,7 @@ mod test { } #[test] - pub fn has_entry_main_chaos_game() { - assert!(has_entry_point( - ExecutionModel::GLCompute, - "main_chaos_game" - )) - } - - #[test] - pub fn has_entry_main_camera() { + fn has_entry_main_camera() { assert!(has_entry_point(ExecutionModel::GLCompute, "main_camera")) } } diff --git a/enkou-shaders/src/camera.rs b/enkou-shaders/src/camera.rs index 34c9ba8..fda29f1 100644 --- a/enkou-shaders/src/camera.rs +++ b/enkou-shaders/src/camera.rs @@ -116,7 +116,7 @@ mod test { use libm::powf; #[test] - pub fn manual_camera() { + fn manual_camera() { let starting_point = vec2(1.0, 1.0); // Move the origin; points move right and up by one unit, giving us (2.0, 2.0) @@ -154,7 +154,7 @@ mod test { } #[test] - pub fn point_outside_camera() { + fn point_outside_camera() { // Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2] let camera = Camera::new( uvec2(1000, 1000), @@ -169,7 +169,7 @@ mod test { } #[test] - pub fn point_outside_camera_negative() { + fn point_outside_camera_negative() { // Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2] let camera = Camera::new( uvec2(1000, 1000), @@ -184,7 +184,7 @@ mod test { } #[test] - pub fn aspect_ratio() { + fn aspect_ratio() { // Scale 100 for an image 1600 x 900 gives an effective X range of [-8, 8], // and effective Y range of [-4.5, 4.5] let camera = Camera::new( diff --git a/enkou-shaders/src/image.rs b/enkou-shaders/src/image.rs new file mode 100644 index 0000000..62b49f9 --- /dev/null +++ b/enkou-shaders/src/image.rs @@ -0,0 +1,79 @@ +//! Image +use glam::Vec4; +use libm::floorf; + +/// Blending modes for mapping IFS color values (which are on a scale `[0, 1]`) +/// to RGBA colors. +#[derive(Copy, Clone, Default)] +#[repr(u32)] +pub enum BlendMode { + /// Map IFS color values to a linear blend of the nearest two palette colors + #[default] + Linear = 0, + + /// Map IFS color values to the nearest single palette color + Step = 1, +} + +impl BlendMode { + /// Map an IFS color value to RGBA color from the provided palette. + pub fn ifs_to_rgb(&self, color: f32, palette: &[Vec4]) -> Vec4 { + let colors_m_one = palette.len() - 1; + let period = 1.0 / colors_m_one as f32; + let index_lower = floorf(color / period) as usize; + let index_upper = (index_lower + 1).clamp(0, colors_m_one); + let rem = color % period / period; + + match self { + BlendMode::Linear => palette[index_lower].lerp(palette[index_upper], rem), + BlendMode::Step => palette[index_lower], + } + } +} + +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Zeroable for BlendMode {} +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Pod for BlendMode {} + +#[cfg(test)] +mod test { + use glam::Vec4; + use crate::image::BlendMode; + + #[test] + fn blend_linear() { + let ifs_to_rgb = |color, palette| BlendMode::Linear.ifs_to_rgb(color, palette); + + let palette = &[Vec4::splat(0.0), Vec4::splat(1.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), Vec4::splat(0.0)); + assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(0.5)); + assert_eq!(ifs_to_rgb(1.0, palette), Vec4::splat(1.0)); + + let palette = &[Vec4::splat(1.0), Vec4::splat(2.0), Vec4::splat(3.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), Vec4::splat(1.0)); + 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)]; + 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)); + } + + #[test] + fn blend_step() { + let ifs_to_rgb = |color, palette| BlendMode::Step.ifs_to_rgb(color, palette); + + let palette = &[Vec4::splat(0.0), Vec4::splat(1.0)]; + assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(0.0)); + + let palette = &[Vec4::splat(1.0), Vec4::splat(2.0), Vec4::splat(3.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), palette[0]); + assert_eq!(ifs_to_rgb(0.25, palette), palette[0]); + assert_eq!(ifs_to_rgb(0.4, palette), palette[0]); + assert_eq!(ifs_to_rgb(0.5, palette), palette[1]); + assert_eq!(ifs_to_rgb(0.7, palette), palette[1]); + assert_eq!(ifs_to_rgb(1.0, palette), palette[2]); + } +} diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index 6241f63..b87ff85 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -8,6 +8,7 @@ pub mod chaos_game; mod rng; pub mod transform; pub mod variation; +pub mod image; use glam::Affine2; diff --git a/enkou-shaders/src/rng.rs b/enkou-shaders/src/rng.rs index 2169847..02d4756 100644 --- a/enkou-shaders/src/rng.rs +++ b/enkou-shaders/src/rng.rs @@ -14,6 +14,8 @@ use rand_xoshiro::Xoshiro256StarStar; /// This function assumes a properly-initialized state array; /// output may silently degenerate if the initial state is all zeros, /// so this module is private to the crate. +// Temporarily unused, will be required once the main image accumulation entry point is implemented +#[allow(unused)] pub(crate) fn xoshiro256starstar_from_seed( rng_state: ::Seed, ) -> Xoshiro256StarStar { diff --git a/enkou-shaders/src/transform.rs b/enkou-shaders/src/transform.rs index ea2d2e0..a5897d7 100644 --- a/enkou-shaders/src/transform.rs +++ b/enkou-shaders/src/transform.rs @@ -125,6 +125,6 @@ mod test { 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.0); + assert_eq!(transform.transform_color(2.0), 1.5); } }