
Reverts zed-industries/zed#29718 We've noticed some issues with Zed on Intel-based Macs where typing has become sluggish, and git bisect has seemed to point towards this PR. Reverting for now, until we can understand why it is causing this issue.
1225 lines
45 KiB
WebGPU Shading Language
1225 lines
45 KiB
WebGPU Shading Language
/* Functions useful for debugging:
|
|
|
|
// A heat map color for debugging (blue -> cyan -> green -> yellow -> red).
|
|
fn heat_map_color(value: f32, minValue: f32, maxValue: f32, position: vec2<f32>) -> vec4<f32> {
|
|
// Normalize value to 0-1 range
|
|
let t = clamp((value - minValue) / (maxValue - minValue), 0.0, 1.0);
|
|
|
|
// Heat map color calculation
|
|
let r = t * t;
|
|
let g = 4.0 * t * (1.0 - t);
|
|
let b = (1.0 - t) * (1.0 - t);
|
|
let heat_color = vec3<f32>(r, g, b);
|
|
|
|
// Create a checkerboard pattern (black and white)
|
|
let sum = floor(position.x / 3) + floor(position.y / 3);
|
|
let is_odd = fract(sum * 0.5); // 0.0 for even, 0.5 for odd
|
|
let checker_value = is_odd * 2.0; // 0.0 for even, 1.0 for odd
|
|
let checker_color = vec3<f32>(checker_value);
|
|
|
|
// Determine if value is in range (1.0 if in range, 0.0 if out of range)
|
|
let in_range = step(minValue, value) * step(value, maxValue);
|
|
|
|
// Mix checkerboard and heat map based on whether value is in range
|
|
let final_color = mix(checker_color, heat_color, in_range);
|
|
|
|
return vec4<f32>(final_color, 1.0);
|
|
}
|
|
|
|
*/
|
|
|
|
struct GlobalParams {
|
|
viewport_size: vec2<f32>,
|
|
premultiplied_alpha: u32,
|
|
pad: u32,
|
|
}
|
|
|
|
var<uniform> globals: GlobalParams;
|
|
var t_sprite: texture_2d<f32>;
|
|
var s_sprite: sampler;
|
|
|
|
const M_PI_F: f32 = 3.1415926;
|
|
const GRAYSCALE_FACTORS: vec3<f32> = vec3<f32>(0.2126, 0.7152, 0.0722);
|
|
|
|
struct Bounds {
|
|
origin: vec2<f32>,
|
|
size: vec2<f32>,
|
|
}
|
|
|
|
struct Corners {
|
|
top_left: f32,
|
|
top_right: f32,
|
|
bottom_right: f32,
|
|
bottom_left: f32,
|
|
}
|
|
|
|
struct Edges {
|
|
top: f32,
|
|
right: f32,
|
|
bottom: f32,
|
|
left: f32,
|
|
}
|
|
|
|
struct Hsla {
|
|
h: f32,
|
|
s: f32,
|
|
l: f32,
|
|
a: f32,
|
|
}
|
|
|
|
struct LinearColorStop {
|
|
color: Hsla,
|
|
percentage: f32,
|
|
}
|
|
|
|
struct Background {
|
|
// 0u is Solid
|
|
// 1u is LinearGradient
|
|
// 2u is PatternSlash
|
|
tag: u32,
|
|
// 0u is sRGB linear color
|
|
// 1u is Oklab color
|
|
color_space: u32,
|
|
solid: Hsla,
|
|
gradient_angle_or_pattern_height: f32,
|
|
colors: array<LinearColorStop, 2>,
|
|
pad: u32,
|
|
}
|
|
|
|
struct AtlasTextureId {
|
|
index: u32,
|
|
kind: u32,
|
|
}
|
|
|
|
struct AtlasBounds {
|
|
origin: vec2<i32>,
|
|
size: vec2<i32>,
|
|
}
|
|
|
|
struct AtlasTile {
|
|
texture_id: AtlasTextureId,
|
|
tile_id: u32,
|
|
padding: u32,
|
|
bounds: AtlasBounds,
|
|
}
|
|
|
|
struct TransformationMatrix {
|
|
rotation_scale: mat2x2<f32>,
|
|
translation: vec2<f32>,
|
|
}
|
|
|
|
fn to_device_position_impl(position: vec2<f32>) -> vec4<f32> {
|
|
let device_position = position / globals.viewport_size * vec2<f32>(2.0, -2.0) + vec2<f32>(-1.0, 1.0);
|
|
return vec4<f32>(device_position, 0.0, 1.0);
|
|
}
|
|
|
|
fn to_device_position(unit_vertex: vec2<f32>, bounds: Bounds) -> vec4<f32> {
|
|
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
|
|
return to_device_position_impl(position);
|
|
}
|
|
|
|
fn to_device_position_transformed(unit_vertex: vec2<f32>, bounds: Bounds, transform: TransformationMatrix) -> vec4<f32> {
|
|
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
|
|
//Note: Rust side stores it as row-major, so transposing here
|
|
let transformed = transpose(transform.rotation_scale) * position + transform.translation;
|
|
return to_device_position_impl(transformed);
|
|
}
|
|
|
|
fn to_tile_position(unit_vertex: vec2<f32>, tile: AtlasTile) -> vec2<f32> {
|
|
let atlas_size = vec2<f32>(textureDimensions(t_sprite, 0));
|
|
return (vec2<f32>(tile.bounds.origin) + unit_vertex * vec2<f32>(tile.bounds.size)) / atlas_size;
|
|
}
|
|
|
|
fn distance_from_clip_rect_impl(position: vec2<f32>, clip_bounds: Bounds) -> vec4<f32> {
|
|
let tl = position - clip_bounds.origin;
|
|
let br = clip_bounds.origin + clip_bounds.size - position;
|
|
return vec4<f32>(tl.x, br.x, tl.y, br.y);
|
|
}
|
|
|
|
fn distance_from_clip_rect(unit_vertex: vec2<f32>, bounds: Bounds, clip_bounds: Bounds) -> vec4<f32> {
|
|
let position = unit_vertex * vec2<f32>(bounds.size) + bounds.origin;
|
|
return distance_from_clip_rect_impl(position, clip_bounds);
|
|
}
|
|
|
|
// https://gamedev.stackexchange.com/questions/92015/optimized-linear-to-srgb-glsl
|
|
fn srgb_to_linear(srgb: vec3<f32>) -> vec3<f32> {
|
|
let cutoff = srgb < vec3<f32>(0.04045);
|
|
let higher = pow((srgb + vec3<f32>(0.055)) / vec3<f32>(1.055), vec3<f32>(2.4));
|
|
let lower = srgb / vec3<f32>(12.92);
|
|
return select(higher, lower, cutoff);
|
|
}
|
|
|
|
fn linear_to_srgb(linear: vec3<f32>) -> vec3<f32> {
|
|
let cutoff = linear < vec3<f32>(0.0031308);
|
|
let higher = vec3<f32>(1.055) * pow(linear, vec3<f32>(1.0 / 2.4)) - vec3<f32>(0.055);
|
|
let lower = linear * vec3<f32>(12.92);
|
|
return select(higher, lower, cutoff);
|
|
}
|
|
|
|
/// Convert a linear color to sRGBA space.
|
|
fn linear_to_srgba(color: vec4<f32>) -> vec4<f32> {
|
|
return vec4<f32>(linear_to_srgb(color.rgb), color.a);
|
|
}
|
|
|
|
/// Convert a sRGBA color to linear space.
|
|
fn srgba_to_linear(color: vec4<f32>) -> vec4<f32> {
|
|
return vec4<f32>(srgb_to_linear(color.rgb), color.a);
|
|
}
|
|
|
|
/// Hsla to linear RGBA conversion.
|
|
fn hsla_to_rgba(hsla: Hsla) -> vec4<f32> {
|
|
let h = hsla.h * 6.0; // Now, it's an angle but scaled in [0, 6) range
|
|
let s = hsla.s;
|
|
let l = hsla.l;
|
|
let a = hsla.a;
|
|
|
|
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
|
|
let x = c * (1.0 - abs(h % 2.0 - 1.0));
|
|
let m = l - c / 2.0;
|
|
var color = vec3<f32>(m);
|
|
|
|
if (h >= 0.0 && h < 1.0) {
|
|
color.r += c;
|
|
color.g += x;
|
|
} else if (h >= 1.0 && h < 2.0) {
|
|
color.r += x;
|
|
color.g += c;
|
|
} else if (h >= 2.0 && h < 3.0) {
|
|
color.g += c;
|
|
color.b += x;
|
|
} else if (h >= 3.0 && h < 4.0) {
|
|
color.g += x;
|
|
color.b += c;
|
|
} else if (h >= 4.0 && h < 5.0) {
|
|
color.r += x;
|
|
color.b += c;
|
|
} else {
|
|
color.r += c;
|
|
color.b += x;
|
|
}
|
|
|
|
// Input colors are assumed to be in sRGB space,
|
|
// but blending and rendering needs to happen in linear space.
|
|
// The output will be converted to sRGB by either the target
|
|
// texture format or the swapchain color space.
|
|
let linear = srgb_to_linear(color);
|
|
return vec4<f32>(linear, a);
|
|
}
|
|
|
|
/// Convert a linear sRGB to Oklab space.
|
|
/// Reference: https://bottosson.github.io/posts/oklab/#converting-from-linear-srgb-to-oklab
|
|
fn linear_srgb_to_oklab(color: vec4<f32>) -> vec4<f32> {
|
|
let l = 0.4122214708 * color.r + 0.5363325363 * color.g + 0.0514459929 * color.b;
|
|
let m = 0.2119034982 * color.r + 0.6806995451 * color.g + 0.1073969566 * color.b;
|
|
let s = 0.0883024619 * color.r + 0.2817188376 * color.g + 0.6299787005 * color.b;
|
|
|
|
let l_ = pow(l, 1.0 / 3.0);
|
|
let m_ = pow(m, 1.0 / 3.0);
|
|
let s_ = pow(s, 1.0 / 3.0);
|
|
|
|
return vec4<f32>(
|
|
0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
|
|
1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
|
|
0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
|
|
color.a
|
|
);
|
|
}
|
|
|
|
/// Convert an Oklab color to linear sRGB space.
|
|
fn oklab_to_linear_srgb(color: vec4<f32>) -> vec4<f32> {
|
|
let l_ = color.r + 0.3963377774 * color.g + 0.2158037573 * color.b;
|
|
let m_ = color.r - 0.1055613458 * color.g - 0.0638541728 * color.b;
|
|
let s_ = color.r - 0.0894841775 * color.g - 1.2914855480 * color.b;
|
|
|
|
let l = l_ * l_ * l_;
|
|
let m = m_ * m_ * m_;
|
|
let s = s_ * s_ * s_;
|
|
|
|
return vec4<f32>(
|
|
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
|
|
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
|
|
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
|
|
color.a
|
|
);
|
|
}
|
|
|
|
fn over(below: vec4<f32>, above: vec4<f32>) -> vec4<f32> {
|
|
let alpha = above.a + below.a * (1.0 - above.a);
|
|
let color = (above.rgb * above.a + below.rgb * below.a * (1.0 - above.a)) / alpha;
|
|
return vec4<f32>(color, alpha);
|
|
}
|
|
|
|
// A standard gaussian function, used for weighting samples
|
|
fn gaussian(x: f32, sigma: f32) -> f32{
|
|
return exp(-(x * x) / (2.0 * sigma * sigma)) / (sqrt(2.0 * M_PI_F) * sigma);
|
|
}
|
|
|
|
// This approximates the error function, needed for the gaussian integral
|
|
fn erf(v: vec2<f32>) -> vec2<f32> {
|
|
let s = sign(v);
|
|
let a = abs(v);
|
|
let r1 = 1.0 + (0.278393 + (0.230389 + (0.000972 + 0.078108 * a) * a) * a) * a;
|
|
let r2 = r1 * r1;
|
|
return s - s / (r2 * r2);
|
|
}
|
|
|
|
fn blur_along_x(x: f32, y: f32, sigma: f32, corner: f32, half_size: vec2<f32>) -> f32 {
|
|
let delta = min(half_size.y - corner - abs(y), 0.0);
|
|
let curved = half_size.x - corner + sqrt(max(0.0, corner * corner - delta * delta));
|
|
let integral = 0.5 + 0.5 * erf((x + vec2<f32>(-curved, curved)) * (sqrt(0.5) / sigma));
|
|
return integral.y - integral.x;
|
|
}
|
|
|
|
// Selects corner radius based on quadrant.
|
|
fn pick_corner_radius(center_to_point: vec2<f32>, radii: Corners) -> f32 {
|
|
if (center_to_point.x < 0.0) {
|
|
if (center_to_point.y < 0.0) {
|
|
return radii.top_left;
|
|
} else {
|
|
return radii.bottom_left;
|
|
}
|
|
} else {
|
|
if (center_to_point.y < 0.0) {
|
|
return radii.top_right;
|
|
} else {
|
|
return radii.bottom_right;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Signed distance of the point to the quad's border - positive outside the
|
|
// border, and negative inside.
|
|
//
|
|
// See comments on similar code using `quad_sdf_impl` in `fs_quad` for
|
|
// explanation.
|
|
fn quad_sdf(point: vec2<f32>, bounds: Bounds, corner_radii: Corners) -> f32 {
|
|
let half_size = bounds.size / 2.0;
|
|
let center = bounds.origin + half_size;
|
|
let center_to_point = point - center;
|
|
let corner_radius = pick_corner_radius(center_to_point, corner_radii);
|
|
let corner_to_point = abs(center_to_point) - half_size;
|
|
let corner_center_to_point = corner_to_point + corner_radius;
|
|
return quad_sdf_impl(corner_center_to_point, corner_radius);
|
|
}
|
|
|
|
fn quad_sdf_impl(corner_center_to_point: vec2<f32>, corner_radius: f32) -> f32 {
|
|
if (corner_radius == 0.0) {
|
|
// Fast path for unrounded corners.
|
|
return max(corner_center_to_point.x, corner_center_to_point.y);
|
|
} else {
|
|
// Signed distance of the point from a quad that is inset by corner_radius.
|
|
// It is negative inside this quad, and positive outside.
|
|
let signed_distance_to_inset_quad =
|
|
// 0 inside the inset quad, and positive outside.
|
|
length(max(vec2<f32>(0.0), corner_center_to_point)) +
|
|
// 0 outside the inset quad, and negative inside.
|
|
min(0.0, max(corner_center_to_point.x, corner_center_to_point.y));
|
|
|
|
return signed_distance_to_inset_quad - corner_radius;
|
|
}
|
|
}
|
|
|
|
// Abstract away the final color transformation based on the
|
|
// target alpha compositing mode.
|
|
fn blend_color(color: vec4<f32>, alpha_factor: f32) -> vec4<f32> {
|
|
let alpha = color.a * alpha_factor;
|
|
let multiplier = select(1.0, alpha, globals.premultiplied_alpha != 0u);
|
|
return vec4<f32>(color.rgb * multiplier, alpha);
|
|
}
|
|
|
|
|
|
struct GradientColor {
|
|
solid: vec4<f32>,
|
|
color0: vec4<f32>,
|
|
color1: vec4<f32>,
|
|
}
|
|
|
|
fn prepare_gradient_color(tag: u32, color_space: u32,
|
|
solid: Hsla, colors: array<LinearColorStop, 2>) -> GradientColor {
|
|
var result = GradientColor();
|
|
|
|
if (tag == 0u || tag == 2u) {
|
|
result.solid = hsla_to_rgba(solid);
|
|
} else if (tag == 1u) {
|
|
// The hsla_to_rgba is returns a linear sRGB color
|
|
result.color0 = hsla_to_rgba(colors[0].color);
|
|
result.color1 = hsla_to_rgba(colors[1].color);
|
|
|
|
// Prepare color space in vertex for avoid conversion
|
|
// in fragment shader for performance reasons
|
|
if (color_space == 0u) {
|
|
// sRGB
|
|
result.color0 = linear_to_srgba(result.color0);
|
|
result.color1 = linear_to_srgba(result.color1);
|
|
} else if (color_space == 1u) {
|
|
// Oklab
|
|
result.color0 = linear_srgb_to_oklab(result.color0);
|
|
result.color1 = linear_srgb_to_oklab(result.color1);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
fn gradient_color(background: Background, position: vec2<f32>, bounds: Bounds,
|
|
solid_color: vec4<f32>, color0: vec4<f32>, color1: vec4<f32>) -> vec4<f32> {
|
|
var background_color = vec4<f32>(0.0);
|
|
|
|
switch (background.tag) {
|
|
default: {
|
|
return solid_color;
|
|
}
|
|
case 1u: {
|
|
// Linear gradient background.
|
|
// -90 degrees to match the CSS gradient angle.
|
|
let angle = background.gradient_angle_or_pattern_height;
|
|
let radians = (angle % 360.0 - 90.0) * M_PI_F / 180.0;
|
|
var direction = vec2<f32>(cos(radians), sin(radians));
|
|
let stop0_percentage = background.colors[0].percentage;
|
|
let stop1_percentage = background.colors[1].percentage;
|
|
|
|
// Expand the short side to be the same as the long side
|
|
if (bounds.size.x > bounds.size.y) {
|
|
direction.y *= bounds.size.y / bounds.size.x;
|
|
} else {
|
|
direction.x *= bounds.size.x / bounds.size.y;
|
|
}
|
|
|
|
// Get the t value for the linear gradient with the color stop percentages.
|
|
let half_size = bounds.size / 2.0;
|
|
let center = bounds.origin + half_size;
|
|
let center_to_point = position - center;
|
|
var t = dot(center_to_point, direction) / length(direction);
|
|
// Check the direct to determine the use x or y
|
|
if (abs(direction.x) > abs(direction.y)) {
|
|
t = (t + half_size.x) / bounds.size.x;
|
|
} else {
|
|
t = (t + half_size.y) / bounds.size.y;
|
|
}
|
|
|
|
// Adjust t based on the stop percentages
|
|
t = (t - stop0_percentage) / (stop1_percentage - stop0_percentage);
|
|
t = clamp(t, 0.0, 1.0);
|
|
|
|
switch (background.color_space) {
|
|
default: {
|
|
background_color = srgba_to_linear(mix(color0, color1, t));
|
|
}
|
|
case 1u: {
|
|
let oklab_color = mix(color0, color1, t);
|
|
background_color = oklab_to_linear_srgb(oklab_color);
|
|
}
|
|
}
|
|
}
|
|
case 2u: {
|
|
let gradient_angle_or_pattern_height = background.gradient_angle_or_pattern_height;
|
|
let pattern_width = (gradient_angle_or_pattern_height / 65535.0f) / 255.0f;
|
|
let pattern_interval = (gradient_angle_or_pattern_height % 65535.0f) / 255.0f;
|
|
let pattern_height = pattern_width + pattern_interval;
|
|
let stripe_angle = M_PI_F / 4.0;
|
|
let pattern_period = pattern_height * sin(stripe_angle);
|
|
let rotation = mat2x2<f32>(
|
|
cos(stripe_angle), -sin(stripe_angle),
|
|
sin(stripe_angle), cos(stripe_angle)
|
|
);
|
|
let relative_position = position - bounds.origin;
|
|
let rotated_point = rotation * relative_position;
|
|
let pattern = rotated_point.x % pattern_period;
|
|
let distance = min(pattern, pattern_period - pattern) - pattern_period * (pattern_width / pattern_height) / 2.0f;
|
|
background_color = solid_color;
|
|
background_color.a *= saturate(0.5 - distance);
|
|
}
|
|
}
|
|
|
|
return background_color;
|
|
}
|
|
|
|
// --- quads --- //
|
|
|
|
struct Quad {
|
|
order: u32,
|
|
border_style: u32,
|
|
bounds: Bounds,
|
|
content_mask: Bounds,
|
|
background: Background,
|
|
border_color: Hsla,
|
|
corner_radii: Corners,
|
|
border_widths: Edges,
|
|
}
|
|
var<storage, read> b_quads: array<Quad>;
|
|
|
|
struct QuadVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) @interpolate(flat) border_color: vec4<f32>,
|
|
@location(1) @interpolate(flat) quad_id: u32,
|
|
// TODO: use `clip_distance` once Naga supports it
|
|
@location(2) clip_distances: vec4<f32>,
|
|
@location(3) @interpolate(flat) background_solid: vec4<f32>,
|
|
@location(4) @interpolate(flat) background_color0: vec4<f32>,
|
|
@location(5) @interpolate(flat) background_color1: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_quad(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> QuadVarying {
|
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
|
let quad = b_quads[instance_id];
|
|
|
|
var out = QuadVarying();
|
|
out.position = to_device_position(unit_vertex, quad.bounds);
|
|
|
|
let gradient = prepare_gradient_color(
|
|
quad.background.tag,
|
|
quad.background.color_space,
|
|
quad.background.solid,
|
|
quad.background.colors
|
|
);
|
|
out.background_solid = gradient.solid;
|
|
out.background_color0 = gradient.color0;
|
|
out.background_color1 = gradient.color1;
|
|
out.border_color = hsla_to_rgba(quad.border_color);
|
|
out.quad_id = instance_id;
|
|
out.clip_distances = distance_from_clip_rect(unit_vertex, quad.bounds, quad.content_mask);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
|
// Alpha clip first, since we don't have `clip_distance`.
|
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
|
return vec4<f32>(0.0);
|
|
}
|
|
|
|
let quad = b_quads[input.quad_id];
|
|
|
|
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
|
input.background_solid, input.background_color0, input.background_color1);
|
|
|
|
let unrounded = quad.corner_radii.top_left == 0.0 &&
|
|
quad.corner_radii.bottom_left == 0.0 &&
|
|
quad.corner_radii.top_right == 0.0 &&
|
|
quad.corner_radii.bottom_right == 0.0;
|
|
|
|
// Fast path when the quad is not rounded and doesn't have any border
|
|
if (quad.border_widths.top == 0.0 &&
|
|
quad.border_widths.left == 0.0 &&
|
|
quad.border_widths.right == 0.0 &&
|
|
quad.border_widths.bottom == 0.0 &&
|
|
unrounded) {
|
|
return blend_color(background_color, 1.0);
|
|
}
|
|
|
|
let size = quad.bounds.size;
|
|
let half_size = size / 2.0;
|
|
let point = input.position.xy - quad.bounds.origin;
|
|
let center_to_point = point - half_size;
|
|
|
|
// Signed distance field threshold for inclusion of pixels. 0.5 is the
|
|
// minimum distance between the center of the pixel and the edge.
|
|
let antialias_threshold = 0.5;
|
|
|
|
// Radius of the nearest corner
|
|
let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
|
|
|
|
// Width of the nearest borders
|
|
let border = vec2<f32>(
|
|
select(
|
|
quad.border_widths.right,
|
|
quad.border_widths.left,
|
|
center_to_point.x < 0.0),
|
|
select(
|
|
quad.border_widths.bottom,
|
|
quad.border_widths.top,
|
|
center_to_point.y < 0.0));
|
|
|
|
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
|
|
// The purpose of this is to not draw antialiasing pixels in this case.
|
|
let reduced_border =
|
|
vec2<f32>(select(border.x, -antialias_threshold, border.x == 0.0),
|
|
select(border.y, -antialias_threshold, border.y == 0.0));
|
|
|
|
// Vector from the corner of the quad bounds to the point, after mirroring
|
|
// the point into the bottom right quadrant. Both components are <= 0.
|
|
let corner_to_point = abs(center_to_point) - half_size;
|
|
|
|
// Vector from the point to the center of the rounded corner's circle, also
|
|
// mirrored into bottom right quadrant.
|
|
let corner_center_to_point = corner_to_point + corner_radius;
|
|
|
|
// Whether the nearest point on the border is rounded
|
|
let is_near_rounded_corner =
|
|
corner_center_to_point.x >= 0 &&
|
|
corner_center_to_point.y >= 0;
|
|
|
|
// Vector from straight border inner corner to point.
|
|
let straight_border_inner_corner_to_point = corner_to_point + reduced_border;
|
|
|
|
// Whether the point is beyond the inner edge of the straight border.
|
|
let is_beyond_inner_straight_border =
|
|
straight_border_inner_corner_to_point.x > 0 ||
|
|
straight_border_inner_corner_to_point.y > 0;
|
|
|
|
// Whether the point is far enough inside the quad, such that the pixels are
|
|
// not affected by the straight border.
|
|
let is_within_inner_straight_border =
|
|
straight_border_inner_corner_to_point.x < -antialias_threshold &&
|
|
straight_border_inner_corner_to_point.y < -antialias_threshold;
|
|
|
|
// Fast path for points that must be part of the background.
|
|
//
|
|
// This could be optimized further for large rounded corners by including
|
|
// points in an inscribed rectangle, or some other quick linear check.
|
|
// However, that might negatively impact performance in the case of
|
|
// reasonable sizes for rounded corners.
|
|
if (is_within_inner_straight_border && !is_near_rounded_corner) {
|
|
return blend_color(background_color, 1.0);
|
|
}
|
|
|
|
// Signed distance of the point to the outside edge of the quad's border. It
|
|
// is positive outside this edge, and negative inside.
|
|
let outer_sdf = quad_sdf_impl(corner_center_to_point, corner_radius);
|
|
|
|
// Approximate signed distance of the point to the inside edge of the quad's
|
|
// border. It is negative outside this edge (within the border), and
|
|
// positive inside.
|
|
//
|
|
// This is not always an accurate signed distance:
|
|
// * The rounded portions with varying border width use an approximation of
|
|
// nearest-point-on-ellipse.
|
|
// * When it is quickly known to be outside the edge, -1.0 is used.
|
|
var inner_sdf = 0.0;
|
|
if (corner_center_to_point.x <= 0 || corner_center_to_point.y <= 0) {
|
|
// Fast paths for straight borders.
|
|
inner_sdf = -max(straight_border_inner_corner_to_point.x,
|
|
straight_border_inner_corner_to_point.y);
|
|
} else if (is_beyond_inner_straight_border) {
|
|
// Fast path for points that must be outside the inner edge.
|
|
inner_sdf = -1.0;
|
|
} else if (reduced_border.x == reduced_border.y) {
|
|
// Fast path for circular inner edge.
|
|
inner_sdf = -(outer_sdf + reduced_border.x);
|
|
} else {
|
|
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - reduced_border);
|
|
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
|
|
}
|
|
|
|
// Negative when inside the border
|
|
let border_sdf = max(inner_sdf, outer_sdf);
|
|
|
|
var color = background_color;
|
|
if (border_sdf < antialias_threshold) {
|
|
var border_color = input.border_color;
|
|
|
|
// Dashed border logic when border_style == 1
|
|
if (quad.border_style == 1) {
|
|
// Position along the perimeter in "dash space", where each dash
|
|
// period has length 1
|
|
var t = 0.0;
|
|
|
|
// Total number of dash periods, so that the dash spacing can be
|
|
// adjusted to evenly divide it
|
|
var max_t = 0.0;
|
|
|
|
// Border width is proportional to dash size. This is the behavior
|
|
// used by browsers, but also avoids dashes from different segments
|
|
// overlapping when dash size is smaller than the border width.
|
|
//
|
|
// Dash pattern: (2 * border width) dash, (1 * border width) gap
|
|
let dash_length_per_width = 2.0;
|
|
let dash_gap_per_width = 1.0;
|
|
let dash_period_per_width = dash_length_per_width + dash_gap_per_width;
|
|
|
|
// Since the dash size is determined by border width, the density of
|
|
// dashes varies. Multiplying a pixel distance by this returns a
|
|
// position in dash space - it has units (dash period / pixels). So
|
|
// a dash velocity of (1 / 10) is 1 dash every 10 pixels.
|
|
var dash_velocity = 0.0;
|
|
|
|
// Dividing this by the border width gives the dash velocity
|
|
let dv_numerator = 1.0 / dash_period_per_width;
|
|
|
|
if (unrounded) {
|
|
// When corners aren't rounded, the dashes are separately laid
|
|
// out on each straight line, rather than around the whole
|
|
// perimeter. This way each line starts and ends with a dash.
|
|
let is_horizontal =
|
|
corner_center_to_point.x <
|
|
corner_center_to_point.y;
|
|
let border_width = select(border.y, border.x, is_horizontal);
|
|
dash_velocity = dv_numerator / border_width;
|
|
t = select(point.y, point.x, is_horizontal) * dash_velocity;
|
|
max_t = select(size.y, size.x, is_horizontal) * dash_velocity;
|
|
} else {
|
|
// When corners are rounded, the dashes are laid out clockwise
|
|
// around the whole perimeter.
|
|
|
|
let r_tr = quad.corner_radii.top_right;
|
|
let r_br = quad.corner_radii.bottom_right;
|
|
let r_bl = quad.corner_radii.bottom_left;
|
|
let r_tl = quad.corner_radii.top_left;
|
|
|
|
let w_t = quad.border_widths.top;
|
|
let w_r = quad.border_widths.right;
|
|
let w_b = quad.border_widths.bottom;
|
|
let w_l = quad.border_widths.left;
|
|
|
|
// Straight side dash velocities
|
|
let dv_t = select(dv_numerator / w_t, 0.0, w_t <= 0.0);
|
|
let dv_r = select(dv_numerator / w_r, 0.0, w_r <= 0.0);
|
|
let dv_b = select(dv_numerator / w_b, 0.0, w_b <= 0.0);
|
|
let dv_l = select(dv_numerator / w_l, 0.0, w_l <= 0.0);
|
|
|
|
// Straight side lengths in dash space
|
|
let s_t = (size.x - r_tl - r_tr) * dv_t;
|
|
let s_r = (size.y - r_tr - r_br) * dv_r;
|
|
let s_b = (size.x - r_br - r_bl) * dv_b;
|
|
let s_l = (size.y - r_bl - r_tl) * dv_l;
|
|
|
|
let corner_dash_velocity_tr = corner_dash_velocity(dv_t, dv_r);
|
|
let corner_dash_velocity_br = corner_dash_velocity(dv_b, dv_r);
|
|
let corner_dash_velocity_bl = corner_dash_velocity(dv_b, dv_l);
|
|
let corner_dash_velocity_tl = corner_dash_velocity(dv_t, dv_l);
|
|
|
|
// Corner lengths in dash space
|
|
let c_tr = r_tr * (M_PI_F / 2.0) * corner_dash_velocity_tr;
|
|
let c_br = r_br * (M_PI_F / 2.0) * corner_dash_velocity_br;
|
|
let c_bl = r_bl * (M_PI_F / 2.0) * corner_dash_velocity_bl;
|
|
let c_tl = r_tl * (M_PI_F / 2.0) * corner_dash_velocity_tl;
|
|
|
|
// Cumulative dash space upto each segment
|
|
let upto_tr = s_t;
|
|
let upto_r = upto_tr + c_tr;
|
|
let upto_br = upto_r + s_r;
|
|
let upto_b = upto_br + c_br;
|
|
let upto_bl = upto_b + s_b;
|
|
let upto_l = upto_bl + c_bl;
|
|
let upto_tl = upto_l + s_l;
|
|
max_t = upto_tl + c_tl;
|
|
|
|
if (is_near_rounded_corner) {
|
|
let radians = atan2(corner_center_to_point.y,
|
|
corner_center_to_point.x);
|
|
let corner_t = radians * corner_radius;
|
|
|
|
if (center_to_point.x >= 0.0) {
|
|
if (center_to_point.y < 0.0) {
|
|
dash_velocity = corner_dash_velocity_tr;
|
|
// Subtracted because radians is pi/2 to 0 when
|
|
// going clockwise around the top right corner,
|
|
// since the y axis has been flipped
|
|
t = upto_r - corner_t * dash_velocity;
|
|
} else {
|
|
dash_velocity = corner_dash_velocity_br;
|
|
// Added because radians is 0 to pi/2 when going
|
|
// clockwise around the bottom-right corner
|
|
t = upto_br + corner_t * dash_velocity;
|
|
}
|
|
} else {
|
|
if (center_to_point.y >= 0.0) {
|
|
dash_velocity = corner_dash_velocity_bl;
|
|
// Subtracted because radians is pi/2 to 0 when
|
|
// going clockwise around the bottom-left corner,
|
|
// since the x axis has been flipped
|
|
t = upto_l - corner_t * dash_velocity;
|
|
} else {
|
|
dash_velocity = corner_dash_velocity_tl;
|
|
// Added because radians is 0 to pi/2 when going
|
|
// clockwise around the top-left corner, since both
|
|
// axis were flipped
|
|
t = upto_tl + corner_t * dash_velocity;
|
|
}
|
|
}
|
|
} else {
|
|
// Straight borders
|
|
let is_horizontal =
|
|
corner_center_to_point.x <
|
|
corner_center_to_point.y;
|
|
if (is_horizontal) {
|
|
if (center_to_point.y < 0.0) {
|
|
dash_velocity = dv_t;
|
|
t = (point.x - r_tl) * dash_velocity;
|
|
} else {
|
|
dash_velocity = dv_b;
|
|
t = upto_bl - (point.x - r_bl) * dash_velocity;
|
|
}
|
|
} else {
|
|
if (center_to_point.x < 0.0) {
|
|
dash_velocity = dv_l;
|
|
t = upto_tl - (point.y - r_tl) * dash_velocity;
|
|
} else {
|
|
dash_velocity = dv_r;
|
|
t = upto_r + (point.y - r_tr) * dash_velocity;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let dash_length = dash_length_per_width / dash_period_per_width;
|
|
let desired_dash_gap = dash_gap_per_width / dash_period_per_width;
|
|
|
|
// Straight borders should start and end with a dash, so max_t is
|
|
// reduced to cause this.
|
|
max_t -= select(0.0, dash_length, unrounded);
|
|
if (max_t >= 1.0) {
|
|
// Adjust dash gap to evenly divide max_t.
|
|
let dash_count = floor(max_t);
|
|
let dash_period = max_t / dash_count;
|
|
border_color.a *= dash_alpha(
|
|
t,
|
|
dash_period,
|
|
dash_length,
|
|
dash_velocity,
|
|
antialias_threshold);
|
|
} else if (unrounded) {
|
|
// When there isn't enough space for the full gap between the
|
|
// two start / end dashes of a straight border, reduce gap to
|
|
// make them fit.
|
|
let dash_gap = max_t - dash_length;
|
|
if (dash_gap > 0.0) {
|
|
let dash_period = dash_length + dash_gap;
|
|
border_color.a *= dash_alpha(
|
|
t,
|
|
dash_period,
|
|
dash_length,
|
|
dash_velocity,
|
|
antialias_threshold);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Blend the border on top of the background and then linearly interpolate
|
|
// between the two as we slide inside the background.
|
|
let blended_border = over(background_color, border_color);
|
|
color = mix(background_color, blended_border,
|
|
saturate(antialias_threshold - inner_sdf));
|
|
}
|
|
|
|
return blend_color(color, saturate(antialias_threshold - outer_sdf));
|
|
}
|
|
|
|
// Returns the dash velocity of a corner given the dash velocity of the two
|
|
// sides, by returning the slower velocity (larger dashes).
|
|
//
|
|
// Since 0 is used for dash velocity when the border width is 0 (instead of
|
|
// +inf), this returns the other dash velocity in that case.
|
|
//
|
|
// An alternative to this might be to appropriately interpolate the dash
|
|
// velocity around the corner, but that seems overcomplicated.
|
|
fn corner_dash_velocity(dv1: f32, dv2: f32) -> f32 {
|
|
if (dv1 == 0.0) {
|
|
return dv2;
|
|
} else if (dv2 == 0.0) {
|
|
return dv1;
|
|
} else {
|
|
return min(dv1, dv2);
|
|
}
|
|
}
|
|
|
|
// Returns alpha used to render antialiased dashes.
|
|
// `t` is within the dash when `fmod(t, period) < length`.
|
|
fn dash_alpha(t: f32, period: f32, length: f32, dash_velocity: f32, antialias_threshold: f32) -> f32 {
|
|
let half_period = period / 2;
|
|
let half_length = length / 2;
|
|
// Value in [-half_period, half_period].
|
|
// The dash is in [-half_length, half_length].
|
|
let centered = fmod(t + half_period - half_length, period) - half_period;
|
|
// Signed distance for the dash, negative values are inside the dash.
|
|
let signed_distance = abs(centered) - half_length;
|
|
// Antialiased alpha based on the signed distance.
|
|
return saturate(antialias_threshold - signed_distance / dash_velocity);
|
|
}
|
|
|
|
// This approximates distance to the nearest point to a quarter ellipse in a way
|
|
// that is sufficient for anti-aliasing when the ellipse is not very eccentric.
|
|
// The components of `point` are expected to be positive.
|
|
//
|
|
// Negative on the outside and positive on the inside.
|
|
fn quarter_ellipse_sdf(point: vec2<f32>, radii: vec2<f32>) -> f32 {
|
|
// Scale the space to treat the ellipse like a unit circle.
|
|
let circle_vec = point / radii;
|
|
let unit_circle_sdf = length(circle_vec) - 1.0;
|
|
// Approximate up-scaling of the length by using the average of the radii.
|
|
//
|
|
// TODO: A better solution would be to use the gradient of the implicit
|
|
// function for an ellipse to approximate a scaling factor.
|
|
return unit_circle_sdf * (radii.x + radii.y) * -0.5;
|
|
}
|
|
|
|
// Modulus that has the same sign as `a`.
|
|
fn fmod(a: f32, b: f32) -> f32 {
|
|
return a - b * trunc(a / b);
|
|
}
|
|
|
|
// --- shadows --- //
|
|
|
|
struct Shadow {
|
|
order: u32,
|
|
blur_radius: f32,
|
|
bounds: Bounds,
|
|
corner_radii: Corners,
|
|
content_mask: Bounds,
|
|
color: Hsla,
|
|
}
|
|
var<storage, read> b_shadows: array<Shadow>;
|
|
|
|
struct ShadowVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) @interpolate(flat) color: vec4<f32>,
|
|
@location(1) @interpolate(flat) shadow_id: u32,
|
|
//TODO: use `clip_distance` once Naga supports it
|
|
@location(3) clip_distances: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_shadow(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> ShadowVarying {
|
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
|
var shadow = b_shadows[instance_id];
|
|
|
|
let margin = 3.0 * shadow.blur_radius;
|
|
// Set the bounds of the shadow and adjust its size based on the shadow's
|
|
// spread radius to achieve the spreading effect
|
|
shadow.bounds.origin -= vec2<f32>(margin);
|
|
shadow.bounds.size += 2.0 * vec2<f32>(margin);
|
|
|
|
var out = ShadowVarying();
|
|
out.position = to_device_position(unit_vertex, shadow.bounds);
|
|
out.color = hsla_to_rgba(shadow.color);
|
|
out.shadow_id = instance_id;
|
|
out.clip_distances = distance_from_clip_rect(unit_vertex, shadow.bounds, shadow.content_mask);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_shadow(input: ShadowVarying) -> @location(0) vec4<f32> {
|
|
// Alpha clip first, since we don't have `clip_distance`.
|
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
|
return vec4<f32>(0.0);
|
|
}
|
|
|
|
let shadow = b_shadows[input.shadow_id];
|
|
let half_size = shadow.bounds.size / 2.0;
|
|
let center = shadow.bounds.origin + half_size;
|
|
let center_to_point = input.position.xy - center;
|
|
|
|
let corner_radius = pick_corner_radius(center_to_point, shadow.corner_radii);
|
|
|
|
// The signal is only non-zero in a limited range, so don't waste samples
|
|
let low = center_to_point.y - half_size.y;
|
|
let high = center_to_point.y + half_size.y;
|
|
let start = clamp(-3.0 * shadow.blur_radius, low, high);
|
|
let end = clamp(3.0 * shadow.blur_radius, low, high);
|
|
|
|
// Accumulate samples (we can get away with surprisingly few samples)
|
|
let step = (end - start) / 4.0;
|
|
var y = start + step * 0.5;
|
|
var alpha = 0.0;
|
|
for (var i = 0; i < 4; i += 1) {
|
|
let blur = blur_along_x(center_to_point.x, center_to_point.y - y,
|
|
shadow.blur_radius, corner_radius, half_size);
|
|
alpha += blur * gaussian(y, shadow.blur_radius) * step;
|
|
y += step;
|
|
}
|
|
|
|
return blend_color(input.color, alpha);
|
|
}
|
|
|
|
// --- path rasterization --- //
|
|
|
|
struct PathVertex {
|
|
xy_position: vec2<f32>,
|
|
st_position: vec2<f32>,
|
|
content_mask: Bounds,
|
|
}
|
|
var<storage, read> b_path_vertices: array<PathVertex>;
|
|
|
|
struct PathRasterizationVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) st_position: vec2<f32>,
|
|
//TODO: use `clip_distance` once Naga supports it
|
|
@location(3) clip_distances: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_path_rasterization(@builtin(vertex_index) vertex_id: u32) -> PathRasterizationVarying {
|
|
let v = b_path_vertices[vertex_id];
|
|
|
|
var out = PathRasterizationVarying();
|
|
out.position = to_device_position_impl(v.xy_position);
|
|
out.st_position = v.st_position;
|
|
out.clip_distances = distance_from_clip_rect_impl(v.xy_position, v.content_mask);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_path_rasterization(input: PathRasterizationVarying) -> @location(0) f32 {
|
|
let dx = dpdx(input.st_position);
|
|
let dy = dpdy(input.st_position);
|
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
|
return 0.0;
|
|
}
|
|
|
|
let gradient = 2.0 * input.st_position.xx * vec2<f32>(dx.x, dy.x) - vec2<f32>(dx.y, dy.y);
|
|
let f = input.st_position.x * input.st_position.x - input.st_position.y;
|
|
let distance = f / length(gradient);
|
|
return saturate(0.5 - distance);
|
|
}
|
|
|
|
// --- paths --- //
|
|
|
|
struct PathSprite {
|
|
bounds: Bounds,
|
|
color: Background,
|
|
tile: AtlasTile,
|
|
}
|
|
var<storage, read> b_path_sprites: array<PathSprite>;
|
|
|
|
struct PathVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) tile_position: vec2<f32>,
|
|
@location(1) @interpolate(flat) instance_id: u32,
|
|
@location(2) @interpolate(flat) color_solid: vec4<f32>,
|
|
@location(3) @interpolate(flat) color0: vec4<f32>,
|
|
@location(4) @interpolate(flat) color1: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_path(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PathVarying {
|
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
|
let sprite = b_path_sprites[instance_id];
|
|
// Don't apply content mask because it was already accounted for when rasterizing the path.
|
|
|
|
var out = PathVarying();
|
|
out.position = to_device_position(unit_vertex, sprite.bounds);
|
|
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
|
out.instance_id = instance_id;
|
|
|
|
let gradient = prepare_gradient_color(
|
|
sprite.color.tag,
|
|
sprite.color.color_space,
|
|
sprite.color.solid,
|
|
sprite.color.colors
|
|
);
|
|
out.color_solid = gradient.solid;
|
|
out.color0 = gradient.color0;
|
|
out.color1 = gradient.color1;
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_path(input: PathVarying) -> @location(0) vec4<f32> {
|
|
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
|
let mask = 1.0 - abs(1.0 - sample % 2.0);
|
|
let sprite = b_path_sprites[input.instance_id];
|
|
let background = sprite.color;
|
|
let color = gradient_color(background, input.position.xy, sprite.bounds,
|
|
input.color_solid, input.color0, input.color1);
|
|
return blend_color(color, mask);
|
|
}
|
|
|
|
// --- underlines --- //
|
|
|
|
struct Underline {
|
|
order: u32,
|
|
pad: u32,
|
|
bounds: Bounds,
|
|
content_mask: Bounds,
|
|
color: Hsla,
|
|
thickness: f32,
|
|
wavy: u32,
|
|
}
|
|
var<storage, read> b_underlines: array<Underline>;
|
|
|
|
struct UnderlineVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) @interpolate(flat) color: vec4<f32>,
|
|
@location(1) @interpolate(flat) underline_id: u32,
|
|
//TODO: use `clip_distance` once Naga supports it
|
|
@location(3) clip_distances: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_underline(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> UnderlineVarying {
|
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
|
let underline = b_underlines[instance_id];
|
|
|
|
var out = UnderlineVarying();
|
|
out.position = to_device_position(unit_vertex, underline.bounds);
|
|
out.color = hsla_to_rgba(underline.color);
|
|
out.underline_id = instance_id;
|
|
out.clip_distances = distance_from_clip_rect(unit_vertex, underline.bounds, underline.content_mask);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_underline(input: UnderlineVarying) -> @location(0) vec4<f32> {
|
|
// Alpha clip first, since we don't have `clip_distance`.
|
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
|
return vec4<f32>(0.0);
|
|
}
|
|
|
|
let underline = b_underlines[input.underline_id];
|
|
if ((underline.wavy & 0xFFu) == 0u)
|
|
{
|
|
return blend_color(input.color, input.color.a);
|
|
}
|
|
|
|
let half_thickness = underline.thickness * 0.5;
|
|
let st = (input.position.xy - underline.bounds.origin) / underline.bounds.size.y - vec2<f32>(0.0, 0.5);
|
|
let frequency = M_PI_F * 3.0 * underline.thickness / 3.0;
|
|
let amplitude = 1.0 / (4.0 * underline.thickness);
|
|
let sine = sin(st.x * frequency) * amplitude;
|
|
let dSine = cos(st.x * frequency) * amplitude * frequency;
|
|
let distance = (st.y - sine) / sqrt(1.0 + dSine * dSine);
|
|
let distance_in_pixels = distance * underline.bounds.size.y;
|
|
let distance_from_top_border = distance_in_pixels - half_thickness;
|
|
let distance_from_bottom_border = distance_in_pixels + half_thickness;
|
|
let alpha = saturate(0.5 - max(-distance_from_bottom_border, distance_from_top_border));
|
|
return blend_color(input.color, alpha * input.color.a);
|
|
}
|
|
|
|
// --- monochrome sprites --- //
|
|
|
|
struct MonochromeSprite {
|
|
order: u32,
|
|
pad: u32,
|
|
bounds: Bounds,
|
|
content_mask: Bounds,
|
|
color: Hsla,
|
|
tile: AtlasTile,
|
|
transformation: TransformationMatrix,
|
|
}
|
|
var<storage, read> b_mono_sprites: array<MonochromeSprite>;
|
|
|
|
struct MonoSpriteVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) tile_position: vec2<f32>,
|
|
@location(1) @interpolate(flat) color: vec4<f32>,
|
|
@location(3) clip_distances: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_mono_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> MonoSpriteVarying {
|
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
|
let sprite = b_mono_sprites[instance_id];
|
|
|
|
var out = MonoSpriteVarying();
|
|
out.position = to_device_position_transformed(unit_vertex, sprite.bounds, sprite.transformation);
|
|
|
|
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
|
out.color = hsla_to_rgba(sprite.color);
|
|
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_mono_sprite(input: MonoSpriteVarying) -> @location(0) vec4<f32> {
|
|
let sample = textureSample(t_sprite, s_sprite, input.tile_position).r;
|
|
// Alpha clip after using the derivatives.
|
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
|
return vec4<f32>(0.0);
|
|
}
|
|
return blend_color(input.color, sample);
|
|
}
|
|
|
|
// --- polychrome sprites --- //
|
|
|
|
struct PolychromeSprite {
|
|
order: u32,
|
|
pad: u32,
|
|
grayscale: u32,
|
|
opacity: f32,
|
|
bounds: Bounds,
|
|
content_mask: Bounds,
|
|
corner_radii: Corners,
|
|
tile: AtlasTile,
|
|
}
|
|
var<storage, read> b_poly_sprites: array<PolychromeSprite>;
|
|
|
|
struct PolySpriteVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) tile_position: vec2<f32>,
|
|
@location(1) @interpolate(flat) sprite_id: u32,
|
|
@location(3) clip_distances: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_poly_sprite(@builtin(vertex_index) vertex_id: u32, @builtin(instance_index) instance_id: u32) -> PolySpriteVarying {
|
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
|
let sprite = b_poly_sprites[instance_id];
|
|
|
|
var out = PolySpriteVarying();
|
|
out.position = to_device_position(unit_vertex, sprite.bounds);
|
|
out.tile_position = to_tile_position(unit_vertex, sprite.tile);
|
|
out.sprite_id = instance_id;
|
|
out.clip_distances = distance_from_clip_rect(unit_vertex, sprite.bounds, sprite.content_mask);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_poly_sprite(input: PolySpriteVarying) -> @location(0) vec4<f32> {
|
|
let sample = textureSample(t_sprite, s_sprite, input.tile_position);
|
|
// Alpha clip after using the derivatives.
|
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
|
return vec4<f32>(0.0);
|
|
}
|
|
|
|
let sprite = b_poly_sprites[input.sprite_id];
|
|
let distance = quad_sdf(input.position.xy, sprite.bounds, sprite.corner_radii);
|
|
|
|
var color = sample;
|
|
if ((sprite.grayscale & 0xFFu) != 0u) {
|
|
let grayscale = dot(color.rgb, GRAYSCALE_FACTORS);
|
|
color = vec4<f32>(vec3<f32>(grayscale), sample.a);
|
|
}
|
|
return blend_color(color, sprite.opacity * saturate(0.5 - distance));
|
|
}
|
|
|
|
// --- surfaces --- //
|
|
|
|
struct SurfaceParams {
|
|
bounds: Bounds,
|
|
content_mask: Bounds,
|
|
}
|
|
|
|
var<uniform> surface_locals: SurfaceParams;
|
|
var t_y: texture_2d<f32>;
|
|
var t_cb_cr: texture_2d<f32>;
|
|
var s_surface: sampler;
|
|
|
|
const ycbcr_to_RGB = mat4x4<f32>(
|
|
vec4<f32>( 1.0000f, 1.0000f, 1.0000f, 0.0),
|
|
vec4<f32>( 0.0000f, -0.3441f, 1.7720f, 0.0),
|
|
vec4<f32>( 1.4020f, -0.7141f, 0.0000f, 0.0),
|
|
vec4<f32>(-0.7010f, 0.5291f, -0.8860f, 1.0),
|
|
);
|
|
|
|
struct SurfaceVarying {
|
|
@builtin(position) position: vec4<f32>,
|
|
@location(0) texture_position: vec2<f32>,
|
|
@location(3) clip_distances: vec4<f32>,
|
|
}
|
|
|
|
@vertex
|
|
fn vs_surface(@builtin(vertex_index) vertex_id: u32) -> SurfaceVarying {
|
|
let unit_vertex = vec2<f32>(f32(vertex_id & 1u), 0.5 * f32(vertex_id & 2u));
|
|
|
|
var out = SurfaceVarying();
|
|
out.position = to_device_position(unit_vertex, surface_locals.bounds);
|
|
out.texture_position = unit_vertex;
|
|
out.clip_distances = distance_from_clip_rect(unit_vertex, surface_locals.bounds, surface_locals.content_mask);
|
|
return out;
|
|
}
|
|
|
|
@fragment
|
|
fn fs_surface(input: SurfaceVarying) -> @location(0) vec4<f32> {
|
|
// Alpha clip after using the derivatives.
|
|
if (any(input.clip_distances < vec4<f32>(0.0))) {
|
|
return vec4<f32>(0.0);
|
|
}
|
|
|
|
let y_cb_cr = vec4<f32>(
|
|
textureSampleLevel(t_y, s_surface, input.texture_position, 0.0).r,
|
|
textureSampleLevel(t_cb_cr, s_surface, input.texture_position, 0.0).rg,
|
|
1.0);
|
|
|
|
return ycbcr_to_RGB * y_cb_cr;
|
|
}
|