mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Merge pull request #3609 from storytold/fix/mesh-banding
Gradient mesh: each tessellated quad shades between its corner colours, no banding
This commit is contained in:
+16
-4
@@ -291,12 +291,15 @@ pub struct GradientMesh {
|
||||
pub points: Vec<MeshPoint>,
|
||||
}
|
||||
|
||||
/// One tessellated piece of a mesh (a small quad with a solid colour).
|
||||
/// One tessellated piece of a mesh: a small quad, its colour at the centre, and its colour and
|
||||
/// opacity at each corner (in `pts` order), which a renderer shades between so neighbouring quads
|
||||
/// meet without a step.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct MeshQuad {
|
||||
pub pts: [Point; 4],
|
||||
pub color: Color,
|
||||
pub opacity: f32,
|
||||
pub corners: [(Color, f32); 4],
|
||||
}
|
||||
|
||||
/// Create Gradient Mesh → Appearance.
|
||||
@@ -832,7 +835,7 @@ impl GradientMesh {
|
||||
(Color::rgb(rgb[0], rgb[1], rgb[2]), op)
|
||||
}
|
||||
|
||||
/// Tessellate into `n`×`n` quads per patch, each coloured at its centre.
|
||||
/// Tessellate into `n`×`n` quads per patch, each coloured at its centre and its corners.
|
||||
pub fn quads(&self, n: usize) -> Vec<MeshQuad> {
|
||||
if !self.is_valid() {
|
||||
return vec![];
|
||||
@@ -840,18 +843,27 @@ impl GradientMesh {
|
||||
let n = n.clamp(1, 64);
|
||||
let mut out = Vec::with_capacity(self.rows as usize * self.cols as usize * n * n);
|
||||
let mut grid = vec![Point::ZERO; (n + 1) * (n + 1)];
|
||||
let mut colors = vec![(Color::default(), 0.0f32); (n + 1) * (n + 1)];
|
||||
for r in 0..self.rows as usize {
|
||||
for c in 0..self.cols as usize {
|
||||
for j in 0..=n {
|
||||
for i in 0..=n {
|
||||
grid[j * (n + 1) + i] = self.eval(r, c, i as f64 / n as f64, j as f64 / n as f64);
|
||||
let (u, v) = (i as f64 / n as f64, j as f64 / n as f64);
|
||||
grid[j * (n + 1) + i] = self.eval(r, c, u, v);
|
||||
colors[j * (n + 1) + i] = self.color_at(r, c, u, v);
|
||||
}
|
||||
}
|
||||
for j in 0..n {
|
||||
for i in 0..n {
|
||||
let (color, opacity) = self.color_at(r, c, (i as f64 + 0.5) / n as f64, (j as f64 + 0.5) / n as f64);
|
||||
let g = |a: usize, b: usize| grid[b * (n + 1) + a];
|
||||
out.push(MeshQuad { pts: [g(i, j), g(i + 1, j), g(i + 1, j + 1), g(i, j + 1)], color, opacity });
|
||||
let k = |a: usize, b: usize| colors[b * (n + 1) + a];
|
||||
out.push(MeshQuad {
|
||||
pts: [g(i, j), g(i + 1, j), g(i + 1, j + 1), g(i, j + 1)],
|
||||
color,
|
||||
opacity,
|
||||
corners: [k(i, j), k(i + 1, j), k(i + 1, j + 1), k(i, j + 1)],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,7 +179,8 @@ const MESH_RASTER_MAX_PIXELS: usize = 4 << 20;
|
||||
|
||||
/// Draw a tessellated gradient mesh as one image: its quads are filled into a pixel buffer of the
|
||||
/// mesh's on-screen box by scanline (each pixel takes the quad its centre is in, so neighbouring
|
||||
/// quads meet without seams or overlap), and the buffer is drawn through the mesh's outline, which
|
||||
/// quads meet without seams or overlap, and is shaded between the quad's corner colours, so the
|
||||
/// colour changes smoothly instead of in steps), and the buffer is drawn through the mesh's outline, which
|
||||
/// antialiases the outer edge. The outline is grown by half a device pixel ([`grown_outline`]), as
|
||||
/// the quads drawn one by one are: a page of meshes laid edge to edge would otherwise show the
|
||||
/// background through each shared, antialiased edge. Drawing every quad as a path of its own costs a
|
||||
@@ -208,9 +209,12 @@ fn draw_mesh_raster(ctx: &mut RenderContext, f: &Frame, m: &GradientMesh, quads:
|
||||
let mut filled = vec![false; w * h];
|
||||
let to_buffer = Affine::translate((-x0, -y0)) * f.view;
|
||||
for q in quads {
|
||||
// A fully transparent quad still claims its pixels, so the edge fill below leaves them clear.
|
||||
let c = if q.opacity > 0.0 { f.ink.color(&q.color, q.opacity * alpha).premultiply().to_rgba8() } else { clear };
|
||||
fill_quad(&mut px, &mut filled, w, h, q.pts.map(|p| to_buffer * p), c);
|
||||
// A fully transparent corner still shades toward clear, so the edge fill below leaves it clear.
|
||||
let corners = q.corners.map(|(color, opacity)| {
|
||||
let c = if opacity > 0.0 { f.ink.color(&color, opacity * alpha).premultiply().to_rgba8() } else { clear };
|
||||
[c.r, c.g, c.b, c.a].map(f32::from)
|
||||
});
|
||||
fill_quad(&mut px, &mut filled, w, h, q.pts.map(|p| to_buffer * p), corners);
|
||||
}
|
||||
// Twice: the grown edge samples pixels up to about a pixel and a half out from the quads.
|
||||
let filled = spread_to_edges(&mut px, filled, w, h);
|
||||
@@ -280,7 +284,7 @@ fn grown_outline(path: &BezPath, grow: f64) -> BezPath {
|
||||
|
||||
/// Fill the pixels of the `w`×`h` buffer whose centres are inside quad `p` (buffer pixels, even-odd
|
||||
/// so a twisted quad is filled as drawn) with `c`, and mark them `filled`.
|
||||
fn fill_quad(px: &mut [vello_cpu::color::PremulRgba8], filled: &mut [bool], w: usize, h: usize, p: [Point; 4], c: vello_cpu::color::PremulRgba8) {
|
||||
fn fill_quad(px: &mut [vello_cpu::color::PremulRgba8], filled: &mut [bool], w: usize, h: usize, p: [Point; 4], c: [[f32; 4]; 4]) {
|
||||
if !p.iter().all(|p| p.x.is_finite() && p.y.is_finite()) {
|
||||
return;
|
||||
}
|
||||
@@ -304,16 +308,52 @@ fn fill_quad(px: &mut [vello_cpu::color::PremulRgba8], filled: &mut [bool], w: u
|
||||
let from = (a - 0.5).ceil().clamp(0.0, w as f64) as usize;
|
||||
let to = (b - 0.5).ceil().clamp(0.0, w as f64) as usize;
|
||||
let row = y * w;
|
||||
for (d, k) in
|
||||
px.get_mut(row + from..row + to).unwrap_or_default().iter_mut().zip(filled.get_mut(row + from..row + to).unwrap_or_default())
|
||||
for ((x, d), k) in (from..)
|
||||
.zip(px.get_mut(row + from..row + to).unwrap_or_default().iter_mut())
|
||||
.zip(filled.get_mut(row + from..row + to).unwrap_or_default())
|
||||
{
|
||||
*d = c;
|
||||
*d = shade(p, &c, Point::new(x as f64 + 0.5, yc));
|
||||
*k = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The colour at `q` in quad `p` with corner colours `c` (premultiplied, 0–255): the corners'
|
||||
/// colours mixed by `q`'s barycentric weights in the quad's triangle 0-1-2 or 0-2-3, whichever
|
||||
/// holds it. A tessellated quad is a few pixels across, so the split along its diagonal doesn't show.
|
||||
fn shade(p: [Point; 4], c: &[[f32; 4]; 4], q: Point) -> vello_cpu::color::PremulRgba8 {
|
||||
let weights = |a: Point, b: Point, d: Point| -> Option<[f64; 3]> {
|
||||
let det = (b.x - a.x) * (d.y - a.y) - (d.x - a.x) * (b.y - a.y);
|
||||
if det.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let wb = ((q.x - a.x) * (d.y - a.y) - (d.x - a.x) * (q.y - a.y)) / det;
|
||||
let wd = ((b.x - a.x) * (q.y - a.y) - (q.x - a.x) * (b.y - a.y)) / det;
|
||||
Some([1.0 - wb - wd, wb, wd])
|
||||
};
|
||||
let inside = |w: &[f64; 3]| w.iter().all(|w| *w >= -1e-6);
|
||||
let first = weights(p[0], p[1], p[2]).map(|w| (w, [0, 1, 2]));
|
||||
let second = weights(p[0], p[2], p[3]).map(|w| (w, [0, 2, 3]));
|
||||
// The triangle holding `q`, else (at the quad's edge, or a folded quad) the nearer one.
|
||||
let pick = match (first, second) {
|
||||
(Some(a), _) if inside(&a.0) => Some(a),
|
||||
(_, Some(b)) if inside(&b.0) => Some(b),
|
||||
(Some(a), Some(b)) => Some(if a.0.iter().fold(0.0f64, |m, w| m.min(*w)) >= b.0.iter().fold(0.0f64, |m, w| m.min(*w)) { a } else { b }),
|
||||
(a, b) => a.or(b),
|
||||
};
|
||||
let mix = match pick {
|
||||
Some((w, idx)) => {
|
||||
let w = w.map(|w| w.clamp(0.0, 1.0));
|
||||
let sum = w.iter().sum::<f64>().max(1e-12);
|
||||
std::array::from_fn(|ch| (0..3).map(|k| c[idx[k]][ch] * (w[k] / sum) as f32).sum::<f32>())
|
||||
}
|
||||
None => std::array::from_fn(|ch| c.iter().map(|corner| corner[ch]).sum::<f32>() / 4.0),
|
||||
};
|
||||
let [r, g, b, a] = mix.map(|v| v.round().clamp(0.0, 255.0) as u8);
|
||||
vello_cpu::color::PremulRgba8 { r: r.min(a), g: g.min(a), b: b.min(a), a }
|
||||
}
|
||||
|
||||
/// Give each pixel no quad covered the colour of a covered neighbour: the outline's antialiased
|
||||
/// edge samples pixels whose centres fall just outside the quads, which would otherwise fade it.
|
||||
/// → the pixels covered now (the given ones and those just given a colour).
|
||||
@@ -555,12 +595,36 @@ mod tests {
|
||||
assert!(holes < 60, "{holes} pixels the smooth render covers are empty with hard edges");
|
||||
}
|
||||
|
||||
/// A quad is shaded between its corner colours: across a quad running from black on the left to
|
||||
/// white on the right, each column is lighter than the one before (no flat step), the left and
|
||||
/// right edges are near their corners' colours, and a column's pixels are alike.
|
||||
#[test]
|
||||
fn a_quad_shades_smoothly_between_its_corner_colours() {
|
||||
let (w, h) = (40, 10);
|
||||
let clear = vello_cpu::color::PremulRgba8 { r: 0, g: 0, b: 0, a: 0 };
|
||||
let mut px = vec![clear; w * h];
|
||||
let mut filled = vec![false; w * h];
|
||||
let (black, white) = ([0.0, 0.0, 0.0, 255.0], [255.0, 255.0, 255.0, 255.0]);
|
||||
let pts = [Point::new(0.0, 0.0), Point::new(40.0, 0.0), Point::new(40.0, 10.0), Point::new(0.0, 10.0)];
|
||||
fill_quad(&mut px, &mut filled, w, h, pts, [black, white, white, black]);
|
||||
let at = |x: usize, y: usize| px[y * w + x].r;
|
||||
for y in 0..h {
|
||||
for x in 1..w {
|
||||
assert!(at(x, y) > at(x - 1, y), "column {x} row {y}: {} after {}", at(x, y), at(x - 1, y));
|
||||
}
|
||||
assert!(at(0, y) < 8 && at(w - 1, y) > 247, "row {y}: {} to {}", at(0, y), at(w - 1, y));
|
||||
}
|
||||
for x in 0..w {
|
||||
assert!(at(x, 0).abs_diff(at(x, h - 1)) <= 1, "column {x}: {} vs {}", at(x, 0), at(x, h - 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// Quads that share edges fill every pixel of their union once: no gap, no overlap.
|
||||
#[test]
|
||||
fn quads_meeting_edge_to_edge_fill_their_union_once() {
|
||||
let (w, h) = (20, 20);
|
||||
let clear = vello_cpu::color::PremulRgba8 { r: 0, g: 0, b: 0, a: 0 };
|
||||
let red = vello_cpu::color::PremulRgba8 { r: 255, g: 0, b: 0, a: 255 };
|
||||
let red = [[255.0, 0.0, 0.0, 255.0]; 4];
|
||||
let mut px = vec![clear; w * h];
|
||||
let mut count = vec![0u8; w * h];
|
||||
// A 2×2 grid over (2.3, 2.7)–(17.6, 16.2) with an inner corner off the pixel grid.
|
||||
|
||||
Reference in New Issue
Block a user