Files
solvecraft/crates/gpu/tests/thin_lines.rs
T
Homero Thompson 4ced1d7bcf Viewport: the grid fades with the distance instead of cutting off (#4214) (#4629)
* claim: #4214

* Viewport: the grid fades with the distance instead of cutting off (#4214)

The ground grid stopped at a fixed distance from the view, so its edge showed
as a cut across the picture. It now fades out with the distance from the
middle of the view, and it is gone before either of the two lines that used to
cut it: its own edge, and the picture's far plane.

Each grid line is drawn in pieces, and each piece is as faint as the fade
makes it at the distance where it lies. The 'under' batch is drawn by the
overlay pipeline with alpha blending, so the colour's alpha reaches the
frame.

* The headless grid fades with the distance too (#4214)

The headless path (solidcraft-cli snapshot, the home-page thumbnail, the MCP
screenshot) drew the grid from the engine's palette, where a line had no
alpha: the grid still ended in a cut, and at a low camera angle the cut was
in the picture.

A SceneLine now carries an alpha, the CPU rasterizer blends with it and the
GPU capture passes it on, so the engine's grid can fade. The fade itself
moves to the engine, next to the grid's other geometry, and both paths use
it: the headless grid now reaches as far on the ground as the view is wide
(the ground stretches as it is seen at a slant) and dissolves into the
distance instead of ending in a line.

* Fix the headless/viewport grid fade divergence (#4214)

The engine stretched the grid's size with the slant of the plane it is seen at, the viewport stretched it only on the ground, so on a sketch's plane the picture's grid reached further than the window's (153.42 mm against 120.00 mm). Both now share one grid_ext, which the picture and the window draw the same grid from.

* The grid fades out before the picture's far plane cuts the ground (#4214)

The window's grid faded against a far plane of its own: the scene's radius
clipped by the projection's camera (260 mm away at 400 mm of distance), while
the picture itself cuts the ground at the far plane of its own projection
(223.7 mm at the same distance, because the projection frames the scene's
bounds and not its radius). The grid was still a third of its strength where
the picture cut the ground, so the cut showed as a straight line across the
view.

The fade now ends at the far plane the picture's projection uses, and the
grid is gone before that plane cuts the ground.

The test that watched the fade now measures the ink of each row of the
picture, and checks that the grid is already gone in the ten rows above the
row where the far plane cuts the ground: the point it compared before lay
beyond that plane, where both renders draw nothing at all, so the comparison
was always true.
2026-10-26 07:59:56 +00:00

55 lines
2.5 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), alpha: 1.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());
}
}