
Closes #ISSUE [git: Use font size to determine pattern slash width #26446](https://github.com/zed-industries/zed/pull/26446) This PR only uses font size as the slant line width, and here it further uses line height as the slant line interval control. before  now  big line height  Release Notes: - N/A *or* Added/Fixed/Improved ...
864 lines
29 KiB
WebGPU Shading Language
864 lines
29 KiB
WebGPU Shading Language
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;
|
|
}
|
|
|
|
fn pick_corner_radius(point: vec2<f32>, radii: Corners) -> f32 {
|
|
if (point.x < 0.0) {
|
|
if (point.y < 0.0) {
|
|
return radii.top_left;
|
|
} else {
|
|
return radii.bottom_left;
|
|
}
|
|
} else {
|
|
if (point.y < 0.0) {
|
|
return radii.top_right;
|
|
} else {
|
|
return radii.bottom_right;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
|
|
return length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
|
|
min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
|
|
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,
|
|
pad: 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 half_size = quad.bounds.size / 2.0;
|
|
let center = quad.bounds.origin + half_size;
|
|
let center_to_point = input.position.xy - center;
|
|
|
|
let background_color = gradient_color(quad.background, input.position.xy, quad.bounds,
|
|
input.background_solid, input.background_color0, input.background_color1);
|
|
|
|
// Fast path when the quad is not rounded and doesn't have any border.
|
|
if (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 && quad.border_widths.top == 0.0 &&
|
|
quad.border_widths.left == 0.0 && quad.border_widths.right == 0.0 &&
|
|
quad.border_widths.bottom == 0.0) {
|
|
return blend_color(background_color, 1.0);
|
|
}
|
|
|
|
let corner_radius = pick_corner_radius(center_to_point, quad.corner_radii);
|
|
let rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius;
|
|
let distance =
|
|
length(max(vec2<f32>(0.0), rounded_edge_to_point)) +
|
|
min(0.0, max(rounded_edge_to_point.x, rounded_edge_to_point.y)) -
|
|
corner_radius;
|
|
|
|
let vertical_border = select(quad.border_widths.left, quad.border_widths.right, center_to_point.x > 0.0);
|
|
let horizontal_border = select(quad.border_widths.top, quad.border_widths.bottom, center_to_point.y > 0.0);
|
|
let inset_size = half_size - corner_radius - vec2<f32>(vertical_border, horizontal_border);
|
|
let point_to_inset_corner = abs(center_to_point) - inset_size;
|
|
|
|
var border_width = 0.0;
|
|
if (point_to_inset_corner.x < 0.0 && point_to_inset_corner.y < 0.0) {
|
|
border_width = 0.0;
|
|
} else if (point_to_inset_corner.y > point_to_inset_corner.x) {
|
|
border_width = horizontal_border;
|
|
} else {
|
|
border_width = vertical_border;
|
|
}
|
|
|
|
var color = background_color;
|
|
if (border_width > 0.0) {
|
|
let inset_distance = distance + border_width;
|
|
// 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, input.border_color);
|
|
color = mix(blended_border, background_color,
|
|
saturate(0.5 - inset_distance));
|
|
}
|
|
|
|
return blend_color(color, saturate(0.5 - distance));
|
|
}
|
|
|
|
// --- 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;
|
|
}
|