From 526cdfd2474830cbdb338797690e49d794d2a01f Mon Sep 17 00:00:00 2001 From: PaddleStroke Date: Sat, 31 Oct 2026 00:03:59 +0000 Subject: [PATCH] Zebra: flat faces keep one stripe colour while the view turns (#2153) (#9761) * claim: #2153 Co-Authored-By: Claude Opus 5.5 * Zebra: flat faces keep one stripe colour while the view turns (#2153) A face whose normal lies in the upright plane through the view (every horizontal face) reflects straight ahead, where the zebra's sideways coordinate is zero: exactly on the edge between a black and a white stripe. Rounding then picked one or the other from frame to frame, so the face flickered while the camera orbited. Straight ahead is now white. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- assets/shaders/viewport.wgsl | 9 +++++++-- crates/gpu/tests/capture.rs | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/assets/shaders/viewport.wgsl b/assets/shaders/viewport.wgsl index 4715d9edfd..2553c97463 100644 --- a/assets/shaders/viewport.wgsl +++ b/assets/shaders/viewport.wgsl @@ -423,8 +423,13 @@ fn analysis(i: TOut, k: f32) -> vec4 { let r = reflect(-v, n); var side = cross(v, vec3(0.0, 0.0, 1.0)); if (length(side) < 1e-3) { side = vec3(1.0, 0.0, 0.0); } - let s = sin(dot(r, normalize(side)) * u.ana.y * 3.14159265); - let c = select(0.08, 0.95, s > 0.0); + let x = dot(r, normalize(side)); + let s = sin(x * u.ana.y * 3.14159265); + // A face whose normal lies in the upright plane through the view (every horizontal + // face) reflects straight ahead: its sideways part is zero, on the edge between a black + // and a white stripe, and rounding picked one or the other from frame to frame while + // the view turned (#2153). Straight ahead is white. + let c = select(0.08, 0.95, s > 0.0 || abs(x) < 1e-4); return out_color(vec4(vec3(c), 1.0)); } if (u.ana.x > 4.5) { diff --git a/crates/gpu/tests/capture.rs b/crates/gpu/tests/capture.rs index 03927f4ba4..d7782b76a1 100644 --- a/crates/gpu/tests/capture.rs +++ b/crates/gpu/tests/capture.rs @@ -319,6 +319,28 @@ fn pictures_show_the_surface_analysis() { assert!(reachable > 1000 && under > 1000, "accessibility: {reachable} reachable, {under} blocked pixels"); } +/// A flat face of a box keeps its zebra colour while the view turns a little at a time (#2153): +/// a horizontal face reflected straight ahead, on the edge between a black and a white stripe, +/// and rounding flipped it from one step to the next. +#[test] +fn zebra_on_flat_faces_holds_still_while_the_view_turns() { + let Some(c) = capturer() else { return }; + let mut s = session(&[("solid.box", json!({"length": 60, "width": 40, "height": 20}))]); + s.execute("inspect.zebra", &json!({})).unwrap(); + let home = view::home_camera(&s); + let whites: Vec = (0..24) + .map(|step| { + let mut cam = home; + cam.yaw += f64::from(step) * 0.11_f64.to_radians(); + let img = c.rgba(&view::scene_with_guides(&s, &cam, None, Guides { grid: false, axes: false }), &cam, 320, 200).unwrap(); + pixels(&img).filter(|p| p[0] > 225 && p[1] > 225 && p[2] > 225).count() + }) + .collect(); + let (least, most) = (whites.iter().min().copied().unwrap_or(0), whites.iter().max().copied().unwrap_or(0)); + assert!(most > 1000, "white stripes: {whites:?}"); + assert!((most - least) * 10 < most, "white pixels jump while the view turns by 0.11 degree steps: {whites:?}"); +} + fn colors_of(c: [[f32; 3]; 2]) -> [[u8; 3]; 2] { c.map(|rgb| rgb.map(|x| (x * 255.0).round() as u8)) }