From 0bef4500c3e60ed36747a7e5fadae3907b89ed62 Mon Sep 17 00:00:00 2001 From: Chad Brokaw Date: Fri, 8 Nov 2024 04:32:25 -0500 Subject: [PATCH] Make image sampling pad in all directions and remove half-pixel offset (#722) Constrains image sampling to the actual atlas region for a given image and adds a half pixel adjustment to sample from the pixel center. Addresses #719 and maybe #656 Example 2x2 image with red, blue, cyan and magenta pixels: vello_image_sampling --------- Co-authored-by: Daniel McNab <36049421+DJMcNab@users.noreply.github.com> --- CHANGELOG.md | 5 ++ examples/scenes/src/test_scenes.rs | 89 ++++++++++++++++++- vello_shaders/build.rs | 5 +- vello_shaders/shader/fine.wgsl | 18 ++-- vello_tests/smoke_snapshots/two_emoji.png | Bin 3165 -> 3070 bytes vello_tests/snapshots/big_bitmap.png | 4 +- vello_tests/snapshots/image_extend_modes.png | 3 + vello_tests/snapshots/image_sampling.png | 3 + vello_tests/snapshots/little_bitmap.png | 4 +- vello_tests/tests/snapshot_test_scenes.rs | 16 ++++ 10 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 vello_tests/snapshots/image_extend_modes.png create mode 100644 vello_tests/snapshots/image_sampling.png diff --git a/CHANGELOG.md b/CHANGELOG.md index bc8b1bb0..d9bc6dfd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ You can find its changes [documented below](#030---2024-10-04). This release has an [MSRV][] of 1.75. +### Fixed + +- Offset in image rendering, and sampling outside correct atlas area ([#722][] by [@dfrg]) + ## [0.3.0][] - 2024-10-04 This release has an [MSRV][] of 1.75. @@ -186,6 +190,7 @@ This release has an [MSRV][] of 1.75. [#701]: https://github.com/linebender/vello/pull/701 [#706]: https://github.com/linebender/vello/pull/706 [#711]: https://github.com/linebender/vello/pull/711 +[#722]: https://github.com/linebender/vello/pull/722 [Unreleased]: https://github.com/linebender/vello/compare/v0.3.0...HEAD diff --git a/examples/scenes/src/test_scenes.rs b/examples/scenes/src/test_scenes.rs index a2032676..89c131d6 100644 --- a/examples/scenes/src/test_scenes.rs +++ b/examples/scenes/src/test_scenes.rs @@ -79,12 +79,15 @@ export_scenes!( mmark(crate::mmark::MMark::new(80_000), "mmark", false), many_draw_objects(many_draw_objects), blurred_rounded_rect(blurred_rounded_rect), + image_sampling(image_sampling), + image_extend_modes(image_extend_modes) ); /// Implementations for the test scenes. /// In a module because the exported [`ExampleScene`] creation functions use the same names. mod impls { - use std::f64::consts::PI; + use std::f64::consts::{FRAC_1_SQRT_2, PI}; + use std::sync::Arc; use crate::SceneParams; use kurbo::RoundedRect; @@ -1761,4 +1764,88 @@ mod impls { std_dev, ); } + + pub(super) fn image_sampling(scene: &mut Scene, params: &mut SceneParams) { + params.resolution = Some(Vec2::new(1100., 1100.)); + params.base_color = Some(Color::WHITE); + let mut blob: Vec = Vec::new(); + [Color::RED, Color::BLUE, Color::CYAN, Color::MAGENTA] + .iter() + .for_each(|c| { + let b = c.to_premul_u32().to_ne_bytes(); + blob.push(b[3]); + blob.push(b[2]); + blob.push(b[1]); + blob.push(b[0]); + }); + let data = vello::peniko::Blob::new(Arc::new(blob)); + let image = vello::peniko::Image::new(data, vello::peniko::Format::Rgba8, 2, 2); + + scene.draw_image( + &image, + Affine::scale(200.).then_translate((100., 100.).into()), + ); + scene.draw_image( + &image, + Affine::translate((-1., -1.)) + // 45° rotation + .then_rotate(PI / 4.) + .then_translate((1., 1.).into()) + // So the major axis is sqrt(2.) larger + .then_scale(200. * FRAC_1_SQRT_2) + .then_translate((100., 600.0).into()), + ); + scene.draw_image( + &image, + Affine::scale_non_uniform(100., 200.).then_translate((600.0, 100.0).into()), + ); + scene.draw_image( + &image, + Affine::skew(0.1, 0.25) + .then_scale(200.0) + .then_translate((600.0, 600.0).into()), + ); + } + + pub(super) fn image_extend_modes(scene: &mut Scene, params: &mut SceneParams) { + params.resolution = Some(Vec2::new(1500., 1500.)); + params.base_color = Some(Color::WHITE); + let mut blob: Vec = Vec::new(); + [Color::RED, Color::BLUE, Color::CYAN, Color::MAGENTA] + .iter() + .for_each(|c| { + let b = c.to_premul_u32().to_ne_bytes(); + blob.push(b[3]); + blob.push(b[2]); + blob.push(b[1]); + blob.push(b[0]); + }); + let data = vello::peniko::Blob::new(Arc::new(blob)); + let image = vello::peniko::Image::new(data, vello::peniko::Format::Rgba8, 2, 2); + let image = image.with_extend(Extend::Pad); + // Pad extend mode + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((100., 100.).into()), + &image, + Some(Affine::translate((2., 2.)).then_scale(100.)), + &Rect::new(0., 0., 6., 6.), + ); + let image = image.with_extend(Extend::Reflect); + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((100., 800.).into()), + &image, + Some(Affine::translate((2., 2.))), + &Rect::new(0., 0., 6., 6.), + ); + let image = image.with_extend(Extend::Repeat); + scene.fill( + Fill::NonZero, + Affine::scale(100.).then_translate((800., 100.).into()), + &image, + Some(Affine::translate((2., 2.))), + &Rect::new(0., 0., 6., 6.), + ); + } } diff --git a/vello_shaders/build.rs b/vello_shaders/build.rs index 79abde65..57930b26 100644 --- a/vello_shaders/build.rs +++ b/vello_shaders/build.rs @@ -24,7 +24,10 @@ fn main() { let mut shaders = match compile::ShaderInfo::from_default() { Ok(s) => s, Err(err) => { - eprintln!("{err}"); + let formatted = err.to_string(); + for line in formatted.lines() { + println!("cargo:warning={line}"); + } return; } }; diff --git a/vello_shaders/shader/fine.wgsl b/vello_shaders/shader/fine.wgsl index 922c7ec2..77b2f7da 100644 --- a/vello_shaders/shader/fine.wgsl +++ b/vello_shaders/shader/fine.wgsl @@ -1160,13 +1160,19 @@ fn main( } case CMD_IMAGE: { let image = read_image(cmd_ix); - let atlas_extents = image.atlas_offset + image.extents; + let atlas_max = image.atlas_offset + image.extents - vec2(1.0); for (var i = 0u; i < PIXELS_PER_THREAD; i += 1u) { - let my_xy = vec2(xy.x + f32(i), xy.y); - let atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat + image.atlas_offset; - // This currently clips to the image bounds. TODO: extend modes - if all(atlas_uv < atlas_extents) && area[i] != 0.0 { - let uv_quad = vec4(max(floor(atlas_uv), image.atlas_offset), min(ceil(atlas_uv), atlas_extents)); + // We only need to load from the textures if the value will be used. + if area[i] != 0.0 { + let my_xy = vec2(xy.x + f32(i), xy.y); + let atlas_uv = image.matrx.xy * my_xy.x + image.matrx.zw * my_xy.y + image.xlat + image.atlas_offset - vec2(0.5); + // This currently only implements the Pad extend mode + // TODO: Support repeat and reflect + // TODO: If the image couldn't be added to the atlas (i.e. was too big), this isn't robust + let atlas_uv_clamped = clamp(atlas_uv, image.atlas_offset, atlas_max); + // We know that the floor and ceil are within the atlas area because atlas_max and + // atlas_offset are integers + let uv_quad = vec4(floor(atlas_uv_clamped), ceil(atlas_uv_clamped)); let uv_frac = fract(atlas_uv); let a = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xy), 0)); let b = premul_alpha(textureLoad(image_atlas, vec2(uv_quad.xw), 0)); diff --git a/vello_tests/smoke_snapshots/two_emoji.png b/vello_tests/smoke_snapshots/two_emoji.png index 2ae63c5f30ef575ab4e905772c2eb492477bfc87..cabbccf4c8197b35047e3d2ca5188f3157213fc0 100644 GIT binary patch delta 2947 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 j1z#y@U-y+=1-v^lyX${2_pbABBjMAegN