feat: Re-write accumulator, add camera settings

We can view the IFS-to-image process as a composition of two affine transforms:
- One for the in-IFS functions (zoom, rotate, offset)
- One for the IFS-to-image functions (scale)

And because a composition of affine transforms is itself an affine transform, we can do everything at once.
Eventually, the final affine matrix should be provided to the GPU directly, I just wanted to prove this works for now.
This commit is contained in:
2025-01-12 20:27:45 -05:00
parent 0a1f96edb0
commit 3f5379843e
10 changed files with 474 additions and 948 deletions

29
Cargo.lock generated
View File

@ -225,9 +225,9 @@ checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]]
name = "cc"
version = "1.2.5"
version = "1.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
checksum = "a012a0df96dd6d06ba9a1b29d6402d1a5d77c6befd2566afdc26e10603dc93d7"
dependencies = [
"jobserver",
"libc",
@ -1158,9 +1158,9 @@ dependencies = [
[[package]]
name = "object"
version = "0.36.5"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"crc32fast",
"flate2",
@ -1218,9 +1218,9 @@ dependencies = [
[[package]]
name = "pin-project-lite"
version = "0.2.15"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
@ -1629,9 +1629,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.133"
version = "1.0.135"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9"
dependencies = [
"itoa",
"memchr",
@ -2048,12 +2048,13 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.45"
version = "0.4.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
@ -2089,18 +2090,18 @@ checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
[[package]]
name = "wasmparser"
version = "0.218.0"
version = "0.222.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09e46c7fceceaa72b2dd1a8a137ea7fd8f93dfaa69806010a709918e496c5dc"
checksum = "4adf50fde1b1a49c1add6a80d47aea500c88db70551805853aa8b88f3ea27ab5"
dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "web-sys"
version = "0.3.72"
version = "0.3.76"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@ -28,7 +28,7 @@ needless_range_loop = "allow"
[workspace.lints.rust.unexpected_cfgs]
# Rust (properly) doesn't recognize the spirv architecture
level = "warn"
check-cfg = ['cfg(target_arch, values("spirv"))', 'cfg(target_feature, values("Int8"))']
check-cfg = ['cfg(target_arch, values("spirv"))']
# Compile build-dependencies in release mode with
# the same settings as regular dependencies.

View File

@ -15,6 +15,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Compile the shader crate with SpirvBuilder.
let result = SpirvBuilder::new(gpu_crate_path, "spirv-unknown-spv1.3")
.print_metadata(MetadataPrintout::Full)
.release(false)
.build()?;
// Copy the SPIR-V to this crate's output directory

View File

@ -1,287 +0,0 @@
use flare_lib::{IterateTransformsEntry, RenderImageEntry, RenderImageHistogramEntry};
use flare_shader::{
Coefs, Color, ImageSettings, IterSettings, IterState, Pixel, Point, TransformSpec,
VariationKind, VariationParams, VariationSpec,
};
use futures::channel::oneshot;
use futures_executor::block_on;
use image::RgbaImage;
use rand::Rng;
use wgpu::util::DeviceExt;
const ITER_SETTINGS: IterSettings = IterSettings {
transform_count: TRANSFORMS.len() as u32,
fuse_count: 20,
iteration_count: 100_000,
};
const TRANSFORMS: [TransformSpec; 3] = [
TransformSpec {
coefs: Coefs::new(0.5, 0., 0., 0., 0.5, 0.),
post_coefs: Coefs::IDENTITY,
weight: 1.,
color: 0.0,
color_speed: 0.0,
variation_offset: 0,
variation_count: 1,
},
TransformSpec {
coefs: Coefs::new(0.5, 0., 0.5, 0., 0.5, 0.),
post_coefs: Coefs::IDENTITY,
weight: 1.,
color: 0.0,
color_speed: 0.0,
variation_offset: 0,
variation_count: 1,
},
TransformSpec {
coefs: Coefs::new(0.5, 0., 0., 0., 0.5, 0.5),
post_coefs: Coefs::IDENTITY,
weight: 1.,
color: 0.0,
color_speed: 0.0,
variation_offset: 0,
variation_count: 1,
},
];
const TRANSFORM_FINAL: TransformSpec = TransformSpec {
coefs: Coefs::IDENTITY,
post_coefs: Coefs::IDENTITY,
weight: 0.,
color: 0.,
color_speed: 0.,
variation_offset: 0,
variation_count: 1,
};
const VARIATIONS: [VariationSpec; 1] = [VariationSpec {
weight: 1.,
kind: VariationKind::Linear,
params: VariationParams::new(),
}];
struct IterateTransformsBuffers {
iter_settings: wgpu::Buffer,
transforms: wgpu::Buffer,
transform_final: wgpu::Buffer,
variations: wgpu::Buffer,
thread_state: wgpu::Buffer,
points: wgpu::Buffer,
}
impl IterateTransformsBuffers {
fn new(device: &wgpu::Device, thread_state: &[IterState]) -> Self {
Self {
iter_settings: device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("iter_settings"),
usage: wgpu::BufferUsages::UNIFORM,
contents: bytemuck::cast_slice(&[ITER_SETTINGS]),
}),
transforms: device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("transforms"),
usage: wgpu::BufferUsages::STORAGE,
contents: bytemuck::cast_slice(&TRANSFORMS),
}),
transform_final: device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("transform_final"),
usage: wgpu::BufferUsages::STORAGE,
contents: bytemuck::cast_slice(&[TRANSFORM_FINAL]),
}),
variations: device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("variations"),
usage: wgpu::BufferUsages::STORAGE,
contents: bytemuck::cast_slice(&[VARIATIONS]),
}),
thread_state: device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("thread_state"),
usage: wgpu::BufferUsages::STORAGE,
contents: bytemuck::cast_slice(thread_state),
}),
points: device.create_buffer(&wgpu::BufferDescriptor {
label: Some("points"),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
size: size_of::<Point>() as u64 * ITER_SETTINGS.iteration_count as u64,
mapped_at_creation: false,
}),
}
}
}
const IMAGE_SETTINGS: ImageSettings = ImageSettings {
size: 600,
point_count: ITER_SETTINGS.iteration_count,
palette_count: PALETTE.len() as u32,
};
const PALETTE: [Color; 2] = [Color::WHITE, Color::WHITE];
fn run_compute_pass(
encoder: &mut wgpu::CommandEncoder,
pipeline: &wgpu::ComputePipeline,
bind_group: &wgpu::BindGroup,
) {
let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: None,
timestamp_writes: None,
});
compute_pass.set_pipeline(pipeline);
compute_pass.set_bind_group(0, bind_group, &[]);
compute_pass.dispatch_workgroups(1, 1, 1);
}
pub fn main() -> anyhow::Result<()> {
let backends = wgpu::util::backend_bits_from_env().unwrap_or_default();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends,
..Default::default()
});
let adapter = instance.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::HighPerformance,
force_fallback_adapter: false,
compatible_surface: None,
});
let adapter = block_on(adapter).expect("Unable to find adapter");
let dq = adapter.request_device(
&wgpu::DeviceDescriptor {
label: Some("gasket"),
required_features: wgpu::Features::SHADER_INT64,
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
},
None,
);
let (device, queue) = block_on(dq)?;
let module = wgpu::include_spirv!(concat!(env!("OUT_DIR"), "/shader_binary.spv"));
let shader = device.create_shader_module(module);
let iterate_transforms = IterateTransformsEntry::new(&device, &shader);
let mut rng = rand::thread_rng();
let thread_state = [IterState {
rng_seed: rng.gen(),
current_point: Point::new_random(&mut rng),
}];
let iterate_transforms_buffers = IterateTransformsBuffers::new(&device, &thread_state);
let iterate_transforms_bindgroup = iterate_transforms.bind_group(
&device,
&iterate_transforms_buffers.iter_settings,
&iterate_transforms_buffers.transforms,
&iterate_transforms_buffers.transform_final,
&iterate_transforms_buffers.variations,
&iterate_transforms_buffers.thread_state,
&iterate_transforms_buffers.points,
);
let render_image_histogram = RenderImageHistogramEntry::new(&device, &shader);
let image_settings_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("image_settings"),
usage: wgpu::BufferUsages::UNIFORM,
contents: bytemuck::cast_slice(&[IMAGE_SETTINGS]),
});
let palette_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("palette"),
usage: wgpu::BufferUsages::STORAGE,
contents: bytemuck::cast_slice(&PALETTE),
});
let pixel_count = (IMAGE_SETTINGS.size * IMAGE_SETTINGS.size) as u64;
let pixel_size = size_of::<Pixel>() as u64 * pixel_count;
let image_histogram_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("image_histogram"),
size: pixel_size,
usage: wgpu::BufferUsages::STORAGE,
mapped_at_creation: false,
});
let render_image_histogram_bindgroup = render_image_histogram.bind_group(
&device,
&image_settings_buffer,
&palette_buffer,
&iterate_transforms_buffers.points,
&image_histogram_buffer,
);
let render_image = RenderImageEntry::new(&device, &shader);
let image_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("image"),
size: image_histogram_buffer.size(),
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let render_image_bindgroup = render_image.bind_group(
&device,
&image_settings_buffer,
&image_histogram_buffer,
&image_buffer,
);
let image_buffer_staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("image_staging"),
size: image_buffer.size(),
usage: wgpu::BufferUsages::MAP_READ | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None });
run_compute_pass(
&mut encoder,
&iterate_transforms.compute_pipeline,
&iterate_transforms_bindgroup,
);
run_compute_pass(
&mut encoder,
&render_image_histogram.compute_pipeline,
&render_image_histogram_bindgroup,
);
run_compute_pass(
&mut encoder,
&render_image.compute_pipeline,
&render_image_bindgroup,
);
encoder.copy_buffer_to_buffer(
&image_buffer,
0,
&image_buffer_staging,
0,
image_buffer_staging.size(),
);
queue.submit(Some(encoder.finish()));
let result: Vec<Pixel> = {
let slice = image_buffer_staging.slice(..);
let (sender, receiver) = oneshot::channel();
slice.map_async(wgpu::MapMode::Read, move |result| {
let _ = sender.send(result.unwrap());
});
device.poll(wgpu::Maintain::Wait);
block_on(receiver)?;
let data = slice.get_mapped_range();
bytemuck::cast_slice(&data).to_vec()
};
let mut pixels_set = 0;
for element in result.iter() {
if element.red > 0.0 || element.green > 0.0 || element.blue > 0.0 || element.alpha > 0.0 {
pixels_set += 1;
}
}
let result: Vec<u8> = result.iter().flat_map(Pixel::to_rgba_u8).collect();
let mut image = RgbaImage::from_raw(IMAGE_SETTINGS.size, IMAGE_SETTINGS.size, result)
.expect("Unable to create image");
image.save("gasket.png")?;
Ok(())
}

View File

@ -1,219 +1,5 @@
extern crate core;
const fn create_bind_group_layout_entry(
binding: u32,
ty: wgpu::BufferBindingType,
) -> wgpu::BindGroupLayoutEntry {
wgpu::BindGroupLayoutEntry {
binding,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}
}
fn create_bind_group_entry(binding: u32, resource: &wgpu::Buffer) -> wgpu::BindGroupEntry {
wgpu::BindGroupEntry {
binding,
resource: resource.as_entire_binding(),
}
}
pub struct IterateTransformsEntry {
pub bind_group_layout: wgpu::BindGroupLayout,
pub pipeline_layout: wgpu::PipelineLayout,
pub compute_pipeline: wgpu::ComputePipeline,
}
impl IterateTransformsEntry {
#[must_use]
const fn bind_group_layout_entries() -> [wgpu::BindGroupLayoutEntry; 6] {
[
create_bind_group_layout_entry(0, wgpu::BufferBindingType::Uniform),
create_bind_group_layout_entry(1, wgpu::BufferBindingType::Storage { read_only: true }),
create_bind_group_layout_entry(2, wgpu::BufferBindingType::Storage { read_only: true }),
create_bind_group_layout_entry(3, wgpu::BufferBindingType::Storage { read_only: true }),
create_bind_group_layout_entry(
4,
wgpu::BufferBindingType::Storage { read_only: false },
),
create_bind_group_layout_entry(
5,
wgpu::BufferBindingType::Storage { read_only: false },
),
]
}
pub fn new(device: &wgpu::Device, shader: &wgpu::ShaderModule) -> Self {
let layout_entries = Self::bind_group_layout_entries();
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("iterate_transforms"),
entries: &layout_entries,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("iterate_transforms"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("iterate_transforms"),
layout: Some(&pipeline_layout),
module: shader,
entry_point: Some("iterate_transforms"),
compilation_options: Default::default(),
cache: Default::default(),
});
Self {
bind_group_layout,
pipeline_layout,
compute_pipeline
}
}
pub fn bind_group(
self: &Self,
device: &wgpu::Device,
iter_settings: &wgpu::Buffer,
transform: &wgpu::Buffer,
transform_final: &wgpu::Buffer,
variation: &wgpu::Buffer,
thread_state: &wgpu::Buffer,
point: &wgpu::Buffer,
) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("iterate_transforms"),
layout: &self.bind_group_layout,
entries: &[
create_bind_group_entry(0, iter_settings),
create_bind_group_entry(1, transform),
create_bind_group_entry(2, transform_final),
create_bind_group_entry(3, variation),
create_bind_group_entry(4, thread_state),
create_bind_group_entry(5, point),
],
})
}
}
pub struct RenderImageHistogramEntry {
pub bind_group_layout: wgpu::BindGroupLayout,
pub pipeline_layout: wgpu::PipelineLayout,
pub compute_pipeline: wgpu::ComputePipeline,
}
impl RenderImageHistogramEntry {
const fn bind_group_layout_entries() -> [wgpu::BindGroupLayoutEntry; 4] {
[
create_bind_group_layout_entry(0, wgpu::BufferBindingType::Uniform),
create_bind_group_layout_entry(1, wgpu::BufferBindingType::Storage { read_only: true }),
create_bind_group_layout_entry(2, wgpu::BufferBindingType::Storage { read_only: true }),
create_bind_group_layout_entry(3, wgpu::BufferBindingType::Storage { read_only: false }),
]
}
pub fn new(device: &wgpu::Device, shader: &wgpu::ShaderModule) -> Self {
let layout_entries = Self::bind_group_layout_entries();
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("render_image_histogram"),
entries: &layout_entries,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("render_image_histogram"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("render_image_histogram"),
layout: Some(&pipeline_layout),
module: shader,
entry_point: Some("render_image_histogram"),
compilation_options: Default::default(),
cache: Default::default(),
});
Self {
bind_group_layout,
pipeline_layout,
compute_pipeline
}
}
pub fn bind_group(self: &Self, device: &wgpu::Device, image_settings: &wgpu::Buffer, palette: &wgpu::Buffer, point_buffer: &wgpu::Buffer, image_histogram: &wgpu::Buffer) -> wgpu::BindGroup{
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("render_image_histogram"),
layout: &self.bind_group_layout,
entries: &[
create_bind_group_entry(0, image_settings),
create_bind_group_entry(1, palette),
create_bind_group_entry(2, point_buffer),
create_bind_group_entry(3, image_histogram),
]
})
}
}
pub struct RenderImageEntry {
pub bind_group_layout: wgpu::BindGroupLayout,
pub pipeline_layout: wgpu::PipelineLayout,
pub compute_pipeline: wgpu::ComputePipeline,
}
impl RenderImageEntry {
const fn bind_group_layout_entries() -> [wgpu::BindGroupLayoutEntry; 3] {
[
create_bind_group_layout_entry(0, wgpu::BufferBindingType::Uniform),
create_bind_group_layout_entry(1, wgpu::BufferBindingType::Storage { read_only: true }),
create_bind_group_layout_entry(2, wgpu::BufferBindingType::Storage { read_only: false }),
]
}
pub fn new(device: &wgpu::Device, shader: &wgpu::ShaderModule) -> Self {
let layout_entries = Self::bind_group_layout_entries();
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("render_image"),
entries: &layout_entries,
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("render_image"),
bind_group_layouts: &[&bind_group_layout],
push_constant_ranges: &[],
});
let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("render_image"),
layout: Some(&pipeline_layout),
module: shader,
entry_point: Some("render_image"),
compilation_options: Default::default(),
cache: Default::default(),
});
Self {
bind_group_layout,
pipeline_layout,
compute_pipeline
}
}
pub fn bind_group(self: &Self, device: &wgpu::Device, image_settings: &wgpu::Buffer, image_histogram: &wgpu::Buffer, image: &wgpu::Buffer) -> wgpu::BindGroup {
device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("render_image"),
layout: &self.bind_group_layout,
entries: &[
create_bind_group_entry(0, image_settings),
create_bind_group_entry(1, image_histogram),
create_bind_group_entry(2, image),
],
})
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {}
}

View File

@ -1,196 +1,259 @@
#![no_std]
pub mod points;
pub mod transforms;
pub mod variations;
#![cfg_attr(not(test), no_std)]
#[cfg(test)]
mod tests;
use core::mem::transmute;
use glam::{vec2, Affine2, FloatExt, UVec2, UVec3, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use rand::distributions::Standard;
use rand::{Rng, RngCore};
use rand_xoshiro::Xoshiro256Plus;
use rand::Rng;
use rand_xoshiro::Xoshiro128Plus;
use spirv_std::num_traits::Float;
pub use points::*;
pub use transforms::*;
pub use variations::*;
use spirv_std::spirv;
#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct IterSettings {
pub transform_count: u32,
pub fuse_count: u32,
pub iteration_count: u32,
pub struct Coefs {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub e: f32,
pub f: f32,
}
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct IterState {
pub rng_seed: [u64; 4],
pub current_point: Point,
}
impl Coefs {
pub const IDENTITY: Coefs = Coefs {
a: 1.0,
b: 0.0,
c: 0.0,
d: 0.0,
e: 1.0,
f: 0.0,
};
fn next_transform<'a>(
rng: &mut impl RngCore,
transform_weight: f32,
transforms: &'a [TransformSpec],
) -> &'a TransformSpec {
let mut current_weight = rng.sample::<f32, _>(Standard) * transform_weight;
let mut transform_index = 0usize;
loop {
current_weight -= transforms[transform_index].weight;
if current_weight <= 0.0 {
return &transforms[transform_index];
}
pub const ZERO: Coefs = Coefs {
a: 0.0,
b: 0.0,
c: 0.0,
d: 0.0,
e: 0.0,
f: 0.0,
};
transform_index += 1;
pub fn transform_point2(&self, point: Vec2) -> Vec2 {
Vec2::new(
self.a * point.x + self.b * point.y + self.c,
self.d * point.x + self.e * point.y + self.f,
)
}
}
#[spirv(compute(threads(1)))]
pub fn iterate_transforms(
#[spirv(global_invocation_id)] _global_id: glam::UVec3,
#[spirv(uniform, descriptor_set = 0, binding = 0)] iter_settings: &IterSettings,
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] transforms: &[TransformSpec],
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] transform_final: &TransformSpec,
#[spirv(storage_buffer, descriptor_set = 0, binding = 3)] variations: &[VariationSpec],
#[spirv(storage_buffer, descriptor_set = 0, binding = 4)] thread_state: &mut [IterState],
#[spirv(storage_buffer, descriptor_set = 0, binding = 5)] point_buffer: &mut [Point],
) {
let thread_id = 0;
let thread_state_current = &mut thread_state[thread_id];
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Transform {
pub coefs: Coefs,
pub post_coefs: Coefs,
pub weight: f32,
pub variation_offset: u32,
pub variation_count: u32,
pub color: f32,
pub color_speed: f32,
}
let mut total_weight = 0.0f32;
for i in 0..iter_settings.transform_count as usize {
impl Transform {
pub fn apply(&self, variations: &[Variation], point: Vec4) -> Vec4 {
// Apply the affine transformation
let point_transform = self.coefs.transform_point2(point.xy()).extend(point.z);
// Accumulate each variation
let offset = self.variation_offset as usize;
let count = self.variation_count as usize;
let mut point_variation = Vec3::ZERO;
for i in offset..count {
point_variation += variations[i].apply(self.coefs, point_transform);
}
// Apply the affine post transformation
let point_transform = self
.post_coefs
.transform_point2(point_variation.xy())
.extend(point_variation.z);
// Mix color
let color = point.w.lerp(self.color, self.color_speed);
point_transform.extend(color)
}
}
#[derive(Debug, Copy, Clone)]
#[repr(u32)]
pub enum VariationKind {
Linear = 0,
}
// UNSAFE: Sound for enums with defined bit patterns and a guaranteed zero discriminant?
unsafe impl bytemuck::Pod for VariationKind {}
unsafe impl bytemuck::Zeroable for VariationKind {}
pub struct Variation {
pub kind: VariationKind,
pub weight: f32,
pub params: [f32; 8],
}
impl Variation {
pub const IDENTITY: Variation = Variation {
kind: VariationKind::Linear,
weight: 1.0,
params: [0.0; 8],
};
pub fn apply(&self, _coefs: Coefs, point: Vec3) -> Vec3 {
match self.kind {
VariationKind::Linear => point,
}
}
}
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct ImageSettings {
pub image_width: i32,
pub image_height: i32,
pub scale: f32,
pub zoom: f32,
pub rotate: f32,
pub offset_x: f32,
pub offset_y: f32,
}
impl ImageSettings {
pub(crate) fn pixel_index(&self, point: UVec2) -> u32 {
point.x + point.y * self.image_width as u32
}
pub fn ifs_pixel_index(&self, point: Vec2) -> Option<u32> {
let camera = Affine2::from_scale_angle_translation(
Vec2::splat(2f32.powf(self.zoom)),
self.rotate.to_radians(),
-vec2(self.offset_x, self.offset_y),
);
let image = Affine2::from_scale_angle_translation(
Vec2::splat(self.scale),
0.0,
vec2(
self.image_width as f32 / 2.0,
self.image_height as f32 / 2.0,
),
);
// NOTE: order matters here; `camera * image` will give bad results
let ifs_to_image = image * camera;
let image_point = ifs_to_image.transform_point2(point).as_ivec2();
if image_point.x < 0
|| image_point.x >= self.image_width
|| image_point.y < 0
|| image_point.y >= self.image_height
{
None
} else {
Some(self.pixel_index(image_point.as_uvec2()))
}
}
}
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct ThreadState {
pub rng: [u32; 4],
pub point: Vec4,
}
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct IterSample {
pub pixel_index: u32,
pub color: f32,
}
fn next_transform<'a, R: Rng>(
rng: &mut R,
total_weight: f32,
transforms: &'a [Transform],
) -> &'a Transform {
let mut sample = rng.sample::<f32, _>(Standard) * total_weight;
for i in 0..transforms.len() {
sample -= transforms[i].weight;
if sample <= 0.0 {
return &transforms[i];
}
}
unreachable!()
}
const FUSE_COUNT: u32 = 20;
const ITER_COUNT: u32 = 100_000;
const ACCUMULATE_SAMPLES_THREADS_PER_WORKGROUP: u32 = 1;
#[spirv(compute(threads(1)))]
pub fn accumulate_samples(
#[spirv(num_workgroups)] num_workgroups: UVec3,
#[spirv(workgroup_id)] workgroup_id: UVec3,
#[spirv(local_invocation_index)] local_invocation_index: u32,
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] image_settings: &ImageSettings,
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] transforms: &[Transform],
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] transform_final: &Transform,
#[spirv(storage_buffer, descriptor_set = 0, binding = 3)] variations: &[Variation],
#[spirv(storage_buffer, descriptor_set = 0, binding = 4)] thread_state: &mut [ThreadState],
#[spirv(storage_buffer, descriptor_set = 0, binding = 5)] samples: &mut [IterSample],
) {
// Re-creating GlobalLinearID - https://webgpufundamentals.org/webgpu/lessons/webgpu-compute-shaders.html
let workgroup_index = workgroup_id.x
+ workgroup_id.y * num_workgroups.x
+ workgroup_id.z * num_workgroups.x
+ num_workgroups.y;
let global_invocation_index =
workgroup_index * ACCUMULATE_SAMPLES_THREADS_PER_WORKGROUP + local_invocation_index;
let global_id = global_invocation_index as usize;
let mut total_weight = 0f32;
for i in 0..transforms.len() {
total_weight += transforms[i].weight;
}
// Rather than dealing with bytes slices, just convert to the RNG holder directly
let mut rng =
unsafe { core::mem::transmute::<_, Xoshiro256Plus>(thread_state_current.rng_seed) };
let mut rng: Xoshiro128Plus =
unsafe { transmute::<_, Xoshiro128Plus>(thread_state[global_id].rng) };
let mut point = thread_state[global_id].point;
let mut current_point = thread_state_current.current_point;
for _i in 0..iter_settings.fuse_count as usize {
let transform = next_transform(&mut rng, total_weight, transforms);
current_point = transform.apply(variations, current_point);
for _i in 0..FUSE_COUNT as usize {
point = next_transform(&mut rng, total_weight, transforms).apply(variations, point);
}
for i in 0..iter_settings.iteration_count as usize {
let transform = next_transform(&mut rng, total_weight, transforms);
current_point = transform.apply(variations, current_point);
// let camera = accum_settings.camera;
let sample_offset = global_id * ITER_COUNT as usize;
for i in 0..ITER_COUNT as usize {
point = next_transform(&mut rng, total_weight, transforms).apply(variations, point);
point_buffer[i] = transform_final.apply(variations, current_point);
}
thread_state_current.rng_seed = unsafe { core::mem::transmute::<_, [u64; 4]>(rng) };
thread_state_current.current_point = current_point;
}
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct ImageSettings {
pub size: u32,
pub point_count: u32,
pub palette_count: u32,
}
#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Color {
pub red: f32,
pub green: f32,
pub blue: f32,
}
#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Pixel {
pub red: f32,
pub green: f32,
pub blue: f32,
pub alpha: f32,
}
impl Pixel {
pub fn to_rgba_u8(&self) -> [u8; 4] {
[
(self.red * 255.0) as u8,
(self.green * 255.0) as u8,
(self.blue * 255.0) as u8,
(self.alpha * 255.0) as u8,
]
}
}
impl Color {
pub const BLACK: Self = Color { red: 0.0, green: 0.0, blue: 0.0 };
pub const WHITE: Self = Color { red: 1.0, green: 1.0, blue: 1.0 };
}
#[spirv(compute(threads(1)))]
pub fn render_image_histogram(
#[spirv(global_invocation_id)] _global_id: glam::UVec3,
#[spirv(uniform, descriptor_set = 0, binding = 0)] image_settings: &ImageSettings,
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] palette: &[Color],
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] point_buffer: &[Point],
#[spirv(storage_buffer, descriptor_set = 0, binding = 3)] image_histogram: &mut [Pixel],
) {
for i in 0..image_settings.point_count as usize {
let point = &point_buffer[i];
// Simple camera - square image using the range -2 to 2
let pixel_x = ((point.x + 2.0) * image_settings.size as f32 / 4f32) as i32;
let pixel_y = ((point.y + 2.0) * image_settings.size as f32 / 4f32) as i32;
// Pixel outside viewable range
if pixel_x < 0 || pixel_x >= image_settings.size as i32 || pixel_y < 0 || pixel_y >= image_settings.size as i32 {
continue;
}
let pixel_index = (pixel_y * image_settings.size as i32 + pixel_x) as usize;
// Get the colors
let palette_index = (point.color * image_settings.palette_count as f32) as u32 * 3;
let color = palette[palette_index as usize];
// Update the image
let mut pixel = &mut image_histogram[pixel_index];
*pixel = Pixel {
red: pixel.red + color.red,
green: pixel.green + color.green,
blue: pixel.blue + color.blue,
alpha: pixel.alpha + 1.0,
};
}
}
#[spirv(compute(threads(1)))]
pub fn render_image(
#[spirv(global_invocation_id)] _global_id: glam::UVec3,
#[spirv(uniform, descriptor_set = 0, binding = 0)] image_settings: &ImageSettings,
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] image_histogram: &[Pixel],
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] image: &mut [Pixel],
) {
// Normalize pixels based on alpha scaling
let pixel_count = image_settings.size * image_settings.size;
for i in 0..pixel_count as usize {
let pixel_histogram = &image_histogram[i];
if pixel_histogram.alpha <= 0.0 {
continue;
}
// `log10` has issues, use a change-of-base instead:
// https://github.com/Rust-GPU/rust-gpu/issues/199
let w_log10 = pixel_histogram.alpha.ln() / (Float::ln(10.0));
let scale = w_log10 / (pixel_histogram.alpha * 1.5);
// Clipping individual channels to 1.0 is bad
image[i] = Pixel {
red: (pixel_histogram.red * scale).min(1.0),
green: (pixel_histogram.green * scale).min(1.0),
blue: (pixel_histogram.blue * scale).min(1.0),
alpha: (pixel_histogram.alpha * scale).min(1.0),
let final_point = transform_final.apply(variations, point);
let final_color = final_point.w;
let final_sample = match image_settings.ifs_pixel_index(final_point.xy()) {
None => IterSample {
pixel_index: u32::MAX,
color: 0.0,
},
Some(pixel_index) => IterSample {
pixel_index,
color: final_color,
},
};
samples[sample_offset + i].pixel_index = final_sample.pixel_index;
samples[sample_offset + i].color = final_sample.color;
}
let current_thread = &mut thread_state[global_id];
current_thread.rng = unsafe { transmute::<_, [u32; 4]>(rng) };
current_thread.point = point;
}

View File

@ -1,55 +0,0 @@
use glam::FloatExt;
use rand::distributions::{Distribution, Standard};
use rand::{Rng, RngCore};
#[derive(Copy, Clone, Debug, Default, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Point {
pub x: f32,
pub y: f32,
pub z: f32,
pub color: f32,
}
pub struct BiUnit;
impl Distribution<f32> for BiUnit {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> f32 {
rng.sample::<f32, _>(Standard) * 2.0 - 1.0
}
}
impl Point {
pub fn new_xy(x: f32, y: f32) -> Point {
Point {
x,
y,
..Default::default()
}
}
pub fn new_color(color: f32) -> Point {
Point {
color,
..Default::default()
}
}
pub fn new_random(rng: &mut impl RngCore) -> Point {
Point {
x: rng.sample(BiUnit),
y: rng.sample(BiUnit),
z: rng.sample(BiUnit),
color: rng.sample(Standard),
}
}
pub fn lerp_color(self, color: f32, color_speed: f32) -> Self {
Point {
x: self.x,
y: self.y,
z: self.z,
color: self.color.lerp(color, color_speed),
}
}
}

View File

@ -1,85 +1,228 @@
use crate::{
iterate_transforms, Coefs, IterSettings, IterState, Point, TransformSpec, VariationKind,
VariationParams, VariationSpec,
};
use crate::{Coefs, ImageSettings, Transform, Variation};
use glam::{uvec2, vec2, vec4};
#[test]
fn apply_transform_coefs() {
let transform = TransformSpec {
coefs: Coefs::new(1., 2., 3., 4., 5., 6.),
post_coefs: Coefs::IDENTITY,
variation_offset: 0,
variation_count: 1,
..Default::default()
};
let variations = [VariationSpec {
kind: VariationKind::Linear,
weight: 1.,
params: VariationParams::default(),
}];
let point_out = transform.apply(&variations, Point::new_xy(1., 1.));
assert_eq!(point_out, Point::new_xy(6., 15.));
fn apply_coefs_identity() {
let point = vec2(0.5, 0.5);
assert_eq!(point, Coefs::IDENTITY.transform_point2(point));
}
#[test]
fn apply_transform_post_coefs() {
let transform = TransformSpec {
coefs: Coefs::IDENTITY,
post_coefs: Coefs::new(1., 2., 3., 4., 5., 6.),
variation_offset: 0,
variation_count: 1,
..Default::default()
};
let variations = [VariationSpec {
kind: VariationKind::Linear,
weight: 1.,
params: VariationParams::default(),
}];
let point = transform.apply(&variations, Point::new_xy(1., 1.));
assert_eq!(point, Point::new_xy(6., 15.));
}
#[test]
fn apply_transform_variation() {
let transform = TransformSpec {
coefs: Coefs::IDENTITY,
post_coefs: Coefs::IDENTITY,
variation_offset: 0,
variation_count: 1,
..Default::default()
};
let variations = [VariationSpec {
kind: VariationKind::Linear,
weight: 0.5,
params: VariationParams::default(),
}];
let point = transform.apply(&variations, Point::new_xy(1., 1.));
assert_eq!(point, Point::new_xy(0.5, 0.5));
}
#[test]
fn iterate_transform_advances_state() {
let mut thread_states = [IterState {
rng_seed: [1, 2, 3, 4],
current_point: Default::default(),
}];
iterate_transforms(
glam::uvec3(0, 0, 0),
&IterSettings {
transform_count: 1,
fuse_count: 1,
iteration_count: 0,
},
&[TransformSpec::IDENTITY],
&TransformSpec::IDENTITY,
&[],
&mut thread_states,
&mut []
fn apply_coefs_translate() {
let point = vec2(0.5, 0.5);
assert_eq!(
vec2(1., 1.),
Coefs {
c: 0.5,
f: 0.5,
..Coefs::IDENTITY
}
.transform_point2(point)
);
assert_ne!(thread_states[0].rng_seed, [1, 2, 3, 4]);
}
#[test]
fn apply_transform_identity() {
let point = vec4(0.5, 0.5, 0.0, 0.0);
let transform = Transform {
coefs: Coefs::IDENTITY,
post_coefs: Coefs::IDENTITY,
weight: 1.0,
variation_offset: 0,
variation_count: 1,
color: 0.0,
color_speed: 0.0,
};
let variations = [Variation::IDENTITY];
assert_eq!(point, transform.apply(&variations, point));
}
#[test]
fn apply_transform_scale() {
let point = vec4(0.5, 0.5, 0.0, 0.0);
let transform = Transform {
coefs: Coefs {
a: 2.0,
e: 2.0,
..Coefs::ZERO
},
post_coefs: Coefs::IDENTITY,
weight: 1.0,
variation_offset: 0,
variation_count: 1,
color: 0.0,
color_speed: 0.0,
};
let variations = [Variation::IDENTITY];
assert_eq!(
vec4(1.0, 1.0, 0.0, 0.0),
transform.apply(&variations, point)
);
}
#[test]
fn apply_transform_color() {
let point = vec4(0.5, 0.5, 0.0, 0.0);
let transform = Transform {
coefs: Coefs {
a: 2.0,
e: 2.0,
..Coefs::ZERO
},
post_coefs: Coefs::IDENTITY,
weight: 1.0,
variation_offset: 0,
variation_count: 1,
color: 1.0,
color_speed: 0.2,
};
let variations = [Variation::IDENTITY];
assert_eq!(
vec4(1.0, 1.0, 0.0, 0.2),
transform.apply(&variations, point)
);
}
#[test]
fn map_pixel_histogram() {
let image = ImageSettings {
image_width: 600,
image_height: 0,
scale: 0.0,
zoom: 0.0,
rotate: 0.0,
offset_x: 0.0,
offset_y: 0.0,
};
assert_eq!(
image.pixel_index(uvec2(100, 100)),
(100 + 100 * image.image_width) as u32
);
}
fn assert_image_index(image: &ImageSettings, ifs_x: f32, ifs_y: f32, pixel_x: u32, pixel_y: u32) {
assert_eq!(
image.ifs_pixel_index(vec2(ifs_x, ifs_y)),
Some(image.pixel_index(uvec2(pixel_x, pixel_y)))
);
}
#[test]
fn ifs_pixel_index_square() {
// Square image with scale chosen to match range [-2, 2]
let image = ImageSettings {
image_width: 1000,
image_height: 1000,
scale: 250.0,
zoom: 0.0,
rotate: 0.0,
offset_x: 0.0,
offset_y: 0.0,
};
assert_eq!(image.ifs_pixel_index(vec2(-2.5, 2.5)), None);
assert_image_index(&image, 0.0, 0.0, 500, 500);
assert_image_index(&image, -2.0, -2.0, 0, 0);
assert_image_index(&image, 1.999, -2.0, 999, 0);
assert_image_index(&image, -2.0, 1.999, 0, 999);
assert_image_index(&image, 1.999, 1.999, 999, 999);
}
#[test]
fn ifs_pixel_index_landscape() {
// 16:9 aspect ratio with scale chosen to give a range of [-2, 2] on the X axis.
// On the Y axis, effective range will be [-1.125, 1.125]
let image = ImageSettings {
image_width: 1600,
image_height: 900,
scale: 400.0,
zoom: 0.0,
rotate: 0.0,
offset_x: 0.0,
offset_y: 0.0,
};
assert_image_index(&image, 0.0, 0.0, 800, 450);
assert_image_index(&image, -2.0, -1.125, 0, 0);
assert_image_index(&image, 1.999, -1.125, 1599, 0);
assert_image_index(&image, -2.0, 1.12499, 0, 899);
assert_image_index(&image, 1.999, 1.12499, 1599, 899);
}
#[test]
fn ifs_pixel_index_portrait() {
// 9:16 aspect ratio; swaps effective range of X/Y axes from landscape test
let image = ImageSettings {
image_width: 900,
image_height: 1600,
scale: 400.0,
zoom: 0.0,
rotate: 0.0,
offset_x: 0.0,
offset_y: 0.0,
};
assert_image_index(&image, 0.0, 0.0, 450, 800);
assert_image_index(&image, -1.125, -2.0, 0, 0);
assert_image_index(&image, 1.12499, -2.0, 899, 0);
assert_image_index(&image, -1.125, 1.999, 0, 1599);
assert_image_index(&image, 1.12499, 1.999, 899, 1599);
}
#[test]
fn ifs_pixel_index_rotate() {
let image = ImageSettings {
image_width: 1000,
image_height: 1000,
scale: 250.,
zoom: 0.,
rotate: 90.,
offset_x: 0.,
offset_y: 0.,
};
// Rotation at the origin has no effect
assert_image_index(&image, 0.0, 0.0, 500, 500);
// 90-degree rotation means this point will become (-0.5, 0.5)
assert_image_index(&image, 0.5, 0.5, 375, 625);
}
#[test]
fn ifs_pixel_index_zoom() {
// Square image with scale chosen to match [-2, 2],
// but zoom 1 leads to an effective range of [-1, 1]
let image = ImageSettings {
image_width: 1000,
image_height: 1000,
scale: 250.,
zoom: 1.,
rotate: 0.,
offset_x: 0.,
offset_y: 0.,
};
// Zoom has no effect at the origin
assert_image_index(&image, 0.0, 0.0, 500, 500);
assert_image_index(&image, 0.5, 0.5, 750, 750);
}
#[test]
fn ifs_pixel_index_offset() {
// Square image with scale chosen to match [-2, 2],
// but offset leads to an effective range of [-1.5, 2.5]
let image = ImageSettings {
image_width: 1000,
image_height: 1000,
scale: 250.,
zoom: 0.0,
rotate: 0.0,
offset_x: 0.5,
offset_y: 0.5,
};
assert_image_index(&image, 0.0, 0.0, 375, 375);
}

View File

@ -1,79 +0,0 @@
use crate::{Point, VariationSpec};
#[derive(Copy, Clone, Default, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct Coefs {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub e: f32,
pub f: f32,
}
impl Coefs {
pub const IDENTITY: Coefs = Coefs {
a: 1.,
b: 0.,
c: 0.,
d: 0.,
e: 1.,
f: 0.
};
pub const fn new(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self {
Coefs { a, b, c, d, e, f }
}
pub fn apply(&self, point: &Point) -> Point {
Point {
x: point.x * self.a + point.y * self.b + self.c,
y: point.x * self.d + point.y * self.e + self.f,
z: point.z,
color: point.color
}
}
}
#[derive(Copy, Clone, Default, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct TransformSpec {
pub coefs: Coefs,
pub post_coefs: Coefs,
pub weight: f32,
pub color: f32,
pub color_speed: f32,
pub variation_offset: u32,
pub variation_count: u32,
}
impl TransformSpec {
pub const IDENTITY: TransformSpec = TransformSpec {
coefs: Coefs::IDENTITY,
post_coefs: Coefs::IDENTITY,
weight: 0.,
color: 0.,
color_speed: 0.,
variation_offset: 0,
variation_count: 0,
};
pub fn apply(&self, variations: &[VariationSpec], point: Point) -> Point {
let point = self.coefs.apply(&point);
let variation_offset = self.variation_offset as usize;
let variation_count = self.variation_count as usize;
let mut point_variation = Point::default();
for i in variation_offset..variation_count {
let point_variation_current = variations[i].apply(&point);
point_variation = Point {
x: point_variation.x + point_variation_current.x,
y: point_variation.y + point_variation_current.y,
z: point_variation.z + point_variation_current.z,
color: point_variation.color,
};
}
self.post_coefs.apply(&point_variation).lerp_color(self.color, self.color_speed)
}
}

View File

@ -1,47 +0,0 @@
use crate::Point;
#[derive(Copy, Clone, Debug)]
#[repr(C)]
pub enum VariationKind {
Linear,
}
// UNSAFE: I think this is fine for repr(C) enums?
unsafe impl bytemuck::Pod for VariationKind {}
unsafe impl bytemuck::Zeroable for VariationKind {}
#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(transparent)]
pub struct VariationParams([f32; 8]);
impl VariationParams {
pub const fn new() -> Self {
Self([0f32; 8])
}
}
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
#[repr(C)]
pub struct VariationSpec {
pub kind: VariationKind,
pub weight: f32,
pub params: VariationParams,
}
impl VariationSpec {
pub fn apply(&self, point: &Point) -> Point {
let point = match self.kind {
VariationKind::Linear => apply_linear(&self.params, point),
};
Point {
x: point.x * self.weight,
y: point.y * self.weight,
z: point.z * self.weight,
color: point.color,
}
}
}
fn apply_linear(_params: &VariationParams, point: &Point) -> Point {
*point
}