// ---------------------------------------------------------------------------- // 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 = (vertex.tex_coords * vec2f(2.0,2.0)) - vec2f(1.0,1.0); // 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, 0.0); } } // ---------------------------------------------------------------------------- // Resolution Handling // ---------------------------------------------------------------------------- struct ScreenUniform { resolution : vec2f, }; @group(0) @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; }