diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 316676e..8cbddf9 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,smoothvoro,wrapper,galaxy2,GPUattractor,peaceGPU] + 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,wrapper,galaxy2,GPUattractor,peaceGPU,kleinian] include: - target: x86_64-unknown-linux-gnu ext: "" diff --git a/Cargo.toml b/Cargo.toml index 71dd3a3..75f1e6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -328,4 +328,8 @@ path = "src/GPUattractor.rs" [[bin]] name= "peaceGPU" -path = "src/peaceGPU.rs" \ No newline at end of file +path = "src/peaceGPU.rs" + +[[bin]] +name= "kleinian" +path = "src/kleinian.rs" \ No newline at end of file diff --git a/shaders/kleinian.wgsl b/shaders/kleinian.wgsl new file mode 100644 index 0000000..b6c12b0 --- /dev/null +++ b/shaders/kleinian.wgsl @@ -0,0 +1,243 @@ +const MAX_STEPS: i32 = 155; +const MAX_DIST: f32 = 155.0; +const SURF_DIST: f32 = 0.001; +const PI: f32 = 3.14159265359; +const LIGHT_SPEED: f32 = 0.5; +const LIGHT_INTENSITY: f32 = 2.0; +const LIGHT_RADIUS: f32 = 144.0; +const BAILOUT: f32 = 4.0; +const AMBIENT: f32 = 0.1; +const SPECULAR_COEFF: f32 = 0.5; +const SHININESS: f32 = 0.5; +struct TimeUniform { + time: f32, +}; + +@group(1) @binding(0) +var u_time: TimeUniform; +struct Params { + lambda: f32, + theta: f32, + alpha: f32, + sigma: f32, + gamma: f32, + blue: f32, + aa: f32, + iter: f32, + bound: f32, + tt: f32, + a: f32, + b: f32, + c: f32, + d: f32, + e: f32, + f: f32, + g: f32, +}; + +@group(0) @binding(1) +var params: Params; +fn oscWithPause(minValue: f32, maxValue: f32, interval: f32, pauseDuration: f32, currentTime: f32) -> f32 { + let cycleTime: f32 = interval * 2.0 + pauseDuration; + let phase: f32 = currentTime % cycleTime; + if (phase < interval) { + return mix(maxValue, minValue, phase / interval); + } else if (phase < interval + pauseDuration) { + return minValue; + } else { + return mix(minValue, maxValue, (phase - interval - pauseDuration) / interval); + } +} +fn smin(a: f32, b: f32, k: f32) -> f32 { + let h: f32 = clamp(0.1 + 0.1 * (b - a) / k, 0.0, 1.0); + return mix(b, a, h) - k * h * (1.0 - h); +} +fn fold(p: vec2) -> vec2 { + let r: f32 = 0.5; + return vec2(abs(p.x + r) - abs(p.x - r) - p.x, p.y); +} +fn sdKleinian(p: vec3, time: f32) -> f32 { + var scale: f32 = params.gamma; + let offset: f32 = params.blue; + let scramb: f32 = oscWithPause(params.lambda, params.theta, params.alpha, params.sigma, time); + + var pp: vec3 = p; + + for (var i: i32 = 0; i < 12; i = i + 1) { + pp = -1.0 + 2.0 * fract(0.5 * pp + 0.5); + + let r2: f32 = dot(pp, pp); + let k: f32 = max(scramb / r2, 0.1); + pp *= k; + scale *= k; + } + let ap: vec3 = abs(pp); + var d: f32 = (ap.x - offset) / scale; + d = smin(d, (ap.y - offset) / scale, 0.01); + d = smin(d, (ap.z - offset) / scale, 0.01); + + return d; +} + +fn getNormal(p: vec3, time: f32) -> vec3 { + let eps: f32 = oscWithPause(params.tt, params.tt, 5.0, 0.0, time); + let e: vec3 = vec3(eps, eps, eps); + + let d: f32 = sdKleinian(p, time); + let n: vec3 = vec3( + d - sdKleinian(p - vec3(e.x, 0.0, 0.0), time), + d - sdKleinian(p - vec3(0.0, e.y, 0.0), time), + d - sdKleinian(p - vec3(0.0, 0.0, e.z), time) + ); + return normalize(n); +} +fn rayMarchFractal(ro: vec3, rd: vec3, time: f32) -> f32 { + var dO: f32 = 0.0; + + for (var i: i32 = 0; i < MAX_STEPS; i = i + 1) { + let p: vec3 = ro + rd * dO; + let dS: f32 = sdKleinian(p, time); + dO += dS; + if (dO > MAX_DIST || abs(dS) < SURF_DIST) { + break; + } + } + return dO; +} +fn softShadow(ro: vec3, rd: vec3, k: f32, time: f32) -> f32 { + var res: f32 = params.a; + var ph: f32 = 1e20; + var dO: f32 = params.tt; + + for (var i: i32 = 0; i < 12; i = i + 1) { + let p: vec3 = ro + rd * dO; + let dS: f32 = sdKleinian(p, time); + let y: f32 = dS * dS / (144.0 * ph); + let d: f32 = sqrt(max(dS * dS - y * y, 0.0)); + res = min(res, k * d / max(0.0, dO - y)); + ph = dS; + dO += dS; + if (dO > MAX_DIST || res < params.b) { + break; + } + } + return clamp(res, 0.0, 1.0); +} +fn palette(d: f32) -> vec3 { + return mix(vec3(params.e, params.f, params.g), vec3(params.c, params.d, params.e), d); +} +fn rotate2D(p: vec2, a: f32) -> vec2 { + let c: f32 = cos(a); + let s: f32 = sin(a); + let rotation: mat2x2 = mat2x2(c, s, -s, c); + return rotation * p; +} +fn mapParticles(p: vec3, time: f32) -> f32 { + var p_mut: vec3 = p; + for (var i: i32 = 0; i < 2; i = i + 1) { + let t: f32 = time; + let p_xz: vec2 = rotate2D(vec2(p_mut.x, p_mut.z), t); + p_mut = vec3(p_xz.x - 0.5, p_mut.y, p_xz.y - 0.5); + } + return dot(sign(p_mut), p_mut) / 1.5; +} + +fn rayMarchParticles(ro: vec3, rd: vec3, time: f32) -> vec4 { + var t: f32 = 0.0; + var col: vec3 = vec3(0.0, 0.0, 0.0); + var d: f32; + for (var i: i32 = 0; i < 4; i = i + 1) { + let p: vec3 = ro + rd * t; + d = mapParticles(p, time) * 0.5; + if (d < 0.02) { + break; + } + if (t > 1.0) { + break; + } + col += palette(length(p) * 8.0) / (300.0 * d); + t += d; + } + return vec4(col, 1.0 / d); +} + +fn getLightPosition(time: f32) -> vec3 { + return vec3( + sin(time * LIGHT_SPEED) * 6.0, + cos(time * LIGHT_SPEED * 0.7) * 2.0, + sin(time * LIGHT_SPEED * 1.3) * 3.0 + ); +} + +fn render(ro: vec3, rd: vec3, time: f32) -> vec3 { + var col: vec3 = vec3(0.0, 0.0, 0.0); + let d: f32 = rayMarchFractal(ro, rd, time); + + if (d < MAX_DIST) { + let p: vec3 = ro + rd * d; + let n: vec3 = getNormal(p, time); + let lightPos: vec3 = getLightPosition(time); + let l: vec3 = normalize(lightPos - p); + + let diff: f32 = max(dot(n, l), 0.0); + let spec: f32 = pow(max(dot(reflect(-l, n), -rd), 0.0), 123.0); + let fresnel: f32 = pow(1.0 - max(dot(n, -rd), 0.0), 2.0); + + let shadow: f32 = softShadow(p, l, 1.0, time); + + let objCol: vec3 = params.bound + 0.5 * cos(vec3(0.0, 0.0, 2.0) * PI * 1.0 + length(p) * 0.0 + time * 0.0); + let lightDistance: f32 = length(lightPos - p); + let lightIntensity: f32 = 1.0 / (1.0 + lightDistance * lightDistance * 0.1); + let lightColor: vec3 = vec3(1.0, 0.8, 0.6) * LIGHT_INTENSITY * lightIntensity; + + let fractalShading1: vec3 = objCol * (diff * shadow + 0.01); + let fractalShading2: vec3 = lightColor * spec * shadow; + let fractalShading3: vec3 = vec3(0.0, 0.0, 0.0) * fresnel; + var fractalShading: vec3 = fractalShading1 + fractalShading2 + fractalShading3; + let particleCol: vec4 = rayMarchParticles(p, l, time); + fractalShading += particleCol.rgb * particleCol.a; + let fogsss: f32 = oscWithPause(2.0, 0.0, 18.0, 6.0, time); + let fogAmount: f32 = 1.0 - exp(-0.06 * d * d); + let lightFog: f32 = exp(-lightDistance / LIGHT_RADIUS); + let fogColor: vec3 = mix(vec3(fogsss, fogsss, fogsss), lightColor, lightFog); + fractalShading = mix(fractalShading, fogColor, fogAmount); + + col = fractalShading; + } + + return col; +} +fn AA(uv: vec2, time: f32, ro: vec3) -> vec3 { + var col: vec3 = vec3(0.0, 0.0, 0.0); + let AA_size: f32 = params.aa; + var count: f32 = 0.0; + + for (var aaY: f32 = 0.0; aaY < 2.0; aaY = aaY + 1.0) { + for (var aaX: f32 = 0.0; aaX < 2.0; aaX = aaX + 1.0) { + let offset: vec2 = vec2(aaX, aaY) / 2.0 - vec2(0.5, 0.5); + let aauv: vec2 = uv + offset * AA_size / 512.0; + let rd: vec3 = normalize(vec3(aauv.x, aauv.y, 1.5)); + col += render(ro, rd, time); + count += 1.0; + } + } + + return col / count; +} + +@fragment +fn main(@builtin(position) FragCoord: vec4) -> @location(0) vec4 { + let resolution: vec2 = vec2(1920.0, 1080.0); + let uv: vec2 = params.iter * (FragCoord.xy - 0.5 * resolution) / resolution.y; + let t: f32 = u_time.time * 0.1; + let radius: f32 = 5.0; + let ro: vec3 = vec3( + radius * cos(t), + 2.0 * sin(t * 0.5), + radius * sin(t) + ); + let lookAt: vec3 = vec3(0.0, 1.0, 0.0); + let col: vec3 = AA(uv, u_time.time, ro); + let colGamma: vec3 = pow(col, vec3(1.4545, 1.4545, 1.4545)); + return vec4(colGamma, 1.0); +} diff --git a/src/kleinian.rs b/src/kleinian.rs new file mode 100644 index 0000000..6459d6e --- /dev/null +++ b/src/kleinian.rs @@ -0,0 +1,261 @@ +use nannou::prelude::*; +use nannou_egui::{self, egui, Egui}; +struct Model { + 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, +} +struct Settings { + lambda: f32, + theta: f32, + alpha:f32, + sigma: f32, + gamma:f32, + blue:f32, + a:f32, + b:f32, + c:f32, + d:f32, + e:f32, + f:f32, + g:f32, + iter:f32, + bound:f32, + aa:f32, + tt:f32, + show_ui: bool, +} +#[repr(C)] +#[derive(Clone, Copy)] +struct Vertex { + position: [f32; 2], +} +const VERTICES: [Vertex; 6] = [ + Vertex { position: [-1.0, -1.0] }, + Vertex { position: [ 1.0, -1.0] }, + 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; + } + egui::Window::new("Shader Settings").show(&ctx, |ui| { + ui.add(egui::Slider::new(&mut model.settings.lambda, 0.5..=2.5).text("l")); + ui.add(egui::Slider::new(&mut model.settings.theta, 0.5..=2.5).text("b")); + ui.add(egui::Slider::new(&mut model.settings.alpha, 5.0..=20.0).text("g")); + ui.add(egui::Slider::new(&mut model.settings.sigma, 6.0..=24.0).text("Mid Sigma")); + ui.add(egui::Slider::new(&mut model.settings.gamma, 3.0..=12.0).text("Fog Density")); + ui.add(egui::Slider::new(&mut model.settings.blue, -2.0..=1.0).text("Fogc")); + ui.add(egui::Slider::new(&mut model.settings.a, 0.5..=2.0).text("er")); + ui.add(egui::Slider::new(&mut model.settings.b, 0.0..=0.01).text("eg")); + ui.add(egui::Slider::new(&mut model.settings.c, 0.0..=0.1).text("eb")); + ui.add(egui::Slider::new(&mut model.settings.d, 0.2..=0.8).text("Bre")); + ui.add(egui::Slider::new(&mut model.settings.e, 0.05..=0.2).text("Bgre")); + ui.add(egui::Slider::new(&mut model.settings.f, 0.05..=0.2).text("Bbe")); + ui.add(egui::Slider::new(&mut model.settings.g, 0.3..=1.2).text("Lighting Power")); + ui.add(egui::Slider::new(&mut model.settings.iter, 0.4..=1.6).step_by(0.1).text("Iterations")); + ui.add(egui::Slider::new(&mut model.settings.bound, 1.25..=5.0).text(" Bound")); + ui.add(egui::Slider::new(&mut model.settings.aa, 0.1..=4.0).text("Anti-Aliasing")); + ui.add(egui::Slider::new(&mut model.settings.tt, 0.0001..=0.01).text("Normal Epsilon")); + }); + let params_data = [model.settings.lambda, model.settings.theta,model.settings.alpha, model.settings.sigma,model.settings.gamma,model.settings.blue,model.settings.aa,model.settings.iter,model.settings.bound,model.settings.tt,model.settings.a,model.settings.b,model.settings.c,model.settings.d,model.settings.e,model.settings.f,model.settings.g]; + 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(512, 512).view(view).build().unwrap(); + // The gpu device associated with the window's swapchain + let window = app.window(w_id).unwrap(); + let device = window.device(); + let format = Frame::TEXTURE_FORMAT; + let sample_count = window.msaa_samples(); + let vs_desc = wgpu::include_wgsl!("../shaders/vs.wgsl"); + let fs_desc = wgpu::include_wgsl!("../shaders/kleinian.wgsl"); + let vs_mod = device.create_shader_module(vs_desc); + let fs_mod = device.create_shader_module(fs_desc); + 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_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 params_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("params_bind_group_layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 1, + 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::() * 17) as _), + }, + count: None, + }], + }); + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Pipeline Layout"), + bind_group_layouts: &[¶ms_bind_group_layout, &time_bind_group_layout], + push_constant_ranges: &[], + }); + let render_pipeline = wgpu::RenderPipelineBuilder::from_layout(&pipeline_layout, &vs_mod) + .fragment_shader(&fs_mod) + .color_format(format) + .add_vertex_buffer::(&wgpu::vertex_attr_array![0 => Float32x2]) + .sample_count(sample_count) + .build(device); + 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 settings = Settings { + lambda: 1.1, + theta: 1.1, + alpha: 10.0, + sigma: 12.0, + gamma: 6.0, + blue: -1.0, + show_ui: true, + aa: 0.5, + iter: 0.8, + bound: 2.5, + tt: 0.001, + a: 1.0, + b: 0.001, + c: 0.0, + d: 0.4, + e: 0.1, + f: 0.1, + g: 0.6, + }; + let params_data = [settings.lambda, settings.theta, settings.alpha,settings.sigma,settings.gamma,settings.blue,settings.aa,settings.iter,settings.bound,settings.tt,settings.a,settings.b,settings.c,settings.d,settings.e,settings.f,settings.g]; + 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 = device.create_bind_group(&wgpu::BindGroupDescriptor { + layout: ¶ms_bind_group_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 1, + 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, + vertex_buffer, + render_pipeline, + time_uniform, + time_bind_group, + } +} +fn view(app: &App, model: &Model, frame: Frame) { + let draw = app.draw(); + draw.background().color(BLACK); + 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.params_bind_group, &[]); + render_pass.set_bind_group(1, &model.time_bind_group, &[]); + render_pass.set_pipeline(&model.render_pipeline); + 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) { + match app.project_path() { + Ok(project_path) => { + let frames_path = project_path.join("frames"); + if let Err(e) = std::fs::create_dir_all(&frames_path) { + eprintln!("Failed to create frames directory: {:?}", e); + return; + } + let file_path = frames_path.join(format!("{:0}.png", app.elapsed_frames())); + app.main_window().capture_frame(file_path); + }, + Err(e) => { + eprintln!("Failed to locate project directory: {:?}", e); + } + } + } +} +fn vertices_as_bytes(data: &[Vertex]) -> &[u8] { + unsafe { wgpu::bytes::from_slice(data) } +} \ No newline at end of file