diff --git a/src/circle_shader.wgsl b/src/circle_shader.wgsl new file mode 100644 index 00000000..b267b4ac --- /dev/null +++ b/src/circle_shader.wgsl @@ -0,0 +1,110 @@ +// ---------------------------------------------------------------------------- +// Vertex shader +// ---------------------------------------------------------------------------- + +struct VertexInput { + @location(0) position : vec3, + @location(1) tex_coords : vec2, +}; + +struct InstanceInput { + @location(5) center: vec2, + @location(6) radius: f32, + @location(7) stroke_width: f32, + @location(8) stroke_color: vec4, + @location(9) fill_color: vec4, +}; + +struct VertexOutput { + @builtin(position) clip_position : vec4, + @location(0) tex_coords : vec2, + @location(1) inner_r2: f32, + @location(2) stroke_color: vec4, + @location(3) fill_color: vec4, +}; + +@vertex fn vs_main(vertex : VertexInput, instance : InstanceInput)->VertexOutput { + var out : VertexOutput; + out.stroke_color = instance.stroke_color; + out.fill_color = instance.fill_color; + + // The circle's coordinate system goes from (-1,-1) to (1,1) but by + // convention we provide ourselves texture coordinates that go from (0,0) + // to (1,1). + out.tex_coords = mix(vec2f(-1.0, -1.0), vec2f(1.0, 1.0), vertex.tex_coords); + + // Compute the squared radius of the inner circle, so we don't do it + // per-pixel. + // + // The radius of the inner circle goes from 0 (no inner circle) to 1 (no + // stroke), because the radius of the outer circle is implicitly 1 (the + // circle in the square we're rendering. + // + // (Honestly I don't even need to do this per-vertex, this is per-instance, + // I can pre-calculate this if I need this to be faster somehow.) + let delta = instance.radius - instance.stroke_width; //, 0, instance.radius); + let inner_radius = delta / instance.radius; + out.inner_r2 = inner_radius * inner_radius; + + let radius = vec2f(instance.radius, instance.radius); + let in_pos = instance.center + mix(-radius, radius, vec2f(vertex.position.x, vertex.position.y)); + + let position = adjust_for_resolution(in_pos); + out.clip_position = vec4f(position.x, position.y, vertex.position.z, 1.0); + return out; +} + +// ---------------------------------------------------------------------------- +// Fragment shader +// ---------------------------------------------------------------------------- + +@fragment fn fs_main(in : VertexOutput)->@location(0) vec4 { + let tc2 = in.tex_coords * in.tex_coords; + if (tc2.x + tc2.y <= in.inner_r2) { + return in.fill_color; + } else if (tc2.x + tc2.y <= 1.0) { + return in.stroke_color; + } else { + return vec4(0.0, 0.0, 0.0, 1.0); + } +} + +// ---------------------------------------------------------------------------- +// Resolution Handling +// ---------------------------------------------------------------------------- + +struct ScreenUniform { + resolution : vec2f, +}; +@group(1) @binding(0) // 1. + var screen : ScreenUniform; + +const RES = vec2f(320.0, 240.0); // The logical resolution of the screen. + +fn adjust_for_resolution(in_pos: vec2) -> vec2 { + // Adjust in_pos for the "resolution" of the screen. + let RES_AR = RES.x / RES.y; // The aspect ratio of the logical screen. + + // the actual resolution of the screen. + let screen_ar = screen.resolution.x / screen.resolution.y; + + // Compute the difference in resolution ... correctly? + // + // nudge is the amount to add to the logical resolution so that the pixels + // stay the same size but we respect the aspect ratio of the screen. (So + // there's more of them in either the x or y direction.) + var nudge = vec2f(0.0); + if (screen_ar > RES_AR) { + nudge.x = (RES.y * screen_ar) - RES.x; + } else { + nudge.y = (RES.x / screen_ar) - RES.y; + } + var new_logical_resolution = RES + nudge; + + // Now we can convert the incoming position to clip space, in the new screen. + let centered = in_pos + (nudge / 2.0); + var position = (2.0 * centered / new_logical_resolution) - 1.0; + position.y = -position.y; + + return position; +} diff --git a/src/lib.rs b/src/lib.rs index afe285da..bdf3b59c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,6 +85,60 @@ impl SpriteInstance { } } +#[repr(C)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] +struct CircleInstance { + center: [f32; 2], + radius: f32, + stroke_width: f32, + stroke_color: [f32; 4], + fill_color: [f32; 4], +} + +impl CircleInstance { + fn desc() -> wgpu::VertexBufferLayout<'static> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Instance, + attributes: &[ + wgpu::VertexAttribute { + offset: 0, + shader_location: 5, + format: wgpu::VertexFormat::Float32x2, + }, + wgpu::VertexAttribute { + offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, + shader_location: 6, + format: wgpu::VertexFormat::Float32, + }, + wgpu::VertexAttribute { + offset: (std::mem::size_of::<[f32; 2]>() + std::mem::size_of::()) + as wgpu::BufferAddress, + shader_location: 7, + format: wgpu::VertexFormat::Float32, + }, + wgpu::VertexAttribute { + offset: (std::mem::size_of::<[f32; 2]>() + + std::mem::size_of::() + + std::mem::size_of::()) + as wgpu::BufferAddress, + shader_location: 8, + format: wgpu::VertexFormat::Float32x4, + }, + wgpu::VertexAttribute { + offset: (std::mem::size_of::<[f32; 2]>() + + std::mem::size_of::() + + std::mem::size_of::() + + std::mem::size_of::<[f32; 4]>()) + as wgpu::BufferAddress, + shader_location: 9, + format: wgpu::VertexFormat::Float32x4, + }, + ], + } + } +} + #[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] struct ScreenUniforms { @@ -208,15 +262,20 @@ struct State { config: wgpu::SurfaceConfiguration, size: winit::dpi::PhysicalSize, window: Window, - render_pipeline: wgpu::RenderPipeline, sprite_vertex_buffer: wgpu::Buffer, + + sprite_pipeline: wgpu::RenderPipeline, sprite_instance_buffers: Vec>, next_free_sprite_instance_buffer: usize, sprite_bind_group_layout: wgpu::BindGroupLayout, sprite_textures: HashMap, + circle_pipeline: wgpu::RenderPipeline, + circle_instance_buffers: Vec>, + next_free_circle_instance_buffer: usize, + write_textures: HashMap, screen_uniform: ScreenUniforms, @@ -366,6 +425,49 @@ impl State { multiview: None, }); + let circle_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Circle Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("circle_shader.wgsl").into()), + }); + + let circle_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Circle Pipeline"), + layout: Some(&sprite_pipeline_layout), + vertex: wgpu::VertexState { + module: &circle_shader, + entry_point: "vs_main", + buffers: &[Vertex::desc(), CircleInstance::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &circle_shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + front_face: wgpu::FrontFace::Ccw, + cull_mode: Some(wgpu::Face::Back), + // Setting this to anything other than Fill requires Features::NON_FILL_POLYGON_MODE + polygon_mode: wgpu::PolygonMode::Fill, + // Requires Features::DEPTH_CLIP_CONTROL + unclipped_depth: false, + // Requires Features::CONSERVATIVE_RASTERIZATION + conservative: false, + }, + depth_stencil: None, + multisample: wgpu::MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + multiview: None, + }); + let sprite_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("Sprite Vertex Buffer"), contents: bytemuck::cast_slice(SPRITE_VERTICES), @@ -379,7 +481,7 @@ impl State { queue: hardware.queue, config, size, - render_pipeline: sprite_pipeline, + sprite_pipeline, sprite_vertex_buffer, sprite_instance_buffers: Vec::new(), @@ -387,6 +489,11 @@ impl State { sprite_bind_group_layout, sprite_textures: HashMap::new(), + + circle_pipeline, + circle_instance_buffers: Vec::new(), + next_free_circle_instance_buffer: 0, + write_textures: HashMap::new(), screen_uniform, screen_uniform_buffer, @@ -809,7 +916,7 @@ impl<'a> FrameBuilder<'a> { depth_stencil_attachment: None, }); - pass.set_pipeline(&self.state.render_pipeline); + pass.set_pipeline(&self.state.sprite_pipeline); for call in &self.draw_calls { call.draw(&self.state, &mut pass); } diff --git a/src/sprite_shader.wgsl b/src/sprite_shader.wgsl index 267aeb3c..20387c57 100644 --- a/src/sprite_shader.wgsl +++ b/src/sprite_shader.wgsl @@ -1,10 +1,6 @@ +// ---------------------------------------------------------------------------- // Vertex shader - -struct ScreenUniform { - resolution : vec2f, -}; -@group(1) @binding(0) // 1. - var screen : ScreenUniform; +// ---------------------------------------------------------------------------- struct VertexInput { @location(0) position : vec3, @@ -23,12 +19,44 @@ struct VertexOutput { @location(0) tex_coords : vec2, }; -const RES = vec2f(320.0, 240.0); // The logical resolution of the screen. - @vertex fn vs_main(vertex : VertexInput, instance : InstanceInput)->VertexOutput { var out : VertexOutput; out.tex_coords = instance.src_top_left + (vertex.tex_coords * instance.src_dims); + let in_pos = instance.dest_top_left + (vec2f(vertex.position.x, vertex.position.y) * instance.dest_dims); + + let position = adjust_for_resolution(in_pos); + out.clip_position = vec4f(position.x, position.y, vertex.position.z, 1.0); + return out; +} + +// ---------------------------------------------------------------------------- +// Fragment shader +// ---------------------------------------------------------------------------- + +@group(0) @binding(0) var t_diffuse : texture_2d; +@group(0) @binding(1) var s_diffuse : sampler; + +@fragment fn fs_main(in : VertexOutput)->@location(0) vec4 { + let tc = vec2(u32(in.tex_coords.x), u32(in.tex_coords.y)); + return textureLoad(t_diffuse, tc, 0); +} + + +// ---------------------------------------------------------------------------- +// Resolution Handling +// ---------------------------------------------------------------------------- + +struct ScreenUniform { + resolution : vec2f, +}; +@group(1) @binding(0) // 1. + var screen : ScreenUniform; + +const RES = vec2f(320.0, 240.0); // The logical resolution of the screen. + +fn adjust_for_resolution(in_pos: vec2) -> vec2 { + // Adjust in_pos for the "resolution" of the screen. let RES_AR = RES.x / RES.y; // The aspect ratio of the logical screen. // the actual resolution of the screen. @@ -47,22 +75,10 @@ const RES = vec2f(320.0, 240.0); // The logical resolution of the screen. } var new_logical_resolution = RES + nudge; - // Now we can convert the incoming position to clip space, in the new screen. - let in_pos = instance.dest_top_left + (vec2f(vertex.position.x, vertex.position.y) * instance.dest_dims); + // Now we can convert the incoming position to clip space, in the new screen. let centered = in_pos + (nudge / 2.0); var position = (2.0 * centered / new_logical_resolution) - 1.0; position.y = -position.y; - - out.clip_position = vec4f(position.x, position.y, vertex.position.z, 1.0); - return out; -} - -// Fragment shader.... - -@group(0) @binding(0) var t_diffuse : texture_2d; -@group(0) @binding(1) var s_diffuse : sampler; - -@fragment fn fs_main(in : VertexOutput)->@location(0) vec4 { - let tc = vec2(u32(in.tex_coords.x), u32(in.tex_coords.y)); - return textureLoad(t_diffuse, tc, 0); + + return position; }