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:
29
Cargo.lock
generated
29
Cargo.lock
generated
@ -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",
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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(())
|
||||
}
|
@ -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() {}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
Reference in New Issue
Block a user