diff --git a/README.md b/README.md index a551894..6a8b9ca 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ Rapidly prototype a simple 2D game, pixel-based animations, software renderers, ## Examples - [Conway's Game of Life](./examples/conway) +- [Custom Shader](./examples/custom-shader) - [Minimal example with SDL2](./examples/minimal-sdl2) - [Minimal example with `winit`](./examples/minimal-winit) - [Pixel Invaders](./examples/invaders) diff --git a/examples/custom-shader/Cargo.toml b/examples/custom-shader/Cargo.toml new file mode 100644 index 0000000..ee410b2 --- /dev/null +++ b/examples/custom-shader/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "custom-shader" +version = "0.1.0" +authors = ["Jay Oster "] +edition = "2018" +publish = false + +[features] +optimize = ["log/release_max_level_warn"] +default = ["optimize"] + +[dependencies] +env_logger = "0.7.1" +log = "0.4.8" +pixels = { path = "../.." } +winit = "0.22.0" +winit_input_helper = "0.6.0" diff --git a/examples/custom-shader/README.md b/examples/custom-shader/README.md new file mode 100644 index 0000000..5ce8d11 --- /dev/null +++ b/examples/custom-shader/README.md @@ -0,0 +1,13 @@ +# Custom Shader Example + +![Custom Shader Example](../../img/custom-shader.png) + +## Running + +```bash +cargo run --release --package custom-shader +``` + +## About + +This example is based on `minimal-winit`, and extends it with a custom renderer that adds white noise to the screen. diff --git a/examples/custom-shader/shaders/README.md b/examples/custom-shader/shaders/README.md new file mode 100644 index 0000000..5be4ff8 --- /dev/null +++ b/examples/custom-shader/shaders/README.md @@ -0,0 +1,13 @@ +# Shaders + +The GLSL shader source is not compiled as part of the normal cargo build process. This was a conscious decision sparked by the current state of the ecosystem; compiling GLSL-to-SPIR-V requires a C++ toolchain including CMake, which is an unacceptable constraint for a simple crate providing a pixel buffer. + +If you need to modify the GLSL sources, you must also recompile the SPIR-V as well. This can be done with `glslang`, `glslc`, etc. + +Compile shaders with `glslangValidator`: + +```bash +glslangValidator -V shader.frag && glslangValidator -V shader.vert +``` + +For more information, see https://github.com/parasyte/pixels/issues/9 diff --git a/examples/custom-shader/shaders/frag.spv b/examples/custom-shader/shaders/frag.spv new file mode 100644 index 0000000..aee9847 Binary files /dev/null and b/examples/custom-shader/shaders/frag.spv differ diff --git a/examples/custom-shader/shaders/shader.frag b/examples/custom-shader/shaders/shader.frag new file mode 100644 index 0000000..6308c43 --- /dev/null +++ b/examples/custom-shader/shaders/shader.frag @@ -0,0 +1,38 @@ +// IMPORTANT: This shader needs to be compiled out-of-band to SPIR-V +// See: https://github.com/parasyte/pixels/issues/9 + +#version 450 + +layout(location = 0) in vec2 v_TexCoord; +layout(location = 0) out vec4 outColor; +layout(set = 0, binding = 0) uniform texture2D t_Color; +layout(set = 0, binding = 1) uniform sampler s_Color; +layout(set = 0, binding = 2) uniform Locals { + float u_Time; +}; + +#define PI 3.1415926535897932384626433832795 +#define TAU PI * 2.0 + +// Offset the circular time input so it is never 0 +#define BIAS 0.2376 + +// Random functions based on https://thebookofshaders.com/10/ +#define RANDOM_SCALE 43758.5453123 +#define RANDOM_X 12.9898 +#define RANDOM_Y 78.233 + +float random(float x) { + return fract(sin(x) * RANDOM_SCALE); +} + +float random_vec2(vec2 st) { + return random(dot(st.xy, vec2(RANDOM_X, RANDOM_Y))); +} + +void main() { + vec4 sampledColor = texture(sampler2D(t_Color, s_Color), v_TexCoord.xy); + vec3 noiseColor = vec3(random_vec2(v_TexCoord.xy * vec2(mod(u_Time, TAU) + BIAS))); + + outColor = vec4(sampledColor.rgb * noiseColor, sampledColor.a); +} diff --git a/examples/custom-shader/shaders/shader.vert b/examples/custom-shader/shaders/shader.vert new file mode 100644 index 0000000..8804d9d --- /dev/null +++ b/examples/custom-shader/shaders/shader.vert @@ -0,0 +1,39 @@ +// IMPORTANT: This shader needs to be compiled out-of-band to SPIR-V +// See: https://github.com/parasyte/pixels/issues/9 + +#version 450 + +out gl_PerVertex { + vec4 gl_Position; +}; + +layout(location = 0) out vec2 v_TexCoord; + +const vec2 positions[6] = vec2[6]( + // Upper left triangle + vec2(-1.0, -1.0), + vec2(1.0, -1.0), + vec2(-1.0, 1.0), + + // Lower right triangle + vec2(-1.0, 1.0), + vec2(1.0, -1.0), + vec2(1.0, 1.0) +); + +const vec2 uv[6] = vec2[6]( + // Upper left triangle + vec2(0.0, 0.0), + vec2(1.0, 0.0), + vec2(0.0, 1.0), + + // Lower right triangle + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) +); + +void main() { + v_TexCoord = uv[gl_VertexIndex]; + gl_Position = vec4(positions[gl_VertexIndex], 0.0, 1.0); +} diff --git a/examples/custom-shader/shaders/vert.spv b/examples/custom-shader/shaders/vert.spv new file mode 100644 index 0000000..ec125ed Binary files /dev/null and b/examples/custom-shader/shaders/vert.spv differ diff --git a/examples/custom-shader/src/main.rs b/examples/custom-shader/src/main.rs new file mode 100644 index 0000000..1189b39 --- /dev/null +++ b/examples/custom-shader/src/main.rs @@ -0,0 +1,173 @@ +#![deny(clippy::all)] +#![forbid(unsafe_code)] + +use crate::renderers::NoiseRenderer; +use log::error; +use pixels::{ + wgpu::{self, Surface}, + Error, Pixels, SurfaceTexture, +}; +use winit::dpi::LogicalSize; +use winit::event::{Event, VirtualKeyCode}; +use winit::event_loop::{ControlFlow, EventLoop}; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +mod renderers; + +const WIDTH: u32 = 320; +const HEIGHT: u32 = 240; +const BOX_SIZE: i16 = 64; + +/// Representation of the application state. In this example, a box will bounce around the screen. +struct World { + box_x: i16, + box_y: i16, + velocity_x: i16, + velocity_y: i16, +} + +fn main() -> Result<(), Error> { + env_logger::init(); + let event_loop = EventLoop::new(); + let mut input = WinitInputHelper::new(); + let window = { + let size = LogicalSize::new(WIDTH as f64, HEIGHT as f64); + WindowBuilder::new() + .with_title("Custom Shader") + .with_inner_size(size) + .with_min_inner_size(size) + .build(&event_loop) + .unwrap() + }; + let mut hidpi_factor = window.scale_factor(); + + let mut pixels = { + let surface = Surface::create(&window); + let surface_texture = SurfaceTexture::new(WIDTH, HEIGHT, surface); + Pixels::new(WIDTH, HEIGHT, surface_texture)? + }; + let mut world = World::new(); + + let mut time = 0.0; + let (scaled_texture, mut noise_renderer) = create_noise_renderer(&pixels); + + event_loop.run(move |event, _, control_flow| { + // Draw the current frame + if let Event::RedrawRequested(_) = event { + world.draw(pixels.get_frame()); + + noise_renderer.update(pixels.device(), pixels.queue(), time); + time += 1.0; + + let render_result = pixels.render_with(|encoder, render_target, scaling_renderer| { + scaling_renderer.render(encoder, &scaled_texture); + noise_renderer.render(encoder, render_target); + }); + + if render_result + .map_err(|e| error!("pixels.render_with() failed: {}", e)) + .is_err() + { + *control_flow = ControlFlow::Exit; + return; + } + } + + // Handle input events + if input.update(event) { + // Close events + if input.key_pressed(VirtualKeyCode::Escape) || input.quit() { + *control_flow = ControlFlow::Exit; + return; + } + + // Adjust high DPI factor + if let Some(factor) = input.scale_factor_changed() { + hidpi_factor = factor; + } + + // Resize the window + if let Some(size) = input.window_resized() { + let size = size.to_logical(hidpi_factor); + pixels.resize(size.width, size.height); + } + + // Update internal state and request a redraw + world.update(); + window.request_redraw(); + } + }); +} + +impl World { + /// Create a new `World` instance that can draw a moving box. + fn new() -> Self { + Self { + box_x: 24, + box_y: 16, + velocity_x: 1, + velocity_y: 1, + } + } + + /// Update the `World` internal state; bounce the box around the screen. + fn update(&mut self) { + if self.box_x <= 0 || self.box_x + BOX_SIZE > WIDTH as i16 { + self.velocity_x *= -1; + } + if self.box_y <= 0 || self.box_y + BOX_SIZE > HEIGHT as i16 { + self.velocity_y *= -1; + } + + self.box_x += self.velocity_x; + self.box_y += self.velocity_y; + } + + /// Draw the `World` state to the frame buffer. + /// + /// Assumes the default texture format: [`wgpu::TextureFormat::Rgba8UnormSrgb`] + fn draw(&self, frame: &mut [u8]) { + for (i, pixel) in frame.chunks_exact_mut(4).enumerate() { + let x = (i % WIDTH as usize) as i16; + let y = (i / WIDTH as usize) as i16; + + let inside_the_box = x >= self.box_x + && x < self.box_x + BOX_SIZE + && y >= self.box_y + && y < self.box_y + BOX_SIZE; + + let rgba = if inside_the_box { + [0x5e, 0x48, 0xe8, 0xff] + } else { + [0x48, 0xb2, 0xe8, 0xff] + }; + + pixel.copy_from_slice(&rgba); + } + } +} + +fn create_noise_renderer(pixels: &Pixels) -> (wgpu::TextureView, NoiseRenderer) { + let texture_descriptor = wgpu::TextureDescriptor { + label: None, + size: pixels::wgpu::Extent3d { + width: WIDTH, + height: HEIGHT, + depth: 1, + }, + array_layer_count: 1, + mip_level_count: 1, + sample_count: 1, + dimension: wgpu::TextureDimension::D2, + format: wgpu::TextureFormat::Bgra8UnormSrgb, + usage: wgpu::TextureUsage::SAMPLED | wgpu::TextureUsage::OUTPUT_ATTACHMENT, + }; + let scaled_texture = pixels + .device() + .create_texture(&texture_descriptor) + .create_default_view(); + let noise_renderer = NoiseRenderer::new(pixels.device(), &scaled_texture); + + (scaled_texture, noise_renderer) +} diff --git a/examples/custom-shader/src/renderers.rs b/examples/custom-shader/src/renderers.rs new file mode 100644 index 0000000..c4ef579 --- /dev/null +++ b/examples/custom-shader/src/renderers.rs @@ -0,0 +1,156 @@ +use pixels::{include_spv, wgpu}; + +pub(crate) struct NoiseRenderer { + bind_group: wgpu::BindGroup, + render_pipeline: wgpu::RenderPipeline, + time_buffer: wgpu::Buffer, +} + +impl NoiseRenderer { + pub(crate) fn new(device: &wgpu::Device, texture_view: &wgpu::TextureView) -> Self { + let vs_module = device.create_shader_module(include_spv!("../shaders/vert.spv")); + let fs_module = device.create_shader_module(include_spv!("../shaders/frag.spv")); + + // Create a texture sampler with nearest neighbor + let sampler = device.create_sampler(&wgpu::SamplerDescriptor { + address_mode_u: wgpu::AddressMode::ClampToEdge, + address_mode_v: wgpu::AddressMode::ClampToEdge, + address_mode_w: wgpu::AddressMode::ClampToEdge, + mag_filter: wgpu::FilterMode::Nearest, + min_filter: wgpu::FilterMode::Nearest, + mipmap_filter: wgpu::FilterMode::Nearest, + lod_min_clamp: 0.0, + lod_max_clamp: 1.0, + compare: wgpu::CompareFunction::Always, + }); + + // Create uniform buffer + let time_buffer = device.create_buffer(&wgpu::BufferDescriptor { + label: Some("NoiseRenderer u_Time"), + size: 4, + usage: wgpu::BufferUsage::UNIFORM | wgpu::BufferUsage::COPY_DST, + }); + + // Create bind group + let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: None, + bindings: &[ + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::SampledTexture { + component_type: wgpu::TextureComponentType::Uint, + multisampled: false, + dimension: wgpu::TextureViewDimension::D2, + }, + }, + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::Sampler { comparison: false }, + }, + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStage::FRAGMENT, + ty: wgpu::BindingType::UniformBuffer { dynamic: false }, + }, + ], + }); + let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor { + label: None, + layout: &bind_group_layout, + bindings: &[ + wgpu::Binding { + binding: 0, + resource: wgpu::BindingResource::TextureView(texture_view), + }, + wgpu::Binding { + binding: 1, + resource: wgpu::BindingResource::Sampler(&sampler), + }, + wgpu::Binding { + binding: 2, + resource: wgpu::BindingResource::Buffer { + buffer: &time_buffer, + range: 0..4, + }, + }, + ], + }); + + // Create pipeline + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + bind_group_layouts: &[&bind_group_layout], + }); + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + layout: &pipeline_layout, + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &vs_module, + entry_point: "main", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &fs_module, + entry_point: "main", + }), + rasterization_state: Some(wgpu::RasterizationStateDescriptor { + front_face: wgpu::FrontFace::Ccw, + cull_mode: wgpu::CullMode::None, + depth_bias: 0, + depth_bias_slope_scale: 0.0, + depth_bias_clamp: 0.0, + }), + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[wgpu::ColorStateDescriptor { + format: wgpu::TextureFormat::Bgra8UnormSrgb, + color_blend: wgpu::BlendDescriptor::REPLACE, + alpha_blend: wgpu::BlendDescriptor::REPLACE, + write_mask: wgpu::ColorWrite::ALL, + }], + depth_stencil_state: None, + vertex_state: wgpu::VertexStateDescriptor { + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + }, + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + Self { + bind_group, + render_pipeline, + time_buffer, + } + } + + pub(crate) fn update(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, time: f32) { + let mut encoder = + device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + + let temp_buf = + device.create_buffer_with_data(&time.to_ne_bytes(), wgpu::BufferUsage::COPY_SRC); + encoder.copy_buffer_to_buffer(&temp_buf, 0, &self.time_buffer, 0, 4); + + queue.submit(&[encoder.finish()]); + } + + pub(crate) fn render( + &self, + encoder: &mut wgpu::CommandEncoder, + render_target: &wgpu::TextureView, + ) { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: render_target, + resolve_target: None, + load_op: wgpu::LoadOp::Clear, + store_op: wgpu::StoreOp::Store, + clear_color: wgpu::Color::BLACK, + }], + depth_stencil_attachment: None, + }); + rpass.set_pipeline(&self.render_pipeline); + rpass.set_bind_group(0, &self.bind_group, &[]); + rpass.draw(0..6, 0..1); + } +} diff --git a/img/custom-shader.png b/img/custom-shader.png new file mode 100644 index 0000000..6f26c9f Binary files /dev/null and b/img/custom-shader.png differ diff --git a/img/minimal-sdl2.png b/img/minimal-sdl2.png old mode 100755 new mode 100644