From 6671475c753eeecb80bc8d9b856e968575fd1909 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 27 Jun 2026 15:11:23 -0400 Subject: [PATCH] Implement basic variation support --- enkou-shaders/src/chaos_game.rs | 22 +++++++-- enkou-shaders/src/lib.rs | 1 + enkou-shaders/src/transform.rs | 27 +++++++++-- enkou-shaders/src/variation.rs | 85 +++++++++++++++++++++++++++++++++ 4 files changed, 127 insertions(+), 8 deletions(-) create mode 100644 enkou-shaders/src/variation.rs diff --git a/enkou-shaders/src/chaos_game.rs b/enkou-shaders/src/chaos_game.rs index 4d59e52..6d43b3a 100644 --- a/enkou-shaders/src/chaos_game.rs +++ b/enkou-shaders/src/chaos_game.rs @@ -13,6 +13,7 @@ //! This algorithm is also known as the ["chaos game"](https://en.wikipedia.org/wiki/Chaos_game), //! and it forms the basic system for producing images. use crate::transform::Transform; +use crate::variation::Variation; use glam::{Vec2, vec2}; use rand::distr::{Distribution, StandardUniform}; use rand::{Rng, RngExt}; @@ -37,6 +38,7 @@ pub fn step_chaos_game( rng: &mut R, transforms: &[Transform], weights: &[f32], + variations: &[Variation], ) -> (Vec2, u32) { let mut choice_weight = rng.sample::(StandardUniform); let mut transform_index: u32 = 0; @@ -51,7 +53,7 @@ pub fn step_chaos_game( } ( - transforms[transform_index as usize].transform_point(point), + transforms[transform_index as usize].transform_point(rng, variations, point), transform_index, ) } @@ -65,17 +67,24 @@ pub struct ChaosGame<'a, R: Rng> { rng: &'a mut R, transforms: &'a [Transform], weights: &'a [f32], + variations: &'a [Variation], } impl<'a, R: Rng> ChaosGame<'a, R> { /// Create a new chaos game iterator - pub fn new(rng: &'a mut R, transforms: &'a [Transform], weights: &'a [f32]) -> Self { + pub fn new( + rng: &'a mut R, + transforms: &'a [Transform], + weights: &'a [f32], + variations: &'a [Variation], + ) -> Self { let current_point = vec2(rng.sample(BiUnit), rng.sample(BiUnit)); ChaosGame { current_point, rng, transforms, weights, + variations, } } } @@ -84,8 +93,13 @@ impl<'a, R: Rng> Iterator for ChaosGame<'a, R> { type Item = Vec2; fn next(&mut self) -> Option { - let (next_point, _) = - step_chaos_game(self.current_point, self.rng, self.transforms, self.weights); + let (next_point, _) = step_chaos_game( + self.current_point, + self.rng, + self.transforms, + self.weights, + self.variations, + ); self.current_point = next_point; Some(next_point) diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index d3d2a4a..d7f9c4d 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -5,6 +5,7 @@ pub mod camera; pub mod chaos_game; pub mod transform; +mod variation; use bytemuck::{Pod, Zeroable}; use core::f32::consts::PI; diff --git a/enkou-shaders/src/transform.rs b/enkou-shaders/src/transform.rs index fddcb56..aeece9e 100644 --- a/enkou-shaders/src/transform.rs +++ b/enkou-shaders/src/transform.rs @@ -3,24 +3,43 @@ //! Transforms are the "functions" in an iterated function system. They take in a point, //! and generate a new point. For fractal flames, transforms are always affine, //! but produce more interesting images once we add variations. +use crate::variation::Variation; use bytemuck::{Pod, Zeroable}; use glam::{Affine2, Vec2}; +use rand::Rng; /// Affine transform for use in the [`chaos_game`](crate::chaos_game). #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct Transform { coefficients: Affine2, + variation_range: [u16; 2], } impl Transform { /// Create a new transform from an affine transformation matrix - pub fn new(coefficients: Affine2) -> Self { - Transform { coefficients } + pub fn new(coefficients: Affine2, variation_range: [u16; 2]) -> Self { + Transform { + coefficients, + variation_range, + } } /// Apply this transform to a point in IFS coordinates, producing a new point - pub fn transform_point(&self, point: Vec2) -> Vec2 { - self.coefficients.transform_point2(point) + pub fn transform_point( + &self, + rng: &mut R, + variations: &[Variation], + point: Vec2, + ) -> Vec2 { + let point = self.coefficients.transform_point2(point); + + let mut point_output = Vec2::ZERO; + let variation_range = self.variation_range[0] as usize..self.variation_range[1] as usize; + for variation in variations[variation_range].iter() { + point_output += variation.transform_point(point, rng, &self.coefficients) + } + + point_output } } diff --git a/enkou-shaders/src/variation.rs b/enkou-shaders/src/variation.rs new file mode 100644 index 0000000..1db7858 --- /dev/null +++ b/enkou-shaders/src/variation.rs @@ -0,0 +1,85 @@ +//! # Variation + +use crate::Coefficients2; +use bytemuck::{Pod, Zeroable}; +use core::f32::consts::PI; +use glam::{Affine2, Vec2, vec2}; +use libm::{atan2f, cosf, powf, sinf, sqrtf, tanf}; +use rand::distr::Bernoulli; +use rand::{Rng, RngExt}; + +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct VariationParams([f32; 4]); + +#[derive(Copy, Clone)] +#[repr(u32)] +pub enum VariationKind { + Linear = 0, + Julia = 13, + Popcorn = 17, + Pdj = 24, +} + +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Zeroable for VariationKind {} +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Pod for VariationKind {} + +#[derive(Copy, Clone, Pod, Zeroable)] +#[repr(C)] +pub struct Variation { + kind: VariationKind, + weight: f32, + params: VariationParams, +} + +impl Variation { + pub fn transform_point( + &self, + point: Vec2, + rng: &mut R, + coefficients: &Affine2, + ) -> Vec2 { + (match self.kind { + VariationKind::Linear => transform_point_linear(point), + VariationKind::Julia => transform_point_julia(point, rng), + VariationKind::Popcorn => transform_point_popcorn(point, coefficients), + VariationKind::Pdj => transform_point_pdj(point, &self.params), + }) * self.weight + } +} + +fn transform_point_linear(point: Vec2) -> Vec2 { + point +} + +fn transform_point_julia(point: Vec2, rng: &mut R) -> Vec2 { + let x2 = powf(point.x, 2.0); + let y2 = powf(point.y, 2.0); + let r = sqrtf(x2 + y2); + + let theta = atan2f(point.x, point.y); + let omega_choice = rng.sample(Bernoulli::new(0.5).unwrap()); + let omega = if omega_choice { PI } else { 0.0 }; + + let sqrt_r = sqrtf(r); + let theta_val = theta / 2.0 + omega; + + vec2(sqrt_r * cosf(theta_val), sqrt_r * sinf(theta_val)) +} + +fn transform_point_popcorn(point: Vec2, coefficients: &Affine2) -> Vec2 { + vec2( + point.x * coefficients.c() * sinf(tanf(3.0 * point.y)), + point.y + coefficients.f() * sinf(tanf(3.0 * point.x)), + ) +} + +fn transform_point_pdj(point: Vec2, params: &VariationParams) -> Vec2 { + let (pdj_a, pdj_b, pdj_c, pdj_d) = (params.0[0], params.0[1], params.0[2], params.0[3]); + vec2( + sinf(pdj_a * point.y) - cosf(pdj_b * point.x), + sinf(pdj_c * point.x) - cosf(pdj_d * point.y), + ) +}