shaders.wgsl

  1struct Globals {
  2    viewport_size: vec2<f32>,
  3    pad: vec2<u32>,
  4}
  5
  6var<uniform> globals: Globals;
  7
  8const M_PI_F: f32 = 3.1415926;
  9
 10struct ViewId {
 11    lo: u32,
 12    hi: u32,
 13}
 14
 15struct Bounds {
 16    origin: vec2<f32>,
 17    size: vec2<f32>,
 18}
 19struct Corners {
 20    top_left: f32,
 21    top_right: f32,
 22    bottom_right: f32,
 23    bottom_left: f32,
 24}
 25struct Edges {
 26    top: f32,
 27    right: f32,
 28    bottom: f32,
 29    left: f32,
 30}
 31struct Hsla {
 32    h: f32,
 33    s: f32,
 34    l: f32,
 35    a: f32,
 36}
 37
 38fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
 39    let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
 40    let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
 41    return vec4<f32>(device_position, 0.0, 1.0);
 42}
 43
 44fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
 45    let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
 46    let tl = position - clip_bounds.origin;
 47    let br = clip_bounds.origin + clip_bounds.size - position;
 48    return vec4<f32>(tl.x, br.x, tl.y, br.y);
 49}
 50
 51fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
 52    let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
 53    let s = hsla.s;
 54    let l = hsla.l;
 55    let a = hsla.a;
 56
 57    let c = (1.0 - abs(2.0 * l - 1.0)) * s;
 58    let x = c * (1.0 - abs(h % 2.0 - 1.0));
 59    let m = l - c / 2.0;
 60
 61    var color = vec4<f32>(m, m, m, a);
 62
 63    if (h >= 0.0 && h < 1.0) {
 64        color.r += c;
 65        color.g += x;
 66    } else if (h >= 1.0 && h < 2.0) {
 67        color.r += x;
 68        color.g += c;
 69    } else if (h >= 2.0 && h < 3.0) {
 70        color.g += c;
 71        color.b += x;
 72    } else if (h >= 3.0 && h < 4.0) {
 73        color.g += x;
 74        color.b += c;
 75    } else if (h >= 4.0 && h < 5.0) {
 76        color.r += x;
 77        color.b += c;
 78    } else {
 79        color.r += c;
 80        color.b += x;
 81    }
 82
 83    return color;
 84}
 85
 86fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
 87    let alpha = above.a + below.a * (1.0 - above.a);
 88    let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
 89    return vec4<f32>(color, alpha);
 90}
 91
 92// A standard gaussian function, used for weighting samples
 93fn gaussian(x: f32, sigma: f32) -> f32{
 94    return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
 95}
 96
 97// This approximates the error function, needed for the gaussian integral
 98fn erf(v: vec2<f32>) -> vec2<f32> {
 99    let s = sign(v);
100    let a = abs(v);
101    let r1 = 1.0 + (0.278393 + (0.230389 + 0.078108 * (a * a)) * a) * a;
102    let r2 = r1 * r1;
103    return s - s / (r2 * r2);
104}
105
106fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
107  let delta = min(half_size.y - corner - abs(y), 0.0);
108  let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
109  let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
110  return integral.y - integral.x;
111}
112
113fn pick_corner_radius(point: vec2<f32>, radii: Corners) -> f32 {
114    if (point.x < 0.0) {
115        if (point.y < 0.0) {
116            return radii.top_left;
117        } else {
118            return radii.bottom_left;
119        }
120    } else {
121        if (point.y < 0.0) {
122            return radii.top_right;
123        } else {
124            return radii.bottom_right;
125        }
126    }
127}
128
129// --- quads --- //
130
131struct Quad {
132    view_id: ViewId,
133    layer_id: u32,
134    order: u32,
135    bounds: Bounds,
136    content_mask: Bounds,
137    background: Hsla,
138    border_color: Hsla,
139    corner_radii: Corners,
140    border_widths: Edges,
141}
142var<storage, read> b_quads: array<Quad>;
143
144struct QuadVarying {
145    @builtin(position) position: vec4<f32>,
146    @location(0) @interpolate(flat) background_color: vec4<f32>,
147    @location(1) @interpolate(flat) border_color: vec4<f32>,
148    @location(2) @interpolate(flat) quad_id: u32,
149    //TODO: use `clip_distance` once Naga supports it
150    @location(3) clip_distances: vec4<f32>,
151}
152
153@vertex
154fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
155    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
156    let quad = b_quads[instance_id];
157
158    var out = QuadVarying();
159    out.position = to_device_position(unit_vertex, quad.bounds);
160    out.background_color = hsla_to_rgba(quad.background);
161    out.border_color = hsla_to_rgba(quad.border_color);
162    out.quad_id = instance_id;
163    out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
164    return out;
165}
166
167@fragment
168fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
169    // Alpha clip first, since we don't have `clip_distance`.
170    if (any(input.clip_distances < vec4<f32>(0.0))) {
171        return vec4<f32>(0.0);
172    }
173
174    let quad = b_quads[input.quad_id];
175    let half_size = quad.bounds.size / 2.0;
176    let center = quad.bounds.origin + half_size;
177    let center_to_point = input.position.xy - center;
178
179    let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
180
181    let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
182    let distance =
183      length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
184      min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
185      corner_radius;
186
187    let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0);
188    let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0);
189    let inset_size = half_size - corner_radius - vec2<f32>(vertical_border, horizontal_border);
190    let point_to_inset_corner = abs(center_to_point) - inset_size;
191
192    var border_width = 0.0;
193    if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) {
194        border_width = 0.0;
195    } else if (point_to_inset_corner.y > point_to_inset_corner.x) {
196        border_width = horizontal_border;
197    } else {
198        border_width = vertical_border;
199    }
200
201    var color = input.background_color;
202    if (border_width > 0.0) {
203        let inset_distance = distance + border_width;
204        // Blend the border on top of the background and then linearly interpolate
205        // between the two as we slide inside the background.
206        let blended_border = over(input.background_color, input.border_color);
207        color = mix(blended_border, input.background_color,
208                    saturate(0.5 - inset_distance));
209    }
210
211    return color * vec4<f32>(1.0, 1.0, 1.0, saturate(0.5 - distance));
212}
213
214// --- shadows --- //
215
216struct Shadow {
217    view_id: ViewId,
218    layer_id: u32,
219    order: u32,
220    bounds: Bounds,
221    corner_radii: Corners,
222    content_mask: Bounds,
223    color: Hsla,
224    blur_radius: f32,
225    pad: u32,
226}
227var<storage, read> b_shadows: array<Shadow>;
228
229struct ShadowVarying {
230    @builtin(position) position: vec4<f32>,
231    @location(0) @interpolate(flat) color: vec4<f32>,
232    @location(1) @interpolate(flat) shadow_id: u32,
233    //TODO: use `clip_distance` once Naga supports it
234    @location(3) clip_distances: vec4<f32>,
235}
236
237@vertex
238fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
239    let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
240    let shadow = b_shadows[instance_id];
241
242    let margin = 3.0 * shadow.blur_radius;
243    // Set the bounds of the shadow and adjust its size based on the shadow's
244    // spread radius to achieve the spreading effect
245    var bounds = shadow.bounds;
246    bounds.origin -= vec2<f32>(margin);
247    bounds.size += 2.0 * vec2<f32>(margin);
248
249    var out = ShadowVarying();
250    out.position = to_device_position(unit_vertex, shadow.bounds);
251    out.color = hsla_to_rgba(shadow.color);
252    out.shadow_id = instance_id;
253    out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
254    return out;
255}
256
257@fragment
258fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
259    // Alpha clip first, since we don't have `clip_distance`.
260    if (any(input.clip_distances < vec4<f32>(0.0))) {
261        return vec4<f32>(0.0);
262    }
263
264    let shadow = b_shadows[input.shadow_id];
265    let half_size = shadow.bounds.size / 2.0;
266    let center = shadow.bounds.origin + half_size;
267    let center_to_point = input.position.xy - center;
268
269    let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
270
271    // The signal is only non-zero in a limited range, so don't waste samples
272    let low = center_to_point.y - half_size.y;
273    let high = center_to_point.y + half_size.y;
274    let start = clamp(-3.0 * shadow.blur_radius, low, high);
275    let end = clamp(3.0 * shadow.blur_radius, low, high);
276
277    // Accumulate samples (we can get away with surprisingly few samples)
278    let step = (end - start) / 4.0;
279    var y = start + step * 0.5;
280    var alpha = 0.0;
281    for (var i = 0; i < 4; i += 1) {
282        let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
283            shadow.blur_radius, corner_radius, half_size);
284        alpha +=  blur * gaussian(y, shadow.blur_radius) * step;
285        y += step;
286    }
287
288    return input.color * vec4<f32>(1.0, 1.0, 1.0, alpha);
289}