Compare commits
10 Commits
221d544a01
...
main
Author | SHA1 | Date | |
---|---|---|---|
74139cc54b | |||
58aad8dbab | |||
0cddc9d9a1 | |||
13ba4368aa | |||
2dfdea361e | |||
38f383a0b2 | |||
ceb772bbec | |||
68843293ff | |||
0a1e0f98c6 | |||
a94ab87eab |
2019
Cargo.lock
generated
2019
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,48 +5,47 @@ use rand::Rng;
|
||||
use rand::distributions::Standard;
|
||||
use rand::prelude::Distribution;
|
||||
use rand_xoshiro::Xoshiro128Plus;
|
||||
use spirv_std::spirv;
|
||||
use spirv_std::num_traits::Float;
|
||||
use spirv_std::spirv;
|
||||
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct ImageConstants {
|
||||
accum_width: i32,
|
||||
accum_height: i32,
|
||||
viewport_width: i32,
|
||||
viewport_height: i32,
|
||||
background_color: Vec4,
|
||||
pub struct ViewportConstants {
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
}
|
||||
|
||||
impl ImageConstants {
|
||||
impl ViewportConstants {
|
||||
pub fn new(
|
||||
accum_width: i32,
|
||||
accum_height: i32,
|
||||
viewport_width: i32,
|
||||
viewport_height: i32,
|
||||
background_color: Vec4,
|
||||
offset_x: i32,
|
||||
offset_y: i32,
|
||||
width: i32,
|
||||
height: i32,
|
||||
) -> Self {
|
||||
Self {
|
||||
accum_width,
|
||||
accum_height,
|
||||
viewport_width,
|
||||
viewport_height,
|
||||
background_color,
|
||||
offset_x, offset_y, width, height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn viewport_dimensions(&self) -> glam::IVec2 {
|
||||
glam::ivec2(self.viewport_width, self.viewport_height)
|
||||
#[derive(Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct AccumConstants {
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
|
||||
pub fn with_accumulate(&mut self, width: i32, height: i32) {
|
||||
self.accum_width = width;
|
||||
self.accum_height = height;
|
||||
impl AccumConstants {
|
||||
pub fn new(width: i32, height: i32) -> Self {
|
||||
Self {
|
||||
width, height
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_viewport(&mut self, width: i32, height: i32) {
|
||||
self.viewport_width = width;
|
||||
self.viewport_height = height;
|
||||
pub fn min_dimension(&self) -> i32 {
|
||||
if self.width < self.height { self.width } else { self.height }
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,11 +60,22 @@ pub struct CameraConstants {
|
||||
}
|
||||
|
||||
impl CameraConstants {
|
||||
pub fn new(scale: f32) -> Self {
|
||||
Self {
|
||||
scale,
|
||||
zoom: 0.0,
|
||||
rotate: 0.0,
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn camera(&self, image_width: i32, image_height: i32) -> glam::Affine2 {
|
||||
let zoom_factor = 2f32.powf(self.zoom);
|
||||
let zoom_rotate_offset = glam::Affine2::from_scale_angle_translation(
|
||||
glam::Vec2::splat(2f32.powf(self.zoom)),
|
||||
glam::Vec2::splat(zoom_factor),
|
||||
self.rotate.to_radians(),
|
||||
-vec2(self.offset_x, self.offset_y),
|
||||
-vec2(self.offset_x, self.offset_y) * zoom_factor,
|
||||
);
|
||||
let ifs_to_pixel = glam::Affine2::from_scale_angle_translation(
|
||||
glam::Vec2::splat(self.scale),
|
||||
@ -119,13 +129,23 @@ impl ThreadState {
|
||||
unsafe { core::mem::transmute::<_, Xoshiro128Plus>(*rng_state) }
|
||||
}
|
||||
|
||||
pub fn new(rng: &mut Xoshiro128Plus) -> Self {
|
||||
pub fn new<R: Rng>(rng: &mut R) -> Self {
|
||||
let mut rng_state: [u32; 4] = [0; 4];
|
||||
rng.fill(&mut rng_state);
|
||||
let point = vec2(rng.sample(BiUnit), rng.sample(BiUnit));
|
||||
let rng_state = Self::xoshiro_to_state(rng);
|
||||
rng.jump();
|
||||
Self { rng_state, point }
|
||||
}
|
||||
|
||||
pub fn jump(&self) -> Self {
|
||||
let mut rng = self.get_rng();
|
||||
rng.jump();
|
||||
let point = vec2(rng.sample(BiUnit), rng.sample(BiUnit));
|
||||
Self {
|
||||
rng_state: Self::xoshiro_to_state(&rng),
|
||||
point,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_rng(&self) -> Xoshiro128Plus {
|
||||
Self::state_to_xoshiro(&self.rng_state)
|
||||
}
|
||||
@ -138,19 +158,32 @@ impl ThreadState {
|
||||
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct Coefs {
|
||||
a: f32,
|
||||
b: f32,
|
||||
c: f32,
|
||||
d: f32,
|
||||
e: f32,
|
||||
f: f32,
|
||||
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.0,
|
||||
b: 0.0,
|
||||
c: 0.0,
|
||||
d: 0.0,
|
||||
e: 1.0,
|
||||
f: 0.0
|
||||
};
|
||||
|
||||
pub fn new(a: f32, b: f32, c: f32, d: f32, e: f32, f: f32) -> Self {
|
||||
Self { a, b, c, d, e, f }
|
||||
}
|
||||
|
||||
pub fn new_from_xml_order(a: f32, d: f32, b: f32, e: f32, c: f32, f: f32) -> Self {
|
||||
Self { a, b, c, d, e, f }
|
||||
}
|
||||
|
||||
pub fn apply(&self, point: glam::Vec2) -> glam::Vec2 {
|
||||
vec2(
|
||||
self.a * point.x + self.b * point.y + self.c,
|
||||
@ -237,11 +270,13 @@ fn next_transform<R: Rng + ?Sized>(
|
||||
|
||||
#[spirv(compute(threads(1)))]
|
||||
pub fn main_cs(
|
||||
#[spirv(push_constant)] image_constants: &ImageConstants,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] thread_state: &mut [ThreadState],
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] transforms: &[Transform],
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] variations: &[Variation],
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] _viewport_constants: &ViewportConstants,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] accum_constants: &AccumConstants,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 2)] _camera_constants: &CameraConstants,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 3)] accum_image: &mut [Vec4],
|
||||
#[spirv(storage_buffer, descriptor_set = 1, binding = 0)] thread_state: &mut [ThreadState],
|
||||
#[spirv(storage_buffer, descriptor_set = 1, binding = 1)] transforms: &[Transform],
|
||||
#[spirv(storage_buffer, descriptor_set = 1, binding = 2)] variations: &[Variation],
|
||||
) {
|
||||
let mut rng = thread_state[0].get_rng();
|
||||
let mut point = thread_state[0].point;
|
||||
@ -256,18 +291,23 @@ pub fn main_cs(
|
||||
point = next_transform(&mut rng, transform_weight, transforms).apply(variations, point);
|
||||
}
|
||||
|
||||
// ...because `<i32>.max(<i32>)` has compilation issues.
|
||||
let max_dimension = if image_constants.accum_width > image_constants.accum_height { image_constants.accum_width } else { image_constants.accum_height };
|
||||
// ...because `<i32>.min(<i32>)` has compilation issues.
|
||||
let min_dimension = if accum_constants.width < accum_constants.height {
|
||||
accum_constants.width
|
||||
} else {
|
||||
accum_constants.height
|
||||
};
|
||||
|
||||
// Fixed camera, should be provided by a uniform in the future
|
||||
let camera = CameraConstants {
|
||||
scale: max_dimension as f32 / 4.0,
|
||||
zoom: 0.0,
|
||||
scale: min_dimension as f32 / 4.0,
|
||||
zoom: 2.0,
|
||||
rotate: 0.0,
|
||||
offset_x: 0.0,
|
||||
offset_y: 0.0,
|
||||
offset_x: 0.5,
|
||||
offset_y: 0.5,
|
||||
}
|
||||
.camera(image_constants.accum_width, image_constants.accum_height);
|
||||
.camera(accum_constants.width, accum_constants.height);
|
||||
// let camera = camera_constants.camera(image_constants.accum_width, image_constants.accum_height);
|
||||
|
||||
// Iterate 100,000, should be provided by a uniform in the future
|
||||
for _i in 0..100_000 {
|
||||
@ -275,9 +315,9 @@ pub fn main_cs(
|
||||
|
||||
let pixel_coordinates = camera.transform_point2(point).as_ivec2();
|
||||
if pixel_coordinates.x < 0
|
||||
|| pixel_coordinates.x >= image_constants.accum_width
|
||||
|| pixel_coordinates.x >= accum_constants.width
|
||||
|| pixel_coordinates.y < 0
|
||||
|| pixel_coordinates.y >= image_constants.accum_height
|
||||
|| pixel_coordinates.y >= accum_constants.height
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -285,7 +325,7 @@ pub fn main_cs(
|
||||
let ii = image_index(
|
||||
pixel_coordinates.x,
|
||||
pixel_coordinates.y,
|
||||
image_constants.accum_height,
|
||||
accum_constants.width,
|
||||
);
|
||||
accum_image[ii as usize] = Color::WHITE;
|
||||
}
|
||||
@ -310,18 +350,24 @@ pub fn main_vs(
|
||||
#[spirv(fragment)]
|
||||
pub fn main_fs(
|
||||
#[spirv(frag_coord)] frag_coord: Vec4,
|
||||
#[spirv(push_constant)] ifs_constants: &ImageConstants,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] accum_image: &[Vec4],
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 0)] viewport_constants: &ViewportConstants,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 1)] accum_constants: &AccumConstants,
|
||||
#[spirv(storage_buffer, descriptor_set = 0, binding = 3)] accum_image: &mut [Vec4],
|
||||
output: &mut Vec4,
|
||||
) {
|
||||
// Bootleg texture sampling; map from viewport image pixel coordinates to accumulator image
|
||||
// pixel coordinates
|
||||
let viewport_coordinate = frag_coord.xy().as_uvec2();
|
||||
let viewport_offset = glam::ivec2(
|
||||
viewport_constants.offset_x,
|
||||
viewport_constants.offset_y,
|
||||
)
|
||||
.as_uvec2();
|
||||
let viewport_coordinate = frag_coord.xy().as_uvec2() - viewport_offset;
|
||||
|
||||
let a_width = ifs_constants.accum_width as f32;
|
||||
let a_height = ifs_constants.accum_height as f32;
|
||||
let v_width = ifs_constants.viewport_width as f32;
|
||||
let v_height = ifs_constants.viewport_height as f32;
|
||||
let a_width = accum_constants.width as f32;
|
||||
let a_height = accum_constants.height as f32;
|
||||
let v_width = viewport_constants.width as f32;
|
||||
let v_height = viewport_constants.height as f32;
|
||||
|
||||
// Scale both width and height by the same factor; preserves aspect ratio
|
||||
let scale_width = a_width / v_width;
|
||||
@ -335,16 +381,16 @@ pub fn main_fs(
|
||||
let accum_coordinate = viewport_coordinate.as_vec2() * scale - vec2(offset_x, offset_y);
|
||||
|
||||
if accum_coordinate.x < 0.0
|
||||
|| accum_coordinate.x >= ifs_constants.accum_width as f32
|
||||
|| accum_coordinate.x >= accum_constants.width as f32
|
||||
|| accum_coordinate.y < 0.0
|
||||
|| accum_coordinate.y >= ifs_constants.accum_height as f32
|
||||
|| accum_coordinate.y >= accum_constants.height as f32
|
||||
{
|
||||
*output = ifs_constants.background_color;
|
||||
*output = vec4(0.0, 0.0, 0.0, 1.0);
|
||||
} else {
|
||||
*output = accum_image[image_index(
|
||||
accum_coordinate.x as i32,
|
||||
accum_coordinate.y as i32,
|
||||
ifs_constants.accum_width,
|
||||
accum_constants.width,
|
||||
) as usize];
|
||||
}
|
||||
}
|
||||
|
@ -7,15 +7,25 @@ license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
approx = "0.5"
|
||||
bytemuck.workspace = true
|
||||
eframe = { version = "0.31", features = ["wgpu"]}
|
||||
egui = "0.31"
|
||||
egui-wgpu = "0.31"
|
||||
env_logger.workspace = true
|
||||
epaint = "0.31"
|
||||
flare-shader = { path = "../flare-shader" }
|
||||
futures = "0.3"
|
||||
futures-executor.workspace = true
|
||||
glam.workspace = true
|
||||
image = "0.25"
|
||||
puffin = "0.19"
|
||||
puffin_http = "0.16"
|
||||
rand = { workspace = true, default-features = true }
|
||||
rand_xoshiro.workspace = true
|
||||
wgpu.workspace = true
|
||||
winit.workspace = true
|
||||
log = "0.4.25"
|
||||
profiling = { version = "1.0", features = ["profile-with-puffin"]}
|
||||
|
||||
[build-dependencies]
|
||||
spirv-builder.workspace = true
|
||||
|
75
crates/flare/examples/transform_editor.rs
Normal file
75
crates/flare/examples/transform_editor.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use egui::{Frame, Ui};
|
||||
use flare::gui::transform_editor::TransformEditor;
|
||||
use flare_shader::Coefs;
|
||||
use log::info;
|
||||
|
||||
#[derive(Clone, Default, Debug)]
|
||||
struct TransformEditorApp {
|
||||
transform_editor: TransformEditor,
|
||||
transforms: Vec<Coefs>,
|
||||
}
|
||||
|
||||
impl TransformEditorApp {
|
||||
}
|
||||
|
||||
impl eframe::App for TransformEditorApp {
|
||||
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
|
||||
egui::menu::bar(ui, |ui| {
|
||||
ui.menu_button("File", |ui| {
|
||||
if ui.button("Quit").clicked() {
|
||||
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||
}
|
||||
});
|
||||
if ui.button("Add Transform").clicked() {
|
||||
self.transforms.push(Coefs::new(1.0, 0.0, 0.0, 0.0, 1.0, 0.0))
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
egui::TopBottomPanel::bottom("bottom_panel").show(ctx, |ui| {
|
||||
self.transform_editor.interact_debug(ui);
|
||||
});
|
||||
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
Frame::canvas(ui.style()).show(ui, |ui| {
|
||||
self.transform_editor.interact(ui, &mut self.transforms)
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
std::env::set_var("RUST_LOG", "info");
|
||||
env_logger::init();
|
||||
|
||||
let initial_dimensions = egui::vec2(800., 600.);
|
||||
let mut wgpu_options = egui_wgpu::WgpuConfiguration::default();
|
||||
wgpu_options.present_mode = wgpu::PresentMode::Immediate;
|
||||
wgpu_options.desired_maximum_frame_latency = Some(1);
|
||||
let native_options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_inner_size(initial_dimensions),
|
||||
wgpu_options,
|
||||
renderer: eframe::Renderer::Wgpu,
|
||||
vsync: false,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
puffin::set_scopes_on(true);
|
||||
match puffin_http::Server::new("localhost:8585") {
|
||||
Ok(server) => {
|
||||
info!("Server open");
|
||||
std::mem::forget(server);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// std::mem::forget(puffin_http::Server::new("127.0.0.1:8585").expect("Unable to start server"));
|
||||
|
||||
eframe::run_native(
|
||||
"transform_editor",
|
||||
native_options,
|
||||
Box::new(|cc| {
|
||||
Ok(Box::new(TransformEditorApp::default()))
|
||||
}),
|
||||
)
|
||||
}
|
1
crates/flare/src/gui/mod.rs
Normal file
1
crates/flare/src/gui/mod.rs
Normal file
@ -0,0 +1 @@
|
||||
pub mod transform_editor;
|
364
crates/flare/src/gui/transform_editor.rs
Normal file
364
crates/flare/src/gui/transform_editor.rs
Normal file
@ -0,0 +1,364 @@
|
||||
use egui::emath::RectTransform;
|
||||
use egui::*;
|
||||
use flare_shader::Coefs;
|
||||
use std::ops::Add;
|
||||
|
||||
/// Radius (in pixels) of the transform element draw circle
|
||||
const ELEMENT_DRAW_RADIUS_PX: f32 = 7.0;
|
||||
|
||||
/// Stroke size (in pixels) of the transform element draw circle
|
||||
const ELEMENT_DRAW_STROKE_PX: f32 = 2.0;
|
||||
|
||||
/// Offset (in pixels) of the transform element draw circle
|
||||
const ELEMENT_DRAW_OFFSET_PX: Vec2 = vec2(-2.0, -2.0);
|
||||
|
||||
fn test_in_circle(pt: Pos2, center: Pos2, radius: f32) -> bool {
|
||||
((pt.x - center.x).powf(2.0) + (pt.y - center.y).powf(2.0)) <= radius.powf(2.0)
|
||||
}
|
||||
|
||||
fn test_in_triangle(pt: Pos2, v1: Pos2, v2: Pos2, v3: Pos2) -> bool {
|
||||
// https://stackoverflow.com/a/2049593
|
||||
let sign = |p1: Pos2, p2: Pos2, p3: Pos2| -> f32 {
|
||||
(p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
|
||||
};
|
||||
|
||||
let d1 = sign(pt, v1, v2);
|
||||
let d2 = sign(pt, v2, v3);
|
||||
let d3 = sign(pt, v3, v1);
|
||||
|
||||
let has_neg = [d1, d2, d3].iter().any(|v| *v < 0.0);
|
||||
let has_pos = [d1, d2, d3].iter().any(|v| *v > 0.0);
|
||||
|
||||
!(has_neg && has_pos)
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum TransformElement {
|
||||
Origin,
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
/// Affine coefficients expressed as three points of a triangle
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub struct TransformTriangle {
|
||||
origin: Pos2,
|
||||
x: Pos2,
|
||||
y: Pos2,
|
||||
}
|
||||
|
||||
impl TransformTriangle {
|
||||
pub fn new(origin: Pos2, x: Pos2, y: Pos2) -> Self {
|
||||
Self { origin, x, y }
|
||||
}
|
||||
|
||||
pub fn interact_drag(self, element: TransformElement, drag_delta: Vec2) -> Self {
|
||||
match element {
|
||||
TransformElement::X => Self::new(self.origin, self.x + drag_delta, self.y),
|
||||
TransformElement::Y => Self::new(self.origin, self.x, self.y + drag_delta),
|
||||
TransformElement::Origin => Self::new(
|
||||
self.origin + drag_delta,
|
||||
self.x + drag_delta,
|
||||
self.y + drag_delta,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transform_pos(self, rect: RectTransform) -> Self {
|
||||
Self {
|
||||
origin: rect.transform_pos(self.origin),
|
||||
x: rect.transform_pos(self.x),
|
||||
y: rect.transform_pos(self.y),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_hovered(
|
||||
&self,
|
||||
hover_pos: Pos2,
|
||||
ifs_to_screen: RectTransform,
|
||||
) -> Option<TransformElement> {
|
||||
let origin_pos = ifs_to_screen.transform_pos(self.origin);
|
||||
let x_pos = ifs_to_screen.transform_pos(self.x);
|
||||
let y_pos = ifs_to_screen.transform_pos(self.y);
|
||||
|
||||
if test_in_circle(hover_pos, x_pos, ELEMENT_DRAW_RADIUS_PX) {
|
||||
Some(TransformElement::X)
|
||||
} else if test_in_circle(hover_pos, y_pos, ELEMENT_DRAW_RADIUS_PX) {
|
||||
Some(TransformElement::Y)
|
||||
} else if test_in_circle(hover_pos, origin_pos, ELEMENT_DRAW_RADIUS_PX)
|
||||
|| test_in_triangle(hover_pos, origin_pos, x_pos, y_pos)
|
||||
{
|
||||
Some(TransformElement::Origin)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Coefs> for TransformTriangle {
|
||||
fn from(value: Coefs) -> Self {
|
||||
let origin = pos2(value.c, -value.f);
|
||||
Self {
|
||||
origin,
|
||||
x: origin + vec2(value.a, -value.d),
|
||||
y: origin + vec2(-value.b, value.e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Coefs> for TransformTriangle {
|
||||
fn into(self) -> Coefs {
|
||||
Coefs {
|
||||
a: self.x.x - self.origin.x,
|
||||
b: self.origin.x - self.y.x,
|
||||
c: self.origin.x,
|
||||
d: self.origin.y - self.x.y,
|
||||
e: self.y.y - self.origin.y,
|
||||
f: -self.origin.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Add<Vec2> for TransformTriangle {
|
||||
type Output = TransformTriangle;
|
||||
|
||||
fn add(self, rhs: Vec2) -> Self::Output {
|
||||
Self {
|
||||
origin: self.origin + rhs,
|
||||
x: self.x + rhs,
|
||||
y: self.y + rhs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget for manipulating IFS transform affine coefficients
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct TransformEditor {
|
||||
/// Center point (in IFS coordinates) of the editor window
|
||||
center_ifs: Pos2,
|
||||
|
||||
/// Total range (in IFS coordinates) of the editor window
|
||||
range_ifs: f32,
|
||||
|
||||
/// Transform index the cursor is hovering over
|
||||
hover_index: Option<usize>,
|
||||
|
||||
/// Specific element of the transform hovered by the cursor
|
||||
hover_element: Option<TransformElement>,
|
||||
|
||||
/// Transform index the cursor is dragging
|
||||
drag_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl Default for TransformEditor {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
center_ifs: Pos2::ZERO,
|
||||
range_ifs: 4.0,
|
||||
hover_index: None,
|
||||
hover_element: None,
|
||||
drag_index: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_viewport_ifs(aspect_ratio: f32, center_ifs: Pos2, range_ifs: f32) -> Rect {
|
||||
let size_ifs = if aspect_ratio >= 1.0 {
|
||||
vec2(range_ifs * aspect_ratio, range_ifs)
|
||||
} else {
|
||||
vec2(range_ifs, range_ifs / aspect_ratio)
|
||||
};
|
||||
let min_ifs = center_ifs - size_ifs / 2.0;
|
||||
let max_ifs = center_ifs + size_ifs / 2.0;
|
||||
|
||||
// IFS coordinates follow the "value increases from top left to bottom right" convention.
|
||||
// Because we want coordinates to behave like a Cartesian plot, the Y-axis is flipped
|
||||
Rect::from_min_max(pos2(min_ifs.x, max_ifs.y), pos2(max_ifs.x, min_ifs.y))
|
||||
}
|
||||
|
||||
impl TransformEditor {
|
||||
/// Interact with the provided transform coefficients. Returns the index of the focused transform, if any
|
||||
pub fn interact(&mut self, ui: &mut Ui, transforms: &mut [Coefs]) -> Option<usize> {
|
||||
let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::drag());
|
||||
|
||||
if transforms.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let interact_rect = response.interact_rect;
|
||||
let ifs_rect = build_viewport_ifs(
|
||||
interact_rect.aspect_ratio(),
|
||||
self.center_ifs,
|
||||
self.range_ifs,
|
||||
);
|
||||
|
||||
// Update internal state based on screen interactions, then paint to screen
|
||||
let ifs_to_screen = RectTransform::from_to(ifs_rect, interact_rect);
|
||||
self.interact_update(
|
||||
ui.input(|i| i.pointer.interact_pos()),
|
||||
ui.input(|i| i.pointer.delta()),
|
||||
ui.input(|i| i.pointer.primary_pressed()),
|
||||
ui.input(|i| i.pointer.primary_released()),
|
||||
ifs_to_screen,
|
||||
transforms,
|
||||
);
|
||||
|
||||
self.interact_draw(painter, ifs_to_screen, transforms);
|
||||
|
||||
self.drag_index
|
||||
}
|
||||
|
||||
/// Update state of the provided transform coefficients based on current interactions,
|
||||
/// return the transform coefficients that have claimed focus (if any)
|
||||
fn interact_update(
|
||||
&mut self,
|
||||
hover_pos: Option<Pos2>,
|
||||
hover_delta: Vec2,
|
||||
primary_pressed: bool,
|
||||
primary_released: bool,
|
||||
ifs_to_screen: RectTransform,
|
||||
transforms: &mut [Coefs],
|
||||
) {
|
||||
// If the cursor is not in this widget, reset state
|
||||
if hover_pos.is_none() {
|
||||
self.hover_index = None;
|
||||
self.hover_element = None;
|
||||
self.drag_index = None;
|
||||
return;
|
||||
}
|
||||
|
||||
let hover_pos = hover_pos.unwrap();
|
||||
|
||||
// If the transform array was modified, reset state and then proceed
|
||||
if self.hover_index.map_or(false, |i| i >= transforms.len())
|
||||
|| self.drag_index.map_or(false, |i| i >= transforms.len())
|
||||
{
|
||||
self.hover_index = None;
|
||||
self.hover_element = None;
|
||||
self.drag_index = None;
|
||||
}
|
||||
|
||||
// If a transform is being dragged, update its position
|
||||
if self.drag_index.is_some() {
|
||||
let hover_index = self.hover_index.unwrap();
|
||||
let hover_element = self.hover_element.unwrap();
|
||||
|
||||
let hover_delta_ifs = hover_delta / ifs_to_screen.scale();
|
||||
let transform_triangle: TransformTriangle = transforms[hover_index].into();
|
||||
transforms[hover_index] = transform_triangle
|
||||
.interact_drag(hover_element, hover_delta_ifs)
|
||||
.into();
|
||||
}
|
||||
|
||||
// Check if the currently-hovered transform is still hovered
|
||||
let mut hover_found = false;
|
||||
|
||||
if let Some(hover_index) = self.hover_index {
|
||||
let transform_triangle: TransformTriangle = transforms[hover_index].into();
|
||||
self.hover_element = transform_triangle.is_hovered(hover_pos, ifs_to_screen);
|
||||
hover_found = self.hover_element.is_some();
|
||||
}
|
||||
|
||||
// Check if any transform is hovered
|
||||
if !hover_found {
|
||||
for (i, transform) in transforms.iter().enumerate() {
|
||||
let transform_triangle: TransformTriangle = (*transform).into();
|
||||
self.hover_element = transform_triangle.is_hovered(hover_pos, ifs_to_screen);
|
||||
|
||||
if self.hover_element.is_some() {
|
||||
hover_found = true;
|
||||
self.hover_index = Some(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !hover_found {
|
||||
// No hovers found, reset state
|
||||
self.hover_index = None;
|
||||
self.hover_element = None;
|
||||
self.drag_index = None;
|
||||
}
|
||||
|
||||
// Check drag state
|
||||
if primary_pressed && self.hover_index.is_some() {
|
||||
self.drag_index = self.hover_index;
|
||||
}
|
||||
if primary_released {
|
||||
self.drag_index = None;
|
||||
}
|
||||
}
|
||||
|
||||
fn interact_draw_transform(
|
||||
hover_element: Option<TransformElement>,
|
||||
painter: &Painter,
|
||||
ifs_to_screen: RectTransform,
|
||||
transform: Coefs,
|
||||
) {
|
||||
// `epaint` doesn't provide an option for whether the stroke is drawn inside, in the middle,
|
||||
// or outside the shape to paint. In manual testing, hover detection works best when assuming
|
||||
// the stroke is outside the shape.
|
||||
// Also in manual testing, hover detection seems to work best when drawing the circle at
|
||||
// a slight offset to the actual center position. Not clear why.
|
||||
let transform_triangle: TransformTriangle = transform.into();
|
||||
let draw_triangle =
|
||||
transform_triangle.transform_pos(ifs_to_screen) + ELEMENT_DRAW_OFFSET_PX;
|
||||
|
||||
let stroke = Stroke::new(ELEMENT_DRAW_STROKE_PX, Color32::BLUE);
|
||||
painter.circle_stroke(
|
||||
draw_triangle.origin,
|
||||
ELEMENT_DRAW_RADIUS_PX - ELEMENT_DRAW_STROKE_PX,
|
||||
stroke,
|
||||
);
|
||||
painter.circle_stroke(
|
||||
draw_triangle.x,
|
||||
ELEMENT_DRAW_RADIUS_PX - ELEMENT_DRAW_STROKE_PX,
|
||||
stroke,
|
||||
);
|
||||
painter.circle_stroke(
|
||||
draw_triangle.y,
|
||||
ELEMENT_DRAW_RADIUS_PX - ELEMENT_DRAW_STROKE_PX,
|
||||
stroke,
|
||||
);
|
||||
|
||||
let body_alpha: u8 = if hover_element.is_some() { 8 } else { 0 };
|
||||
let body_fill = Color32::from_rgba_unmultiplied(0, 0, u8::MAX, body_alpha);
|
||||
let body = Shape::convex_polygon(
|
||||
vec![draw_triangle.origin, draw_triangle.x, draw_triangle.y],
|
||||
body_fill,
|
||||
stroke,
|
||||
);
|
||||
painter.add(body);
|
||||
}
|
||||
|
||||
/// Draw the provided transform coefficients to the screen.
|
||||
fn interact_draw(&self, painter: Painter, ifs_to_screen: RectTransform, transforms: &[Coefs]) {
|
||||
transforms
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, _)| Some(*i) != self.hover_index)
|
||||
.for_each(|(_, transform)| {
|
||||
Self::interact_draw_transform(None, &painter, ifs_to_screen, *transform)
|
||||
});
|
||||
|
||||
self.hover_index.map(|i| {
|
||||
Self::interact_draw_transform(
|
||||
self.hover_element,
|
||||
&painter,
|
||||
ifs_to_screen,
|
||||
transforms[i],
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn interact_debug(&self, ui: &mut Ui) {
|
||||
Grid::new("transform_editor_debug")
|
||||
.num_columns(3)
|
||||
.show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.label(format!("Hover index: {:?}", self.hover_index));
|
||||
ui.label(format!("Hover element: {:?}", self.hover_element));
|
||||
ui.label(format!("Drag index: {:?}", self.drag_index));
|
||||
});
|
||||
}
|
||||
}
|
2
crates/flare/src/lib.rs
Normal file
2
crates/flare/src/lib.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod transform_editor;
|
||||
pub mod gui;
|
File diff suppressed because it is too large
Load Diff
581
crates/flare/src/main_winit.rs
Normal file
581
crates/flare/src/main_winit.rs
Normal file
@ -0,0 +1,581 @@
|
||||
use flare_shader::{Coefs, Color, ImageConstants, ThreadState, Transform, Variation, VariationKind};
|
||||
use futures_executor::block_on;
|
||||
use glam::Vec4;
|
||||
use rand::SeedableRng;
|
||||
use rand_xoshiro::Xoshiro128Plus;
|
||||
use std::sync::Arc;
|
||||
use wgpu::util::DeviceExt;
|
||||
use wgpu::{
|
||||
Adapter, Device, Features, Instance, Queue, ShaderModule, Surface, SurfaceConfiguration,
|
||||
};
|
||||
use winit::event::MouseButton;
|
||||
use winit::{
|
||||
application::ApplicationHandler,
|
||||
dpi::LogicalSize,
|
||||
event::WindowEvent,
|
||||
event_loop::{ActiveEventLoop, EventLoop},
|
||||
window::{Window, WindowAttributes, WindowId},
|
||||
};
|
||||
|
||||
struct AccumulatePipeline {
|
||||
device: Device,
|
||||
bind_group_layout: wgpu::BindGroupLayout,
|
||||
pipeline: wgpu::ComputePipeline,
|
||||
|
||||
thread_state: Option<wgpu::Buffer>,
|
||||
transforms: Option<wgpu::Buffer>,
|
||||
variations: Option<wgpu::Buffer>,
|
||||
accum_image: Option<wgpu::Buffer>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
}
|
||||
|
||||
impl AccumulatePipeline {
|
||||
const BIND_GROUP_LAYOUT_DESCRIPTOR: wgpu::BindGroupLayoutDescriptor<'static> =
|
||||
wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("accumulate"),
|
||||
entries: &[
|
||||
// thread_state
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: false },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
// transforms
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 1,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
// variations
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 2,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
// accum_image
|
||||
wgpu::BindGroupLayoutEntry {
|
||||
binding: 3,
|
||||
visibility: wgpu::ShaderStages::COMPUTE,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: false },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const N_THREADS: usize = 1;
|
||||
|
||||
pub fn new(device: &Device, module: &ShaderModule) -> Self {
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&Self::BIND_GROUP_LAYOUT_DESCRIPTOR);
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("accumulate"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[wgpu::PushConstantRange {
|
||||
stages: wgpu::ShaderStages::COMPUTE,
|
||||
range: 0..size_of::<ImageConstants>() as u32,
|
||||
}],
|
||||
});
|
||||
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
|
||||
label: Some("accumulate"),
|
||||
layout: Some(&pipeline_layout),
|
||||
module,
|
||||
entry_point: Some("main_cs"),
|
||||
compilation_options: Default::default(),
|
||||
cache: None,
|
||||
});
|
||||
|
||||
let rng = rand::thread_rng();
|
||||
let mut rng_xoshiro = Xoshiro128Plus::from_rng(rng).expect("Unable to seed thread_state");
|
||||
let mut thread_state_elements = vec![];
|
||||
for _i in 0..Self::N_THREADS {
|
||||
thread_state_elements.push(ThreadState::new(&mut rng_xoshiro));
|
||||
}
|
||||
let thread_state = Some(
|
||||
device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("accumulate/thread_state"),
|
||||
contents: bytemuck::cast_slice(&thread_state_elements),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
}),
|
||||
);
|
||||
|
||||
Self {
|
||||
device: device.clone(),
|
||||
bind_group_layout,
|
||||
pipeline,
|
||||
thread_state,
|
||||
transforms: None,
|
||||
variations: None,
|
||||
accum_image: None,
|
||||
bind_group: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_transforms(&mut self, transforms: &[Transform]) {
|
||||
// Should be smarter about allocation in the future
|
||||
self.transforms = Some(
|
||||
self.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("accumulate/transforms"),
|
||||
contents: bytemuck::cast_slice(transforms),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
}),
|
||||
);
|
||||
|
||||
// Setting a new buffer invalidates the existing bindings
|
||||
self.bind_group.take();
|
||||
}
|
||||
|
||||
pub fn set_variations(&mut self, variations: &[Variation]) {
|
||||
self.variations = Some(
|
||||
self.device
|
||||
.create_buffer_init(&wgpu::util::BufferInitDescriptor {
|
||||
label: Some("accumulate/variations"),
|
||||
contents: bytemuck::cast_slice(variations),
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
}),
|
||||
);
|
||||
|
||||
self.bind_group.take();
|
||||
}
|
||||
|
||||
pub fn set_accum_image(&mut self, accum_image: &wgpu::Buffer) {
|
||||
self.accum_image = Some(accum_image.clone());
|
||||
|
||||
// Setting a new buffer invalidates the existing bindings
|
||||
self.bind_group.take();
|
||||
}
|
||||
|
||||
fn fetch_bind_group(&mut self) -> &wgpu::BindGroup {
|
||||
self.bind_group.get_or_insert_with(|| {
|
||||
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("accumulate"),
|
||||
layout: &self.bind_group_layout,
|
||||
entries: &[
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: self
|
||||
.thread_state
|
||||
.as_ref()
|
||||
.expect("thread_state missing")
|
||||
.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 1,
|
||||
resource: self
|
||||
.transforms
|
||||
.as_ref()
|
||||
.expect("transforms missing")
|
||||
.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 2,
|
||||
resource: self
|
||||
.variations
|
||||
.as_ref()
|
||||
.expect("variations missing")
|
||||
.as_entire_binding(),
|
||||
},
|
||||
wgpu::BindGroupEntry {
|
||||
binding: 3,
|
||||
resource: self
|
||||
.accum_image
|
||||
.as_ref()
|
||||
.expect("accum_image missing")
|
||||
.as_entire_binding(),
|
||||
},
|
||||
],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(&mut self, encoder: &mut wgpu::CommandEncoder, constants: &ImageConstants) {
|
||||
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
|
||||
label: Some("accumulate"),
|
||||
timestamp_writes: None,
|
||||
});
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_push_constants(0, bytemuck::cast_slice(&[*constants]));
|
||||
pass.set_bind_group(0, self.fetch_bind_group(), &[]);
|
||||
pass.dispatch_workgroups(1, 1, 1);
|
||||
}
|
||||
}
|
||||
|
||||
struct RenderPipeline {
|
||||
device: Device,
|
||||
bind_group_layout: wgpu::BindGroupLayout,
|
||||
pipeline: wgpu::RenderPipeline,
|
||||
|
||||
accum_image: Option<wgpu::Buffer>,
|
||||
bind_group: Option<wgpu::BindGroup>,
|
||||
}
|
||||
|
||||
impl RenderPipeline {
|
||||
const BIND_GROUP_LAYOUT_DESCRIPTOR: wgpu::BindGroupLayoutDescriptor<'static> =
|
||||
wgpu::BindGroupLayoutDescriptor {
|
||||
label: Some("render"),
|
||||
entries: &[wgpu::BindGroupLayoutEntry {
|
||||
binding: 0,
|
||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||
ty: wgpu::BindingType::Buffer {
|
||||
ty: wgpu::BufferBindingType::Storage { read_only: true },
|
||||
has_dynamic_offset: false,
|
||||
min_binding_size: None,
|
||||
},
|
||||
count: None,
|
||||
}],
|
||||
};
|
||||
|
||||
pub fn new(device: &Device, module: &ShaderModule, format: wgpu::TextureFormat) -> Self {
|
||||
let bind_group_layout =
|
||||
device.create_bind_group_layout(&Self::BIND_GROUP_LAYOUT_DESCRIPTOR);
|
||||
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||
label: Some("render"),
|
||||
bind_group_layouts: &[&bind_group_layout],
|
||||
push_constant_ranges: &[wgpu::PushConstantRange {
|
||||
stages: wgpu::ShaderStages::FRAGMENT,
|
||||
range: 0..size_of::<ImageConstants>() as u32,
|
||||
}],
|
||||
});
|
||||
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||
label: Some("render"),
|
||||
layout: Some(&pipeline_layout),
|
||||
vertex: wgpu::VertexState {
|
||||
module,
|
||||
entry_point: Some("main_vs"),
|
||||
compilation_options: Default::default(),
|
||||
buffers: &[],
|
||||
},
|
||||
primitive: Default::default(),
|
||||
depth_stencil: None,
|
||||
multisample: Default::default(),
|
||||
fragment: Some(wgpu::FragmentState {
|
||||
module,
|
||||
entry_point: Some("main_fs"),
|
||||
compilation_options: Default::default(),
|
||||
targets: &[Some(wgpu::ColorTargetState {
|
||||
format,
|
||||
blend: None,
|
||||
write_mask: Default::default(),
|
||||
})],
|
||||
}),
|
||||
multiview: None,
|
||||
cache: None,
|
||||
});
|
||||
Self {
|
||||
device: device.clone(),
|
||||
bind_group_layout,
|
||||
pipeline,
|
||||
accum_image: None,
|
||||
bind_group: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_accum_image(&mut self, accum_image: &wgpu::Buffer) {
|
||||
self.accum_image = Some(accum_image.clone());
|
||||
self.bind_group.take();
|
||||
}
|
||||
|
||||
fn fetch_bind_group(&mut self) -> &wgpu::BindGroup {
|
||||
self.bind_group.get_or_insert_with(|| {
|
||||
self.device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||
label: Some("render"),
|
||||
layout: &self.bind_group_layout,
|
||||
entries: &[wgpu::BindGroupEntry {
|
||||
binding: 0,
|
||||
resource: self
|
||||
.accum_image
|
||||
.as_ref()
|
||||
.expect("accum_image missing")
|
||||
.as_entire_binding(),
|
||||
}],
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn run(
|
||||
&mut self,
|
||||
encoder: &mut wgpu::CommandEncoder,
|
||||
output_view: &wgpu::TextureView,
|
||||
constants: &ImageConstants,
|
||||
) {
|
||||
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||
label: Some("render"),
|
||||
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
|
||||
view: output_view,
|
||||
resolve_target: None,
|
||||
ops: Default::default(),
|
||||
})],
|
||||
depth_stencil_attachment: None,
|
||||
timestamp_writes: None,
|
||||
occlusion_query_set: None,
|
||||
});
|
||||
pass.set_pipeline(&self.pipeline);
|
||||
pass.set_push_constants(
|
||||
wgpu::ShaderStages::FRAGMENT,
|
||||
0,
|
||||
bytemuck::cast_slice(&[*constants]),
|
||||
);
|
||||
pass.set_bind_group(0, self.fetch_bind_group(), &[]);
|
||||
pass.draw(0..3, 0..1);
|
||||
}
|
||||
}
|
||||
|
||||
struct Flare {
|
||||
instance: Instance,
|
||||
adapter: Adapter,
|
||||
device: Device,
|
||||
queue: Queue,
|
||||
module: ShaderModule,
|
||||
}
|
||||
|
||||
impl Flare {
|
||||
pub fn new() -> Self {
|
||||
let backends = wgpu::Backends::from_env().unwrap_or_default();
|
||||
let instance = 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("Failed to find GPU adapter");
|
||||
|
||||
let required_limits = wgpu::Limits {
|
||||
max_push_constant_size: size_of::<ImageConstants>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let device = adapter.request_device(
|
||||
&wgpu::DeviceDescriptor {
|
||||
label: Some("flare"),
|
||||
required_features: Features::TIMESTAMP_QUERY | Features::PUSH_CONSTANTS,
|
||||
required_limits,
|
||||
memory_hints: Default::default(),
|
||||
},
|
||||
None,
|
||||
);
|
||||
let (device, queue) = block_on(device).expect("Failed to find GPU device");
|
||||
let module = device
|
||||
.create_shader_module(wgpu::include_spirv!(concat!(env!("OUT_DIR"), "/flare.spv")));
|
||||
|
||||
Flare {
|
||||
instance,
|
||||
adapter,
|
||||
device,
|
||||
queue,
|
||||
module,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FlareRender<'window> {
|
||||
flare: Arc<Flare>,
|
||||
surface: Surface<'window>,
|
||||
surface_configuration: SurfaceConfiguration,
|
||||
accumulate_pipeline: AccumulatePipeline,
|
||||
render_pipeline: RenderPipeline,
|
||||
ifs_constants: ImageConstants,
|
||||
accum_image: wgpu::Buffer,
|
||||
}
|
||||
|
||||
impl FlareRender<'_> {
|
||||
fn create_accum_image(device: &Device, width: u32, height: u32) -> wgpu::Buffer {
|
||||
let pixels = (width * height) as u64;
|
||||
let pixel_size = 4u64 * size_of::<f32>() as u64;
|
||||
let size = pixels * pixel_size;
|
||||
device.create_buffer(&wgpu::BufferDescriptor {
|
||||
label: Some("accum_image"),
|
||||
size,
|
||||
usage: wgpu::BufferUsages::STORAGE,
|
||||
mapped_at_creation: false,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new(flare: Arc<Flare>, window: Arc<Window>) -> Self {
|
||||
let window_size = window.inner_size();
|
||||
|
||||
let surface = flare
|
||||
.instance
|
||||
.create_surface(window.clone())
|
||||
.expect("Unable to create surface");
|
||||
let mut surface_configuration = surface
|
||||
.get_default_config(&flare.adapter, window_size.width, window_size.height)
|
||||
.expect("Unable to get surface config");
|
||||
surface_configuration.present_mode = wgpu::PresentMode::AutoVsync;
|
||||
surface.configure(&flare.device, &surface_configuration);
|
||||
|
||||
let mut accumulate_pipeline = AccumulatePipeline::new(&flare.device, &flare.module);
|
||||
let mut render_pipeline =
|
||||
RenderPipeline::new(&flare.device, &flare.module, surface_configuration.format);
|
||||
|
||||
let ifs_constants = ImageConstants::new(
|
||||
window_size.width as i32,
|
||||
window_size.height as i32,
|
||||
window_size.width as i32,
|
||||
window_size.height as i32,
|
||||
Vec4::BLACK,
|
||||
);
|
||||
|
||||
let accum_image =
|
||||
Self::create_accum_image(&flare.device, window_size.width, window_size.height);
|
||||
|
||||
accumulate_pipeline.set_accum_image(&accum_image);
|
||||
render_pipeline.set_accum_image(&accum_image);
|
||||
|
||||
Self {
|
||||
flare,
|
||||
surface,
|
||||
surface_configuration,
|
||||
accumulate_pipeline,
|
||||
render_pipeline,
|
||||
ifs_constants,
|
||||
accum_image,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(&mut self, run_accumulate: bool) {
|
||||
let output = self
|
||||
.surface
|
||||
.get_current_texture()
|
||||
.expect("Failed to get current texture");
|
||||
let output_view = output.texture.create_view(&Default::default());
|
||||
let mut encoder = self
|
||||
.flare
|
||||
.device
|
||||
.create_command_encoder(&Default::default());
|
||||
|
||||
if run_accumulate {
|
||||
self.accumulate_pipeline
|
||||
.run(&mut encoder, &self.ifs_constants);
|
||||
}
|
||||
self.render_pipeline
|
||||
.run(&mut encoder, &output_view, &self.ifs_constants);
|
||||
|
||||
self.flare.queue.submit(Some(encoder.finish()));
|
||||
output.present();
|
||||
}
|
||||
|
||||
pub fn resize_accumulate(&mut self) {
|
||||
let vp_dimensions = self.ifs_constants.viewport_dimensions();
|
||||
self.ifs_constants
|
||||
.with_accumulate(vp_dimensions.x, vp_dimensions.y);
|
||||
|
||||
let accum_image =
|
||||
Self::create_accum_image(&self.flare.device, vp_dimensions.x as u32, vp_dimensions.y as u32);
|
||||
self.accumulate_pipeline.set_accum_image(&accum_image);
|
||||
self.render_pipeline.set_accum_image(&accum_image);
|
||||
self.accum_image = accum_image;
|
||||
}
|
||||
|
||||
pub fn resize_viewport(&mut self, width: u32, height: u32) {
|
||||
self.surface_configuration.width = width;
|
||||
self.surface_configuration.height = height;
|
||||
self.surface
|
||||
.configure(&self.flare.device, &self.surface_configuration);
|
||||
self.ifs_constants.with_viewport(width as i32, height as i32);
|
||||
}
|
||||
}
|
||||
|
||||
struct Application<'window> {
|
||||
flare: Arc<Flare>,
|
||||
window: Option<Arc<Window>>,
|
||||
flare_render: Option<FlareRender<'window>>,
|
||||
}
|
||||
|
||||
impl Application<'_> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
flare: Arc::new(Flare::new()),
|
||||
window: None,
|
||||
flare_render: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ApplicationHandler for Application<'_> {
|
||||
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
|
||||
let attributes = WindowAttributes::default()
|
||||
.with_title("Flare")
|
||||
.with_inner_size(LogicalSize::new(1024, 768));
|
||||
|
||||
let window = Arc::new(
|
||||
event_loop
|
||||
.create_window(attributes)
|
||||
.expect("Failed to create window"),
|
||||
);
|
||||
|
||||
self.window = Some(window.clone());
|
||||
|
||||
let mut flare_render = FlareRender::new(self.flare.clone(), window);
|
||||
|
||||
let f0 = Transform::new(Coefs::new(0.5, 0., 0., 0., 0.5, 0.), 0, 1, 1.0);
|
||||
let f1 = Transform::new(Coefs::new(0.5, 0., 0.5, 0., 0.5, 0.), 0, 1, 1.0);
|
||||
let f2 = Transform::new(Coefs::new(0.5, 0., 0., 0., 0.5, 0.5), 0, 1, 1.0);
|
||||
let variation = Variation::new(VariationKind::Linear, 1.0);
|
||||
|
||||
flare_render.accumulate_pipeline.set_transforms(&[f0, f1, f2]);
|
||||
flare_render.accumulate_pipeline.set_variations(&[variation]);
|
||||
|
||||
flare_render.render(true);
|
||||
|
||||
self.flare_render = Some(flare_render);
|
||||
}
|
||||
|
||||
fn window_event(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
_window_id: WindowId,
|
||||
event: WindowEvent,
|
||||
) {
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
let flare_render = self.flare_render.as_mut().unwrap();
|
||||
flare_render.resize_viewport(size.width, size.height);
|
||||
flare_render.render(false);
|
||||
}
|
||||
WindowEvent::MouseInput {
|
||||
button: MouseButton::Left,
|
||||
..
|
||||
} => {
|
||||
let flare_render = self.flare_render.as_mut().unwrap();
|
||||
flare_render.resize_accumulate();
|
||||
flare_render.render(true);
|
||||
}
|
||||
WindowEvent::CloseRequested => event_loop.exit(),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
let _module = wgpu::include_spirv!(concat!(env!("OUT_DIR"), "/flare.spv"));
|
||||
|
||||
let event_loop = EventLoop::new()?;
|
||||
let mut application = Application::new();
|
||||
event_loop.run_app(&mut application)?;
|
||||
|
||||
Ok(())
|
||||
}
|
303
crates/flare/src/transform_editor.rs
Normal file
303
crates/flare/src/transform_editor.rs
Normal file
@ -0,0 +1,303 @@
|
||||
use bytemuck::Contiguous;
|
||||
use egui::{DragValue, Rect, Sense, Ui, emath};
|
||||
use epaint::{Color32, Shape, Stroke};
|
||||
use log::info;
|
||||
use flare_shader::Coefs;
|
||||
|
||||
pub const TRANSFORM_POINT_RADIUS: f32 = 0.03;
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||
pub enum TransformElement {
|
||||
Origin,
|
||||
X,
|
||||
Y,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Transform {
|
||||
origin: egui::Pos2,
|
||||
x: egui::Pos2,
|
||||
y: egui::Pos2,
|
||||
active_element: Option<TransformElement>,
|
||||
}
|
||||
|
||||
impl Transform {
|
||||
pub fn check_active(&mut self, hover_pos: Option<egui::Pos2>, drag_delta: egui::Vec2) -> bool {
|
||||
if hover_pos.is_none() {
|
||||
self.active_element.take();
|
||||
return false;
|
||||
}
|
||||
|
||||
let hover_pos = hover_pos.unwrap();
|
||||
self.active_element = if test_point_in_circle(hover_pos, self.x + drag_delta, TRANSFORM_POINT_RADIUS * 2.0) {
|
||||
Some(TransformElement::X)
|
||||
} else if test_point_in_circle(hover_pos, self.y + drag_delta, TRANSFORM_POINT_RADIUS * 2.0) {
|
||||
Some(TransformElement::Y)
|
||||
} else if test_point_in_circle(hover_pos, self.origin + drag_delta, TRANSFORM_POINT_RADIUS * 2.0)
|
||||
|| test_point_in_triangle(hover_pos, self.origin + drag_delta, self.x + drag_delta, self.y + drag_delta)
|
||||
{
|
||||
Some(TransformElement::Origin)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
self.active_element.is_some()
|
||||
}
|
||||
|
||||
pub fn drag_update(&mut self, drag_delta: egui::Vec2) {
|
||||
if let Some(active_element) = self.active_element {
|
||||
match active_element {
|
||||
TransformElement::X => self.x += drag_delta,
|
||||
TransformElement::Y => self.y += drag_delta,
|
||||
TransformElement::Origin => {
|
||||
self.origin += drag_delta;
|
||||
self.x += drag_delta;
|
||||
self.y += drag_delta;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui_draw(&self, painter: &egui::Painter, to_screen: emath::RectTransform, drag_delta: egui::Vec2) {
|
||||
let color_active = Color32::from_rgba_unmultiplied(0, 0, u8::MAX_VALUE, 8);
|
||||
|
||||
let stroke = Stroke::new(2.0, Color32::BLUE);
|
||||
|
||||
let origin_ifs = match self.active_element {
|
||||
Some(TransformElement::Origin) => self.origin + drag_delta,
|
||||
_ => self.origin
|
||||
};
|
||||
let origin_screen = to_screen.transform_pos(origin_ifs);
|
||||
|
||||
let x_ifs = match self.active_element {
|
||||
Some(TransformElement::X | TransformElement::Origin) => self.x + drag_delta,
|
||||
_ => self.x
|
||||
};
|
||||
let x_screen = to_screen.transform_pos(x_ifs);
|
||||
|
||||
let y_ifs = match self.active_element {
|
||||
Some(TransformElement::Y | TransformElement::Origin) => self.y + drag_delta,
|
||||
_ => self.y
|
||||
};
|
||||
let y_screen = to_screen.transform_pos(y_ifs);
|
||||
|
||||
let body_color = if self.active_element.is_some() {
|
||||
color_active
|
||||
} else {
|
||||
Color32::TRANSPARENT
|
||||
};
|
||||
painter.add(Shape::convex_polygon(
|
||||
vec![origin_screen, x_screen, y_screen],
|
||||
body_color,
|
||||
stroke,
|
||||
));
|
||||
|
||||
let point_radius = to_screen.scale().min_elem() * TRANSFORM_POINT_RADIUS;
|
||||
|
||||
let origin_color = match self.active_element {
|
||||
Some(TransformElement::Origin) => color_active,
|
||||
_ => Color32::TRANSPARENT,
|
||||
};
|
||||
painter.add(Shape::circle_stroke(origin_screen, point_radius, stroke));
|
||||
painter.add(Shape::circle_filled(
|
||||
origin_screen,
|
||||
point_radius,
|
||||
origin_color,
|
||||
));
|
||||
|
||||
let x_color = match self.active_element {
|
||||
Some(TransformElement::X) => color_active,
|
||||
_ => Color32::TRANSPARENT,
|
||||
};
|
||||
painter.add(Shape::circle_stroke(x_screen, point_radius, stroke));
|
||||
painter.add(Shape::circle_filled(x_screen, point_radius, x_color));
|
||||
|
||||
let y_color = match self.active_element {
|
||||
Some(TransformElement::X) => color_active,
|
||||
_ => Color32::TRANSPARENT,
|
||||
};
|
||||
painter.add(Shape::circle_stroke(y_screen, point_radius, stroke));
|
||||
painter.add(Shape::circle_filled(y_screen, point_radius, y_color));
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Transform {
|
||||
fn default() -> Self {
|
||||
Transform {
|
||||
origin: egui::pos2(0.0, 0.0),
|
||||
x: egui::pos2(1.0, 0.0),
|
||||
y: egui::pos2(0.0, -1.0),
|
||||
active_element: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct TransformEditor {
|
||||
transforms: Vec<Transform>,
|
||||
hover_index: Option<usize>,
|
||||
hover_pos: Option<egui::Pos2>,
|
||||
drag_index: Option<usize>,
|
||||
drag_start_pos: Option<egui::Pos2>,
|
||||
}
|
||||
|
||||
fn test_point_in_triangle(pt: egui::Pos2, v1: egui::Pos2, v2: egui::Pos2, v3: egui::Pos2) -> bool {
|
||||
puffin::profile_function!();
|
||||
|
||||
// https://stackoverflow.com/a/2049593
|
||||
let sign = |p1: egui::Pos2, p2: egui::Pos2, p3: egui::Pos2| -> f32 {
|
||||
(p1.x - p3.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p3.y)
|
||||
};
|
||||
|
||||
let d1 = sign(pt, v1, v2);
|
||||
let d2 = sign(pt, v2, v3);
|
||||
let d3 = sign(pt, v3, v1);
|
||||
|
||||
let has_neg = [d1, d2, d3].iter().any(|v| *v < 0.0);
|
||||
let has_pos = [d1, d2, d3].iter().any(|v| *v > 0.0);
|
||||
|
||||
!(has_neg && has_pos)
|
||||
}
|
||||
|
||||
fn test_point_in_circle(pt: egui::Pos2, center: egui::Pos2, radius: f32) -> bool {
|
||||
((pt.x - center.x).powf(2.0) + (pt.y - center.y).powf(2.0)) < radius.powf(2.0)
|
||||
}
|
||||
|
||||
impl TransformEditor {
|
||||
pub fn new() -> Self {
|
||||
let mut editor = TransformEditor::default();
|
||||
editor.add_transform();
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
pub fn add_transform(&mut self) {
|
||||
self.transforms.push(Default::default());
|
||||
}
|
||||
|
||||
fn check_active(&mut self, hover_pos: Option<egui::Pos2>) {
|
||||
// Find the active transform; the previously active transform has priority
|
||||
let drag_delta = self.drag_delta();
|
||||
if let Some(hover_index) = self.hover_index {
|
||||
if self.transforms[hover_index].check_active(hover_pos, drag_delta) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
for (i, transform) in self.transforms.iter_mut().enumerate() {
|
||||
if transform.check_active(hover_pos, drag_delta) {
|
||||
self.hover_index = Some(i);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
self.hover_index.take();
|
||||
}
|
||||
|
||||
fn drag_delta(&self) -> egui::Vec2 {
|
||||
if let (Some(hover_pos), Some(drag_start_pos)) = (self.hover_pos, self.drag_start_pos) {
|
||||
hover_pos - drag_start_pos
|
||||
} else {
|
||||
egui::Vec2::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui_update(
|
||||
&mut self,
|
||||
hover_pos: Option<egui::Pos2>,
|
||||
drag_delta: egui::Vec2,
|
||||
clicked: bool,
|
||||
drag_started: bool,
|
||||
drag_ended: bool,
|
||||
) {
|
||||
if self.transforms.is_empty() {
|
||||
self.hover_index.take();
|
||||
self.drag_index.take();
|
||||
self.drag_start_pos.take();
|
||||
return;
|
||||
}
|
||||
|
||||
self.hover_pos = hover_pos;
|
||||
self.check_active(hover_pos);
|
||||
|
||||
if clicked || drag_started {
|
||||
self.drag_index = self.hover_index;
|
||||
self.drag_start_pos = Some(hover_pos.unwrap() - drag_delta);
|
||||
}
|
||||
|
||||
if drag_ended {
|
||||
if let Some(drag_index) = self.drag_index {
|
||||
let drag_delta = self.drag_delta();
|
||||
info!("Applying drag delta {:?}", drag_delta);
|
||||
self.transforms[drag_index].drag_update(drag_delta);
|
||||
}
|
||||
|
||||
self.drag_index.take();
|
||||
self.drag_start_pos.take();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ui_draw(&mut self, painter: &egui::Painter, to_screen: emath::RectTransform) {
|
||||
let drag_delta = if let (Some(hover_pos), Some(drag_start_pos)) = (self.hover_pos, self.drag_start_pos) {
|
||||
hover_pos - drag_start_pos
|
||||
} else {
|
||||
egui::Vec2::ZERO
|
||||
};
|
||||
|
||||
for (index, transform) in self.transforms.iter().enumerate() {
|
||||
// Active transform is painted at the end to maintain priority
|
||||
if self.hover_index.map_or(false, |i| i == index) {
|
||||
continue;
|
||||
}
|
||||
|
||||
transform.ui_draw(painter, to_screen, drag_delta);
|
||||
}
|
||||
|
||||
self.hover_index.map(|i| self.transforms[i].ui_draw(painter, to_screen, drag_delta));
|
||||
}
|
||||
|
||||
pub fn ui(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) -> egui::Response {
|
||||
let (response, painter) = ui.allocate_painter(ui.available_size(), Sense::click_and_drag());
|
||||
|
||||
// Step one: set up conversions between screen space and IFS space coordinates
|
||||
let interact_rect = response.interact_rect;
|
||||
let interact_max_dim = interact_rect.width().max(interact_rect.height());
|
||||
let interact_min_dim = interact_rect.width().min(interact_rect.height());
|
||||
let interact_max_is_width = interact_max_dim == interact_rect.width();
|
||||
let ifs_scale = interact_max_dim / interact_min_dim * 4.0;
|
||||
|
||||
let ifs_min = if interact_max_is_width {
|
||||
egui::pos2(-ifs_scale / 2.0, -2.0)
|
||||
} else {
|
||||
egui::pos2(-2.0, -ifs_scale / 2.0)
|
||||
};
|
||||
|
||||
let transform_area = Rect::from_min_max(ifs_min, ifs_min * -1.0);
|
||||
let to_screen = emath::RectTransform::from_to(transform_area, response.interact_rect);
|
||||
let to_ifs = emath::RectTransform::from_to(response.interact_rect, transform_area);
|
||||
|
||||
egui::TopBottomPanel::bottom("response_stats").show(ctx, |ui| {
|
||||
let hover_pos_string = response.hover_pos().map_or_else(|| "None".to_owned(), |p| {
|
||||
let ifs_pos = to_ifs.transform_pos(p);
|
||||
format!("({} {})", ifs_pos.x, ifs_pos.y)
|
||||
});
|
||||
ui.label(format!("Hover Pos: {}", hover_pos_string));
|
||||
ui.label(format!("Hover Index: {:?}", self.hover_index));
|
||||
ui.label(format!("Drag Index: {:?}", self.drag_index));
|
||||
});
|
||||
|
||||
// Step two: update internal state based on recent interactions
|
||||
self.ui_update(
|
||||
response.hover_pos().map(|v| to_ifs.transform_pos(v)),
|
||||
response.drag_delta() / interact_max_dim,
|
||||
response.clicked(),
|
||||
response.drag_started(),
|
||||
response.drag_stopped(),
|
||||
);
|
||||
|
||||
// Step three: draw the transforms
|
||||
self.ui_draw(&painter, to_screen);
|
||||
|
||||
response
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user