Viewport picking: a BVH per body mesh and a GPU pick buffer for visible edges (#1776)
Thanks @oursland.
This commit is contained in:
1 parent
a9413fe385
commit
8907da5e86
14 files changed
+1707
-50
No files matched your search
@@ -0,0 +1,141 @@
|
||||
use egui::{Rect, pos2};
|
||||
use serde_json::json;
|
||||
use solidcraft_engine::geom::Vec3;
|
||||
use solidcraft_gpu::pick::{PickTile, TILE, tile_around};
|
||||
|
||||
use super::*;
|
||||
use crate::viewport::{Hit, pick, projection};
|
||||
|
||||
fn app() -> SolveApp {
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
s.execute("solid.fillet", &json!({"edges": [[40, 15, 20]], "radius": 4})).unwrap();
|
||||
s.execute("solid.cylinder", &json!({"radius": 8, "height": 35, "base": [10, 10, 0], "operation": "new_body"})).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.ui.show_origin = false;
|
||||
app.cam.target = Vec3::new(20.0, 15.0, 12.0);
|
||||
app.cam.distance = 110.0;
|
||||
// A GPU viewport (no device is needed to hand the picker a tile).
|
||||
app.viewport.gpu = Some(crate::gpu::GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess: false });
|
||||
app
|
||||
}
|
||||
|
||||
const RECT: Rect = Rect { min: pos2(0.0, 0.0), max: pos2(640.0, 480.0) };
|
||||
|
||||
/// Installs the pass's scene, as `paint` makes it, and returns the view's key.
|
||||
fn install_scene(app: &SolveApp, proj: &Proj) -> u64 {
|
||||
let st = app.session.world_state();
|
||||
let model = model_key(app, &st);
|
||||
let s = scene(app, &st, model).unwrap();
|
||||
*app.viewport.pick_buffer.scene.lock().unwrap() = Some(s);
|
||||
view_key(app, proj, model)
|
||||
}
|
||||
|
||||
/// A tile only narrows the edges: the edge it names near the cursor is picked as the CPU picks
|
||||
/// it, an edge it does not name (hidden there) is not, and without a tile the CPU indexes answer
|
||||
/// and a tile is asked for.
|
||||
#[test]
|
||||
fn a_tile_names_the_edges_the_picker_tests() {
|
||||
let app = app();
|
||||
let proj = projection(&app, RECT);
|
||||
// A point on the box's top front edge (y = 0, z = 20).
|
||||
let at = proj.to_screen(Vec3::new(20.0, 0.0, 20.0)).unwrap();
|
||||
let cpu = pick(&app, &proj, at);
|
||||
let Some(Hit::Edge { index, body, .. }) = cpu.iter().find(|h| matches!(h, Hit::Edge { .. })).cloned() else { panic!("{cpu:?}") };
|
||||
assert!(app.viewport.pick_buffer.want.lock().unwrap().is_some(), "no tile yet: one is asked for");
|
||||
let key = install_scene(&app, &proj);
|
||||
let st = app.session.world_state();
|
||||
let bi = st.bodies.iter().position(|b| b.name == body).unwrap();
|
||||
let s = app.viewport.pick_buffer.scene.lock().unwrap().clone().unwrap();
|
||||
let id = s.edges.iter().position(|&(b, e)| b as usize == bi && e as usize == index).unwrap() as u32 + 1;
|
||||
let size = view_size(&app, &proj);
|
||||
let (origin, tsize) = tile_around([at.x as u32, at.y as u32], size);
|
||||
let tile = |ids: Vec<u32>| PickTile { key, origin, size: tsize, ids };
|
||||
let n = (tsize[0] * tsize[1]) as usize;
|
||||
// The edge's id at the cursor's pixel.
|
||||
let mut ids = vec![0; n];
|
||||
ids[((at.y as u32 - origin[1]) * tsize[0] + (at.x as u32 - origin[0])) as usize] = id;
|
||||
app.viewport.pick_buffer.out.lock().unwrap().tiles = vec![tile(ids)];
|
||||
assert_eq!(pick(&app, &proj, at), cpu, "the named edge is picked as the CPU picks it");
|
||||
// A tile naming no edge there: no edge or vertex is picked.
|
||||
app.viewport.pick_buffer.out.lock().unwrap().tiles = vec![tile(vec![0; n])];
|
||||
let none = pick(&app, &proj, at);
|
||||
assert!(!none.iter().any(|h| matches!(h, Hit::Edge { .. } | Hit::Vertex { .. })), "{none:?}");
|
||||
assert!(none.iter().any(|h| matches!(h, Hit::Face { .. })), "faces are still picked: {none:?}");
|
||||
// A tile of another view does not answer.
|
||||
app.viewport.pick_buffer.out.lock().unwrap().tiles = vec![PickTile { key: key ^ 1, ..tile(vec![0; n]) }];
|
||||
assert_eq!(pick(&app, &proj, at), cpu);
|
||||
// Wireframe: nothing hides an edge, and the buffer is not used.
|
||||
let mut wire = app;
|
||||
wire.ui.visual_style = 2;
|
||||
wire.viewport.pick_buffer.out.lock().unwrap().tiles = vec![tile(vec![0; n])];
|
||||
assert!(edges_near(&wire, &proj, at, &wire.session.world_state()).is_none());
|
||||
}
|
||||
|
||||
/// On a real device: the pass drawn for the view, and picks over a grid of the view helped by its
|
||||
/// tiles answer as the CPU picks (an edge's pixels and the CPU's visibility test can disagree by
|
||||
/// a pixel at a silhouette, so a few may differ). Skipped without a GPU adapter.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn picks_through_the_pass_match_the_cpu() {
|
||||
let Some((_, device, queue)) = crate::gpu::offscreen_device() else {
|
||||
assert!(std::env::var_os("SOLIDCRAFT_REQUIRE_GPU").is_none(), "a GPU is required here");
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
let mut app = app();
|
||||
for (perspective, yaw, pitch) in [(false, 0.6, 0.5), (true, 2.4, 0.3)] {
|
||||
app.ui.perspective = perspective;
|
||||
app.cam.yaw = yaw;
|
||||
app.cam.pitch = pitch;
|
||||
let proj = projection(&app, RECT);
|
||||
let key = install_scene(&app, &proj);
|
||||
let s = app.viewport.pick_buffer.scene.lock().unwrap().clone().unwrap();
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let view = PickView {
|
||||
key,
|
||||
view_proj: proj.vp.to_f32(),
|
||||
eye: [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }],
|
||||
back: proj.cam.back().to_f32(),
|
||||
lift: lift(&app) as f32,
|
||||
size: view_size(&app, &proj),
|
||||
};
|
||||
let mut pb = PickBuffer::new(&device);
|
||||
let out = app.viewport.pick_buffer.out.clone();
|
||||
// Tiles over the whole view.
|
||||
let mut wanted = Vec::new();
|
||||
for ty in (0..view.size[1]).step_by(TILE as usize - 32) {
|
||||
for tx in (0..view.size[0]).step_by(TILE as usize - 32) {
|
||||
wanted.push([tx + TILE / 2, ty + TILE / 2]);
|
||||
}
|
||||
}
|
||||
let mut tiles = Vec::new();
|
||||
for c in wanted {
|
||||
for _ in 0..6 {
|
||||
let mut encoder = device.create_command_encoder(&Default::default());
|
||||
pb.prepare(&device, &queue, &mut encoder, s.key, &s.scene, &view, Some(c), &out);
|
||||
queue.submit([encoder.finish()]);
|
||||
let _ = device.poll(egui_wgpu::wgpu::PollType::wait_indefinitely());
|
||||
}
|
||||
tiles.append(&mut out.lock().unwrap().tiles);
|
||||
}
|
||||
let (mut same, mut differ, mut edges) = (0, 0, 0);
|
||||
for j in 0..46 {
|
||||
for i in 0..62 {
|
||||
let p = pos2(10.0 + i as f32 * 10.0 + (j % 3) as f32, 10.0 + j as f32 * 10.0);
|
||||
let r = 8i64;
|
||||
let w = [p.x as i64 - r, p.y as i64 - r, p.x as i64 + r, p.y as i64 + r];
|
||||
let Some(t) = tiles.iter().find(|t| t.covers(key, view.size, w)).cloned() else { continue };
|
||||
out.lock().unwrap().tiles = vec![t];
|
||||
let through = pick(&app, &proj, p);
|
||||
out.lock().unwrap().tiles.clear();
|
||||
let cpu = pick(&app, &proj, p);
|
||||
edges += usize::from(cpu.iter().any(|h| matches!(h, Hit::Edge { .. })));
|
||||
if through == cpu { same += 1 } else { differ += 1 }
|
||||
}
|
||||
}
|
||||
eprintln!("perspective {perspective}: {same} same, {differ} differ, {edges} with an edge");
|
||||
assert!(edges > 100, "the grid reaches edges ({edges})");
|
||||
assert!(differ * 100 <= same + differ, "{differ} of {} picks differ", same + differ);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user