From fc76a8308ab67a38bbd8854ed18884739c0295cc Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sun, 2 Aug 2026 11:10:25 -0400 Subject: [PATCH] Add image accumulation --- enkou-shaders/examples/flame.rs | 106 ++++++++++++++++++++++++++++++++ enkou-shaders/src/image.rs | 9 ++- enkou-shaders/src/lib.rs | 2 +- enkou-shaders/src/variation.rs | 9 ++- 4 files changed, 122 insertions(+), 4 deletions(-) create mode 100644 enkou-shaders/examples/flame.rs diff --git a/enkou-shaders/examples/flame.rs b/enkou-shaders/examples/flame.rs new file mode 100644 index 0000000..c532f5e --- /dev/null +++ b/enkou-shaders/examples/flame.rs @@ -0,0 +1,106 @@ +//! # Fractal flame +//! +//! The `flam3` sources include example parameters (`test.flam3`) to demonstrate the fractal flame algorithm, +//! which are copied here and used to generate an image that can be compared against the reference `flam3` renderer + +use anyhow::Result; +use enkou_shaders::chaos_game::ChaosGame; +use enkou_shaders::transform::Transform; +use enkou_shaders::variation::{Variation, VariationKind}; +use glam::{UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec4}; +use rand::SeedableRng; +use rand_xoshiro::Xoshiro256StarStar; +use enkou_shaders::camera::Camera; +use enkou_shaders::image::BlendMode; + +const ITERATIONS_DISCARD: usize = 100; +const IMAGE_DIMENSIONS: UVec2 = UVec2::new(640, 480); +const IMAGE_QUALITY: f32 = 10.0; + +// Palette data copied from `flam3-palettes.xml`. +// Palettes are ASCII hexadecimal strings; two characters form a byte, +// and bytes are grouped in triplets to get a single RGB color. +const PALETTE: &'static str = "\ +00dadebc00eee6c500eef2ce00eef2cf00e6eee100eaeed800f2f1eb00f2f5d8 +00e6f2ce00deeac500d6dac600ced2bc00c2caa900becaa000ced6aa00dee2c5 +00eaedce00eaf2c500dee2c500c2caaa00aebeaa00a5b29600a2a98d0096a284 +008d8d7a0085897100858d710085856700797d6700797d670071795e00656d55 +004d5d4200344025003040250030381c002c3c1c002c341c00242c1200242400 +00242c0900283409003840120030401c0040502f0055694200657555006c7d5e +00748d710074898400748d8400788d840079897a0079857100757d670071795e +006c715e006d705e006c795e0068755e00697155006d7555006d755500697155 +0065715500696d550064715e006870670068706700686c67006c6c5e0071715e +0079796700818571007d91710085927a0085927a007d9284007992840078928d +00788d8d00748d84007492840075927a006c85670064795e0059694b00aa5700 +0038441c00303c1c002c3c1c003440250050614b005d6d5e0064715e0060715e +0060755e0068755e006c795e006c795e0071796700707967006c7d6700687967 +006c7967006c75670071755e0071755e0075795e00757d5e00818d5e008d925e +008d9267009a9a71009aa27a009aa27a009aa17a00929a71008992670081855e +007d7d550069794b00616d4200444c250038441c0040512500454d2500716d42 +00797d4b00817d5500797955006d755500697d55006c795e006579540068795e +00647967006479670068755e0064715e00646c5e00656d55004d584200344025 +002c381c0020281c001c1409001818000004140000081000000c1800001c2809 +00243012003c4425005d6555007579550085895e008991710096a271009aa27a +009eaa7a009eaa7a00aaae7100a6aa7a00a2aa7a00a1a57a00969e7a0085967a +0081927a0078927a0075927a00758d7a00708167007d7d670089896700929a71 +009eaa7a00aab68400b2b68d00b6ba9700c2ca9700b2be8d00b2b68d00aab28d +00a2ae84009aa67a00929e7a00859a7a007d967a007d927a007d9284007d9284 +0081968400859684008596840081928400859a8400859a84008d9a8400929684 +009ea98400aeb28400aaba8400b2be8d00b6c2a000c6caa000c6ceaa00d6dab3 +00dae2c500d2d6bc00bec2a000aab68d009ea67a00929a710089897100817d67 +007d7d6700817867007d7d5e0079795e0079815e00817d6700817d6700818167 +008189710085917a0089927a00969d7a00969e7a0092968400969a8d00929284 +0089918400819284007d928d0078928d0074928d0078928d007896970081968d +0081968d00819a8d00859a8d00899e8d00899e8d008da2970095a297008da297 +0096a68d009aa18d009ea984009ea67a00a2a571009ea671009aa67100959d71"; + +/// Generate an image +pub fn main() -> Result<()> { + let transforms: &[Transform] = &[]; + + let weights = [0.25; 4]; + + let variations = [Variation::new( + VariationKind::Spherical, + 1.0, + [0.0; 4].into(), + )]; + + let camera = Camera::new(IMAGE_DIMENSIONS, Vec2::ZERO, 0.0, Vec2::ZERO, (IMAGE_DIMENSIONS / 2).as_vec2()); + + let palette_chars: String = PALETTE.chars().filter(|c| c.is_alphanumeric()).collect(); + let palette_bytes: Vec = (0..palette_chars.len()) + .step_by(2) + .map(|i| { + let s = &palette_chars[i..i + 2]; + let b = u8::from_str_radix(s, 16).expect("illegal palette value"); + b + }) + .collect(); + let palette_colors: Vec = palette_bytes + .chunks(3) + .map(|chunk| [chunk[0] as f32, chunk[1] as f32, chunk[2] as f32, 255.0].into()) + .collect(); + + let mut rng = Xoshiro256StarStar::from_seed([4u8; 32]); + let chaos_game = ChaosGame::new(&mut rng, transforms, &weights, &variations); + + let mut image_accum: Vec = Vec::new(); + image_accum.resize(IMAGE_DIMENSIONS.element_product() as usize, Vec4::ZERO); + + let iterations: usize = (IMAGE_DIMENSIONS.element_product() as f32 * IMAGE_QUALITY) as usize; + chaos_game.skip(ITERATIONS_DISCARD).take(iterations).for_each(|(ifs_point, ifs_color)| { + let pixel_point = camera.transform_point_to_image(ifs_point); + if pixel_point.is_none() { + return; + } + + let pixel_point = pixel_point.unwrap(); + let pixel_index = pixel_point.y * IMAGE_DIMENSIONS.x + pixel_point.x; + + let pixel_color = BlendMode::Linear.ifs_to_rgb(ifs_color, palette_colors.as_ref()); + image_accum[pixel_index as usize] += pixel_color; + }); + + Ok(()) +} diff --git a/enkou-shaders/src/image.rs b/enkou-shaders/src/image.rs index 62b49f9..64aa835 100644 --- a/enkou-shaders/src/image.rs +++ b/enkou-shaders/src/image.rs @@ -38,8 +38,8 @@ unsafe impl bytemuck::Pod for BlendMode {} #[cfg(test)] mod test { - use glam::Vec4; use crate::image::BlendMode; + use glam::Vec4; #[test] fn blend_linear() { @@ -55,7 +55,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)); diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index 38d0cba..82524cb 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -4,9 +4,9 @@ pub mod camera; pub mod chaos_game; +pub mod image; pub mod transform; pub mod variation; -mod image; use glam::Affine2; diff --git a/enkou-shaders/src/variation.rs b/enkou-shaders/src/variation.rs index 81c3463..87e8629 100644 --- a/enkou-shaders/src/variation.rs +++ b/enkou-shaders/src/variation.rs @@ -35,7 +35,7 @@ impl From<[f32; 4]> for VariationParams { pub enum VariationKind { /// Identity variation, returns the point as-is Linear = 0, - + Spherical = 2, Julia = 13, Popcorn = 17, Pdj = 24, @@ -87,6 +87,7 @@ impl Variation { ) -> Vec2 { (match self.kind { VariationKind::Linear => transform_point_linear(point), + VariationKind::Spherical => transform_point_spherical(point), VariationKind::Julia => transform_point_julia(point, rng), VariationKind::Popcorn => transform_point_popcorn(point, coefficients), VariationKind::Pdj => transform_point_pdj(point, &self.params), @@ -98,6 +99,12 @@ fn transform_point_linear(point: Vec2) -> Vec2 { point } +fn transform_point_spherical(point: Vec2) -> Vec2 { + let r2 = (point * point).element_sum(); + let r2_inv = 1.0 / r2; + + point * Vec2::splat(r2_inv) +} fn transform_point_julia(point: Vec2, rng: &mut R) -> Vec2 { let x2 = powf(point.x, 2.0); let y2 = powf(point.y, 2.0);