Viewport: hidden edges no longer show through faces, and thin or coplanar faces stop z-fighting (#1434, #1739) (#1768)
Thanks @CamWight.
This commit is contained in:
1 parent
c7bb64dd74
commit
c5f3515073
10 files changed
+599
-62
No files matched your search
@@ -27,7 +27,9 @@ mod web {
|
|||||||
|
|
||||||
const CANVAS_ID: &str = "solidcraft_canvas";
|
const CANVAS_ID: &str = "solidcraft_canvas";
|
||||||
const LOADING_ID: &str = "solidcraft_loading";
|
const LOADING_ID: &str = "solidcraft_loading";
|
||||||
const DEPTH_BITS: u8 = 24;
|
/// A 32-bit float depth buffer (no stencil): WebGPU and WebGL2 both have it, and the viewport
|
||||||
|
/// draws it with reversed depth, whose precision follows the distance from the eye (#1434).
|
||||||
|
const DEPTH_BITS: u8 = 32;
|
||||||
|
|
||||||
/// Seconds between autosaves of unsaved work.
|
/// Seconds between autosaves of unsaved work.
|
||||||
const AUTOSAVE_S: f64 = 30.0;
|
const AUTOSAVE_S: f64 = 30.0;
|
||||||
|
|||||||
@@ -416,6 +416,10 @@ fn msaa() -> u16 {
|
|||||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||||
|
|
||||||
const DEPTH_BITS: u8 = 24;
|
const DEPTH_BITS: u8 = 24;
|
||||||
|
/// A 32-bit float depth buffer, drawn with reversed depth (1 near, 0 far) so its precision follows
|
||||||
|
/// the distance from the eye (#1434). Only where every adapter offers it with a stencil
|
||||||
|
/// ([`startup::float_depth_everywhere`]); else [`DEPTH_BITS`]. Not in safe mode.
|
||||||
|
const FLOAT_DEPTH_BITS: u8 = 32;
|
||||||
/// The section cap marks where it shows in the stencil.
|
/// The section cap marks where it shows in the stencil.
|
||||||
const STENCIL_BITS: u8 = 8;
|
const STENCIL_BITS: u8 = 8;
|
||||||
const MSAA: u16 = 4;
|
const MSAA: u16 = 4;
|
||||||
@@ -497,6 +501,8 @@ fn main() -> eframe::Result {
|
|||||||
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
|
||||||
}
|
}
|
||||||
let samples = if safe_mode { MSAA } else { msaa() };
|
let samples = if safe_mode { MSAA } else { msaa() };
|
||||||
|
let wgpu_options = wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref());
|
||||||
|
let depth_bits = if !safe_mode && startup::float_depth_everywhere(&wgpu_options) { FLOAT_DEPTH_BITS } else { DEPTH_BITS };
|
||||||
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
|
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
|
||||||
let crashed = solidcraft_engine::crash::default_dir()
|
let crashed = solidcraft_engine::crash::default_dir()
|
||||||
.map(|dir| {
|
.map(|dir| {
|
||||||
@@ -516,7 +522,7 @@ fn main() -> eframe::Result {
|
|||||||
solidcraft_engine::crash::phase("window");
|
solidcraft_engine::crash::phase("window");
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: app_viewport()?,
|
viewport: app_viewport()?,
|
||||||
depth_buffer: DEPTH_BITS,
|
depth_buffer: depth_bits,
|
||||||
stencil_buffer: STENCIL_BITS,
|
stencil_buffer: STENCIL_BITS,
|
||||||
multisampling: samples,
|
multisampling: samples,
|
||||||
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
|
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
|
||||||
@@ -529,7 +535,7 @@ fn main() -> eframe::Result {
|
|||||||
settings.as_ref().map(|d| d.join("app.ron"))
|
settings.as_ref().map(|d| d.join("app.ron"))
|
||||||
},
|
},
|
||||||
persist_window: !safe_mode,
|
persist_window: !safe_mode,
|
||||||
wgpu_options: wgpu_options(cpu_renderer, if safe_mode { None } else { backend_preference() }.as_deref()),
|
wgpu_options,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
#[cfg(all(unix, not(target_os = "macos")))]
|
#[cfg(all(unix, not(target_os = "macos")))]
|
||||||
@@ -579,7 +585,7 @@ fn main() -> eframe::Result {
|
|||||||
if let Some(rs) = &cc.wgpu_render_state {
|
if let Some(rs) = &cc.wgpu_render_state {
|
||||||
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
|
eprintln!("SolidCraft graphics: {:?}, MSAA {}x (requested {}x)", rs.adapter.get_info().backend, rs.msaa_samples, samples);
|
||||||
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
solidcraft_engine::crash::phase("viewport pipelines (shader compile)");
|
||||||
app.set_wgpu(rs, DEPTH_BITS, STENCIL_BITS, rs.msaa_samples);
|
app.set_wgpu(rs, depth_bits, STENCIL_BITS, rs.msaa_samples);
|
||||||
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
|
solidcraft_ui_egui::gpu::install_picture_renderer(rs);
|
||||||
// The graphics driver is loaded now; it may have put its own exception filter
|
// The graphics driver is loaded now; it may have put its own exception filter
|
||||||
// in front of ours.
|
// in front of ours.
|
||||||
@@ -685,6 +691,13 @@ fn wgpu_options(cpu: bool, preference: Option<&str>) -> eframe::egui_wgpu::WgpuC
|
|||||||
if cpu {
|
if cpu {
|
||||||
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
|
||||||
}
|
}
|
||||||
|
// A float depth buffer with a stencil, wherever the adapter has it (see `FLOAT_DEPTH_BITS`).
|
||||||
|
let base = setup.device_descriptor.clone();
|
||||||
|
setup.device_descriptor = std::sync::Arc::new(move |adapter| {
|
||||||
|
let mut d = base(adapter);
|
||||||
|
d.required_features |= adapter.features() & startup::FLOAT_DEPTH_STENCIL;
|
||||||
|
d
|
||||||
|
});
|
||||||
}
|
}
|
||||||
options
|
options
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -157,6 +157,23 @@ pub fn choose_backends(
|
|||||||
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
Some(WINDOWS_ORDER.into_iter().find(|b| has_hardware(*b)).unwrap_or(Backends::DX12))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The device feature for a 32-bit float depth buffer with a stencil (reversed depth, #1434).
|
||||||
|
pub const FLOAT_DEPTH_STENCIL: eframe::wgpu::Features = eframe::wgpu::Features::DEPTH32FLOAT_STENCIL8;
|
||||||
|
|
||||||
|
/// Whether every adapter `options` could start offers a float depth buffer with a stencil. eframe
|
||||||
|
/// makes the window's depth buffer from the depth bits before it picks the adapter, so the window
|
||||||
|
/// asks for a float one only when whichever it picks has it.
|
||||||
|
pub fn float_depth_everywhere(options: &eframe::egui_wgpu::WgpuConfiguration) -> bool {
|
||||||
|
let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &options.wgpu_setup else { return false };
|
||||||
|
let mut desc = eframe::wgpu::InstanceDescriptor::new_without_display_handle_from_env();
|
||||||
|
desc.backends = setup.instance_descriptor.backends;
|
||||||
|
let instance = eframe::wgpu::Instance::new(desc);
|
||||||
|
let adapters = pollster::block_on(instance.enumerate_adapters(setup.instance_descriptor.backends));
|
||||||
|
let all = !adapters.is_empty() && adapters.iter().all(|a| a.features().contains(FLOAT_DEPTH_STENCIL));
|
||||||
|
log::info!("graphics: float depth with stencil on every adapter: {all}");
|
||||||
|
all
|
||||||
|
}
|
||||||
|
|
||||||
/// The order backends are tried in on Windows.
|
/// The order backends are tried in on Windows.
|
||||||
pub const WINDOWS_ORDER: [eframe::wgpu::Backends; 3] = [eframe::wgpu::Backends::DX12, eframe::wgpu::Backends::VULKAN, eframe::wgpu::Backends::GL];
|
pub const WINDOWS_ORDER: [eframe::wgpu::Backends; 3] = [eframe::wgpu::Backends::DX12, eframe::wgpu::Backends::VULKAN, eframe::wgpu::Backends::GL];
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
struct U {
|
struct U {
|
||||||
vp: mat4x4<f32>,
|
vp: mat4x4<f32>,
|
||||||
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
|
back: vec4<f32>, // xyz = toward the eye, w = 1 for linear output
|
||||||
screen: vec4<f32>, // viewport width, height (px), depth bias (see `biased_z`), silhouettes on (1)
|
screen: vec4<f32>, // viewport width, height (px), unused, silhouettes on (1)
|
||||||
clip: vec4<f32>, // section plane: normal, d (zero normal: no section)
|
clip: vec4<f32>, // section plane: normal, d (zero normal: no section)
|
||||||
cap: vec4<f32>, // colour of the inside of cut bodies
|
cap: vec4<f32>, // colour of the inside of cut bodies
|
||||||
ana: vec4<f32>, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter
|
ana: vec4<f32>, // surface analysis: mode (1 zebra, 2 draft, 3 curvature), parameter
|
||||||
@@ -14,8 +14,19 @@ struct U {
|
|||||||
// blocked; curvature flat, middle, tight
|
// blocked; curvature flat, middle, tight
|
||||||
ana_c: array<vec4<f32>, 8>,
|
ana_c: array<vec4<f32>, 8>,
|
||||||
ana_f: vec4<f32>, // x = 1: stripe the negative draft and blocked faces
|
ana_f: vec4<f32>, // x = 1: stripe the negative draft and blocked faces
|
||||||
|
depth: vec4<f32>, // x = 1: reversed depth (1 near, 0 far); y, z: the faces' push (`FACE_BIAS`)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
fn reversed() -> bool {
|
||||||
|
return u.depth.x > 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clip-space depth measured the usual way (0 at the near plane, w at the far one), whichever
|
||||||
|
/// way the depth buffer runs.
|
||||||
|
fn near_z(c: vec4<f32>) -> f32 {
|
||||||
|
return select(c.z, c.w - c.z, reversed());
|
||||||
|
}
|
||||||
|
|
||||||
@group(1) @binding(2) var acc_t: texture_2d<f32>;
|
@group(1) @binding(2) var acc_t: texture_2d<f32>;
|
||||||
|
|
||||||
fn clipped(p: vec3<f32>) -> bool {
|
fn clipped(p: vec3<f32>) -> bool {
|
||||||
@@ -70,11 +81,22 @@ struct CutOut {
|
|||||||
@fragment
|
@fragment
|
||||||
fn fs_solid_cut(i: TOut, @builtin(front_facing) front: bool) -> CutOut {
|
fn fs_solid_cut(i: TOut, @builtin(front_facing) front: bool) -> CutOut {
|
||||||
var o: CutOut;
|
var o: CutOut;
|
||||||
|
// Derivatives (in `pushed`) first, before `solid` may discard.
|
||||||
|
o.depth = pushed(cap_depth(i.wp, i.pos.z));
|
||||||
o.c = solid(i, front);
|
o.c = solid(i, front);
|
||||||
o.depth = cap_depth(i.wp, i.pos.z);
|
|
||||||
return o;
|
return o;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A depth the shader writes itself, pushed back like the faces the GPU pushes (`FACE_BIAS`,
|
||||||
|
/// which does not apply then): its own slope over `depth.z` pixels and `depth.y` depth steps.
|
||||||
|
fn pushed(z: f32) -> f32 {
|
||||||
|
let slope = max(abs(dpdx(z)), abs(dpdy(z)));
|
||||||
|
// One step: 2⁻²⁴ in a 24-bit buffer; a float buffer's steps follow the value (2⁻²³ of it).
|
||||||
|
let step = select(1.0 / 16777216.0, abs(z) * 1.1920929e-7, reversed());
|
||||||
|
let push = slope * u.depth.z + step * u.depth.y;
|
||||||
|
return clamp(z - select(-push, push, reversed()), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
/// Depth of the section plane on the view ray through `wp` (a far wall seen through the cut),
|
/// Depth of the section plane on the view ray through `wp` (a far wall seen through the cut),
|
||||||
/// never behind the wall itself (`z`).
|
/// never behind the wall itself (`z`).
|
||||||
fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
|
fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
|
||||||
@@ -85,6 +107,7 @@ fn cap_depth(wp: vec3<f32>, z: f32) -> f32 {
|
|||||||
let p = wp + dir * ((u.clip.w - dot(u.clip.xyz, wp)) / dn);
|
let p = wp + dir * ((u.clip.w - dot(u.clip.xyz, wp)) / dn);
|
||||||
let c = u.vp * vec4<f32>(p, 1.0);
|
let c = u.vp * vec4<f32>(p, 1.0);
|
||||||
if (c.w <= 0.0) { return z; }
|
if (c.w <= 0.0) { return z; }
|
||||||
|
if (reversed()) { return clamp(c.z / c.w, z, 1.0); }
|
||||||
return clamp(c.z / c.w, 0.0, z);
|
return clamp(c.z / c.w, 0.0, z);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,28 +246,8 @@ fn shade(i: TOut) -> vec4<f32> {
|
|||||||
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
|
return out_color(vec4<f32>(min(i.c.rgb * k + vec3<f32>(spec), vec3<f32>(1.0)), i.c.a));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clip-space z of `p` (clipped to `c`) pulled `s` depth biases toward the eye (pushed away
|
// Lines, decals and ghosts are drawn at their own depth: opaque faces are pushed back by their
|
||||||
/// when negative). Orthographic depth is linear in distance, so the bias is a fixed step of it.
|
// slope instead (`FACE_BIAS`, #1434), so what lies on a face draws over it at any angle.
|
||||||
/// Perspective depth is not: near the far plane a fixed step spans tens of millimetres (edges
|
|
||||||
/// showed through walls), so the point moves toward the eye by a fraction of its distance.
|
|
||||||
fn biased_z(p: vec3<f32>, c: vec4<f32>, s: f32) -> f32 {
|
|
||||||
if (u.eye.w > 0.5) {
|
|
||||||
let q = u.vp * vec4<f32>(mix(p, u.eye.xyz, u.screen.z * 8.0 * s), 1.0);
|
|
||||||
return q.z / max(q.w, 1e-6) * c.w;
|
|
||||||
}
|
|
||||||
return c.z - u.screen.z * s * c.w;
|
|
||||||
}
|
|
||||||
|
|
||||||
@vertex
|
|
||||||
fn vs_ghost(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c: vec4<f32>) -> TOut {
|
|
||||||
var o: TOut;
|
|
||||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
|
||||||
o.pos.z = biased_z(p, o.pos, -4.0);
|
|
||||||
o.n = n;
|
|
||||||
o.c = c;
|
|
||||||
o.wp = p;
|
|
||||||
return o;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LOut {
|
struct LOut {
|
||||||
@builtin(position) pos: vec4<f32>,
|
@builtin(position) pos: vec4<f32>,
|
||||||
@@ -261,12 +264,14 @@ fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(
|
|||||||
// An end behind the near plane (clip z < 0) has no screen point: its divide flips the
|
// An end behind the near plane (clip z < 0) has no screen point: its divide flips the
|
||||||
// line or drops it whole (ground grid lines running past the eye at a low angle). Cut
|
// line or drops it whole (ground grid lines running past the eye at a low angle). Cut
|
||||||
// the segment at the plane instead; clip space is linear in the world point.
|
// the segment at the plane instead; clip space is linear in the world point.
|
||||||
if (ca.z < 0.0 && cb.z > 0.0) {
|
let za = near_z(ca);
|
||||||
let k = ca.z / (ca.z - cb.z);
|
let zb = near_z(cb);
|
||||||
|
if (za < 0.0 && zb > 0.0) {
|
||||||
|
let k = za / (za - zb);
|
||||||
ca = mix(ca, cb, k);
|
ca = mix(ca, cb, k);
|
||||||
pa = mix(a, b, k);
|
pa = mix(a, b, k);
|
||||||
} else if (cb.z < 0.0 && ca.z > 0.0) {
|
} else if (zb < 0.0 && za > 0.0) {
|
||||||
let k = cb.z / (cb.z - ca.z);
|
let k = zb / (zb - za);
|
||||||
cb = mix(cb, ca, k);
|
cb = mix(cb, ca, k);
|
||||||
pb = mix(b, a, k);
|
pb = mix(b, a, k);
|
||||||
}
|
}
|
||||||
@@ -285,7 +290,7 @@ fn vs_line(@builtin(vertex_index) vi: u32, @location(0) a: vec3<f32>, @location(
|
|||||||
let base = select(ca, cb, t > 0.5);
|
let base = select(ca, cb, t > 0.5);
|
||||||
let off = nrm * s * w * 0.5 + dir * (t * 2.0 - 1.0) * w * 0.5;
|
let off = nrm * s * w * 0.5 + dir * (t * 2.0 - 1.0) * w * 0.5;
|
||||||
var o: LOut;
|
var o: LOut;
|
||||||
o.pos = vec4<f32>(base.xy + off / half * base.w, biased_z(select(pa, pb, t > 0.5), base, 1.0), base.w);
|
o.pos = vec4<f32>(base.xy + off / half * base.w, base.z, base.w);
|
||||||
o.c = c;
|
o.c = c;
|
||||||
o.wp = select(pa, pb, t > 0.5);
|
o.wp = select(pa, pb, t > 0.5);
|
||||||
return o;
|
return o;
|
||||||
@@ -310,9 +315,8 @@ struct IOut {
|
|||||||
@vertex
|
@vertex
|
||||||
fn vs_img(@location(0) p: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) a: f32) -> IOut {
|
fn vs_img(@location(0) p: vec3<f32>, @location(1) uv: vec2<f32>, @location(2) a: f32) -> IOut {
|
||||||
var o: IOut;
|
var o: IOut;
|
||||||
|
// On a face it shows over it: the face is pushed back (`FACE_BIAS`).
|
||||||
o.pos = u.vp * vec4<f32>(p, 1.0);
|
o.pos = u.vp * vec4<f32>(p, 1.0);
|
||||||
// Pulled toward the eye like lines, so a decal on a face is not lost in it.
|
|
||||||
o.pos.z = biased_z(p, o.pos, 1.0);
|
|
||||||
o.uv = uv;
|
o.uv = uv;
|
||||||
o.a = a;
|
o.a = a;
|
||||||
o.wp = p;
|
o.wp = p;
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ use crate::{GpuError, Result, popped};
|
|||||||
/// The colour format pictures are drawn in: sRGB bytes as they are, like the viewport's usual
|
/// The colour format pictures are drawn in: sRGB bytes as they are, like the viewport's usual
|
||||||
/// (non-sRGB) target.
|
/// (non-sRGB) target.
|
||||||
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba8Unorm;
|
||||||
const DEPTH: wgpu::TextureFormat = wgpu::TextureFormat::Depth24PlusStencil8;
|
|
||||||
/// Samples per pixel (MSAA), as the desktop viewport's default; 1 where the device has no 4×.
|
/// Samples per pixel (MSAA), as the desktop viewport's default; 1 where the device has no 4×.
|
||||||
pub const SAMPLES: u32 = 4;
|
pub const SAMPLES: u32 = 4;
|
||||||
/// The largest picture side (pixels); the device's texture limit may lower it.
|
/// The largest picture side (pixels); the device's texture limit may lower it.
|
||||||
@@ -39,6 +38,8 @@ pub struct Capturer {
|
|||||||
/// The adapter is a GPU driver running on the CPU (llvmpipe, lavapipe, WARP).
|
/// The adapter is a GPU driver running on the CPU (llvmpipe, lavapipe, WARP).
|
||||||
software: bool,
|
software: bool,
|
||||||
samples: u32,
|
samples: u32,
|
||||||
|
/// The depth format: 32-bit float (reversed depth) where the device has it ([`crate::depth_format`]).
|
||||||
|
depth: wgpu::TextureFormat,
|
||||||
max_side: u32,
|
max_side: u32,
|
||||||
state: Mutex<State>,
|
state: Mutex<State>,
|
||||||
}
|
}
|
||||||
@@ -54,17 +55,25 @@ struct State {
|
|||||||
impl Capturer {
|
impl Capturer {
|
||||||
/// The pipelines on an existing device (the application's).
|
/// The pipelines on an existing device (the application's).
|
||||||
pub fn new(adapter: &wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue) -> Result<Self> {
|
pub fn new(adapter: &wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue) -> Result<Self> {
|
||||||
|
let depth = crate::depth_format(&device, true);
|
||||||
|
Self::with_depth(adapter, device, queue, depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// As [`Capturer::new`], drawing with the depth format `depth` (with a stencil): the fallback
|
||||||
|
/// `Depth24PlusStencil8` on a device that has a float one too, to compare the two.
|
||||||
|
pub fn with_depth(adapter: &wgpu::Adapter, device: wgpu::Device, queue: wgpu::Queue, depth: wgpu::TextureFormat) -> Result<Self> {
|
||||||
let flags = |f: wgpu::TextureFormat| adapter.get_texture_format_features(f).flags;
|
let flags = |f: wgpu::TextureFormat| adapter.get_texture_format_features(f).flags;
|
||||||
let samples = if flags(FORMAT).sample_count_supported(SAMPLES) && flags(DEPTH).sample_count_supported(SAMPLES) { SAMPLES } else { 1 };
|
let samples = if flags(FORMAT).sample_count_supported(SAMPLES) && flags(depth).sample_count_supported(SAMPLES) { SAMPLES } else { 1 };
|
||||||
let scopes = error_scopes(&device);
|
let scopes = error_scopes(&device);
|
||||||
let renderer = Renderer::new(&device, GpuTarget { format: FORMAT, depth: Some(DEPTH), samples, tess: false }, None);
|
let renderer = Renderer::new(&device, GpuTarget { format: FORMAT, depth: Some(depth), samples, tess: false }, None);
|
||||||
let background = background_pipeline(&device, samples);
|
let background = background_pipeline(&device, samples, depth);
|
||||||
caught(scopes).map_or(Ok(()), |e| Err(GpuError::Pipeline(e)))?;
|
caught(scopes).map_or(Ok(()), |e| Err(GpuError::Pipeline(e)))?;
|
||||||
let max_side = device.limits().max_texture_dimension_2d.min(MAX_SIDE);
|
let max_side = device.limits().max_texture_dimension_2d.min(MAX_SIDE);
|
||||||
Ok(Capturer {
|
Ok(Capturer {
|
||||||
adapter: adapter.get_info().name,
|
adapter: adapter.get_info().name,
|
||||||
software: adapter.get_info().device_type == wgpu::DeviceType::Cpu,
|
software: adapter.get_info().device_type == wgpu::DeviceType::Cpu,
|
||||||
samples,
|
samples,
|
||||||
|
depth,
|
||||||
max_side,
|
max_side,
|
||||||
state: Mutex::new(State { renderer, background, key: 0, access_version: 0 }),
|
state: Mutex::new(State { renderer, background, key: 0, access_version: 0 }),
|
||||||
device,
|
device,
|
||||||
@@ -82,6 +91,19 @@ impl Capturer {
|
|||||||
Self::new(&adapter, device, queue)
|
Self::new(&adapter, device, queue)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// As [`Capturer::headless`], drawing with the depth format `depth` ([`Capturer::with_depth`]).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
pub fn headless_with_depth(depth: wgpu::TextureFormat) -> Result<Self> {
|
||||||
|
let (adapter, device, queue) = crate::open_headless()?;
|
||||||
|
device.on_uncaptured_error(Arc::new(|e: wgpu::Error| log::error!("GPU: {e}")));
|
||||||
|
Self::with_depth(&adapter, device, queue, depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The depth format pictures are drawn with; float formats are drawn with reversed depth.
|
||||||
|
pub fn depth_format(&self) -> wgpu::TextureFormat {
|
||||||
|
self.depth
|
||||||
|
}
|
||||||
|
|
||||||
/// The adapter's name.
|
/// The adapter's name.
|
||||||
pub fn adapter_name(&self) -> &str {
|
pub fn adapter_name(&self) -> &str {
|
||||||
&self.adapter
|
&self.adapter
|
||||||
@@ -132,7 +154,7 @@ impl Capturer {
|
|||||||
};
|
};
|
||||||
let resolved = texture("sc_capture", FORMAT, 1, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC);
|
let resolved = texture("sc_capture", FORMAT, 1, wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC);
|
||||||
let multisampled = (self.samples > 1).then(|| texture("sc_capture_ms", FORMAT, self.samples, wgpu::TextureUsages::RENDER_ATTACHMENT));
|
let multisampled = (self.samples > 1).then(|| texture("sc_capture_ms", FORMAT, self.samples, wgpu::TextureUsages::RENDER_ATTACHMENT));
|
||||||
let depth = texture("sc_capture_depth", DEPTH, self.samples, wgpu::TextureUsages::RENDER_ATTACHMENT);
|
let depth = texture("sc_capture_depth", self.depth, self.samples, wgpu::TextureUsages::RENDER_ATTACHMENT);
|
||||||
let (resolved_view, depth_view) = (resolved.create_view(&Default::default()), depth.create_view(&Default::default()));
|
let (resolved_view, depth_view) = (resolved.create_view(&Default::default()), depth.create_view(&Default::default()));
|
||||||
let multisampled_view = multisampled.as_ref().map(|t| t.create_view(&Default::default()));
|
let multisampled_view = multisampled.as_ref().map(|t| t.create_view(&Default::default()));
|
||||||
let (top, bottom) = scene.background.unwrap_or(BACKGROUND);
|
let (top, bottom) = scene.background.unwrap_or(BACKGROUND);
|
||||||
@@ -155,7 +177,10 @@ impl Capturer {
|
|||||||
})],
|
})],
|
||||||
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
|
||||||
view: &depth_view,
|
view: &depth_view,
|
||||||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
|
depth_ops: Some(wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(crate::view::far_depth(crate::view::reversed_depth(Some(self.depth)))),
|
||||||
|
store: wgpu::StoreOp::Discard,
|
||||||
|
}),
|
||||||
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: wgpu::StoreOp::Discard }),
|
stencil_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: wgpu::StoreOp::Discard }),
|
||||||
}),
|
}),
|
||||||
timestamp_writes: None,
|
timestamp_writes: None,
|
||||||
@@ -387,7 +412,7 @@ fn background_vertices(device: &wgpu::Device, top: Rgb, bottom: Rgb) -> wgpu::Bu
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn background_pipeline(device: &wgpu::Device, samples: u32) -> wgpu::RenderPipeline {
|
fn background_pipeline(device: &wgpu::Device, samples: u32, depth: wgpu::TextureFormat) -> wgpu::RenderPipeline {
|
||||||
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
label: Some("sc_capture_background"),
|
label: Some("sc_capture_background"),
|
||||||
source: wgpu::ShaderSource::Wgsl(BACKGROUND_SHADER.into()),
|
source: wgpu::ShaderSource::Wgsl(BACKGROUND_SHADER.into()),
|
||||||
@@ -409,7 +434,7 @@ fn background_pipeline(device: &wgpu::Device, samples: u32) -> wgpu::RenderPipel
|
|||||||
},
|
},
|
||||||
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
|
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
|
||||||
depth_stencil: Some(wgpu::DepthStencilState {
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
format: DEPTH,
|
format: depth,
|
||||||
depth_write_enabled: Some(false),
|
depth_write_enabled: Some(false),
|
||||||
depth_compare: Some(wgpu::CompareFunction::Always),
|
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||||
stencil: Default::default(),
|
stencil: Default::default(),
|
||||||
|
|||||||
@@ -29,6 +29,26 @@ pub mod view;
|
|||||||
/// The tessellation compute shader (WGSL).
|
/// The tessellation compute shader (WGSL).
|
||||||
pub const SHADER: &str = include_str!("tess.wgsl");
|
pub const SHADER: &str = include_str!("tess.wgsl");
|
||||||
|
|
||||||
|
/// The device feature for a 32-bit float depth buffer with a stencil, which the viewport draws
|
||||||
|
/// with reversed depth ([`view::reversed_depth`]) where the device has it. A device is opened
|
||||||
|
/// with it whenever its adapter offers it ([`depth_features`]).
|
||||||
|
pub const FLOAT_DEPTH_STENCIL: wgpu::Features = wgpu::Features::DEPTH32FLOAT_STENCIL8;
|
||||||
|
|
||||||
|
/// The depth features to open a device on `adapter` with: [`FLOAT_DEPTH_STENCIL`] if it has it.
|
||||||
|
pub fn depth_features(adapter: &wgpu::Adapter) -> wgpu::Features {
|
||||||
|
adapter.features() & FLOAT_DEPTH_STENCIL
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The depth format to draw with on `device`: 32-bit float (reversed depth) where it can, with a
|
||||||
|
/// stencil when `stencil` (the section cap needs one), else 24 bits with a stencil.
|
||||||
|
pub fn depth_format(device: &wgpu::Device, stencil: bool) -> wgpu::TextureFormat {
|
||||||
|
match (stencil, device.features().contains(FLOAT_DEPTH_STENCIL)) {
|
||||||
|
(false, _) => wgpu::TextureFormat::Depth32Float,
|
||||||
|
(true, true) => wgpu::TextureFormat::Depth32FloatStencil8,
|
||||||
|
(true, false) => wgpu::TextureFormat::Depth24PlusStencil8,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The error a popped error scope caught. Native devices answer at once; a browser's answer
|
/// The error a popped error scope caught. Native devices answer at once; a browser's answer
|
||||||
/// comes later, and is not waited for (no error is assumed).
|
/// comes later, and is not waited for (no error is assumed).
|
||||||
fn popped(f: impl std::future::Future<Output = Option<wgpu::Error>>) -> Option<wgpu::Error> {
|
fn popped(f: impl std::future::Future<Output = Option<wgpu::Error>>) -> Option<wgpu::Error> {
|
||||||
@@ -48,6 +68,7 @@ fn open_headless() -> Result<(wgpu::Adapter, wgpu::Device, wgpu::Queue)> {
|
|||||||
let (adapter, _) = select::pick(policy).ok_or(GpuError::NoAdapter(policy.name()))?;
|
let (adapter, _) = select::pick(policy).ok_or(GpuError::NoAdapter(policy.name()))?;
|
||||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||||
label: Some("solidcraft headless"),
|
label: Some("solidcraft headless"),
|
||||||
|
required_features: depth_features(&adapter),
|
||||||
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
|
|||||||
+154
-18
@@ -19,9 +19,9 @@ pub const TRI_SIZE: usize = 28;
|
|||||||
pub const PBR_SIZE: usize = pbr::VERTEX_SIZE;
|
pub const PBR_SIZE: usize = pbr::VERTEX_SIZE;
|
||||||
/// Line instance: a, b (3 × f32 each), colour (4 × u8), width in pixels (f32).
|
/// Line instance: a, b (3 × f32 each), colour (4 × u8), width in pixels (f32).
|
||||||
pub const LINE_SIZE: usize = 32;
|
pub const LINE_SIZE: usize = 32;
|
||||||
/// The shader's `U`: the view-projection matrix, 9 `vec4`s, the 8 analysis colours and their
|
/// The shader's `U`: the view-projection matrix, 9 `vec4`s, the 8 analysis colours, their
|
||||||
/// flags (`uniform_bytes`).
|
/// flags and the depth direction (`uniform_bytes`).
|
||||||
const UNIFORM_SIZE: u64 = 352;
|
const UNIFORM_SIZE: u64 = 368;
|
||||||
|
|
||||||
/// A body drawn from its display plan, evaluated on the GPU: the plan, and a colour per face
|
/// A body drawn from its display plan, evaluated on the GPU: the plan, and a colour per face
|
||||||
/// (the plan's face numbering; faces past the end take the last colour).
|
/// (the plan's face numbering; faces past the end take the last colour).
|
||||||
@@ -599,8 +599,26 @@ pub struct Renderer {
|
|||||||
ss: Option<Supersampled>,
|
ss: Option<Supersampled>,
|
||||||
/// This frame was drawn supersampled (in `prepare`): `paint` only filters it down.
|
/// This frame was drawn supersampled (in `prepare`): `paint` only filters it down.
|
||||||
ss_ready: bool,
|
ss_ready: bool,
|
||||||
|
/// Reversed depth drawn into the window's pass: egui clears its depth to 1, which is the near
|
||||||
|
/// plane then, so the viewport first sets its rectangle to the far plane (0).
|
||||||
|
depth_reset: Option<wgpu::RenderPipeline>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A triangle over the whole viewport at depth 0 (the far plane of reversed depth), writing no
|
||||||
|
/// colour: [`Renderer::depth_reset`].
|
||||||
|
const DEPTH_RESET_SHADER: &str = r#"
|
||||||
|
@vertex
|
||||||
|
fn vs(@builtin(vertex_index) i: u32) -> @builtin(position) vec4<f32> {
|
||||||
|
let x = f32((i << 1u) & 2u);
|
||||||
|
let y = f32(i & 2u);
|
||||||
|
return vec4<f32>(x * 2.0 - 1.0, 1.0 - y * 2.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
@fragment
|
||||||
|
fn fs() -> @location(0) vec4<f32> {
|
||||||
|
return vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
"#;
|
||||||
|
|
||||||
/// A supersampled frame: the scene drawn at a multiple of the viewport's pixels.
|
/// A supersampled frame: the scene drawn at a multiple of the viewport's pixels.
|
||||||
struct Supersampled {
|
struct Supersampled {
|
||||||
size: [u32; 2],
|
size: [u32; 2],
|
||||||
@@ -735,6 +753,55 @@ pub struct GpuTarget {
|
|||||||
pub tess: bool,
|
pub tess: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl GpuTarget {
|
||||||
|
/// Depth runs from 1 at the near plane to 0 at the far one: see [`reversed_depth`].
|
||||||
|
pub fn reversed_depth(&self) -> bool {
|
||||||
|
reversed_depth(self.depth)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Is depth reversed (1 at the near plane, 0 at the far one) in a buffer of `format`? It is with a
|
||||||
|
/// 32-bit float buffer: a float's steps grow with its value, so with depth falling away from the
|
||||||
|
/// eye they follow the distance, about 2⁻²⁴ of it everywhere, instead of piling up at the near
|
||||||
|
/// plane (#1434: a face 0.3 mm in front of another speckled at 10 m). Other formats keep 0 near.
|
||||||
|
pub fn reversed_depth(format: Option<wgpu::TextureFormat>) -> bool {
|
||||||
|
matches!(format, Some(wgpu::TextureFormat::Depth32Float | wgpu::TextureFormat::Depth32FloatStencil8))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The depth test `compare`, written for depth growing away from the eye, for a buffer whose depth
|
||||||
|
/// may run the other way ([`reversed_depth`]). Every pass drawn with the viewport's depth takes
|
||||||
|
/// its tests from here, so it agrees with the viewport whichever way depth runs.
|
||||||
|
pub fn depth_compare(reversed: bool, compare: wgpu::CompareFunction) -> wgpu::CompareFunction {
|
||||||
|
use wgpu::CompareFunction::{Greater, GreaterEqual, Less, LessEqual};
|
||||||
|
match compare {
|
||||||
|
Less if reversed => Greater,
|
||||||
|
LessEqual if reversed => GreaterEqual,
|
||||||
|
Greater if reversed => Less,
|
||||||
|
GreaterEqual if reversed => LessEqual,
|
||||||
|
c => c,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A depth bias written for depth growing away from the eye (negative: toward it), turned round
|
||||||
|
/// for reversed depth.
|
||||||
|
pub fn depth_bias(reversed: bool, b: wgpu::DepthBiasState) -> wgpu::DepthBiasState {
|
||||||
|
if reversed { wgpu::DepthBiasState { constant: -b.constant, slope_scale: -b.slope_scale, clamp: -b.clamp } } else { b }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The depth of the far plane, which a depth buffer is cleared to.
|
||||||
|
pub fn far_depth(reversed: bool) -> f32 {
|
||||||
|
if reversed { 0.0 } else { 1.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The view-projection the GPU draws with: `vp` (depth 0 near, 1 far, as the CPU uses it), with
|
||||||
|
/// depth turned round (z' = w - z) for reversed depth. Columns, as `Mat4::to_f32` gives them.
|
||||||
|
pub fn gpu_view_proj(vp: [[f32; 4]; 4], reversed: bool) -> [[f32; 4]; 4] {
|
||||||
|
if !reversed {
|
||||||
|
return vp;
|
||||||
|
}
|
||||||
|
vp.map(|c| [c[0], c[1], c[3] - c[2], c[3]])
|
||||||
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
/// The pipelines for target `t` on `device`; `tess` evaluates planned bodies on the GPU.
|
/// The pipelines for target `t` on `device`; `tess` evaluates planned bodies on the GPU.
|
||||||
pub fn new(device: &wgpu::Device, t: GpuTarget, tess: Option<crate::TessGpu>) -> Self {
|
pub fn new(device: &wgpu::Device, t: GpuTarget, tess: Option<crate::TessGpu>) -> Self {
|
||||||
@@ -766,15 +833,20 @@ impl Renderer {
|
|||||||
layout: &bgl,
|
layout: &bgl,
|
||||||
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
|
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
|
||||||
});
|
});
|
||||||
|
let reversed = t.reversed_depth();
|
||||||
let depth = |compare: wgpu::CompareFunction, write: bool| -> Option<wgpu::DepthStencilState> {
|
let depth = |compare: wgpu::CompareFunction, write: bool| -> Option<wgpu::DepthStencilState> {
|
||||||
t.depth.map(|format| wgpu::DepthStencilState {
|
t.depth.map(|format| wgpu::DepthStencilState {
|
||||||
format,
|
format,
|
||||||
depth_write_enabled: Some(write),
|
depth_write_enabled: Some(write),
|
||||||
depth_compare: Some(compare),
|
depth_compare: Some(depth_compare(reversed, compare)),
|
||||||
stencil: Default::default(),
|
stencil: Default::default(),
|
||||||
bias: Default::default(),
|
bias: Default::default(),
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
// Opaque faces pushed back by their own slope (FACE_BIAS); ghosts twice as far.
|
||||||
|
let pushed = |compare: wgpu::CompareFunction, write: bool, bias: wgpu::DepthBiasState| -> Option<wgpu::DepthStencilState> {
|
||||||
|
depth(compare, write).map(|d| wgpu::DepthStencilState { bias: depth_bias(reversed, bias), ..d })
|
||||||
|
};
|
||||||
let pipeline = |label: &str,
|
let pipeline = |label: &str,
|
||||||
vs: &str,
|
vs: &str,
|
||||||
fs: &str,
|
fs: &str,
|
||||||
@@ -926,9 +998,9 @@ impl Renderer {
|
|||||||
let mark = wgpu::DepthStencilState {
|
let mark = wgpu::DepthStencilState {
|
||||||
format,
|
format,
|
||||||
depth_write_enabled: Some(true),
|
depth_write_enabled: Some(true),
|
||||||
depth_compare: Some(wgpu::CompareFunction::Less),
|
depth_compare: Some(depth_compare(reversed, wgpu::CompareFunction::Less)),
|
||||||
stencil: wgpu::StencilState { front: face(Zero), back: face(Replace), read_mask: 0xff, write_mask: 0xff },
|
stencil: wgpu::StencilState { front: face(Zero), back: face(Replace), read_mask: 0xff, write_mask: 0xff },
|
||||||
bias: Default::default(),
|
bias: depth_bias(reversed, FACE_BIAS),
|
||||||
};
|
};
|
||||||
// Then the cap's back faces again, only there, at the section plane's depth.
|
// Then the cap's back faces again, only there, at the section plane's depth.
|
||||||
let only = wgpu::StencilFaceState { compare: wgpu::CompareFunction::Equal, ..face(Keep) };
|
let only = wgpu::StencilFaceState { compare: wgpu::CompareFunction::Equal, ..face(Keep) };
|
||||||
@@ -1002,7 +1074,41 @@ impl Renderer {
|
|||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
cache: None,
|
cache: None,
|
||||||
});
|
});
|
||||||
|
let depth_reset = t.depth.filter(|_| reversed).map(|format| {
|
||||||
|
let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
|
||||||
|
label: Some("sc_depth_reset"),
|
||||||
|
source: wgpu::ShaderSource::Wgsl(DEPTH_RESET_SHADER.into()),
|
||||||
|
});
|
||||||
|
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
|
label: Some("sc_depth_reset"),
|
||||||
|
bind_group_layouts: &[],
|
||||||
|
immediate_size: 0,
|
||||||
|
});
|
||||||
|
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
|
||||||
|
label: Some("sc_depth_reset"),
|
||||||
|
layout: Some(&layout),
|
||||||
|
vertex: wgpu::VertexState { module: &module, entry_point: Some("vs"), buffers: &[], compilation_options: Default::default() },
|
||||||
|
primitive: wgpu::PrimitiveState { topology: wgpu::PrimitiveTopology::TriangleList, ..Default::default() },
|
||||||
|
depth_stencil: Some(wgpu::DepthStencilState {
|
||||||
|
format,
|
||||||
|
depth_write_enabled: Some(true),
|
||||||
|
depth_compare: Some(wgpu::CompareFunction::Always),
|
||||||
|
stencil: Default::default(),
|
||||||
|
bias: Default::default(),
|
||||||
|
}),
|
||||||
|
multisample: wgpu::MultisampleState { count: t.samples.max(1), mask: !0, alpha_to_coverage_enabled: false },
|
||||||
|
fragment: Some(wgpu::FragmentState {
|
||||||
|
module: &module,
|
||||||
|
entry_point: Some("fs"),
|
||||||
|
targets: &[Some(wgpu::ColorTargetState { format: t.format, blend: None, write_mask: wgpu::ColorWrites::empty() })],
|
||||||
|
compilation_options: Default::default(),
|
||||||
|
}),
|
||||||
|
multiview_mask: None,
|
||||||
|
cache: None,
|
||||||
|
})
|
||||||
|
});
|
||||||
Renderer {
|
Renderer {
|
||||||
|
depth_reset,
|
||||||
target: t,
|
target: t,
|
||||||
max_texture: device.limits().max_texture_dimension_2d,
|
max_texture: device.limits().max_texture_dimension_2d,
|
||||||
ss_blit,
|
ss_blit,
|
||||||
@@ -1014,20 +1120,20 @@ impl Renderer {
|
|||||||
sampler,
|
sampler,
|
||||||
textures: Default::default(),
|
textures: Default::default(),
|
||||||
quads: Vec::new(),
|
quads: Vec::new(),
|
||||||
tri: solid("sc_tris", "fs_solid", None, depth(wgpu::CompareFunction::Less, true), false),
|
tri: solid("sc_tris", "fs_solid", None, pushed(wgpu::CompareFunction::Less, true, FACE_BIAS), false),
|
||||||
tri_cut: cut(false),
|
tri_cut: cut(false),
|
||||||
tri_planned: solid("sc_tris_planned", "fs_solid", None, depth(wgpu::CompareFunction::Less, true), true),
|
tri_planned: solid("sc_tris_planned", "fs_solid", None, pushed(wgpu::CompareFunction::Less, true, FACE_BIAS), true),
|
||||||
tri_cut_planned: cut(true),
|
tri_cut_planned: cut(true),
|
||||||
tess,
|
tess,
|
||||||
access_bgl,
|
access_bgl,
|
||||||
access_bind,
|
access_bind,
|
||||||
access_version: 0,
|
access_version: 0,
|
||||||
tri_hl: pipeline("sc_tris_hl", "vs_tri", "fs_tri", tri_layout(), highlight_depth(t.depth), alpha),
|
tri_hl: pipeline("sc_tris_hl", "vs_tri", "fs_tri", tri_layout(), highlight_depth(t.depth), alpha),
|
||||||
pbr: pipeline("sc_pbr", "vs_pbr", "fs_pbr", pbr_layout(), depth(wgpu::CompareFunction::Less, true), None),
|
pbr: pipeline("sc_pbr", "vs_pbr", "fs_pbr", pbr_layout(), pushed(wgpu::CompareFunction::Less, true, FACE_BIAS), None),
|
||||||
trans: pipeline("sc_trans", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
trans: pipeline("sc_trans", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||||
glass_far: glass("sc_glass_far", wgpu::Face::Front),
|
glass_far: glass("sc_glass_far", wgpu::Face::Front),
|
||||||
glass_near: glass("sc_glass_near", wgpu::Face::Back),
|
glass_near: glass("sc_glass_near", wgpu::Face::Back),
|
||||||
ghost: pipeline("sc_ghost", "vs_ghost", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
ghost: pipeline("sc_ghost", "vs_tri", "fs_tri", tri_layout(), pushed(wgpu::CompareFunction::LessEqual, false, GHOST_BIAS), alpha),
|
||||||
xray: pipeline("sc_xray", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
|
xray: pipeline("sc_xray", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Always, false), alpha),
|
||||||
line: pipeline("sc_lines", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
line: pipeline("sc_lines", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||||
line_hidden: pipeline("sc_lines_hidden", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::Greater, false), alpha),
|
line_hidden: pipeline("sc_lines_hidden", "vs_line", "fs_line", line_layout(), depth(wgpu::CompareFunction::Greater, false), alpha),
|
||||||
@@ -1053,18 +1159,40 @@ impl Renderer {
|
|||||||
/// depths differ by rounding and an unbiased equal test lights the face in speckles and wedges.
|
/// depths differ by rounding and an unbiased equal test lights the face in speckles and wedges.
|
||||||
/// The bias is a few depth steps plus the face's own slope: far less than any real gap.
|
/// The bias is a few depth steps plus the face's own slope: far less than any real gap.
|
||||||
fn highlight_depth(format: Option<wgpu::TextureFormat>) -> Option<wgpu::DepthStencilState> {
|
fn highlight_depth(format: Option<wgpu::TextureFormat>) -> Option<wgpu::DepthStencilState> {
|
||||||
|
let reversed = reversed_depth(format);
|
||||||
format.map(|format| wgpu::DepthStencilState {
|
format.map(|format| wgpu::DepthStencilState {
|
||||||
format,
|
format,
|
||||||
depth_write_enabled: Some(false),
|
depth_write_enabled: Some(false),
|
||||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
depth_compare: Some(depth_compare(reversed, wgpu::CompareFunction::LessEqual)),
|
||||||
stencil: Default::default(),
|
stencil: Default::default(),
|
||||||
bias: HIGHLIGHT_BIAS,
|
bias: depth_bias(reversed, HIGHLIGHT_BIAS),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// See [`highlight_depth`]. Negative: toward the eye (depth grows away from it).
|
/// See [`highlight_depth`]. Negative: toward the eye (depth grows away from it).
|
||||||
const HIGHLIGHT_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: -8, slope_scale: -1.0, clamp: 0.0 };
|
const HIGHLIGHT_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: -8, slope_scale: -1.0, clamp: 0.0 };
|
||||||
|
|
||||||
|
/// Opaque faces are drawn pushed back from the eye by a few depth steps and by their own depth
|
||||||
|
/// slope over [`FACE_SLOPE`] pixels, so whatever lies on a face (its edges, an origin plane, a
|
||||||
|
/// sketch, a decal) is drawn over it at any angle, and an edge behind it stays hidden however
|
||||||
|
/// large the scene (#1434, #1739). The CPU renderer has the same rule (`raster.rs`, #983).
|
||||||
|
/// Lines used to be pulled toward the eye by a share of the depth range instead, which on a large
|
||||||
|
/// design reached tens of millimetres. Positive: away from the eye.
|
||||||
|
pub const FACE_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: FACE_STEPS, slope_scale: FACE_SLOPE, clamp: 0.0 };
|
||||||
|
/// The depth steps of [`FACE_BIAS`]: f32 rounding between a face and what lies on it (the GPU
|
||||||
|
/// evaluates planned bodies in f32; edges and overlays come from the CPU).
|
||||||
|
pub const FACE_STEPS: i32 = 8;
|
||||||
|
/// The pixels of [`FACE_BIAS`]: half a 2 px line, so edges (1.2 px) draw whole on a steep face.
|
||||||
|
/// It is also how far behind a face an edge may show, in that face's own depth per pixel: at 10 m
|
||||||
|
/// a pixel is about 9 mm, and a face turned 25° gains 4 mm of depth across it, so a larger value
|
||||||
|
/// let a washer 12 mm behind a plate show through it (#1434). A thicker line (a selected edge,
|
||||||
|
/// 3.5 px) may lose its outer edge on one side where it lies on a steep face. The section cap,
|
||||||
|
/// which writes its own depth, adds the same in the shader.
|
||||||
|
pub const FACE_SLOPE: f32 = 1.0;
|
||||||
|
/// The removed material of a preview, drawn translucent: behind the faces it shares with the new
|
||||||
|
/// shape, so twice [`FACE_BIAS`].
|
||||||
|
const GHOST_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: 2 * FACE_STEPS, slope_scale: 2.0 * FACE_SLOPE, clamp: 0.0 };
|
||||||
|
|
||||||
fn upload(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize) -> Option<Batch> {
|
fn upload(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize) -> Option<Batch> {
|
||||||
upload_with(device, label, bytes, stride, wgpu::BufferUsages::VERTEX)
|
upload_with(device, label, bytes, stride, wgpu::BufferUsages::VERTEX)
|
||||||
}
|
}
|
||||||
@@ -1162,7 +1290,7 @@ impl Renderer {
|
|||||||
if cb.product {
|
if cb.product {
|
||||||
res.product.take(device, cb.key, &cb.slot, None);
|
res.product.take(device, cb.key, &cb.slot, None);
|
||||||
res.product.sort_glass(queue, cb.back);
|
res.product.sort_glass(queue, cb.back);
|
||||||
queue.write_buffer(&res.uniform, 0, &uniform_bytes(cb, cb.size_px[0], cb.size_px[1], res.linear_out));
|
queue.write_buffer(&res.uniform, 0, &uniform_bytes(cb, cb.size_px[0], cb.size_px[1], res.linear_out, res.target.reversed_depth()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.model.take(device, cb.key, &cb.slot, res.tess.as_ref().map(|t| (t, &mut *encoder)));
|
res.model.take(device, cb.key, &cb.slot, res.tess.as_ref().map(|t| (t, &mut *encoder)));
|
||||||
@@ -1170,7 +1298,7 @@ impl Renderer {
|
|||||||
res.highlight.take(device, cb.hl_key, &cb.hl_slot, None);
|
res.highlight.take(device, cb.hl_key, &cb.hl_slot, None);
|
||||||
res.preview.take(device, cb.pv_key, &cb.pv_slot, None);
|
res.preview.take(device, cb.pv_key, &cb.pv_slot, None);
|
||||||
res.model.sort_glass(queue, cb.back);
|
res.model.sort_glass(queue, cb.back);
|
||||||
queue.write_buffer(&res.uniform, 0, &uniform_bytes(cb, cb.size_px[0], cb.size_px[1], res.linear_out));
|
queue.write_buffer(&res.uniform, 0, &uniform_bytes(cb, cb.size_px[0], cb.size_px[1], res.linear_out, res.target.reversed_depth()));
|
||||||
if let Some(a) = &cb.access
|
if let Some(a) = &cb.access
|
||||||
&& a.version != res.access_version
|
&& a.version != res.access_version
|
||||||
{
|
{
|
||||||
@@ -1245,6 +1373,10 @@ impl Renderer {
|
|||||||
pass.draw(0..3, 0..1);
|
pass.draw(0..3, 0..1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if let Some(reset) = &self.depth_reset {
|
||||||
|
pass.set_pipeline(reset);
|
||||||
|
pass.draw(0..3, 0..1);
|
||||||
|
}
|
||||||
self.draw(frame, pass);
|
self.draw(frame, pass);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1367,7 +1499,10 @@ impl Renderer {
|
|||||||
})],
|
})],
|
||||||
depth_stencil_attachment: ss.depth.as_ref().map(|view| wgpu::RenderPassDepthStencilAttachment {
|
depth_stencil_attachment: ss.depth.as_ref().map(|view| wgpu::RenderPassDepthStencilAttachment {
|
||||||
view,
|
view,
|
||||||
depth_ops: Some(wgpu::Operations { load: wgpu::LoadOp::Clear(1.0), store: wgpu::StoreOp::Discard }),
|
depth_ops: Some(wgpu::Operations {
|
||||||
|
load: wgpu::LoadOp::Clear(far_depth(self.target.reversed_depth())),
|
||||||
|
store: wgpu::StoreOp::Discard,
|
||||||
|
}),
|
||||||
stencil_ops: depth_format
|
stencil_ops: depth_format
|
||||||
.filter(|f| f.has_stencil_aspect())
|
.filter(|f| f.has_stencil_aspect())
|
||||||
.map(|_| wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: wgpu::StoreOp::Discard }),
|
.map(|_| wgpu::Operations { load: wgpu::LoadOp::Clear(0), store: wgpu::StoreOp::Discard }),
|
||||||
@@ -1420,10 +1555,10 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Uniform bytes for a frame.
|
/// Uniform bytes for a frame.
|
||||||
pub fn uniform_bytes(cb: &Frame, w: f32, h: f32, linear: bool) -> Vec<u8> {
|
pub fn uniform_bytes(cb: &Frame, w: f32, h: f32, linear: bool, reversed: bool) -> Vec<u8> {
|
||||||
let mut v: Vec<f32> = cb.view_proj.iter().flatten().copied().collect();
|
let mut v: Vec<f32> = gpu_view_proj(cb.view_proj, reversed).iter().flatten().copied().collect();
|
||||||
v.extend([cb.back[0], cb.back[1], cb.back[2], if linear { 1.0 } else { 0.0 }]);
|
v.extend([cb.back[0], cb.back[1], cb.back[2], if linear { 1.0 } else { 0.0 }]);
|
||||||
v.extend([w.max(1.0), h.max(1.0), 2e-4, if cb.outline { 1.0 } else { 0.0 }]);
|
v.extend([w.max(1.0), h.max(1.0), 0.0, if cb.outline { 1.0 } else { 0.0 }]);
|
||||||
v.extend(cb.clip);
|
v.extend(cb.clip);
|
||||||
v.extend(cb.cap);
|
v.extend(cb.cap);
|
||||||
v.extend(cb.analysis);
|
v.extend(cb.analysis);
|
||||||
@@ -1436,6 +1571,7 @@ pub fn uniform_bytes(cb: &Frame, w: f32, h: f32, linear: bool) -> Vec<u8> {
|
|||||||
}
|
}
|
||||||
v.extend(cb.eye);
|
v.extend(cb.eye);
|
||||||
v.extend(cb.analysis_colors);
|
v.extend(cb.analysis_colors);
|
||||||
|
v.extend([if reversed { 1.0 } else { 0.0 }, FACE_STEPS as f32, FACE_SLOPE, 0.0]);
|
||||||
v.iter().flat_map(|x| x.to_le_bytes()).collect()
|
v.iter().flat_map(|x| x.to_le_bytes()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,243 @@
|
|||||||
|
//! Depth in headless pictures, drawn with the viewport's pipelines (#1434, #1739): an edge behind
|
||||||
|
//! a face stays hidden however large the design and however far the camera, and whatever lies on
|
||||||
|
//! a face (its edges, a thin label) draws over it at any angle. Each check runs with the depth the
|
||||||
|
//! device draws with (a 32-bit float buffer, reversed, where it has one) and with the 24-bit
|
||||||
|
//! fallback. Skipped, with the reason printed, without a GPU adapter (`SOLIDCRAFT_REQUIRE_GPU=1`
|
||||||
|
//! makes that a failure).
|
||||||
|
|
||||||
|
use serde_json::json;
|
||||||
|
use solidcraft_engine::Session;
|
||||||
|
use solidcraft_engine::geom::{Mesh, Vec3};
|
||||||
|
use solidcraft_engine::render::{Camera, Rgb, Scene, SceneLine, SceneMesh, StandardView};
|
||||||
|
use solidcraft_engine::view::{self, Guides};
|
||||||
|
use solidcraft_gpu::GpuError;
|
||||||
|
use solidcraft_gpu::capture::{self, Capturer};
|
||||||
|
use std::sync::{Arc, OnceLock};
|
||||||
|
|
||||||
|
fn or_skip(c: Result<&'static Capturer, &'static GpuError>) -> Option<&'static Capturer> {
|
||||||
|
match c {
|
||||||
|
Ok(c) => Some(c),
|
||||||
|
Err(e @ GpuError::NoAdapter(_)) if std::env::var_os("SOLIDCRAFT_REQUIRE_GPU").is_none() => {
|
||||||
|
eprintln!("skipping: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
Err(e) => panic!("{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The device's own depth, then the 24-bit fallback (the same device kind, its own instance).
|
||||||
|
fn capturers() -> Vec<(&'static str, &'static Capturer)> {
|
||||||
|
static FALLBACK: OnceLock<solidcraft_gpu::Result<Capturer>> = OnceLock::new();
|
||||||
|
let Some(own) = or_skip(capture::shared()) else { return Vec::new() };
|
||||||
|
let Some(fallback) = or_skip(FALLBACK.get_or_init(|| Capturer::headless_with_depth(wgpu::TextureFormat::Depth24PlusStencil8)).as_ref()) else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
eprintln!("GPU: {}, depth {:?} and {:?}", own.adapter_name(), own.depth_format(), fallback.depth_format());
|
||||||
|
vec![("own", own), ("24-bit", fallback)]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reversed(c: &Capturer) -> bool {
|
||||||
|
solidcraft_gpu::view::reversed_depth(Some(c.depth_format()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RGBA pixels as `[r, g, b, a]`.
|
||||||
|
fn pixels(img: &[u8]) -> impl Iterator<Item = [u8; 4]> + '_ {
|
||||||
|
img.as_chunks::<4>().0.iter().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn red(p: [u8; 4]) -> bool {
|
||||||
|
p[0] > 150 && p[1] < 90 && p[2] < 90
|
||||||
|
}
|
||||||
|
|
||||||
|
fn session(script: &[(&str, serde_json::Value)]) -> Session {
|
||||||
|
let mut s = Session::default();
|
||||||
|
for (c, p) in script {
|
||||||
|
s.execute(c, p).unwrap();
|
||||||
|
}
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn picture(c: &Capturer, s: &Session, cam: &Camera, w: u32, h: u32) -> Vec<u8> {
|
||||||
|
c.rgba(&view::scene_with_guides(s, cam, None, Guides { grid: false, axes: false }), cam, w, h).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn differing(a: &[u8], b: &[u8]) -> usize {
|
||||||
|
pixels(a).zip(pixels(b)).filter(|(p, q)| p.iter().zip(q).any(|(x, y)| x.abs_diff(*y) > 24)).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The reproduction of #1434: a 20 mm plate with a washer 22–25 mm behind its front face, and a
|
||||||
|
/// 30 m slab far away that makes the design 34 m across. The washer never shows: the picture is
|
||||||
|
/// the same without it. (Lines were pulled toward the eye by 2e-4 of the depth range, about
|
||||||
|
/// 27 mm here in orthographic.)
|
||||||
|
#[test]
|
||||||
|
fn a_washer_behind_a_plate_stays_hidden_on_a_large_design() {
|
||||||
|
let plate = [
|
||||||
|
("solid.box", json!({"length": 30000, "width": 8000, "height": 100, "corner": [-15000, 3000, -3000], "body_name": "slab"})),
|
||||||
|
("solid.box", json!({"length": 200, "width": 20, "height": 200, "corner": [0, 0, 0], "body_name": "plate"})),
|
||||||
|
("solid.cylinder", json!({"radius": 8, "height": 60, "base": [100, -20, 100], "axis": "Y", "body_name": "bolt"})),
|
||||||
|
];
|
||||||
|
let washer = ("solid.cylinder", json!({"radius": 15, "height": 3, "base": [100, 22, 100], "axis": "Y", "body_name": "washer"}));
|
||||||
|
let without = session(&plate);
|
||||||
|
let with = session(&[plate[0].clone(), plate[1].clone(), plate[2].clone(), washer]);
|
||||||
|
for (mode, c) in capturers() {
|
||||||
|
for fov in [0.0, 0.6] {
|
||||||
|
let cam = Camera { target: Vec3::new(100.0, 10.0, 100.0), yaw: 0.35, pitch: 0.25, roll: 0.0, distance: 450.0, fov, ..Default::default() };
|
||||||
|
let n = differing(&picture(c, &with, &cam, 400, 300), &picture(c, &without, &cam, 400, 300));
|
||||||
|
assert_eq!(n, 0, "{mode} depth, fov {fov}: the washer shows through the plate in {n} pixels");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// #1434's second reproduction, in perspective at 10 m: a 2 m plate, a 600 mm washer 12–15 mm
|
||||||
|
/// behind its front face, and a red label 0.3 mm thick on it. The washer stays hidden in both
|
||||||
|
/// depths; with reversed float depth the label is red all over (in 24 bits one depth step there
|
||||||
|
/// is about 0.6 mm, so the label speckled).
|
||||||
|
#[test]
|
||||||
|
fn at_ten_metres_a_thin_label_is_whole_and_a_washer_behind_stays_hidden() {
|
||||||
|
let base = [
|
||||||
|
("solid.box", json!({"length": 2000, "width": 10, "height": 2000, "corner": [0, 0, 0], "body_name": "plate"})),
|
||||||
|
("solid.box", json!({"length": 1000, "width": 0.3, "height": 400, "corner": [500, -0.3, 1400], "body_name": "label"})),
|
||||||
|
("appearance.assign", json!({"bodies": ["label"], "color": [204, 34, 34]})),
|
||||||
|
];
|
||||||
|
let washer = ("solid.cylinder", json!({"radius": 300, "height": 3, "base": [1000, 12, 1000], "axis": "Y", "body_name": "washer"}));
|
||||||
|
let without = session(&base);
|
||||||
|
let with = session(&[base[0].clone(), base[1].clone(), base[2].clone(), washer]);
|
||||||
|
let cam =
|
||||||
|
Camera { target: Vec3::new(1000.0, 5.0, 1000.0), yaw: 0.35, pitch: 0.25, roll: 0.0, distance: 10_000.0, fov: 0.6, ..Default::default() };
|
||||||
|
let (w, h) = (800, 600);
|
||||||
|
for (mode, c) in capturers() {
|
||||||
|
let img = picture(c, &with, &cam, w, h);
|
||||||
|
let n = differing(&img, &picture(c, &without, &cam, w, h));
|
||||||
|
assert_eq!(n, 0, "{mode} depth: the washer shows through the plate in {n} pixels");
|
||||||
|
if !reversed(c) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// The label's front face (y = -0.3), sampled inside its outline.
|
||||||
|
let radius = view::scene(&with, &cam).radius;
|
||||||
|
let mut grey = Vec::new();
|
||||||
|
for i in 1..20 {
|
||||||
|
for j in 1..20 {
|
||||||
|
let p = Vec3::new(500.0 + 1000.0 * f64::from(i) / 20.0, -0.3, 1400.0 + 400.0 * f64::from(j) / 20.0);
|
||||||
|
let Some((x, y, _)) = cam.to_screen(p, f64::from(w), f64::from(h), radius) else { continue };
|
||||||
|
let at = (y as usize * w as usize + x as usize) * 4;
|
||||||
|
if let Some(px) = img.get(at..at + 4).and_then(|p| <[u8; 4]>::try_from(p).ok())
|
||||||
|
&& !red(px)
|
||||||
|
{
|
||||||
|
grey.push((x as u32, y as u32, px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(grey.is_empty(), "{mode} depth: the label speckles at {} of 361 points: {:?}", grey.len(), &grey[..grey.len().min(5)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A 30 × 40 mm sheet at y = z·tan(`tilt`) (facing the front camera when `tilt` is 0).
|
||||||
|
fn sheet(tilt: f64) -> Mesh {
|
||||||
|
let t = tilt.tan();
|
||||||
|
let c = [(-15.0, -20.0), (15.0, -20.0), (15.0, 20.0), (-15.0, 20.0)].map(|(x, z)| Vec3::new(x, z * t, z));
|
||||||
|
Mesh {
|
||||||
|
positions: c.to_vec(),
|
||||||
|
normals: vec![Vec3::new(0.0, -tilt.cos(), tilt.sin()); 4],
|
||||||
|
triangles: vec![[0, 1, 2], [0, 2, 3]],
|
||||||
|
tri_face: vec![0, 0],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A front view of `mesh` with `lines`, as part of a design of `radius`, 40 mm high on 200 px.
|
||||||
|
fn front(mesh: Mesh, lines: Vec<SceneLine>, radius: f64, fov: f64) -> (Scene, Camera) {
|
||||||
|
let scene = Scene {
|
||||||
|
meshes: vec![SceneMesh { mesh: Arc::new(mesh), color: Rgb(160, 165, 175), face_colors: Vec::new() }],
|
||||||
|
lines,
|
||||||
|
radius,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
let mut cam = Camera { distance: if fov > 0.0 { 25.0 / (fov * 0.5).tan() } else { 62.5 }, fov, ..Default::default() };
|
||||||
|
cam.set_view(StandardView::Front);
|
||||||
|
(scene, cam)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn red_line(a: Vec3, b: Vec3, on_top: bool) -> SceneLine {
|
||||||
|
SceneLine { points: vec![a, b], color: Rgb(255, 0, 0), width: 1.2, on_top }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn red_pixels(c: &Capturer, scene: &Scene, cam: &Camera) -> usize {
|
||||||
|
pixels(&c.rgba(scene, cam, 200, 200).unwrap()).filter(|p| red(*p)).count()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An edge lying on a face draws its whole width over it, on a face seen square and on one turned
|
||||||
|
/// to 80°, in orthographic and perspective, in a small design and a large one.
|
||||||
|
#[test]
|
||||||
|
fn an_edge_on_a_face_draws_whole_at_any_angle() {
|
||||||
|
for (mode, c) in capturers() {
|
||||||
|
for tilt in [0.0_f64, 60.0, 80.0] {
|
||||||
|
let t = tilt.to_radians().tan();
|
||||||
|
// Across the sheet, at z = 5: y = 5·tan(tilt).
|
||||||
|
let (a, b) = (Vec3::new(-12.0, 5.0 * t, 5.0), Vec3::new(12.0, 5.0 * t, 5.0));
|
||||||
|
for radius in [30.0, 17_000.0] {
|
||||||
|
for fov in [0.0, 0.6] {
|
||||||
|
let (on, cam) = front(sheet(tilt.to_radians()), vec![red_line(a, b, false)], radius, fov);
|
||||||
|
let (top, _) = front(sheet(tilt.to_radians()), vec![red_line(a, b, true)], radius, fov);
|
||||||
|
let (shown, all) = (red_pixels(c, &on, &cam), red_pixels(c, &top, &cam));
|
||||||
|
assert!(all > 50, "{mode}: the line itself ({all} px)");
|
||||||
|
assert!(shown * 10 >= all * 9, "{mode} depth, tilt {tilt}°, radius {radius}, fov {fov}: {shown} of {all} px drawn");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An edge 0.5 mm behind a face seen square is hidden, however large the design: lines were pulled
|
||||||
|
/// toward the eye by a share of the depth range, about 27 mm in a design 34 m across.
|
||||||
|
#[test]
|
||||||
|
fn an_edge_just_behind_a_face_stays_hidden_on_a_large_design() {
|
||||||
|
for (mode, c) in capturers() {
|
||||||
|
for radius in [30.0, 17_000.0] {
|
||||||
|
for fov in [0.0, 0.6] {
|
||||||
|
// The sheet is at y = 0; the camera looks along +y, so behind is +y.
|
||||||
|
let (a, b) = (Vec3::new(-12.0, 0.5, 5.0), Vec3::new(12.0, 0.5, 5.0));
|
||||||
|
let (scene, cam) = front(sheet(0.0), vec![red_line(a, b, false)], radius, fov);
|
||||||
|
let n = red_pixels(c, &scene, &cam);
|
||||||
|
assert_eq!(n, 0, "{mode} depth, radius {radius}, fov {fov}: the edge behind shows in {n} px");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The application's device draws with reversed float depth where it can: every desktop backend
|
||||||
|
/// this runs on (D3D12, Vulkan, Metal) has `Depth32FloatStencil8`.
|
||||||
|
#[test]
|
||||||
|
fn the_device_draws_with_float_depth_where_it_has_it() {
|
||||||
|
let Some(c) = or_skip(capture::shared()) else { return };
|
||||||
|
eprintln!("GPU: {}, depth {:?}", c.adapter_name(), c.depth_format());
|
||||||
|
let float = c.depth_format() == wgpu::TextureFormat::Depth32FloatStencil8;
|
||||||
|
assert_eq!(float, reversed(c));
|
||||||
|
if std::env::var_os("SOLIDCRAFT_REQUIRE_GPU").is_some() {
|
||||||
|
assert!(float, "a desktop GPU without Depth32FloatStencil8: {:?}", c.depth_format());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What lies on a section's cut face draws over the cap, which writes its own depth (so the GPU's
|
||||||
|
/// push does not apply to it; the shader pushes it the same way).
|
||||||
|
#[test]
|
||||||
|
fn an_edge_on_the_cut_face_draws_over_the_cap() {
|
||||||
|
let mut s = session(&[("solid.box", json!({"length": 40, "width": 30, "height": 20}))]);
|
||||||
|
// Cut away the front (y < 15), so the iso camera looks at the cut face at y = 15.
|
||||||
|
s.execute("inspect.section", &json!({"plane": {"origin": [0, 15, 0], "normal": [0, -1, 0]}})).unwrap();
|
||||||
|
for (mode, c) in capturers() {
|
||||||
|
for fov in [0.0, 0.6] {
|
||||||
|
let mut cam = view::home_camera(&s);
|
||||||
|
cam.fov = fov;
|
||||||
|
let count = |on_top: bool| {
|
||||||
|
// The session's section (`scene_with_guides` takes its own). The line is 2 µm on
|
||||||
|
// the kept side: exactly on the plane, rounding cuts parts of it away with the rest.
|
||||||
|
let mut sc = view::scene(&s, &cam);
|
||||||
|
sc.lines.push(red_line(Vec3::new(5.0, 15.002, 10.0), Vec3::new(35.0, 15.002, 10.0), on_top));
|
||||||
|
pixels(&c.rgba(&sc, &cam, 300, 220).unwrap()).filter(|p| red(*p)).count()
|
||||||
|
};
|
||||||
|
let (shown, all) = (count(false), count(true));
|
||||||
|
assert!(all > 50, "{mode}: the line itself ({all} px)");
|
||||||
|
assert!(shown * 10 >= all * 9, "{mode} depth, fov {fov}: {shown} of {all} px drawn over the cap");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -142,6 +142,7 @@ pub(crate) fn offscreen_device() -> Option<(wgpu::Adapter, wgpu::Device, wgpu::Q
|
|||||||
let instance = wgpu::Instance::new(desc);
|
let instance = wgpu::Instance::new(desc);
|
||||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||||
|
required_features: solidcraft_gpu::depth_features(&adapter),
|
||||||
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
required_limits: wgpu::Limits::downlevel_defaults().using_resolution(adapter.limits()),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
}))
|
||||||
@@ -253,7 +254,7 @@ fn offscreen(
|
|||||||
let (adapter, device, queue) = offscreen_device()?;
|
let (adapter, device, queue) = offscreen_device()?;
|
||||||
eprintln!("offscreen adapter: {:?}", adapter.get_info().backend);
|
eprintln!("offscreen adapter: {:?}", adapter.get_info().backend);
|
||||||
let format = if srgb { wgpu::TextureFormat::Rgba8UnormSrgb } else { wgpu::TextureFormat::Rgba8Unorm };
|
let format = if srgb { wgpu::TextureFormat::Rgba8UnormSrgb } else { wgpu::TextureFormat::Rgba8Unorm };
|
||||||
let depth = wgpu::TextureFormat::Depth24PlusStencil8;
|
let depth = solidcraft_gpu::depth_format(&device, true);
|
||||||
let compute = tess.and_then(|shader| solidcraft_gpu::view::tessellation(&adapter, &device, &queue, shader));
|
let compute = tess.and_then(|shader| solidcraft_gpu::view::tessellation(&adapter, &device, &queue, shader));
|
||||||
let target = GpuTarget { format, depth: Some(depth), samples: 1, tess: compute.is_some() };
|
let target = GpuTarget { format, depth: Some(depth), samples: 1, tess: compute.is_some() };
|
||||||
let mut resources = egui_wgpu::CallbackResources::default();
|
let mut resources = egui_wgpu::CallbackResources::default();
|
||||||
|
|||||||
@@ -3726,6 +3726,81 @@ mod tests {
|
|||||||
assert!(checked >= 3, "{checked} faces seen lit");
|
assert!(checked >= 3, "{checked} faces seen lit");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// An origin plane lying on a body's face is tinted evenly over it (#1739: a box built at the
|
||||||
|
/// origin, seen from below in orthographic; the XY plane's square z-fought with the bottom
|
||||||
|
/// face in stripes). Faces are pushed back by their slope (`FACE_BIAS`), the planes are not.
|
||||||
|
/// Skipped without a GPU adapter.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
#[test]
|
||||||
|
fn an_origin_plane_on_a_face_is_tinted_evenly() {
|
||||||
|
let mut s = solidcraft_engine::Session::default();
|
||||||
|
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 10, "corner": [0, 0, 0]})).unwrap();
|
||||||
|
let mut app = SolveApp::new(s, crate::Services::default());
|
||||||
|
app.cam = Camera {
|
||||||
|
target: Vec3::new(16.904, 10.024, 3.661),
|
||||||
|
yaw: -0.32,
|
||||||
|
pitch: -0.9652,
|
||||||
|
roll: 0.0,
|
||||||
|
distance: 98.557,
|
||||||
|
fov: 0.0,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
app.ui.perspective = false;
|
||||||
|
const SIZE: u32 = 256;
|
||||||
|
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||||
|
let mut render = |origin: bool| {
|
||||||
|
app.ui.show_origin = origin;
|
||||||
|
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
|
||||||
|
let (scene, highlight) = (build_scene(&app), build_highlight(&app).merged());
|
||||||
|
let proj = projection(&app, rect);
|
||||||
|
let e = proj.cam.eye().to_f32();
|
||||||
|
let img = crate::gpu::render_offscreen(
|
||||||
|
scene,
|
||||||
|
Some(highlight),
|
||||||
|
proj.vp.to_f32(),
|
||||||
|
proj.cam.back().to_f32(),
|
||||||
|
[e[0], e[1], e[2], 0.0],
|
||||||
|
SIZE,
|
||||||
|
None,
|
||||||
|
[0.0; 8],
|
||||||
|
crate::gpu::analysis_colors(&Tokens::get().analysis),
|
||||||
|
);
|
||||||
|
(img, proj)
|
||||||
|
};
|
||||||
|
let (Some(with), proj) = render(true) else {
|
||||||
|
eprintln!("skipping: no GPU adapter");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (Some(without), _) = render(false) else { return };
|
||||||
|
app.ui.show_origin = true;
|
||||||
|
let xy = origin_planes(&app).into_iter().find(|(n, _, _)| *n == "XY").map(|(_, _, q)| q).unwrap();
|
||||||
|
let (w, h, r) = (f64::from(SIZE), f64::from(SIZE), scene_radius(&app));
|
||||||
|
let (mut samples, mut plain) = (0, Vec::new());
|
||||||
|
// Points inside the square (away from its outline), where it lies on the bottom face.
|
||||||
|
for i in 1..12 {
|
||||||
|
for j in 1..12 {
|
||||||
|
let (u, v) = (f64::from(i) / 12.0, f64::from(j) / 12.0);
|
||||||
|
let p = xy[0] + (xy[1] - xy[0]) * u + (xy[3] - xy[0]) * v;
|
||||||
|
if !(p.x > 0.5 && p.x < 39.5 && p.y > 0.5 && p.y < 29.5) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some((x, y, _)) = proj.cam.to_screen(p, w, h, r) else { continue };
|
||||||
|
let at = (y as usize * SIZE as usize + x as usize) * 4;
|
||||||
|
samples += 1;
|
||||||
|
if with.get(at..at + 4) == without.get(at..at + 4) {
|
||||||
|
plain.push((x as u32, y as u32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(samples > 40, "{samples} points of the square on the face");
|
||||||
|
assert!(
|
||||||
|
plain.is_empty(),
|
||||||
|
"{} of {samples} points of the XY plane's square do not show over the face: {:?}",
|
||||||
|
plain.len(),
|
||||||
|
&plain[..plain.len().min(6)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn construction_drawing_and_hover_overlays_preserve_visible_gaps() {
|
fn construction_drawing_and_hover_overlays_preserve_visible_gaps() {
|
||||||
fn length(shape: &Shape) -> f32 {
|
fn length(shape: &Shape) -> f32 {
|
||||||
|
|||||||
Reference in new issue
Block a user