diff --git a/ui/src/gridview/render/shaders/octree.frag b/ui/src/gridview/render/shaders/octree.frag index 8713d492..5c014dee 100644 --- a/ui/src/gridview/render/shaders/octree.frag +++ b/ui/src/gridview/render/shaders/octree.frag @@ -169,8 +169,7 @@ void main() { t1 = mix(tm_stack[layer], t1_stack[layer], next_child); tm = (t0 + t1) / 2.0; - // Compute the next sibling of `next_child` to visit after this - // one. + // Compute the sibling of `next_child` to visit after this one. uint exit_axis = exitAxis(t1); // `== true` is required to work around a bug in AMD drivers. // Yes this is cursed. diff --git a/ui/src/gridview/view3d.rs b/ui/src/gridview/view3d.rs index bbcff447..ce39d2ef 100644 --- a/ui/src/gridview/view3d.rs +++ b/ui/src/gridview/view3d.rs @@ -4,9 +4,10 @@ use ndcell_core::prelude::*; use super::generic::{GenericGridView, GridViewDimension}; use super::render::{CellDrawParams, GridViewRender3D, RenderParams, RenderResult}; -use super::viewpoint::{Viewpoint, Viewpoint3D}; +use super::viewpoint::{CellTransform3D, Viewpoint, Viewpoint3D}; use super::{DragHandler, DragType}; use crate::commands::*; +use crate::math::raycast; pub type GridView3D = GenericGridView; @@ -71,15 +72,80 @@ impl GridViewDimension for GridViewDim3D { } } impl GridView3D { - /// Returns the cell position underneath the cursor. Floor this to get an - /// integer cell position. - pub fn hovered_cell_pos(&self, mouse_pos: Option) -> Option { - // TODO: Raycast or something - let z = Viewpoint3D::DISTANCE_TO_PIVOT; - mouse_pos.and_then(|pos| { - self.viewpoint() - .cell_transform() - .pixel_to_global_pos(pos, z) + pub fn screen_pos(&self, pixel: FVec2D) -> ScreenPos3D { + let vp = self.viewpoint(); + let xform = vp.cell_transform(); + + let mut raycast_hit; + { + // Get the `NdTreeSlice` containing all of the visible cells. + let global_visible_rect = vp.global_visible_rect(); + let visible_octree = self.automaton.ndtree.slice_containing(&global_visible_rect); + let local_octree_base_pos = + xform.global_to_local_int(&visible_octree.base_pos).unwrap(); + + // Compute the ray, relative to the root node of the octree slice. + let (mut start, delta) = xform.pixel_to_local_ray(pixel); + printlnd!("octree off {}", local_octree_base_pos); + start -= local_octree_base_pos.to_fvec(); + + let layer = xform.render_cell_layer; + let node = visible_octree.root.as_ref(); + raycast_hit = crate::math::raycast::octree_raycast(start, delta, layer, node); + + // Convert coordinates back from octree node space into local space. + if let Some(hit) = &mut raycast_hit { + hit.start += local_octree_base_pos.to_fvec(); + hit.pos_int += local_octree_base_pos; + hit.pos_float += local_octree_base_pos.to_fvec(); + } + }; + + ScreenPos3D { + pixel, + xform, + raycast_hit, + } + } +} + +#[derive(Debug, Clone)] +pub struct ScreenPos3D { + pixel: FVec2D, + xform: CellTransform3D, + raycast_hit: Option, +} +impl ScreenPos3D { + /// Returns the position of the mouse in pixel space. + pub fn pixel(&self) -> FVec2D { + self.pixel + } + /// Returns the global cell position at the mouse cursor in the plane parallel + /// to the screen at the distance of the viewpoint pivot. + pub fn global_pos_at_pivot_depth(&self) -> FixedVec3D { + self.xform + .pixel_to_global_pos(self.pixel, Viewpoint3D::DISTANCE_TO_PIVOT) + } + + /// Returns the global position of the cell visible at the mouse cursor. + pub fn raycast(&self) -> Option { + self.raycast_hit.map(|hit| RaycastHit { + pos: self.xform.local_to_global_float(hit.pos_float), + cell: self.xform.local_to_global_int(hit.pos_int), + face: (hit.face_axis, hit.face_sign), }) } + /// Returns the global position of the cell visible at the mouse cursor. + pub fn raycast_face(&self) -> Option<(Axis, Sign)> { + self.raycast_hit.map(|hit| (hit.face_axis, hit.face_sign)) + } +} + +pub struct RaycastHit { + /// Point of intersection between ray and cell. + pub pos: FixedVec3D, + /// Position of intersected cell. + pub cell: BigVec3D, + /// Face of cell intersected. + pub face: (Axis, Sign), } diff --git a/ui/src/math/mod.rs b/ui/src/math/mod.rs index a2338860..6beb26d1 100644 --- a/ui/src/math/mod.rs +++ b/ui/src/math/mod.rs @@ -1 +1,2 @@ pub mod bresenham; +pub mod raycast; diff --git a/ui/src/math/raycast.rs b/ui/src/math/raycast.rs new file mode 100644 index 00000000..9153bc44 --- /dev/null +++ b/ui/src/math/raycast.rs @@ -0,0 +1,182 @@ +//! Octree traversal algorithm based on An Efficient Parametric Algorithm for +//! Octree Traversal by J. Revelles, C. Ureña, M. Lastra. +//! +//! This is also implemented in GLSL in the octree fragment shader. + +use ndcell_core::prelude::*; + +#[derive(Debug, Copy, Clone)] +pub struct Hit { + pub start: FVec3D, + pub delta: FVec3D, + + pub t0: R64, + pub t1: R64, + + pub pos_int: IVec3D, + pub pos_float: FVec3D, + + pub face_axis: Axis, + pub face_sign: Sign, +} + +/// Computes a 3D octree raycast. `start` and `delta` are both in units of +/// `min_layer` nodes, and `start` is relative to the lower corner of `node`. +pub fn octree_raycast( + mut start: FVec3D, + mut delta: FVec3D, + min_layer: Layer, + node: NodeRef<'_, Dim3D>, +) -> Option { + let node_len = r64((node.layer() - min_layer).big_len().to_f64().unwrap()); + + // Check each component of the delta vector to see if it's negative. If it + // is, then mirror the ray along that axis so that the delta vector is + // positive and also mirror the quadtree along that axis using + // `invert_mask`, which will flip bits of child indices. + let mut invert_mask = 0; + for &ax in Dim3D::axes() { + if delta[ax].is_negative() { + invert_mask |= ax.bit(); + start[ax] = -start[ax] + node_len; + delta[ax] = -delta[ax]; + } + } + + // At what `t` does the ray enter the root node (considering only one axis + // at a time)? + let t0: FVec3D = -start / delta; + // At what `t` does the ray exit the root node (considering only one axis at + // a time)? + let t1: FVec3D = (-start + node_len) / delta; + + // At what `t` has the ray entered the root node along all axes? + let max_t0: R64 = *t0.max_component(); + // At what `t` has the ray entered the root node along all axes? + let min_t1: R64 = *t1.min_component(); + // If we enter AFTER we exit, or exit at a negative `t` ... + if max_t0 >= min_t1 || r64(0.0) > min_t1 { + // ... then the ray does not intersect with the root node. + return None; + } else { + // Otherwise, the ray does intersect with the root node. + return raycast_node_child(start, delta, t0, t1, min_layer, node, invert_mask); + } +} + +fn raycast_node_child( + start: FVec3D, + delta: FVec3D, + t0: FVec3D, + t1: FVec3D, + min_layer: Layer, + node: NodeRef<'_, Dim3D>, + invert_mask: usize, +) -> Option { + if *t1.min_component() < r64(0.0) { + // This node is completely behind the ray; skip it. + return None; + } else if *t0.max_component() < r64(0.0) && node.layer() <= min_layer { + // This is a leaf node and the ray starts inside it; skip it. + return None; + } + + if node.is_empty() { + // This is an empty node; skip it. + return None; + } + + // At what `t` does the ray cross the middle of the current node + // (considering only one axis at a time)? + let tm: FVec3D = (t0 + t1) / 2.0; + + if node.layer() <= min_layer { + // This is a nonzero leaf node, so return a hit. + let pos_int = (start + delta * tm).floor().to_ivec(); + let pos_float = start + delta * t0; + let face_axis = entry_axis(t0); + let face_sign = if invert_mask & face_axis.bit() == 0 { + Sign::Minus // ray is positive; hit negative face of cell + } else { + Sign::Plus // ray is negative; hit positive face of cell + }; + + return Some(Hit { + start, + delta, + + t0: *t0.max_component(), + t1: *t1.min_component(), + + pos_int, + pos_float, + + face_axis, + face_sign, + }); + } + + let children = node.subdivide().unwrap(); + let mut child_index = entry_child(t0, tm); + loop { + // Compute the parameter `t` values for th `child_index`. + let mut child_t0 = t0; + let mut child_t1 = tm; + for &ax in Dim3D::axes() { + if child_index & ax.bit() != 0 { + child_t0[ax] = tm[ax]; + child_t1[ax] = t1[ax]; + } + } + + // Recurse! + match raycast_node_child( + start, + delta, + child_t0, + child_t1, + min_layer, + children[child_index ^ invert_mask], + invert_mask, + ) { + Some(hit) => return Some(hit), + None => (), + } + + // Compute the sibling of `child_index` to visit after this one. + let exit_axis = exit_axis(child_t1); + if child_index & exit_axis.bit() != 0 { + // The ray has exited the current node. + return None; + } else { + // Advance along `exit_axis` to get the next child to visit. + child_index |= exit_axis.bit(); + } + } +} + +// Given the parameters `t0` at which the ray enters a node along each axis and +// `tm` at which the ray crosses the middle of a node along each axis, returns +// the child index of the first child of that node intersected by the ray. +fn entry_child(t0: FVec3D, tm: FVec3D) -> usize { + let max_t0 = t0.max_component(); // when the ray actually enters the node + let mut child_index = 0; + for &ax in Dim3D::axes() { + if *max_t0 > tm[ax] { + child_index |= ax.bit(); + } + } + return child_index; +} + +// Given the parameters `t0` at which the ray enters a node along each axis, +// returns the axis along which the ray enters the node. +fn entry_axis(t0: FVec3D) -> Axis { + t0.max_axis() +} + +// Given the parameters `t1` at which the ray exits a node along each axis, +// returns the axis along which the ray exits the node. +fn exit_axis(t1: FVec3D) -> Axis { + t1.min_axis() +} diff --git a/ui/src/windows/mod.rs b/ui/src/windows/mod.rs index 44f2d4ba..b8c2e02f 100644 --- a/ui/src/windows/mod.rs +++ b/ui/src/windows/mod.rs @@ -2,7 +2,6 @@ use imgui::*; use std::time::Duration; use ndcell_core::prelude::*; -use Axis::{X, Y, Z}; #[cfg(debug_assertions)] mod debug; @@ -105,7 +104,7 @@ impl MainWindow { } if let Some(pixel) = mouse.pos { let cell = view2d.screen_pos(pixel).cell(); - ui.text(format!("Cursor: X = {}, Y = {}", cell[X], cell[Y])); + ui.text(format!("Cursor: {}", cell)); } else { ui.text(""); } @@ -124,13 +123,17 @@ impl MainWindow { } ui.text(format!("Pitch = {:.2?}°", vp.pitch().0)); ui.text(format!("Yaw = {:.2?}°", vp.yaw().0)); - if let Some(hover_pos) = view3d.hovered_cell_pos(mouse.pos) { - ui.text(format!( - "Cursor: X = {}, Y = {}, Z = {}", - hover_pos[X].floor(), - hover_pos[Y].floor(), - hover_pos[Z].floor(), - )); + if let Some(hit) = mouse + .pos + .and_then(|pixel| view3d.screen_pos(pixel).raycast()) + { + let (axis, sign) = hit.face; + let sign = match sign { + Sign::Minus => "-", + Sign::NoSign => "", + Sign::Plus => "+", + }; + ui.text(format!("Cursor: {} {}{:?}", hit.cell, sign, axis)); } else { ui.text(""); }