diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f86208..97e4a1d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest, macos-latest] - bin: [footsteps, blobs, fibonacci, triangles, peace, attractors, waves, waves2, cafe_wall, munker, pdiamond, lilac, fourier, nblur, pixelrain, pina, hole,neuralnet, bhole, oscillation, chladni, hilbert, hilbertimg, snowflake, mandelbrot, leviant, eyes, imgblob, scramble, pixelate, rainbowimage,gaborill,munkerclock, voronoi, lorenz, ulam, dfft, butter2d, gaborwgpu, galaxy, chladniwgpu, snowflakewgpu, spiralimgwgpu, neuralnetwgpu, imlenswgpu, fbmflowgpu, lovewgpu, neurons, asahi, voronoiwgpu, fluid, asahi2, sinh, tree, expmandelbrotgpu, pupils, pixelflow,darkclouds, tunnel, neurons2, nebula, pixelsum, smoothneurons,gaussiansplat,gaborimage,rorschach,stripes,psychology,3dneuron,mandelbulb,dottedlines,ornaments,faketunnel] + bin: [footsteps, blobs, fibonacci, triangles, peace, attractors, waves, waves2, cafe_wall, munker, pdiamond, lilac, fourier, nblur, pixelrain, pina, hole,neuralnet, bhole, oscillation, chladni, hilbert, hilbertimg, snowflake, mandelbrot, leviant, eyes, imgblob, scramble, pixelate, rainbowimage,gaborill,munkerclock, voronoi, lorenz, ulam, dfft, butter2d, gaborwgpu, galaxy, chladniwgpu, snowflakewgpu, spiralimgwgpu, neuralnetwgpu, imlenswgpu, fbmflowgpu, lovewgpu, neurons, asahi, voronoiwgpu, fluid, asahi2, sinh, tree, expmandelbrotgpu, pupils, pixelflow,darkclouds, tunnel, neurons2, nebula, pixelsum, smoothneurons,gaussiansplat,gaborimage,rorschach,stripes,psychology,3dneuron,mandelbulb,dottedlines,ornaments,faketunnel,smoothvoro] include: - target: x86_64-unknown-linux-gnu ext: "" diff --git a/Cargo.toml b/Cargo.toml index 50718c2..5b4636b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -309,4 +309,7 @@ path = "src/ornaments.rs" [[bin]] name= "faketunnel" -path = "src/faketunnel.rs" \ No newline at end of file +path = "src/faketunnel.rs" +[[bin]] +name= "smoothvoro" +path = "src/smoothvoro.rs" diff --git a/shaders/smoothvoro.wgsl b/shaders/smoothvoro.wgsl new file mode 100644 index 0000000..872e1f0 --- /dev/null +++ b/shaders/smoothvoro.wgsl @@ -0,0 +1,126 @@ +@group(0) @binding(0) var tex: texture_2d; +@group(0) @binding(1) var tex_sampler: sampler; + +struct TimeUniform { + time: f32, +}; + +@group(1) @binding(0) +var u_time: TimeUniform; + +const PI: f32 = 3.14; + +struct Params { + lambda: f32, + theta: f32, + alpha: f32, + sigma: f32, + gamma: f32, + blue: f32, +}; + +@group(2) @binding(2) +var params: Params; + +fn Hash11(n: f32) -> f32 { + return fract(sin(n) * 12.5453123); +} + +fn osc2(minValue: f32, maxValue: f32, interval: f32, currentTime: f32) -> f32 { + return minValue + (maxValue - minValue) * 0.5 * (sin(2.0 * PI * currentTime / interval) + 1.0); +} + +fn StaticPoint(cell: vec2) -> vec2 { + let freq = Hash11(dot(cell, vec2(12.9898, 4.1414))) * 2.0 + 1.0; + let phase = Hash11(dot(cell, vec2(63.7264, 10.873))); + let amp = 0.5 + 0.4 * Hash11(dot(cell, vec2(305.21, 532.83))); + return vec2(cos(phase * 6.2831), sin(phase * 6.2831)) * amp; +} + +fn points(cell: vec2, time: f32) -> vec2 { + let freq = Hash11(dot(cell, vec2(12.9898, 4.1414))) * params.gamma +params.gamma; + let phase = Hash11(dot(cell, vec2(63.7264, 10.873))); + let amp = 0.5 + 0.4 * Hash11(dot(cell, vec2(305.21, 532.83))); + let t = time * freq + phase * 6.2831; + return vec2(cos(t), sin(t)) * amp; +} + +fn sminExp(a: f32, b: f32, k: f32) -> f32 { + let res = exp(-k * a) + exp(-k * b); + return -log(res) / k; +} + +fn borders(minDist: f32, smd: f32) -> f32 { + let edgeWidth = params.blue; + let edgeBlend = smoothstep(minDist, minDist + edgeWidth, smd); + return 1.0 - edgeBlend; +} + +fn smoothVoronoi(uv: vec2, time: f32, border: ptr, smd: ptr, ctp: ptr>, closepoint: ptr>) -> f32 { + let g = floor(uv); + let f = fract(uv); + let POWER = osc2(params.alpha, params.alpha, 11.0, time); + + let k = 1.0; + var minDist = POWER; + *smd = 1.0; + *ctp = vec2(0.0); + *closepoint = vec2(0.0); + for (var y = -2; y <= 2; y = y + 1) { + for (var x = -2; x <= 2; x = x + 1) { + let lattice = vec2(f32(x), f32(y)); + let offset = points(g + lattice, time); + let staticOffset = StaticPoint(g + lattice); + let point = lattice + offset - f; + let staticPoint = lattice + staticOffset - f; + let dist = dot(point, point); + if (dist < minDist) { + *smd = minDist; + minDist = dist; + *ctp = g + lattice + offset; + *closepoint = g + lattice + staticOffset; + } else if (dist < *smd) { + *smd = dist; + } + } + } + *border = borders(minDist, *smd); + return sqrt(minDist); +} + +fn calcNormal(uv: vec2, time: f32) -> vec3 { + let eps = 0.001; + var border: f32 = 0.0; + var smd: f32 = 0.0; + var ctp: vec2 = vec2(0.0); + var closepoint: vec2 = vec2(0.0); + let dist = smoothVoronoi(uv, time, &border, &smd, &ctp, &closepoint); + let dx = smoothVoronoi(uv + vec2(eps, 0.0), time, &border, &smd, &ctp, &closepoint) - dist; + let dy = smoothVoronoi(uv + vec2(0.0, eps), time, &border, &smd, &ctp, &closepoint) - dist; + return normalize(vec3(dx, dy, eps)); +} + +@fragment +fn main(@builtin(position) FragCoord: vec4, @location(0) tex_coords: vec2) -> @location(0) vec4 { + let resolution: vec2 = vec2(1920.0, 1080.0); + let uv: vec2 = params.lambda*(FragCoord.xy / resolution); + let time = u_time.time; + var border: f32 = 0.0; + var smd: f32 = 0.0; + var ctp: vec2 = vec2(0.0); + var closepoint: vec2 = vec2(0.0); + let dist = smoothVoronoi(uv, time, &border, &smd, &ctp, &closepoint); + + let textureColor = textureSample(tex, tex_sampler, closepoint.xy / params.lambda); + + let normal = calcNormal(uv, time); + let lightDir = normalize(vec3(1.0, 1.0, 1.0)); + let lightIntensity = max(dot(normal, lightDir), params.theta); + + let lighting = mix(vec3(0.1), vec3(1.0), lightIntensity); + var color = textureColor.rgb * lighting; + + color = mix(color, vec3(1.0, 1.0, 1.0), border * params.sigma); + + return vec4(color, 1.0); +} diff --git a/src/smoothvoro.rs b/src/smoothvoro.rs new file mode 100644 index 0000000..119ccfc --- /dev/null +++ b/src/smoothvoro.rs @@ -0,0 +1,335 @@ +use nannou::image; +use nannou::image::{open,RgbaImage,DynamicImage}; +use nannou::prelude::*; +use nannou_egui::{self, egui, Egui}; +use rfd::FileDialog; +use std::option::Option; +use nannou::wgpu::Texture; +struct Model { + bind_group: wgpu::BindGroup, + render_pipeline: wgpu::RenderPipeline, + vertex_buffer: wgpu::Buffer, + time_uniform: wgpu::Buffer, + time_bind_group: wgpu::BindGroup, + params_uniform: wgpu::Buffer, + params_bind_group: wgpu::BindGroup, + settings:Settings, + egui:Egui, + img: Option, + texture: Option, + sampler: wgpu::Sampler, + bind_group_layout: wgpu::BindGroupLayout, +} + +struct Settings { + lambda: f32, + theta: f32, + alpha:f32, + sigma: f32, + gamma:f32, + blue:f32, + show_ui: bool, + open_file_dialog: bool, +} +#[repr(C)] +#[derive(Clone, Copy)] +struct Vertex { + position: [f32; 2], +} +const VERTICES: [Vertex; 4] = [ + Vertex { + position: [-1.0, 1.0], + }, + Vertex { + position: [-1.0, -1.0], + }, + Vertex { + position: [1.0, 1.0], + }, + Vertex { + position: [1.0, -1.0], + }, +]; +fn main() { + nannou::app(model) + .update(update) + .run(); +} +fn update(app: &App, model: &mut Model, update: Update) { + let egui = &mut model.egui; + egui.set_elapsed_time(update.since_start); + let ctx = egui.begin_frame(); + if app.keys.down.contains(&Key::H) { + model.settings.show_ui = !model.settings.show_ui; + } + let mut open_file_dialog: bool = model.settings.open_file_dialog; + egui::Window::new("Shader Settings").show(&ctx, |ui| { + if ui.button("Load Image").clicked() { + open_file_dialog = true; + } + ui.add(egui::Slider::new(&mut model.settings.lambda, 1.0..=400.0).text("sample")); + ui.add(egui::Slider::new(&mut model.settings.theta, -1.0..=1.0).text("S")); + ui.add(egui::Slider::new(&mut model.settings.alpha, 0.0..=1.0).text("a")); + ui.add(egui::Slider::new(&mut model.settings.sigma, -1.0..=1.0).text("r")); + ui.add(egui::Slider::new(&mut model.settings.gamma, 0.0..=10.0).text("g")); + ui.add(egui::Slider::new(&mut model.settings.blue, -15.0..=15.0).text("b")); + }); + if open_file_dialog { + if let Some(file_path) = FileDialog::new().pick_file() { + if let Ok(img) = open(&file_path).map(|i| i.to_rgba8()) { + let dyn_image = DynamicImage::ImageRgba8(img.clone()); + model.img = Some(img); + let main_window = app.main_window(); + let device = main_window.device(); + let new_texture = Texture::from_image(app, &dyn_image); + model.texture = Some(new_texture); + let new_texture_view = model.texture.as_ref().unwrap().view().build(); + model.bind_group = create_bind_group(device, &model.bind_group_layout, &new_texture_view, &model.sampler); + } + model.settings.open_file_dialog = false; + } + } + let params_data = [model.settings.lambda, model.settings.theta, model.settings.alpha, model.settings.sigma, model.settings.gamma, model.settings.blue]; + let params_bytes = bytemuck::cast_slice(¶ms_data); + app.main_window().queue().write_buffer(&model.params_uniform, 0, ¶ms_bytes); +} + +fn raw_window_event(app: &App, model: &mut Model, event: &nannou::winit::event::WindowEvent) { + model.egui.handle_raw_event(event); + if let nannou::winit::event::WindowEvent::KeyboardInput { input, .. } = event { + if let (Some(nannou::winit::event::VirtualKeyCode::F), true) = + (input.virtual_keycode, input.state == nannou::winit::event::ElementState::Pressed) + { + let window = app.main_window(); + let fullscreen = window.fullscreen().is_some(); + window.set_fullscreen(!fullscreen); + } + } +} +fn model(app: &App) -> Model { + + let w_id = app.new_window().raw_event(raw_window_event).size(800, 600).view(view).build().unwrap(); + let window = app.window(w_id).unwrap(); + let device = window.device(); + let format = Frame::TEXTURE_FORMAT; + let msaa_samples = window.msaa_samples(); + let vs_desc = wgpu::include_wgsl!("../shaders/verteximg.wgsl"); + let fs_desc = wgpu::include_wgsl!("../shaders/smoothvoro.wgsl"); + let vs_mod = device.create_shader_module(vs_desc); + let fs_mod = device.create_shader_module(fs_desc); + let sampler_desc = wgpu::SamplerBuilder::new().into_descriptor(); + let sampler_filtering = wgpu::sampler_filtering(&sampler_desc); + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + label: Some("Texture Sampler"), + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Linear, + min_filter: wgpu::FilterMode::Linear, + mipmap_filter: wgpu::FilterMode::Linear, + ..Default::default() + }); + let settings = Settings { + lambda:50.0, + theta:0.5, + alpha:1.0, + sigma:0.1, + gamma:2.0, + blue:0.05, + show_ui:true, + open_file_dialog:false, + }; + let params_data = [settings.lambda, settings.theta, settings.alpha,settings.sigma,settings.gamma,settings.blue]; + let params_bytes = bytemuck::cast_slice(¶ms_data); + let params_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Params Uniform"), + contents: params_bytes, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + }); + let params_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("params_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new((std::mem::size_of::() * 6) as _), + }, + count: None, + }], + }); + let time_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + entries: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: wgpu::BufferSize::new(std::mem::size_of::() as _), + }, + count: None, + }, + ], + label: Some("time_bind_group_layout"), + }); + let mut dummy_img = RgbaImage::new(800, 600); + dummy_img.put_pixel(0, 0, image::Rgba([255, 255, 255, 255])); + let texture = Texture::from_image(app, &image::DynamicImage::ImageRgba8(dummy_img)); + let texture_view = texture.view().build(); + + let texture_bind_group_layout = create_bind_group_layout(device, wgpu::TextureSampleType::Float { filterable: true }, true); + let bind_group_layout = + create_bind_group_layout(device, texture_view.sample_type(), sampler_filtering); + let bind_group = create_bind_group(device, &bind_group_layout, &texture_view, &sampler); + let pipeline_layout = create_pipeline_layout(device, &[&texture_bind_group_layout, &time_bind_group_layout, ¶ms_bind_group_layout]); + let render_pipeline = create_render_pipeline( + device, + &pipeline_layout, + &vs_mod, + &fs_mod, + format, + msaa_samples, + ); + let vertices_bytes = vertices_as_bytes(&VERTICES[..]); + let usage = wgpu::BufferUsages::VERTEX; + let vertex_buffer = device.create_buffer_init(&BufferInitDescriptor { + label: None, + contents: vertices_bytes, + usage, + }); + let time_uniform = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("Time Uniform Buffer"), + size: std::mem::size_of::() as wgpu::BufferAddress, + usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, + mapped_at_creation: false, + }); + let time_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: &time_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: time_uniform.as_entire_binding(), + }, + ], + label: Some("time_bind_group"), + }); + let params_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: ¶ms_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 2, + resource: params_uniform.as_entire_binding(), + }, + ], + label: Some("params_bind_group"), + }); + let window = app.window(w_id).unwrap(); + let egui = Egui::from_window(&window); + Model { + params_bind_group, + settings, + params_uniform, + egui, + bind_group, + vertex_buffer, + render_pipeline, + time_uniform, + time_bind_group, + img: None, + texture: Some(texture), + sampler, + bind_group_layout, + } +} +fn view(app: &App, model: &Model, frame: Frame) { + let time = app.time; + let time_bytes = time.to_ne_bytes(); + let binding = app.main_window(); + let queue = binding.queue(); + + { + let mut encoder = frame.command_encoder(); + + queue.write_buffer(&model.time_uniform, 0, &time_bytes); + let mut render_pass = wgpu::RenderPassBuilder::new() + .color_attachment(frame.texture_view(), |color| color) + .begin(&mut encoder); + render_pass.set_bind_group(0, &model.bind_group, &[]); + render_pass.set_pipeline(&model.render_pipeline); + render_pass.set_bind_group(1, &model.time_bind_group, &[]); + render_pass.set_bind_group(2, &model.params_bind_group, &[]); + render_pass.set_vertex_buffer(0, model.vertex_buffer.slice(..)); + let vertex_range = 0..VERTICES.len() as u32; + let instance_range = 0..1; + render_pass.draw(vertex_range, instance_range); +} +if model.settings.show_ui { + model.egui.draw_to_frame(&frame).unwrap(); +} +if app.keys.down.contains(&Key::Space) { + let file_path = app + .project_path() + .expect("failed to locate project directory") + .join("frames") + .join(format!("{:0}.png", app.elapsed_frames())); + app.main_window().capture_frame(file_path); +} +} +fn create_bind_group_layout( + device: &wgpu::Device, + texture_sample_type: wgpu::TextureSampleType, + sampler_filtering: bool, +) -> wgpu::BindGroupLayout { + wgpu::BindGroupLayoutBuilder::new() + .texture( + wgpu::ShaderStages::FRAGMENT, + false, + wgpu::TextureViewDimension::D2, + texture_sample_type, + ) + .sampler(wgpu::ShaderStages::FRAGMENT, sampler_filtering) + .build(device) +} +fn create_bind_group( + device: &wgpu::Device, + layout: &wgpu::BindGroupLayout, + texture: &wgpu::TextureView, + sampler: &wgpu::Sampler, +) -> wgpu::BindGroup { + wgpu::BindGroupBuilder::new() + .texture_view(texture) + .sampler(sampler) + .build(device, layout) +} +fn create_pipeline_layout( + device: &wgpu::Device, + bind_group_layouts: &[&wgpu::BindGroupLayout], +) -> wgpu::PipelineLayout { + let desc = wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts, + push_constant_ranges: &[], + }; + device.create_pipeline_layout(&desc) +} +fn create_render_pipeline( + device: &wgpu::Device, + layout: &wgpu::PipelineLayout, + vs_mod: &wgpu::ShaderModule, + fs_mod: &wgpu::ShaderModule, + dst_format: wgpu::TextureFormat, + sample_count: u32, +) -> wgpu::RenderPipeline { + wgpu::RenderPipelineBuilder::from_layout(layout, vs_mod) + .fragment_shader(fs_mod) + .color_format(dst_format) + .add_vertex_buffer::(&wgpu::vertex_attr_array![0 => Float32x2]) + .sample_count(sample_count) + .primitive_topology(wgpu::PrimitiveTopology::TriangleStrip) + .build(device) +} +fn vertices_as_bytes(data: &[Vertex]) -> &[u8] { + unsafe { wgpu::bytes::from_slice(data) } +} \ No newline at end of file