From 15cd306af6b2baa99904b2c4155c328f69df81eb Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Sun, 30 Apr 2023 23:11:57 -0400 Subject: [PATCH] Extend modes for gradients This patch implements the pad, repeat and reflect extend modes for gradient brushes. Adds a new example demonstrating the functionality. Also fixes a few bugs: * Clamps alpha in blend.wgsl for the `blend_compose` function. The `Plus` mode was generating `alpha > 1.0` leading to incorrect rendering. * Small change to radial gradients in fine.wgsl to reject pixels outside the cone when the circles don't nest. This requires further work to properly extend the cone when one of the radii is not 0. --- crates/encoding/src/encoding.rs | 18 +++++--- crates/encoding/src/resolve.rs | 12 +++++- examples/scenes/src/test_scenes.rs | 67 ++++++++++++++++++++++++++++++ shader/fine.wgsl | 50 +++++++++++++++++----- shader/shared/blend.wgsl | 3 +- shader/shared/ptcl.wgsl | 2 + 6 files changed, 133 insertions(+), 19 deletions(-) diff --git a/crates/encoding/src/encoding.rs b/crates/encoding/src/encoding.rs index 65dcbbb..1fc4a1d 100644 --- a/crates/encoding/src/encoding.rs +++ b/crates/encoding/src/encoding.rs @@ -113,11 +113,13 @@ impl Encoding { Patch::Ramp { draw_data_offset: offset, stops, + extend, } => { let stops = stops.start + stops_base..stops.end + stops_base; Patch::Ramp { draw_data_offset: offset + offsets.draw_data, stops, + extend: *extend, } } Patch::GlyphRun { index } => Patch::GlyphRun { @@ -264,9 +266,9 @@ impl Encoding { gradient: DrawLinearGradient, color_stops: impl Iterator, alpha: f32, - _extend: Extend, + extend: Extend, ) { - self.add_ramp(color_stops, alpha); + self.add_ramp(color_stops, alpha, extend); self.draw_tags.push(DrawTag::LINEAR_GRADIENT); self.draw_data .extend_from_slice(bytemuck::bytes_of(&gradient)); @@ -278,9 +280,9 @@ impl Encoding { gradient: DrawRadialGradient, color_stops: impl Iterator, alpha: f32, - _extend: Extend, + extend: Extend, ) { - self.add_ramp(color_stops, alpha); + self.add_ramp(color_stops, alpha, extend); self.draw_tags.push(DrawTag::RADIAL_GRADIENT); self.draw_data .extend_from_slice(bytemuck::bytes_of(&gradient)); @@ -331,7 +333,12 @@ impl Encoding { self.path_tags.swap(len - 1, len - 2); } - fn add_ramp(&mut self, color_stops: impl Iterator, alpha: f32) { + fn add_ramp( + &mut self, + color_stops: impl Iterator, + alpha: f32, + extend: Extend, + ) { let offset = self.draw_data.len(); let stops_start = self.color_stops.len(); if alpha != 1.0 { @@ -343,6 +350,7 @@ impl Encoding { self.patches.push(Patch::Ramp { draw_data_offset: offset, stops: stops_start..self.color_stops.len(), + extend, }); } } diff --git a/crates/encoding/src/resolve.rs b/crates/encoding/src/resolve.rs index 0f72939..4e6dbc6 100644 --- a/crates/encoding/src/resolve.rs +++ b/crates/encoding/src/resolve.rs @@ -4,7 +4,7 @@ use std::ops::Range; use bytemuck::{Pod, Zeroable}; -use peniko::Image; +use peniko::{Extend, Image}; use super::{ glyph_cache::{CachedRange, GlyphCache, GlyphKey}, @@ -221,11 +221,13 @@ impl Resolver { ResolvedPatch::Ramp { draw_data_offset, ramp_id, + extend, } => { if pos < *draw_data_offset { data.extend_from_slice(&encoding.draw_data[pos..*draw_data_offset]); } - data.extend_from_slice(bytemuck::bytes_of(ramp_id)); + let index_mode = (ramp_id << 2) | *extend as u32; + data.extend_from_slice(bytemuck::bytes_of(&index_mode)); pos = *draw_data_offset + 4; } ResolvedPatch::GlyphRun { .. } => {} @@ -340,11 +342,13 @@ impl Resolver { Patch::Ramp { draw_data_offset, stops, + extend, } => { let ramp_id = self.ramp_cache.add(&encoding.color_stops[stops.clone()]); self.patches.push(ResolvedPatch::Ramp { draw_data_offset: *draw_data_offset + sizes.draw_data, ramp_id, + extend: *extend, }); } Patch::GlyphRun { index } => { @@ -472,6 +476,8 @@ pub enum Patch { draw_data_offset: usize, /// Range of the gradient stops in the resource set. stops: Range, + /// Extend mode for the gradient. + extend: Extend, }, /// Glyph run resource. GlyphRun { @@ -501,6 +507,8 @@ enum ResolvedPatch { draw_data_offset: usize, /// Resolved ramp index. ramp_id: u32, + /// Extend mode for the gradient. + extend: Extend, }, GlyphRun { /// Index of the original glyph run in the encoding. diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index 5e4ca8e..e5aff38 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -30,6 +30,7 @@ pub fn test_scenes() -> SceneSet { scene!(funky_paths), scene!(cardioid_and_friends), scene!(animated_text: animated), + scene!(gradient_extend), scene!(brush_transform: animated), scene!(blend_grid), scene!(conflation_artifacts), @@ -249,6 +250,72 @@ fn brush_transform(sb: &mut SceneBuilder, params: &mut SceneParams) { ); } +fn gradient_extend(sb: &mut SceneBuilder, params: &mut SceneParams) { + fn square( + sb: &mut SceneBuilder, + is_radial: bool, + transform: Affine, + brush_transform: Option, + extend: Extend, + ) { + let colors = [Color::RED, Color::GREEN, Color::BLUE]; + let width = 400f64; + let height = 400f64; + let gradient: Brush = if is_radial { + Gradient::new_radial((width * 0.5, height * 0.5), (width * 0.25) as f32) + .with_stops(colors) + .with_extend(extend) + .into() + } else { + Gradient::new_linear((width * 0.35, height * 0.5), (width * 0.65, height * 0.5)) + .with_stops(colors) + .with_extend(extend) + .into() + }; + sb.fill( + Fill::NonZero, + transform, + &gradient, + brush_transform, + &Rect::new(0.0, 0.0, width, height), + ); + } + let extend_modes = [Extend::Pad, Extend::Repeat, Extend::Reflect]; + for x in 0..3 { + let extend = extend_modes[x]; + for y in 0..2 { + let is_radial = y & 1 != 0; + let transform = Affine::translate((x as f64 * 450.0 + 50.0, y as f64 * 450.0 + 100.0)); + square(sb, is_radial, transform, None, extend); + } + } + let white = Color::WHITE.into(); + params.text.add( + sb, + None, + 32.0, + Some(&white), + Affine::translate((50.0, 70.0)), + "Pad", + ); + params.text.add( + sb, + None, + 32.0, + Some(&white), + Affine::translate((500.0, 70.0)), + "Repeat", + ); + params.text.add( + sb, + None, + 32.0, + Some(&white), + Affine::translate((950.0, 70.0)), + "Reflect", + ); +} + fn blend_grid(sb: &mut SceneBuilder, _: &mut SceneParams) { const BLEND_MODES: &[Mix] = &[ Mix::Normal, diff --git a/shader/fine.wgsl b/shader/fine.wgsl index ed47bcb..42970e5 100644 --- a/shader/fine.wgsl +++ b/shader/fine.wgsl @@ -61,16 +61,20 @@ fn read_color(cmd_ix: u32) -> CmdColor { } fn read_lin_grad(cmd_ix: u32) -> CmdLinGrad { - let index = ptcl[cmd_ix + 1u]; + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; let info_offset = ptcl[cmd_ix + 2u]; let line_x = bitcast(info[info_offset]); let line_y = bitcast(info[info_offset + 1u]); let line_c = bitcast(info[info_offset + 2u]); - return CmdLinGrad(index, line_x, line_y, line_c); + return CmdLinGrad(index, extend_mode, line_x, line_y, line_c); } fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { - let index = ptcl[cmd_ix + 1u]; + let index_mode = ptcl[cmd_ix + 1u]; + let index = index_mode >> 2u; + let extend_mode = index_mode & 0x3u; let info_offset = ptcl[cmd_ix + 2u]; let m0 = bitcast(info[info_offset]); let m1 = bitcast(info[info_offset + 1u]); @@ -81,7 +85,7 @@ fn read_rad_grad(cmd_ix: u32) -> CmdRadGrad { let c1 = vec2(bitcast(info[info_offset + 6u]), bitcast(info[info_offset + 7u])); let ra = bitcast(info[info_offset + 8u]); let roff = bitcast(info[info_offset + 9u]); - return CmdRadGrad(index, matrx, xlat, c1, ra, roff); + return CmdRadGrad(index, extend_mode, matrx, xlat, c1, ra, roff); } fn read_image(cmd_ix: u32) -> CmdImage { @@ -108,6 +112,25 @@ fn read_end_clip(cmd_ix: u32) -> CmdEndClip { return CmdEndClip(blend, alpha); } +fn extend_mode(t: f32, mode: u32) -> f32 { + // This can be replaced with two selects, exchanging the cost + // of a branch for additional ALU + switch mode { + // PAD + case 0u: { + return clamp(t, 0.0, 1.0); + } + // REPEAT + case 1u: { + return fract(t); + } + // REFLECT (2) + default: { + return abs(t - 2.0 * round(0.5 * t)); + } + } +} + #else @group(0) @binding(3) @@ -262,7 +285,7 @@ fn main( let d = lin.line_x * xy.x + lin.line_y * xy.y + lin.line_c; for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { let my_d = d + lin.line_x * f32(i); - let x = i32(round(clamp(my_d, 0.0, 1.0) * f32(GRADIENT_WIDTH - 1))); + let x = i32(round(extend_mode(my_d, lin.extend_mode) * f32(GRADIENT_WIDTH - 1))); let fg_rgba = textureLoad(gradients, vec2(x, i32(lin.index)), 0); let fg_i = fg_rgba * area[i]; rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; @@ -278,11 +301,16 @@ fn main( let xy_xformed = rad.matrx.xy * my_xy.x + rad.matrx.zw * my_xy.y + rad.xlat; let ba = dot(xy_xformed, rad.c1); let ca = rad.ra * dot(xy_xformed, xy_xformed); - let t = sqrt(ba * ba + ca) - ba - rad.roff; - let x = i32(round(clamp(t, 0.0, 1.0) * f32(GRADIENT_WIDTH - 1))); - let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); - let fg_i = fg_rgba * area[i]; - rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; + let t0 = sqrt(ba * ba + ca) - ba; + // For radial gradients that generate a cone, reject pixels outside + // the region. + if t0 >= 0.0 { + let t = t0 - rad.roff; + let x = i32(round(extend_mode(t, rad.extend_mode) * f32(GRADIENT_WIDTH - 1))); + let fg_rgba = textureLoad(gradients, vec2(x, i32(rad.index)), 0); + let fg_i = fg_rgba * area[i]; + rgba[i] = rgba[i] * (1.0 - fg_i.a) + fg_i; + } } cmd_ix += 3u; } @@ -352,7 +380,7 @@ fn main( let fg = rgba[i]; // 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); + let rgba_sep = vec4(fg.rgb * a_inv, fg.a); textureStore(output, vec2(coords), rgba_sep); } } diff --git a/shader/shared/blend.wgsl b/shader/shared/blend.wgsl index e101c70..e8ec3c7 100644 --- a/shader/shared/blend.wgsl +++ b/shader/shared/blend.wgsl @@ -306,7 +306,8 @@ fn blend_compose( let as_fa = as_ * fa; let ab_fb = ab * fb; let co = as_fa * cs + ab_fb * cb; - return vec4(co, as_fa + ab_fb); + // Modes like COMPOSE_PLUS can generate alpha > 1.0, so clamp. + return vec4(co, min(as_fa + ab_fb, 1.0)); } // Apply color mixing and composition. Both input and output colors are diff --git a/shader/shared/ptcl.wgsl b/shader/shared/ptcl.wgsl index 5d5e528..8137f32 100644 --- a/shader/shared/ptcl.wgsl +++ b/shader/shared/ptcl.wgsl @@ -44,6 +44,7 @@ struct CmdColor { struct CmdLinGrad { index: u32, + extend_mode: u32, line_x: f32, line_y: f32, line_c: f32, @@ -51,6 +52,7 @@ struct CmdLinGrad { struct CmdRadGrad { index: u32, + extend_mode: u32, matrx: vec4, xlat: vec2, c1: vec2,