Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for blurred rounded rectangle. #665

Merged
merged 9 commits into from
Aug 27, 2024
52 changes: 52 additions & 0 deletions examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export_scenes!(
longpathdash_round(impls::longpathdash(Cap::Round), "longpathdash (round caps)", false),
mmark(crate::mmark::MMark::new(80_000), "mmark", false),
many_draw_objects(many_draw_objects),
blurred_rounded_rect(blurred_rounded_rect),
);

/// Implementations for the test scenes.
Expand Down Expand Up @@ -1686,4 +1687,55 @@ mod impls {
splash_screen(scene, params);
}
}

pub(super) fn blurred_rounded_rect(scene: &mut Scene, params: &mut SceneParams) {
msiglreith marked this conversation as resolved.
Show resolved Hide resolved
params.resolution = Some(Vec2::new(1200., 1200.));
params.base_color = Some(Color::WHITE);

let rect = Rect::from_center_size((0.0, 0.0), (300.0, 240.0));
let radius = 50.0;
scene.draw_blurred_rounded_rect(
Affine::translate((300.0, 300.0)),
rect,
Color::BLUE,
radius,
params.time.sin() * 50.0 + 50.0,
);

// Skewed affine transformation.
scene.draw_blurred_rounded_rect(
Affine::translate((900.0, 300.0)) * Affine::skew(20f64.to_radians().tan(), 0.0),
rect,
Color::BLACK,
radius,
params.time.sin() * 50.0 + 50.0,
);

// Stretch affine transformation.
scene.draw_blurred_rounded_rect(
Affine::translate((600.0, 600.0)) * Affine::scale_non_uniform(2.2, 0.9),
rect,
Color::BLACK,
radius,
params.time.sin() * 50.0 + 50.0,
);

// Circle.
scene.draw_blurred_rounded_rect(
Affine::IDENTITY,
Rect::new(100.0, 800.0, 400.0, 1100.0),
Color::BLACK,
150.0,
params.time.sin() * 50.0 + 50.0,
);

// Radius larger than one size.
scene.draw_blurred_rounded_rect(
Affine::IDENTITY,
Rect::new(600.0, 800.0, 900.0, 900.0),
Color::BLACK,
150.0,
params.time.sin() * 50.0 + 50.0,
);
}
}
30 changes: 30 additions & 0 deletions vello/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,36 @@ impl Scene {
self.encoding.encode_end_clip();
}

/// Draw a rounded rectangle blurred with a gaussian filter.
pub fn draw_blurred_rounded_rect(
&mut self,
transform: Affine,
rect: Rect,
brush: Color,
radius: f64,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not because I expect you to do this, but because it's important that it's raised, but is it at all plausible for the corner radii to be different? I.e. make this function accept a RoundedRect - which despite the docs, can have different radii on each corner since linebender/kurbo#166

I don't have a good understanding of the underlying maths to make this work

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about this as well but seems tricky at least.
If it would be dependent on the (continuous) distance alone it would be fine in my opinion but the parameters (e.g squircle shape) derived from the corner radius would introduce dicontinuities at the quadrant boundaries, probably visible at larger filter lengths. We could derive shared parameters for the whole shape with some tradeoff in quality but without larger adjustments to the approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - that's fine. I've not thought about it deeply, and I don't think that it is a necessary feature.

Thought it would be important to at least ensure that it is discussed, as much for when Raph comes through to gut-check this.

Copy link
Contributor

@nicoburns nicoburns Aug 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that it is a necessary feature.

FWIW, this definitely is a necessary feature for full CSS box-shadow support. Not sure how common they are in the wild, but the spec certainly allows authors to specify such shadows.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should be able to emulate it with multiple rounded rectangles, each in their own clip layer. That means that you would be responsible for the behaviour in the degerenate cases. I don't think these clip layers would need to be nested.

I think it's possible you would get conflation artifacts, though...

Copy link

@jkelleyrtp jkelleyrtp Aug 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/DioxusLabs/blitz/blob/main/packages/blitz/src/renderer/multicolor_rounded_rect.rs

we implemented the maths here for rounded-rect, you might be able to find some of it helpful

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just rounded rect can be done without too much difficulty in a distance field context by, basically, setting the radius by quadrant. But in the blurred case you will definitely see discontinuities depending on the exact parameters. I think it is possible to fix, but will require some doing.

Of course another possibility is to render the rounded rect as a vector shape (as in the code Jon linked) and blur it, but (a) that's going to be slower than doing it in a shader, even with heavier math, and (b) we don't have the infrastructure in place. It would be nice to have though, as you do need that for text shadows.

I don't recommend using clips for this, it adds complexity and possible performance issues, as well as potential conflation artifacts.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I quickly tested how the artifacts look like and there are indeed visible discontinuities at larger kernel sizes (see figure below). It seems to be primarily caused by the scale factor. Using a fixed or interpolated value would be an option here. But I would postpone it for now as followup.
grafik

std_dev: f64,
) {
// The impulse response of a gaussian filter is infinite.
// For performance reason we cut off the filter at some extent where the response is close to zero.
let kernel_size = 4.0 * std_dev;
msiglreith marked this conversation as resolved.
Show resolved Hide resolved

let t = Transform::from_kurbo(&transform.pre_translate(rect.center().to_vec2()));
self.encoding.encode_transform(t);

let shape: Rect =
Rect::from_center_size((0.0, 0.0), rect.size()).inflate(kernel_size, kernel_size);
self.encoding.encode_fill_style(Fill::NonZero);
if self.encoding.encode_shape(&shape, true) {
self.encoding.encode_blurred_rounded_rect(
brush,
rect.width() as _,
rect.height() as _,
radius as _,
std_dev as _,
);
}
}

/// Fills a shape using the specified style and brush.
pub fn fill<'b>(
&mut self,
Expand Down
19 changes: 19 additions & 0 deletions vello_encoding/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ impl DrawTag {
/// Image fill.
pub const IMAGE: Self = Self(0x248);

/// Blurred rounded rectangle.
pub const BLUR_RECT: Self = Self(0x2d4); // info: 11, scene: 5 (DrawBlurRoundedRect)

/// Begin layer/clip.
pub const BEGIN_CLIP: Self = Self(0x9);

Expand Down Expand Up @@ -126,6 +129,22 @@ pub struct DrawImage {
pub width_height: u32,
}

/// Draw data for a blurred rounded rectangle.
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
#[repr(C)]
pub struct DrawBlurRoundedRect {
/// Solid color brush.
pub color: DrawColor,
/// Rectangle width.
pub width: f32,
/// Rectangle height.
pub height: f32,
/// Rectangle corner radius.
pub radius: f32,
/// Standard deviation of gaussian filter.
pub std_dev: f32,
}

/// Draw data for a clip or layer.
#[derive(Clone, Copy, Debug, Default, Zeroable, Pod)]
#[repr(C)]
Expand Down
26 changes: 23 additions & 3 deletions vello_encoding/src/encoding.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use super::{DrawColor, DrawTag, PathEncoder, PathTag, Style, Transform};
use super::{DrawBlurRoundedRect, DrawColor, DrawTag, PathEncoder, PathTag, Style, Transform};

use peniko::kurbo::{Shape, Stroke};
use peniko::{BlendMode, BrushRef, Fill};
use peniko::{BlendMode, BrushRef, Color, Fill};

#[cfg(feature = "full")]
use {
super::{
DrawImage, DrawLinearGradient, DrawRadialGradient, DrawSweepGradient, Glyph, GlyphRun,
Patch,
},
peniko::{Color, ColorStop, Extend, GradientKind, Image},
peniko::{ColorStop, Extend, GradientKind, Image},
skrifa::instance::NormalizedCoord,
};

Expand Down Expand Up @@ -433,6 +433,26 @@ impl Encoding {
}));
}

// Encodes a blurred rounded rectangle brush.
pub fn encode_blurred_rounded_rect(
&mut self,
color: Color,
width: f32,
height: f32,
radius: f32,
std_dev: f32,
) {
self.draw_tags.push(DrawTag::BLUR_RECT);
self.draw_data
.extend_from_slice(bytemuck::bytes_of(&DrawBlurRoundedRect {
color: DrawColor::new(color),
width,
height,
radius,
std_dev,
}));
}

/// Encodes a begin clip command.
pub fn encode_begin_clip(&mut self, blend_mode: BlendMode, alpha: f32) {
use super::DrawBeginClip;
Expand Down
4 changes: 2 additions & 2 deletions vello_encoding/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ pub use config::{
RenderConfig, WorkgroupCounts, WorkgroupSize,
};
pub use draw::{
DrawBbox, DrawBeginClip, DrawColor, DrawImage, DrawLinearGradient, DrawMonoid,
DrawRadialGradient, DrawSweepGradient, DrawTag, DRAW_INFO_FLAGS_FILL_RULE_BIT,
DrawBbox, DrawBeginClip, DrawBlurRoundedRect, DrawColor, DrawImage, DrawLinearGradient,
DrawMonoid, DrawRadialGradient, DrawSweepGradient, DrawTag, DRAW_INFO_FLAGS_FILL_RULE_BIT,
};
pub use encoding::{Encoding, StreamOffsets};
pub use mask::{make_mask_lut, make_mask_lut_16};
Expand Down
14 changes: 14 additions & 0 deletions vello_shaders/shader/coarse.wgsl
msiglreith marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ fn write_end_clip(end_clip: CmdEndClip) {
cmd_offset += 3u;
}

fn write_blurred_rounded_rect(color: CmdColor, info_offset: u32) {
alloc_cmd(3u);
ptcl[cmd_offset] = CMD_BLUR_RECT;
ptcl[cmd_offset + 1u] = info_offset;
ptcl[cmd_offset + 2u] = color.rgba_color;
cmd_offset += 3u;
}

@compute @workgroup_size(256)
fn main(
@builtin(local_invocation_id) local_id: vec3<u32>,
Expand Down Expand Up @@ -374,6 +382,12 @@ fn main(
let rgba_color = scene[dd];
write_color(CmdColor(rgba_color));
}
case DRAWTAG_BLURRED_ROUNDED_RECT: {
write_path(tile, tile_ix, draw_flags);
let rgba_color = scene[dd];
let info_offset = di + 1u;
write_blurred_rounded_rect(CmdColor(rgba_color), info_offset);
}
case DRAWTAG_FILL_LIN_GRADIENT: {
write_path(tile, tile_ix, draw_flags);
let index = scene[dd];
Expand Down
19 changes: 17 additions & 2 deletions vello_shaders/shader/draw_leaf.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ fn main(
let di = m.info_offset;
if tag_word == DRAWTAG_FILL_COLOR || tag_word == DRAWTAG_FILL_LIN_GRADIENT ||
tag_word == DRAWTAG_FILL_RAD_GRADIENT || tag_word == DRAWTAG_FILL_SWEEP_GRADIENT ||
tag_word == DRAWTAG_FILL_IMAGE || tag_word == DRAWTAG_BEGIN_CLIP
tag_word == DRAWTAG_FILL_IMAGE || tag_word == DRAWTAG_BEGIN_CLIP || tag_word == DRAWTAG_BLURRED_ROUNDED_RECT
{
let bbox = path_bbox[m.path_ix];
// TODO: bbox is mostly yagni here, sort that out. Maybe clips?
Expand All @@ -122,7 +122,8 @@ fn main(
var transform = Transform();
let draw_flags = bbox.draw_flags;
if tag_word == DRAWTAG_FILL_LIN_GRADIENT || tag_word == DRAWTAG_FILL_RAD_GRADIENT ||
tag_word == DRAWTAG_FILL_SWEEP_GRADIENT || tag_word == DRAWTAG_FILL_IMAGE
tag_word == DRAWTAG_FILL_SWEEP_GRADIENT || tag_word == DRAWTAG_FILL_IMAGE ||
tag_word == DRAWTAG_BLURRED_ROUNDED_RECT
{
transform = read_transform(config.transform_base, bbox.trans_ix);
}
Expand Down Expand Up @@ -253,6 +254,20 @@ fn main(
info[di + 7u] = scene[dd];
info[di + 8u] = scene[dd + 1u];
}
case DRAWTAG_BLURRED_ROUNDED_RECT: {
info[di] = draw_flags;
let inv = transform_inverse(transform);
info[di + 1u] = bitcast<u32>(inv.matrx.x);
info[di + 2u] = bitcast<u32>(inv.matrx.y);
info[di + 3u] = bitcast<u32>(inv.matrx.z);
info[di + 4u] = bitcast<u32>(inv.matrx.w);
info[di + 5u] = bitcast<u32>(inv.translate.x);
info[di + 6u] = bitcast<u32>(inv.translate.y);
info[di + 7u] = scene[dd + 1u];
info[di + 8u] = scene[dd + 2u];
info[di + 9u] = scene[dd + 3u];
info[di + 10u] = scene[dd + 4u];
}
default: {}
}
}
Expand Down
84 changes: 84 additions & 0 deletions vello_shaders/shader/fine.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -720,6 +720,22 @@ fn fill_path_ms_evenodd(fill: CmdFill, local_id: vec2<u32>, result: ptr<function
}
#endif // msaa

// Error function approximation.
//
// https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html
fn erf7(x: f32) -> f32 {
// Clamp to prevent overflow.
// Intermediate steps calculate pow(x, 14).
let y = clamp(x * 1.1283791671, -100.0, 100.0);
let yy = y * y;
let z = y + (0.24295 + (0.03395 + 0.0104 * yy) * yy) * (y * yy);
return z / sqrt(1.0 + z * z);
}

fn hypot(a: f32, b: f32) -> f32 {
return sqrt(a * a + b * b);
}

fn read_fill(cmd_ix: u32) -> CmdFill {
let size_and_rule = ptcl[cmd_ix + 1u];
let seg_data = ptcl[cmd_ix + 2u];
Expand All @@ -732,6 +748,24 @@ fn read_color(cmd_ix: u32) -> CmdColor {
return CmdColor(rgba_color);
}

fn read_blur_rect(cmd_ix: u32) -> CmdBlurRect {
let info_offset = ptcl[cmd_ix + 1u];
let rgba_color = ptcl[cmd_ix + 2u];

let m0 = bitcast<f32>(info[info_offset]);
let m1 = bitcast<f32>(info[info_offset + 1u]);
let m2 = bitcast<f32>(info[info_offset + 2u]);
let m3 = bitcast<f32>(info[info_offset + 3u]);
let matrx = vec4(m0, m1, m2, m3);
let xlat = vec2(bitcast<f32>(info[info_offset + 4u]), bitcast<f32>(info[info_offset + 5u]));
let width = bitcast<f32>(info[info_offset + 6u]);
let height = bitcast<f32>(info[info_offset + 7u]);
let radius = bitcast<f32>(info[info_offset + 8u]);
let std_dev = bitcast<f32>(info[info_offset + 9u]);

return CmdBlurRect(rgba_color, matrx, xlat, width, height, radius, std_dev);
}

fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad {
let index_mode = ptcl[cmd_ix + 1u];
let index = index_mode >> 2u;
Expand Down Expand Up @@ -983,6 +1017,56 @@ fn main(
case CMD_JUMP: {
cmd_ix = ptcl[cmd_ix + 1u];
}
case CMD_BLUR_RECT: {
/// Approximation for the convolution of a gaussian filter with a rounded rectangle.
///
/// See https://raphlinus.github.io/graphics/2020/04/21/blurred-rounded-rects.html

let blur = read_blur_rect(cmd_ix);

// Avoid division by 0
let std_dev = max(blur.std_dev, 1e-5);
let inv_std_dev = 1.0 / std_dev;

let min_edge = min(blur.width, blur.height);
let radius_max = 0.5 * min_edge;
let r0 = min(hypot(blur.radius, std_dev * 1.15), radius_max);
let r1 = min(hypot(blur.radius, std_dev * 2.0), radius_max);

let exponent = 2.0 * r1 / r0;
let inv_exponent = 1.0 / exponent;

// Pull in long end (make less eccentric).
let delta = 1.25 * std_dev * (exp(-pow(0.5 * inv_std_dev * blur.width, 2.0)) - exp(-pow(0.5 * inv_std_dev * blur.height, 2.0)));
let width = blur.width + min(delta, 0.0);
let height = blur.height - max(delta, 0.0);

let scale = 0.5 * erf7(inv_std_dev * 0.5 * (max(width, height) - 0.5 * blur.radius));

for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) {
// Transform fragment location to local 'uv' space of the rounded rectangle.
let my_xy = vec2(xy.x + f32(i), xy.y);
let local_xy = blur.matrx.xy * my_xy.x + blur.matrx.zw * my_xy.y + blur.xlat;
let x = local_xy.x;
let y = local_xy.y;

let y0 = abs(y) - (height * 0.5 - r1);
let y1 = max(y0, 0.0);

let x0 = abs(x) - (width * 0.5 - r1);
let x1 = max(x0, 0.0);

let d_pos = pow(pow(x1, exponent) + pow(y1, exponent), inv_exponent);
let d_neg = min(max(x0, y0), 0.0);
let d = d_pos + d_neg - r1;
let alpha = scale * (erf7(inv_std_dev * (min_edge + d)) - erf7(inv_std_dev * d));

let fg_rgba = unpack4x8unorm(blur.rgba_color).wzyx * alpha;
let fg_i = fg_rgba * area[i];
rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i;
}
cmd_ix += 3u;
}
#ifdef full
case CMD_LIN_GRAD: {
let lin = read_lin_grad(cmd_ix);
Expand Down
1 change: 1 addition & 0 deletions vello_shaders/shader/shared/drawtag.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let DRAWTAG_FILL_LIN_GRADIENT = 0x114u;
let DRAWTAG_FILL_RAD_GRADIENT = 0x29cu;
let DRAWTAG_FILL_SWEEP_GRADIENT = 0x254u;
let DRAWTAG_FILL_IMAGE = 0x248u;
let DRAWTAG_BLURRED_ROUNDED_RECT = 0x2d4u;
let DRAWTAG_BEGIN_CLIP = 0x9u;
let DRAWTAG_END_CLIP = 0x21u;

Expand Down
Loading