Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ This release has an [MSRV][] of 1.86.
## Added

- `register_texture`, a helper for using `wgpu` textures in a Vello `Renderer`. ([#1161][] by [@DJMcNab][])
- `push_luminance_mask_layer`, content within which is used as a luminance mask. ([#1183][] by [@DJMcNab][]).
This is a breaking change to Vello Encoding.

## Fixed

Expand Down Expand Up @@ -316,6 +318,7 @@ This release has an [MSRV][] of 1.75.
[#1161]: https://github.com/linebender/vello/pull/1161
[#1169]: https://github.com/linebender/vello/pull/1169
[#1182]: https://github.com/linebender/vello/pull/1182
[#1183]: https://github.com/linebender/vello/pull/1183
[#1187]: https://github.com/linebender/vello/pull/1187

<!-- Note that this still comparing against 0.5.0, because 0.5.1 is a cherry-picked patch -->
Expand Down
135 changes: 135 additions & 0 deletions examples/scenes/src/test_scenes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ export_scenes!(
image_sampling(image_sampling),
image_extend_modes_bilinear(impls::image_extend_modes(ImageQuality::Medium), "image_extend_modes (bilinear)", false),
image_extend_modes_nearest_neighbor(impls::image_extend_modes(ImageQuality::Low), "image_extend_modes (nearest neighbor)", false),
luminance_mask(luminance_mask),
image_luminance_mask(image_luminance_mask),
);

/// Implementations for the test scenes.
Expand Down Expand Up @@ -1870,4 +1872,137 @@ mod impls {
);
}
}

pub(super) fn luminance_mask(scene: &mut Scene, params: &mut SceneParams<'_>) {
params.resolution = Some((55., 55.).into());
// Porter-Duff "over" pure white, to match example in https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/mask-type
// Note that using `base_color` doesn't work here, because of how it interacts with masks
scene.fill(
Fill::EvenOdd,
Affine::IDENTITY,
Color::WHITE,
None,
&Rect {
x0: 0.,
y0: 0.,
x1: 60.,
y1: 60.,
},
);
scene.push_layer(
BlendMode::new(Mix::Normal, Compose::SrcOver),
1.0,
Affine::IDENTITY,
&Rect {
x0: 5.,
y0: 5.,
x1: 50.,
y1: 50.,
},
);
scene.fill(
Fill::EvenOdd,
Affine::IDENTITY,
palette::css::RED,
None,
&Rect {
x0: 5.,
y0: 5.,
x1: 50.,
y1: 50.,
},
);
scene.push_luminance_mask_layer(
1.0,
Affine::IDENTITY,
&Rect {
x0: 5.,
y0: 5.,
x1: 50.,
y1: 50.,
},
);
scene.fill(
Fill::EvenOdd,
Affine::IDENTITY,
color::parse_color("rgb(10% 10% 10% / 0.4)").unwrap(),
None,
&Rect {
x0: 5.,
y0: 5.,
x1: 50.,
y1: 50.,
},
);
scene.fill(
Fill::EvenOdd,
Affine::IDENTITY,
color::parse_color("rgb(90% 90% 90% / 0.6)").unwrap(),
None,
&Circle {
center: (0., 55.).into(),
radius: 35.,
},
);
scene.pop_layer();
scene.pop_layer();
}

pub(super) fn image_luminance_mask(scene: &mut Scene, params: &mut SceneParams<'_>) {
// Flower image is 640x480
params.resolution = Some((700., 500.).into());
let flower_image = params
.images
.from_bytes(FLOWER_IMAGE.as_ptr() as usize, FLOWER_IMAGE)
.unwrap();
// HACK: Porter-Duff "over" the base color, restoring full alpha
scene.push_layer(
BlendMode::new(Mix::Normal, Compose::SrcOver),
1.0,
Affine::IDENTITY,
&Rect {
x0: 0.,
y0: 0.,
x1: 700.,
y1: 500.,
},
);
scene.fill(
Fill::EvenOdd,
Affine::IDENTITY,
palette::css::BEIGE,
None,
&Rect {
x0: 0.,
y0: 0.,
x1: 640.,
y1: 240.,
},
);
scene.fill(
Fill::EvenOdd,
Affine::IDENTITY,
palette::css::AQUAMARINE,
None,
&Rect {
x0: 0.,
y0: 240.,
x1: 320.,
y1: 480.,
},
);
scene.push_luminance_mask_layer(
1.0,
Affine::IDENTITY,
&Rect {
x0: 0.,
y0: 0.,
x1: 640.,
y1: 480.,
},
);
scene.draw_image(&flower_image, Affine::IDENTITY);
scene.pop_layer();
scene.pop_layer();
}
}
49 changes: 46 additions & 3 deletions vello/src/scene.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ use skrifa::{
};
#[cfg(feature = "bump_estimate")]
use vello_encoding::BumpAllocatorMemory;
use vello_encoding::{Encoding, Glyph, GlyphRun, NormalizedCoord, Patch, Transform};
use vello_encoding::{DrawBeginClip, Encoding, Glyph, GlyphRun, NormalizedCoord, Patch, Transform};

// TODO - Document invariants and edge cases (#470)
// - What happens when we pass a transform matrix with NaN values to the Scene?
Expand Down Expand Up @@ -100,6 +100,50 @@ impl Scene {
if blend.mix == Mix::Clip && alpha != 1.0 {
log::warn!("Clip mix mode used with semitransparent alpha");
}
self.push_layer_inner(
DrawBeginClip::new(blend, alpha.clamp(0.0, 1.0)),
transform,
clip,
);
}

/// Pushes a new layer clipped by the specified shape and treated like a luminance
/// mask for previous layers.
///
/// That is, content drawn between this and the next `pop_layer` call will serve
/// as a luminance mask
///
/// Every drawing command after this call will be clipped by the shape
/// until the layer is popped.
///
/// **However, the transforms are *not* saved or modified by the layer stack.**
///
/// # Transparency and premultiplication
///
/// In the current version of Vello, this can lead to some unexpected behaviour
/// when it is used to draw directly onto a render target which disregards transparency
/// (which includes surfaces in most cases).
/// This happens because the luminance mask only impacts the transparency of the returned value,
/// so if the transparency is ignored, it looks like the result had no effect.
///
/// This issue only occurs if there are no intermediate opaque layers, so can be worked around
/// by drawing something opaque (or having an opaque `base_color`), then putting a layer around your entire scene
/// with a [`Compose::SrcOver`].
pub fn push_luminance_mask_layer(&mut self, alpha: f32, transform: Affine, clip: &impl Shape) {
self.push_layer_inner(
DrawBeginClip::luminance_mask(alpha.clamp(0.0, 1.0)),
transform,
clip,
);
}

/// Helper for logic shared between [`Self::push_layer`] and [`Self::push_luminance_mask_layer`]
fn push_layer_inner(
&mut self,
parameters: DrawBeginClip,
transform: Affine,
clip: &impl Shape,
) {
let t = Transform::from_kurbo(&transform);
self.encoding.encode_transform(t);
self.encoding.encode_fill_style(Fill::NonZero);
Expand All @@ -117,8 +161,7 @@ impl Scene {
#[cfg(feature = "bump_estimate")]
self.estimator.count_path(clip.path_elements(0.1), &t, None);
}
self.encoding
.encode_begin_clip(blend, alpha.clamp(0.0, 1.0));
self.encoding.encode_begin_clip(parameters);
}

/// Pops the current layer.
Expand Down
17 changes: 16 additions & 1 deletion vello_encoding/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -196,13 +196,28 @@ pub struct DrawBeginClip {
}

impl DrawBeginClip {
/// Creates new clip draw data.
/// The `blend_mode` used to indicate that a layer should be
/// treated as a luminance mask.
///
/// The least significant 16 bits are reserved for Mix + Compose
/// combinations.
pub const LUMINANCE_MASK_LAYER: u32 = 0x10000;

/// Creates new clip draw data for a Porter-Duff blend mode.
pub fn new(blend_mode: BlendMode, alpha: f32) -> Self {
Self {
blend_mode: ((blend_mode.mix as u32) << 8) | blend_mode.compose as u32,
alpha,
}
}

/// Creates a new clip draw data for a luminance mask.
pub fn luminance_mask(alpha: f32) -> Self {
Self {
blend_mode: Self::LUMINANCE_MASK_LAYER,
alpha,
}
}
}

/// Monoid for the draw tag stream.
Expand Down
11 changes: 5 additions & 6 deletions vello_encoding/src/encoding.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
// Copyright 2022 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use crate::DrawBeginClip;

use super::{
DrawBlurRoundedRect, DrawColor, DrawImage, DrawLinearGradient, DrawRadialGradient,
DrawSweepGradient, DrawTag, Glyph, GlyphRun, NormalizedCoord, Patch, PathEncoder, PathTag,
Expand All @@ -9,7 +11,7 @@ use super::{

use peniko::color::{DynamicColor, palette};
use peniko::kurbo::{Shape, Stroke};
use peniko::{BlendMode, BrushRef, ColorStop, Extend, Fill, GradientKind, Image};
use peniko::{BrushRef, ColorStop, Extend, Fill, GradientKind, Image};

/// Encoded data streams for a scene.
///
Expand Down Expand Up @@ -461,13 +463,10 @@ impl Encoding {
}

/// Encodes a begin clip command.
pub fn encode_begin_clip(&mut self, blend_mode: BlendMode, alpha: f32) {
use super::DrawBeginClip;
pub fn encode_begin_clip(&mut self, parameters: DrawBeginClip) {
self.draw_tags.push(DrawTag::BEGIN_CLIP);
self.draw_data
.extend_from_slice(bytemuck::cast_slice(bytemuck::bytes_of(
&DrawBeginClip::new(blend_mode, alpha),
)));
.extend_from_slice(bytemuck::cast_slice(bytemuck::bytes_of(&parameters)));
self.n_clips += 1;
self.n_open_clips += 1;
}
Expand Down
18 changes: 17 additions & 1 deletion vello_shaders/shader/fine.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ const IMAGE_QUALITY_LOW = 0u;
const IMAGE_QUALITY_MEDIUM = 1u;
const IMAGE_QUALITY_HIGH = 2u;

const LUMINANCE_MASK_LAYER = 0x10000u;

@group(0) @binding(2)
var<storage> ptcl: array<u32>;

Expand Down Expand Up @@ -1017,7 +1019,20 @@ fn main(
}
let bg = unpack4x8unorm(bg_rgba);
let fg = rgba[i] * area[i] * end_clip.alpha;
rgba[i] = blend_mix_compose(bg, fg, end_clip.blend);
if end_clip.blend == LUMINANCE_MASK_LAYER {
// TODO: Does this case apply more generally?
// See https://github.com/linebender/vello/issues/1061
// TODO: How do we handle anti-aliased edges here?
Copy link
Collaborator

Choose a reason for hiding this comment

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

Presumably they're accounted for in the multiplication of rgba[i] and area[i] above?

Copy link
Member Author

Choose a reason for hiding this comment

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

Kind of; area[i] indicates "how much the blending should apply to this pixel".

However, we're instead using it as an alpha mask over the foreground. For Porter-Duff Over, this is equivalent. However, for the "destructive" blend modes, that doesn't give anywhere near the same results.
In the extreme case, consider the case where area[i] is zero; that is the cause of #1061.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Makes sense. That seems unrelated to this PR and shouldn't block landing IMO. Let's try to resolve that separately.

Copy link
Collaborator

Choose a reason for hiding this comment

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

As an aside, that issue is interesting because I don't think we anticipated that someone would push/pop an empty layer simply for the side effects of the blend operation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ahah:

Note: Shape is defined by the mathematical description of the shape. A particular point is either inside the shape or it is not. There are no gradations.

https://drafts.fxtf.org/compositing/#whatiscompositing

// This is really an imaging model question
if area[i] == 0f {
rgba[i] = bg;
continue;
}
let luminance = clamp(svg_lum(unpremultiply(fg)) * fg.a, 0.0, 1.0);
rgba[i] = bg * luminance;
} else {
rgba[i] = blend_mix_compose(bg, fg, end_clip.blend);
}
}
cmd_ix += 3u;
}
Expand Down Expand Up @@ -1226,6 +1241,7 @@ fn main(
let coords = xy_uint + vec2(i, 0u);
if coords.x < config.target_width && coords.y < config.target_height {
let fg = rgba[i];
// let fg = base_color * (1.0 - foreground.a) + foreground;
// Max with a small epsilon to avoid NaNs
let a_inv = 1.0 / max(fg.a, 1e-6);
let rgba_sep = vec4(fg.rgb * a_inv, fg.a);
Expand Down
25 changes: 17 additions & 8 deletions vello_shaders/shader/shared/blend.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,11 @@ fn lum(c: vec3<f32>) -> f32 {
return dot(c, f);
}

fn svg_lum(c: vec3<f32>) -> f32 {
let f = vec3(0.2125, 0.7154, 0.0721);
return dot(c, f);
}

fn clip_color(c_in: vec3<f32>) -> vec3<f32> {
var c = c_in;
let l = lum(c);
Expand Down Expand Up @@ -218,11 +223,11 @@ fn blend_compose(
cs: vec3<f32>,
ab: f32,
as_: f32,
mode: u32
compose_mode: u32,
) -> vec4<f32> {
var fa = 0.0;
var fb = 0.0;
switch mode {
switch compose_mode {
case COMPOSE_COPY: {
fa = 1.0;
fb = 0.0;
Expand Down Expand Up @@ -283,20 +288,24 @@ fn blend_compose(
return vec4(co, min(as_fa + ab_fb, 1.0));
}

fn unpremultiply(color: vec4<f32>) -> vec3<f32> {
let EPSILON = 1e-15;
// Max with a small epsilon to avoid NaNs.
let inv_alpha = 1.0 / max(color.a, EPSILON);
return color.rgb * inv_alpha;
}

// Apply color mixing and composition. Both input and output colors are
// premultiplied RGB.
fn blend_mix_compose(backdrop: vec4<f32>, src: vec4<f32>, mode: u32) -> vec4<f32> {
let BLEND_DEFAULT = ((MIX_NORMAL << 8u) | COMPOSE_SRC_OVER);
let EPSILON = 1e-15;
if (mode & 0x7fffu) == BLEND_DEFAULT {
// Both normal+src_over blend and clip case
return backdrop * (1.0 - src.a) + src;
}
// Un-premultiply colors for blending. Max with a small epsilon to avoid NaNs.
let inv_src_a = 1.0 / max(src.a, EPSILON);
var cs = src.rgb * inv_src_a;
let inv_backdrop_a = 1.0 / max(backdrop.a, EPSILON);
let cb = backdrop.rgb * inv_backdrop_a;
// Un-premultiply colors for blending.
var cs = unpremultiply(src);
let cb = unpremultiply(backdrop);
let mix_mode = mode >> 8u;
let mixed = blend_mix(cb, cs, mix_mode);
cs = mix(cs, mixed, backdrop.a);
Expand Down
3 changes: 3 additions & 0 deletions vello_tests/snapshots/image_luminance_mask.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading