mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-07 04:12:15 +00:00
4ced1d7bcf
* 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.
55 lines
2.5 KiB
Rust
55 lines
2.5 KiB
Rust
//! 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());
|
||
}
|
||
}
|