├── .gitignore ├── Cargo.toml ├── README.md ├── assets └── screenshot.png └── src ├── main.rs ├── prelude.wgsl ├── shader.rs └── shader.wgsl /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "shadertoy" 3 | version = "0.1.0" 4 | authors = ["adamnemecek "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | wgpu = { git = "https://github.com/gfx-rs/wgpu-rs/" , rev = "82b7068498864de44bbdf3e02d086c03d83a04e0" } 11 | winit = "*" 12 | pollster = "0.2" 13 | notify = "*" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # shadertoy 2 | 3 | This is an [`wgpu`](https://github.com/gfx-rs/wgpu-rs/) backed implementation of [shadertoy](https://www.shadertoy.com). 4 | 5 | Edit the file `shader.wgsl` to change the shader. Shaders are rebuilt when you save `shader.wgsl`. 6 | 7 | ![screenshot](assets/screenshot.png) -------------------------------------------------------------------------------- /assets/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adamnemecek/shadertoy/15146cb8a620a3e27edb7f1cf47fcc638646c0c8/assets/screenshot.png -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use winit::{ 2 | event::{ElementState, Event, KeyboardInput, MouseButton, VirtualKeyCode, WindowEvent}, 3 | event_loop::{ControlFlow, EventLoop}, 4 | window::Window, 5 | }; 6 | 7 | mod shader; 8 | pub use shader::*; 9 | 10 | unsafe fn as_u8_slice(p: &T) -> &[u8] { 11 | ::std::slice::from_raw_parts((p as *const T) as *const u8, ::std::mem::size_of::()) 12 | } 13 | 14 | async fn run( 15 | event_loop: EventLoop<()>, 16 | window: Window, 17 | swapchain_format: wgpu::TextureFormat, 18 | rx: std::sync::mpsc::Receiver, 19 | ) { 20 | let size = window.inner_size(); 21 | let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); 22 | 23 | let mut surface = Some(unsafe { instance.create_surface(&window) }); 24 | 25 | let adapter = instance 26 | .request_adapter(&wgpu::RequestAdapterOptions { 27 | power_preference: wgpu::PowerPreference::default(), 28 | // Request an adapter which can render to our surface 29 | compatible_surface: surface.as_ref(), 30 | }) 31 | .await 32 | .expect("Failed to find an appropriate adapter"); 33 | 34 | // Create the logical device and command queue 35 | let (device, queue) = adapter 36 | .request_device( 37 | &wgpu::DeviceDescriptor { 38 | label: None, 39 | features: wgpu::Features::PUSH_CONSTANTS, 40 | limits: wgpu::Limits { 41 | max_push_constant_size: 4096, 42 | ..Default::default() 43 | }, 44 | }, 45 | None, 46 | ) 47 | .await 48 | .expect("Failed to create device"); 49 | 50 | let mut sc_desc = wgpu::SwapChainDescriptor { 51 | usage: wgpu::TextureUsage::RENDER_ATTACHMENT, 52 | format: swapchain_format, 53 | width: size.width, 54 | height: size.height, 55 | present_mode: wgpu::PresentMode::Mailbox, 56 | }; 57 | 58 | let mut swap_chain = surface 59 | .as_ref() 60 | .map(|surface| device.create_swap_chain(&surface, &sc_desc)); 61 | 62 | let mut shader = Shader::new(&device, swapchain_format); 63 | 64 | let start = std::time::Instant::now(); 65 | let (mut cursor_x, mut cursor_y) = (0.0, 0.0); 66 | let (mut drag_start_x, mut drag_start_y) = (0.0, 0.0); 67 | let (mut drag_end_x, mut drag_end_y) = (0.0, 0.0); 68 | let mut mouse_left_pressed = false; 69 | let mut mouse_left_clicked = false; 70 | 71 | let mut frame_count = 0.0; 72 | 73 | event_loop.run(move |event, _, control_flow| { 74 | // Have the closure take ownership of the resources. 75 | // `event_loop.run` never returns, therefore we must do this to ensure 76 | // the resources are properly cleaned up. 77 | let _ = (&instance, &adapter, &shader); 78 | 79 | *control_flow = ControlFlow::Poll; 80 | match event { 81 | Event::MainEventsCleared => { 82 | for n in rx.try_recv() { 83 | match n { 84 | notify::DebouncedEvent::Write(_) => { 85 | shader.rebuild(&device, swapchain_format); 86 | } 87 | _ => {} 88 | } 89 | 90 | } 91 | window.request_redraw(); 92 | } 93 | Event::Resumed => { 94 | let s = unsafe { instance.create_surface(&window) }; 95 | swap_chain = Some(device.create_swap_chain(&s, &sc_desc)); 96 | surface = Some(s); 97 | } 98 | Event::Suspended => { 99 | surface = None; 100 | swap_chain = None; 101 | } 102 | Event::WindowEvent { 103 | event: WindowEvent::Resized(size), 104 | .. 105 | } => { 106 | // Recreate the swap chain with the new size 107 | sc_desc.width = size.width; 108 | sc_desc.height = size.height; 109 | if let Some(surface) = &surface { 110 | swap_chain = Some(device.create_swap_chain(surface, &sc_desc)); 111 | } 112 | } 113 | Event::RedrawRequested(_) => { 114 | if let Some(swap_chain) = &mut swap_chain { 115 | let frame = swap_chain 116 | .get_current_frame() 117 | .expect("Failed to acquire next swap chain texture") 118 | .output; 119 | let mut encoder = device 120 | .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); 121 | { 122 | let clear_color = wgpu::Color { 123 | r: 0.2, 124 | g: 0.2, 125 | b: 0.25, 126 | a: 1.0, 127 | }; 128 | let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { 129 | label: None, 130 | color_attachments: &[wgpu::RenderPassColorAttachment { 131 | view: &frame.view, 132 | resolve_target: None, 133 | ops: wgpu::Operations { 134 | load: wgpu::LoadOp::Clear(clear_color), 135 | store: true, 136 | }, 137 | }], 138 | depth_stencil_attachment: None, 139 | }); 140 | let push_constants = ShaderConstants { 141 | width: window.inner_size().width as _, 142 | height: window.inner_size().height as _, 143 | frame: frame_count, 144 | time: start.elapsed().as_secs_f32(), 145 | cursor_x, 146 | cursor_y, 147 | drag_start_x, 148 | drag_start_y, 149 | drag_end_x, 150 | drag_end_y, 151 | mouse_left_pressed, 152 | mouse_left_clicked, 153 | }; 154 | mouse_left_clicked = false; 155 | rpass.set_pipeline(shader.pipeline()); 156 | rpass.set_push_constants(wgpu::ShaderStage::all(), 0, unsafe { 157 | as_u8_slice(&push_constants) 158 | }); 159 | rpass.draw(0..3, 0..1); 160 | 161 | frame_count += 1.0; 162 | } 163 | 164 | queue.submit(Some(encoder.finish())); 165 | } 166 | } 167 | Event::WindowEvent { 168 | event: WindowEvent::CloseRequested, 169 | .. 170 | } => *control_flow = ControlFlow::Exit, 171 | Event::WindowEvent { 172 | event: 173 | WindowEvent::KeyboardInput { 174 | input: 175 | KeyboardInput { 176 | virtual_keycode: Some(VirtualKeyCode::Escape), 177 | .. 178 | }, 179 | .. 180 | }, 181 | .. 182 | } => *control_flow = ControlFlow::Exit, 183 | Event::WindowEvent { 184 | event: 185 | WindowEvent::MouseInput { 186 | state, 187 | button: MouseButton::Left, 188 | .. 189 | }, 190 | .. 191 | } => { 192 | mouse_left_pressed = state == ElementState::Pressed; 193 | if mouse_left_pressed { 194 | drag_start_x = cursor_x; 195 | drag_start_y = cursor_y; 196 | drag_end_x = cursor_x; 197 | drag_end_y = cursor_y; 198 | mouse_left_clicked = true; 199 | } 200 | } 201 | Event::WindowEvent { 202 | event: WindowEvent::CursorMoved { position, .. }, 203 | .. 204 | } => { 205 | cursor_x = position.x as f32; 206 | cursor_y = position.y as f32; 207 | if mouse_left_pressed { 208 | drag_end_x = cursor_x; 209 | drag_end_y = cursor_y; 210 | } 211 | } 212 | _ => {} 213 | } 214 | }); 215 | } 216 | 217 | fn main() { 218 | let event_loop = EventLoop::new(); 219 | let window = winit::window::WindowBuilder::new() 220 | .with_title("Rust GPU - wgpu") 221 | .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0)) 222 | .build(&event_loop) 223 | .unwrap(); 224 | // join filesd 225 | let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 226 | 227 | let shader_path = root.join("src/shader.wgsl"); 228 | 229 | use notify::{Watcher}; 230 | let (tx, rx) = std::sync::mpsc::channel(); 231 | let mut watcher = notify::watcher(tx, std::time::Duration::from_millis(500)).unwrap(); 232 | watcher 233 | .watch(&shader_path, notify::RecursiveMode::NonRecursive) 234 | .unwrap(); 235 | 236 | let format = wgpu::TextureFormat::Bgra8UnormSrgb; 237 | 238 | pollster::block_on(run(event_loop, window, format, rx)); 239 | } 240 | -------------------------------------------------------------------------------- /src/prelude.wgsl: -------------------------------------------------------------------------------- 1 | 2 | 3 | [[stage(vertex)]] 4 | fn vs_main([[builtin(vertex_index)]] vertex_index: u32) -> [[builtin(position)]] vec4 { 5 | // var out: vec2; 6 | 7 | const x = f32(i32((vertex_index << 1u32) & 2u32)); 8 | const y = f32(i32(vertex_index & 2u32)); 9 | const uv = vec2(x, y); 10 | const out = 2.0 * uv - vec2(1.0, 1.0); 11 | return vec4(out.x, out.y, 0.0, 1.0); 12 | 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/shader.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone)] 2 | #[allow(unused_attributes)] 3 | // #[spirv(block)] 4 | pub struct ShaderConstants { 5 | pub width: f32, 6 | pub height: f32, 7 | pub frame: f32, 8 | pub time: f32, 9 | pub cursor_x: f32, 10 | pub cursor_y: f32, 11 | pub drag_start_x: f32, 12 | pub drag_start_y: f32, 13 | pub drag_end_x: f32, 14 | pub drag_end_y: f32, 15 | pub mouse_left_pressed: bool, 16 | pub mouse_left_clicked: bool, 17 | } 18 | 19 | pub struct Shader { 20 | pipeline_layout: wgpu::PipelineLayout, 21 | pipeline: wgpu::RenderPipeline, 22 | } 23 | 24 | fn create_pipeline( 25 | device: &wgpu::Device, 26 | pipeline_layout: &wgpu::PipelineLayout, 27 | shader: &wgpu::ShaderModule, 28 | format: wgpu::TextureFormat, 29 | ) -> wgpu::RenderPipeline { 30 | let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { 31 | label: None, 32 | layout: Some(&pipeline_layout), 33 | vertex: wgpu::VertexState { 34 | module: &shader, 35 | entry_point: "vs_main", 36 | buffers: &[], 37 | }, 38 | fragment: Some(wgpu::FragmentState { 39 | module: &shader, 40 | entry_point: "fs_main", 41 | targets: &[format.into()], 42 | }), 43 | primitive: wgpu::PrimitiveState::default(), 44 | depth_stencil: None, 45 | multisample: wgpu::MultisampleState::default(), 46 | }); 47 | 48 | render_pipeline 49 | } 50 | 51 | fn create_shader(device: &wgpu::Device) -> wgpu::ShaderModule { 52 | let root = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); 53 | let shader_path = root.join("src/shader.wgsl"); 54 | 55 | let prelude_path = root.join("src/prelude.wgsl"); 56 | 57 | let prelude = std::fs::read_to_string(prelude_path).unwrap(); 58 | let shader = std::fs::read_to_string(&shader_path).unwrap(); 59 | 60 | let source = format!("{}{}", prelude, shader); 61 | 62 | let shader_desc = wgpu::ShaderModuleDescriptor { 63 | label: None, 64 | source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(&source)), 65 | flags: wgpu::ShaderFlags::all(), 66 | }; 67 | 68 | device.create_shader_module(&shader_desc) 69 | } 70 | 71 | impl Shader { 72 | pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self { 73 | let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { 74 | label: None, 75 | bind_group_layouts: &[], 76 | push_constant_ranges: &[wgpu::PushConstantRange { 77 | stages: wgpu::ShaderStage::all(), 78 | range: 0..std::mem::size_of::() as u32, 79 | }], 80 | }); 81 | let shader = create_shader(device); 82 | 83 | Self { 84 | pipeline: create_pipeline(device, &pipeline_layout, &shader, format), 85 | pipeline_layout, 86 | } 87 | } 88 | 89 | pub fn rebuild(&mut self, device: &wgpu::Device, format: wgpu::TextureFormat) { 90 | let shader = create_shader(device); 91 | self.pipeline = create_pipeline(device, &self.pipeline_layout, &shader, format); 92 | } 93 | 94 | pub fn pipeline(&self) -> &wgpu::RenderPipeline { 95 | &self.pipeline 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/shader.wgsl: -------------------------------------------------------------------------------- 1 | [[block]] 2 | struct Uniforms { 3 | resolution: vec2; 4 | frame: f32; 5 | time: f32; 6 | cursor: vec2; 7 | drag_start: vec2; 8 | drag_end: vec2; 9 | mouse_left_pressed: bool; 10 | mouse_left_clicked: bool; 11 | }; 12 | 13 | var u: Uniforms; 14 | 15 | [[stage(fragment)]] 16 | fn fs_main_default( 17 | [[builtin(position)]] frag_coord: vec4 18 | ) -> [[location(0)]] vec4 { 19 | // return vec4(cos(u.time), sin(u.time), 1.0 - cos(u.time), 1.0); 20 | 21 | const uv = frag_coord.xy / u.resolution; 22 | const half = vec3(0.5, 0.5, 0.5); 23 | const time = vec3(u.time, u.time, u.time); 24 | const col: vec3 = half + half * cos(time + uv.xyx + vec3(0.0, 2.0, 4.0)); 25 | return vec4(col.x, col.y, col.z, 1.0); 26 | } 27 | 28 | // float sdRoundBox( in vec2 p, in vec2 b, in vec4 r ) 29 | // { 30 | // r.xy = (p.x>0.0)?r.xy : r.zw; 31 | // r.x = (p.y>0.0)?r.x : r.y; 32 | 33 | // vec2 q = abs(p)-b+r.x; 34 | // return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; 35 | // } 36 | 37 | // void mainImage( out vec4 fragColor, in vec2 fragCoord ) 38 | // { 39 | // vec2 p = (2.0*fragCoord-iResolution.xy)/iResolution.y; 40 | 41 | // vec2 si = vec2(0.9,0.6); 42 | // vec4 ra = 0.3 + 0.3*cos( 2.0*iTime + vec4(0,1,2,3) ); 43 | 44 | // float d = sdRoundBox( p, si, ra ); 45 | 46 | // vec3 col = vec3(1.0) - sign(d)*vec3(0.1,0.4,0.7); 47 | // col *= 1.0 - exp(-3.0*abs(d)); 48 | // col *= 0.8 + 0.2*cos(150.0*d); 49 | // col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.02,abs(d)) ); 50 | 51 | // fragColor = vec4(col,1.0); 52 | // } 53 | 54 | fn min2(a: vec2, b: vec2) -> vec2 { 55 | return vec2( 56 | min(a.x, b.x), 57 | min(a.y, b.y) 58 | ); 59 | } 60 | 61 | fn min3(a: vec3, b: vec3) -> vec3 { 62 | return vec3( 63 | min(a.x, b.x), 64 | min(a.y, b.y), 65 | min(a.z, b.z) 66 | ); 67 | } 68 | 69 | fn min4(a: vec4, b: vec4) -> vec4 { 70 | return vec4( 71 | min(a.x, b.x), 72 | min(a.y, b.y), 73 | min(a.z, b.z), 74 | min(a.w, b.w) 75 | ); 76 | } 77 | 78 | fn max2(a: vec2, b: vec2) -> vec2 { 79 | return vec2( 80 | max(a.x, b.x), 81 | max(a.y, b.y) 82 | ); 83 | } 84 | 85 | fn max3(a: vec3, b: vec3) -> vec3 { 86 | return vec3( 87 | max(a.x, b.x), 88 | max(a.y, b.y), 89 | max(a.z, b.z) 90 | ); 91 | } 92 | 93 | fn max4(a: vec4, b: vec4) -> vec4 { 94 | return vec4( 95 | max(a.x, b.x), 96 | max(a.y, b.y), 97 | max(a.z, b.z), 98 | max(a.w, b.w) 99 | ); 100 | } 101 | 102 | fn splat2(v: f32) -> vec2 { 103 | return vec2(v, v); 104 | } 105 | 106 | fn splat3(v: f32) -> vec3 { 107 | return vec3(v, v, v); 108 | } 109 | 110 | fn splat4(v: f32) -> vec4 { 111 | return vec4(v, v, v, v); 112 | } 113 | 114 | fn mix3(a: vec3, b: vec3, d: f32) -> vec3 { 115 | return vec3( 116 | mix(a.x, b.x, d), 117 | mix(a.y, b.y, d), 118 | mix(a.z, b.z, d) 119 | ); 120 | } 121 | 122 | fn sd_round_box(p: vec2, b: vec2, in_r: vec4) -> f32 { 123 | var r: vec4; 124 | 125 | r = in_r; 126 | 127 | if (p.x > 0.0) { 128 | r.x = r.x; 129 | r.y = r.y; 130 | } else { 131 | r.x = r.z; 132 | r.y = r.w; 133 | }; 134 | // r.x = (p.y>0.0)?r.x : r.y; 135 | if (p.y > 0.0) { 136 | r.x = r.x; 137 | } else { 138 | r.x = r.x; 139 | } 140 | // r.xy = (p.x>0.0)?r.xy : r.zw; 141 | // r.x = (p.y>0.0)?r.x : r.y; 142 | const q = abs(p) - b + vec2(r.x, r.x); 143 | // vec2 q = abs(p)-b+r.x; 144 | // return min(max(q.x,q.y),0.0) + length(max(q,0.0)) - r.x; 145 | return min(max(q.x, q.y), 0.0) + length(max2(q, splat2(0.0))) - r.x; 146 | // return 0.0; 147 | } 148 | 149 | 150 | 151 | [[stage(fragment)]] 152 | fn fs_main( 153 | [[builtin(position)]] frag_coord: vec4 154 | ) -> [[location(0)]] vec4 { 155 | // return vec4(cos(u.time), sin(u.time), 1.0 - cos(u.time), 1.0); 156 | 157 | // const uv = frag_coord.xy / u.resolution; 158 | // const half = vec3(0.5, 0.5, 0.5); 159 | // const time = vec3(u.time, u.time, u.time); 160 | // const col: vec3 = half + half * cos(time + uv.xyx + vec3(0.0, 2.0, 4.0)); 161 | // return vec4(col.x, col.y, col.z, 1.0); 162 | 163 | // const p = 164 | // const two = vec2(2.0, 2.0); 165 | const p = (splat2(2.0)*frag_coord.xy-u.resolution)/splat2(u.resolution.y); 166 | 167 | const si = vec2(0.9, 0.6); 168 | // vec2 si = vec2(0.9,0.6); 169 | // vec4 ra = 0.3 + 0.3*cos( 2.0*iTime + vec4(0,1,2,3) ); 170 | // const third = vec4(0.3, 0.3, 0.3, 0.3); 171 | // const t = vec2() 172 | const ra = splat4(0.3) + splat4(0.3) * cos(splat4(u.time) * vec4(0.0, 1.0, 2.0, 3.0)); 173 | const d = sd_round_box(p, si, ra); 174 | // float d = sdRoundBox( p, si, ra ); 175 | const col = splat3(1.0) - sign(d) * vec3(0.1, 0.4, 0.7); 176 | const col1 = col * splat3(1.0) - splat3(exp(-3.0 * abs(d))); 177 | const col2 = col1 * splat3(0.8) * splat3(0.2) * cos(150.0 * d); 178 | const col3 = mix3(col2, splat3(1.0), 1.0 - smoothStep(0.0, 0.02, abs(d))); 179 | 180 | // vec3 col = vec3(1.0) - sign(d)*vec3(0.1,0.4,0.7); 181 | // col *= 1.0 - exp(-3.0*abs(d)); 182 | // col *= 0.8 + 0.2*cos(150.0*d); 183 | // col = mix( col, vec3(1.0), 1.0-smoothstep(0.0,0.02,abs(d)) ); 184 | 185 | // fragColor = vec4(col,1.0); 186 | // return vec4(0.5, 0.5, 0.5, 0.5); 187 | return vec4(col3, 1.0); 188 | 189 | } --------------------------------------------------------------------------------