Files
solidcraft/crates/gpu/tests/thin_lines.rs
T
DuBearteandClaude Opus 5.5 a9c7616e7f Lines thinner than a pixel no longer vanish without multisampling (grid lines, #2823) (#2871)
* claim: #2823

* fix(view): lines thinner than a pixel no longer vanish without multisampling (#2823)

A line quad narrower than a pixel covers no pixel centre when it falls between
two rows, so the grid's 0.7 px lines came and went as the view zoomed where the
viewport draws without 4x MSAA (some browsers and devices). Such lines are now
drawn a pixel wide, faded by the width they lack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-19 16:05:56 -04:00

54 lines
2.4 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Lines thinner than a pixel (the grid's minor lines are 0.7 px) are drawn wherever they fall,
//! without multisampling (#2823). A quad narrower than a pixel covers no pixel centre when it
//! falls between two rows, so such lines came and went as the view zoomed. Skipped, with the
//! reason printed, without a GPU adapter (`SOLIDCRAFT_REQUIRE_GPU=1` makes that a failure).
use solidcraft_engine::geom::Vec3;
use solidcraft_engine::render::{Camera, Rgb, Scene, SceneLine, StandardView};
use solidcraft_gpu::GpuError;
use solidcraft_gpu::capture::Capturer;
/// Pictures drawn with one sample per pixel: with 4× multisampling a thin line covers some
/// samples wherever it falls, which hides the fault.
fn capturer() -> Option<&'static Capturer> {
static SINGLE: std::sync::OnceLock<solidcraft_gpu::Result<Capturer>> = std::sync::OnceLock::new();
match SINGLE.get_or_init(Capturer::headless_single_sample).as_ref() {
Ok(c) => {
assert_eq!(c.samples(), 1);
Some(c)
}
Err(e @ GpuError::NoAdapter(_)) if std::env::var_os("SOLIDCRAFT_REQUIRE_GPU").is_none() => {
eprintln!("skipping: {e}");
None
}
Err(e) => panic!("{e}"),
}
}
/// Lines `width` px wide across a front view, each drawn alone at its own height so it falls at
/// its own fraction of a pixel (0.23 mm apart, about 1.15 px at 5 px per mm); returns the ones
/// that left no pixel.
fn missing(c: &Capturer, width: f64) -> Vec<usize> {
const N: usize = 40;
let mut cam = Camera { distance: 62.5, fov: 0.0, ..Default::default() };
cam.set_view(StandardView::Front);
(0..N)
.filter(|&i| {
let z = -4.0 + i as f64 * 0.23;
let line = SceneLine { points: vec![Vec3::new(-8.0, 0.0, z), Vec3::new(8.0, 0.0, z)], color: Rgb(255, 0, 0), width, on_top: true };
let scene = Scene { lines: vec![line], radius: 30.0, ..Default::default() };
let img = c.rgba(&scene, &cam, 200, 200).unwrap();
!img.as_chunks::<4>().0.iter().any(|p| p[0] > 60 && p[0] > p[1].saturating_add(40) && p[0] > p[2].saturating_add(40))
})
.collect()
}
#[test]
fn lines_thinner_than_a_pixel_are_drawn_wherever_they_fall() {
let Some(c) = capturer() else { return };
for width in [0.4, 0.7, 1.0, 1.5] {
let gone = missing(c, width);
assert!(gone.is_empty(), "{width} px lines: {} of 40 left no pixel ({gone:?})", gone.len());
}
}