Compare commits

...

1 Commits

Author SHA1 Message Date
bspeice 6671475c75 Implement basic variation support
CI / cargo fmt (push) Successful in 24s
CI / cargo test (push) Failing after 26s
CI / cargo test (GPU) (push) Successful in 17m57s
2026-06-27 15:11:23 -04:00
4 changed files with 127 additions and 8 deletions
+18 -4
View File
@@ -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<R: Rng>(
rng: &mut R,
transforms: &[Transform],
weights: &[f32],
variations: &[Variation],
) -> (Vec2, u32) {
let mut choice_weight = rng.sample::<f32, _>(StandardUniform);
let mut transform_index: u32 = 0;
@@ -51,7 +53,7 @@ pub fn step_chaos_game<R: Rng>(
}
(
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<Self::Item> {
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)
+1
View File
@@ -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;
+23 -4
View File
@@ -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<R: Rng>(
&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
}
}
+85
View File
@@ -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<R: Rng>(
&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<R: Rng>(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),
)
}