diff --git a/examples/boxes.py b/examples/boxes.py new file mode 100644 index 0000000..4166c86 --- /dev/null +++ b/examples/boxes.py @@ -0,0 +1,78 @@ +"""Demo scene using boxes — an exploded Rubik's cube.""" + +import time + +import slamd +import numpy as np +from scipy.spatial.transform import Rotation + + +def make_transform(pos, rot=None): + T = np.eye(4, dtype=np.float32) + T[:3, 3] = pos + if rot is not None: + T[:3, :3] = rot + return T + + +vis = slamd.Visualizer("Boxes") +scene = vis.scene("scene") + +# Rubik's cube face colors (classic scheme) +face_colors = { + (0, 0, 1): [1.0, 0.0, 0.0], # front = red + (0, 0, -1): [1.0, 0.5, 0.0], # back = orange + (0, 1, 0): [1.0, 1.0, 1.0], # top = white + (0, -1, 0): [1.0, 1.0, 0.0], # bottom = yellow + (1, 0, 0): [0.0, 0.0, 0.8], # right = blue + (-1, 0, 0): [0.0, 0.6, 0.0], # left = green +} + +cube_size = 0.45 +gap = 0.55 +explode = 0.3 # how far apart the layers spread + +rng = np.random.default_rng(42) + +for x in range(-1, 2): + for y in range(-1, 2): + for z in range(-1, 2): + pos = np.array([x, y, z], dtype=np.float32) * (gap + explode) + + # Pick color: outermost face determines it + color = np.array([0.12, 0.12, 0.12], dtype=np.float32) # interior = dark + for axis, face_col in face_colors.items(): + ax = np.array(axis) + coord = np.array([x, y, z]) + if np.dot(coord, ax) == 1: + color = np.array(face_col, dtype=np.float32) + break + + # Slight random tumble for the exploded feel + tumble = ( + Rotation.from_rotvec(rng.normal(0, 0.15, size=3)) + .as_matrix() + .astype(np.float32) + ) + + # Stretch cubes along their explosion direction + direction = np.array([x, y, z], dtype=np.float32) + dist = np.linalg.norm(direction) + if dist > 0: + stretch = 1.0 + 0.4 * dist + direction /= dist + dims = np.full(3, cube_size, dtype=np.float32) + dims += np.abs(direction) * cube_size * (stretch - 1.0) + else: + dims = np.full(3, cube_size, dtype=np.float32) + + scene.set_object( + f"/rubik/{x}_{y}_{z}", + slamd.geom.Box(dims, color), + ) + scene.set_transform( + f"/rubik/{x}_{y}_{z}", + make_transform(pos, tumble), + ) + +time.sleep(10) diff --git a/examples/bunch_of_triads.py b/examples/bunch_of_triads.py index d7acbf9..0aec289 100644 --- a/examples/bunch_of_triads.py +++ b/examples/bunch_of_triads.py @@ -1,3 +1,5 @@ +import time + import slamd import numpy as np @@ -32,7 +34,7 @@ def main(): scene.set_object("/triad_x", slamd.geom.Triad(last_pose_mat, 10)) - vis.hang_forever() + time.sleep(10) if __name__ == "__main__": diff --git a/examples/galaxy.py b/examples/galaxy.py index f34a583..f6a3a13 100644 --- a/examples/galaxy.py +++ b/examples/galaxy.py @@ -4,6 +4,7 @@ import slamd import numpy as np +import time def make_galaxy(n: int, rng: np.random.Generator) -> tuple[np.ndarray, np.ndarray]: @@ -74,6 +75,7 @@ def main(): cloud = slamd.geom.PointCloud(positions, colors, 0.08, 0.7) scene.set_object("/galaxy", cloud) + time.sleep(10) if __name__ == "__main__": diff --git a/examples/hello_world.py b/examples/hello_world.py index b16f2b0..8ac7b7d 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -1,4 +1,5 @@ import slamd +import time if __name__ == "__main__": vis = slamd.Visualizer("Hello world") @@ -6,3 +7,6 @@ scene = vis.scene("scene") scene.set_object("/origin", slamd.geom.Triad()) + + # let the visualizer connect and sync state + time.sleep(0.1) diff --git a/examples/large_scale_stress_test.py b/examples/large_scale_stress_test.py index 894d23b..fc69f8d 100644 --- a/examples/large_scale_stress_test.py +++ b/examples/large_scale_stress_test.py @@ -1,5 +1,6 @@ import slamd import numpy as np +import time def main(): @@ -31,6 +32,7 @@ def main(): "/point_cloud", slamd.geom.PointCloud(points, colors, triad_size * 0.2), ) + time.sleep(10) if __name__ == "__main__": diff --git a/examples/lots_of_paths.py b/examples/lots_of_paths.py index 8480706..8eb0dd9 100644 --- a/examples/lots_of_paths.py +++ b/examples/lots_of_paths.py @@ -1,5 +1,6 @@ import slamd import numpy as np +import time def main(): @@ -17,7 +18,8 @@ def main(): pth = f"/box_bruh_stuff_long_ass_path_what_is_this_{i}" scene.set_transform(pth, transform) - scene.set_object(pth, slamd.geom.Box()) + scene.set_object(pth, slamd.geom.Box(np.array([1.0, 1.0, 1.0], dtype=np.float32), np.array([0.8, 0.2, 0.0], dtype=np.float32))) + time.sleep(10) if __name__ == "__main__": diff --git a/examples/planes.py b/examples/planes.py index fc5796c..841339b 100644 --- a/examples/planes.py +++ b/examples/planes.py @@ -1,5 +1,6 @@ import slamd import numpy as np +import time def main(): @@ -24,6 +25,7 @@ def main(): scene.set_object( f"/plane_{i}", slamd.geom.Plane(normal, pos, color, radius, alpha) ) + time.sleep(10) if __name__ == "__main__": diff --git a/examples/poly_lines.py b/examples/poly_lines.py new file mode 100644 index 0000000..eaf9958 --- /dev/null +++ b/examples/poly_lines.py @@ -0,0 +1,186 @@ +"""Stress test for polyline rendering at sharp angles.""" + +import time + +import slamd +import numpy as np + +vis = slamd.Visualizer("Polyline stress test") +scene = vis.scene("scene") + +x_offset = 0.0 + +# 1. Zigzag with increasing sharpness +points = [] +for i in range(20): + x = i * 0.5 + y = (1.0 if i % 2 == 0 else -1.0) * min(i * 0.3, 3.0) + points.append([x + x_offset, y, 0.0]) + +scene.set_object( + "/zigzag", + slamd.geom.PolyLine( + np.array(points, dtype=np.float32), + 0.15, + np.array([1.0, 0.4, 0.1], dtype=np.float32), + 0.4, + ), +) + +x_offset += 12.0 + +# 2. 90-degree corners (staircase) +staircase = [] +for i in range(8): + staircase.append([x_offset + i, 0.0, float(i)]) + staircase.append([x_offset + i, 0.0, float(i + 1)]) + +scene.set_object( + "/staircase", + slamd.geom.PolyLine( + np.array(staircase, dtype=np.float32), + 0.12, + np.array([0.4, 1.0, 0.5], dtype=np.float32), + 0.4, + ), +) + +x_offset += 10.0 + +# 3. Hairpin / tight U-bend +hairpin = np.array( + [ + [0, 0, 0], + [0, 2, 0], + [0.2, 2.3, 0], + [0.4, 2.3, 0], + [0.6, 2, 0], + [0.6, 0, 0], + ], + dtype=np.float32, +) +hairpin[:, 0] += x_offset +scene.set_object( + "/hairpin", + slamd.geom.PolyLine( + hairpin, + 0.15, + np.array([1.0, 0.85, 0.1], dtype=np.float32), + 0.4, + ), +) + +x_offset += 3.0 + +# 4. Spiral tightening (decreasing radius) +t = np.linspace(0, 6 * np.pi, 200, dtype=np.float32) +r = 3.0 * np.exp(-0.1 * t) +spiral = np.column_stack([r * np.cos(t) + x_offset + 3, r * np.sin(t), np.zeros_like(t)]) +scene.set_object( + "/spiral", + slamd.geom.PolyLine( + spiral.astype(np.float32), + 0.08, + np.array([0.6, 0.4, 1.0], dtype=np.float32), + 0.4, + ), +) + +x_offset += 10.0 + +# 5. Very short segments with sharp angles (worst case for overlap) +short_zigzag = [] +for i in range(15): + x = i * 0.2 + y = (0.3 if i % 2 == 0 else -0.3) + short_zigzag.append([x + x_offset, y, 0.0]) + +scene.set_object( + "/short_zigzag", + slamd.geom.PolyLine( + np.array(short_zigzag, dtype=np.float32), + 0.1, + np.array([1.0, 0.3, 0.7], dtype=np.float32), + 0.4, + ), +) + +x_offset += 5.0 + +# 6. Near-180 degree reversal +reversal = np.array( + [ + [0, 0, 0], + [2, 0, 0], + [2.05, 0.1, 0], + [0, 0.2, 0], + ], + dtype=np.float32, +) +reversal[:, 0] += x_offset +scene.set_object( + "/reversal", + slamd.geom.PolyLine( + reversal, + 0.1, + np.array([0.2, 0.8, 0.8], dtype=np.float32), + 0.4, + ), +) + +x_offset += 5.0 + +# 7. Square (four 90-degree corners in sequence) +square = np.array( + [ + [0, 0, 0], + [2, 0, 0], + [2, 2, 0], + [0, 2, 0], + [0, 0, 0], + ], + dtype=np.float32, +) +square[:, 0] += x_offset +scene.set_object( + "/square", + slamd.geom.PolyLine( + square, + 0.12, + np.array([1.0, 1.0, 1.0], dtype=np.float32), + 0.4, + ), +) + +x_offset += 5.0 + +# 8. Helix (smooth reference — should look perfect) +t = np.linspace(0, 8 * np.pi, 300, dtype=np.float32) +helix = np.column_stack([np.cos(t) + x_offset, np.sin(t), t * 0.1]) +scene.set_object( + "/helix", + slamd.geom.PolyLine( + helix.astype(np.float32), + 0.08, + np.array([0.2, 0.7, 1.0], dtype=np.float32), + 0.4, + ), +) + +x_offset += 5.0 + +# 9. Random walk through uniformly sampled points in a box +rng = np.random.default_rng(42) +random_points = rng.uniform(-2.0, 2.0, size=(30, 3)).astype(np.float32) +random_points[:, 0] += x_offset + 2.0 +scene.set_object( + "/random_box", + slamd.geom.PolyLine( + random_points, + 0.08, + np.array([1.0, 0.6, 0.2], dtype=np.float32), + 0.4, + ), +) + +time.sleep(10) diff --git a/examples/spheres.py b/examples/spheres.py index 6ed9e75..8516309 100644 --- a/examples/spheres.py +++ b/examples/spheres.py @@ -1,5 +1,6 @@ import slamd import numpy as np +import time def main(): @@ -39,6 +40,7 @@ def main(): spheres = slamd.geom.Spheres(positions, colors, radii, 0.3) scene.set_object("/spheres", spheres) + time.sleep(10) if __name__ == "__main__": diff --git a/examples/torus_planar_graphs.py b/examples/torus_planar_graphs.py index 29b6fdd..d02c6a3 100644 --- a/examples/torus_planar_graphs.py +++ b/examples/torus_planar_graphs.py @@ -1,6 +1,7 @@ import slamd import numpy as np from itertools import combinations +import time TAU = 2 * np.pi @@ -139,6 +140,7 @@ def main(): add_kn_on_torus(scene, 5, R, r, offset=(-spacing, 0, 0), path_prefix="/k5") add_kn_on_torus(scene, 6, R, r, offset=(0, 0, 0), path_prefix="/k6") add_kn_on_torus(scene, 7, R, r, offset=(spacing, 0, 0), path_prefix="/k7") + time.sleep(10) if __name__ == "__main__": diff --git a/examples/transparency.py b/examples/transparency.py new file mode 100644 index 0000000..1362ce0 --- /dev/null +++ b/examples/transparency.py @@ -0,0 +1,186 @@ +"""Depth peeling demo — transparent planes and meshes mixed with opaque objects.""" + +import time + +import slamd +import numpy as np +from scipy.spatial.transform import Rotation + + +def make_transform(pos, rot=None): + T = np.eye(4, dtype=np.float32) + T[:3, 3] = pos + if rot is not None: + T[:3, :3] = rot + return T + + +def uv_sphere(radius, u_res=32, v_res=16): + """Generate a UV sphere mesh. Returns (verts, indices, normals).""" + u = np.linspace(0, 2 * np.pi, u_res, endpoint=False) + v = np.linspace(0, np.pi, v_res + 1) + verts, normals = [], [] + for vi in range(len(v)): + for ui in range(len(u)): + nx = np.sin(v[vi]) * np.cos(u[ui]) + ny = np.sin(v[vi]) * np.sin(u[ui]) + nz = np.cos(v[vi]) + normals.append([nx, ny, nz]) + verts.append([radius * nx, radius * ny, radius * nz]) + + indices = [] + for vi in range(len(v) - 1): + for ui in range(len(u)): + i0 = vi * len(u) + ui + i1 = vi * len(u) + (ui + 1) % len(u) + i2 = i0 + len(u) + i3 = i1 + len(u) + indices.extend([i0, i2, i1, i1, i2, i3]) + + return ( + np.array(verts, dtype=np.float32), + np.array(indices, dtype=np.uint32), + np.array(normals, dtype=np.float32), + ) + + +def torus(major_r, minor_r, u_res=48, v_res=24): + """Generate a torus mesh. Returns (verts, indices, normals).""" + u = np.linspace(0, 2 * np.pi, u_res, endpoint=False) + v = np.linspace(0, 2 * np.pi, v_res, endpoint=False) + verts, normals = [], [] + for ui in range(len(u)): + cu, su = np.cos(u[ui]), np.sin(u[ui]) + for vi in range(len(v)): + cv, sv = np.cos(v[vi]), np.sin(v[vi]) + x = (major_r + minor_r * cv) * cu + y = (major_r + minor_r * cv) * su + z = minor_r * sv + verts.append([x, y, z]) + normals.append([cv * cu, cv * su, sv]) + + indices = [] + for ui in range(u_res): + for vi in range(v_res): + i0 = ui * v_res + vi + i1 = ui * v_res + (vi + 1) % v_res + i2 = ((ui + 1) % u_res) * v_res + vi + i3 = ((ui + 1) % u_res) * v_res + (vi + 1) % v_res + indices.extend([i0, i2, i1, i1, i2, i3]) + + return ( + np.array(verts, dtype=np.float32), + np.array(indices, dtype=np.uint32), + np.array(normals, dtype=np.float32), + ) + + +vis = slamd.Visualizer("Transparency") +scene = vis.scene("scene") +rng = np.random.default_rng(7) + +# --- Opaque objects --- + +# Scattered spheres +n_spheres = 40 +positions = rng.uniform(-4, 4, size=(n_spheres, 3)).astype(np.float32) +sphere_colors = rng.uniform(0.3, 1.0, size=(n_spheres, 3)).astype(np.float32) +radii = rng.uniform(0.1, 0.35, size=n_spheres).astype(np.float32) +scene.set_object("/opaque/spheres", slamd.geom.Spheres(positions, sphere_colors, radii)) + +# Boxes at various rotations +boxes = [ + ([2.5, 0, 0], [0.9, 0.3, 0.1], [0.8, 1.2, 0.6], [15, 0, 10]), + ([-2.5, 1, -1], [0.1, 0.4, 0.9], [1.0, 0.5, 0.8], [0, 25, -10]), + ([0, -2.5, 2], [0.2, 0.8, 0.3], [0.6, 0.6, 1.0], [-20, 10, 30]), + ([1, 2, -2], [0.9, 0.7, 0.1], [0.5, 0.5, 0.5], [45, 15, 0]), +] +for i, (pos, color, dims, euler_deg) in enumerate(boxes): + rot = Rotation.from_euler("xyz", euler_deg, degrees=True).as_matrix() + scene.set_object( + f"/opaque/box_{i}", + slamd.geom.Box( + np.array(dims, dtype=np.float32), + np.array(color, dtype=np.float32), + ), + ) + scene.set_transform(f"/opaque/box_{i}", make_transform(pos, rot.astype(np.float32))) + +# Polyline threading through the scene +t = np.linspace(0, 4 * np.pi, 100, dtype=np.float32) +path = np.column_stack([2 * np.cos(t), 2 * np.sin(t), np.linspace(-3, 3, len(t))]) +scene.set_object( + "/opaque/helix", + slamd.geom.PolyLine(path.astype(np.float32), 0.06, np.array([1.0, 1.0, 1.0], dtype=np.float32)), +) + +# Origin triad +scene.set_object("/opaque/triad", slamd.geom.Triad()) + +# --- Transparent planes (intersecting at origin) --- + +planes = [ + ("yz", [1, 0, 0], [0, 0, 0], [0.9, 0.2, 0.1], 5.0, 0.3), + ("xz", [0, 1, 0], [0, 0, 0], [0.1, 0.8, 0.2], 5.0, 0.3), + ("xy", [0, 0, 1], [0, 0, 0], [0.1, 0.3, 0.9], 5.0, 0.3), + ("diag", [1, 1, 1], [1, 1, 1], [1.0, 0.85, 0.1], 3.0, 0.2), +] +for name, normal, pos, color, radius, alpha in planes: + scene.set_object( + f"/transparent/plane_{name}", + slamd.geom.Plane( + np.array(normal, dtype=np.float32), + np.array(pos, dtype=np.float32), + np.array(color, dtype=np.float32), + radius, + alpha, + ), + ) + +# --- Transparent meshes --- + +# Glass sphere at origin +sv, si, sn = uv_sphere(2.0) +scene.set_object( + "/transparent/glass_sphere", + slamd.geom.Mesh( + sv, + np.full_like(sv, [0.6, 0.8, 1.0]), + si, + sn, + alpha=0.2, + ), +) + +# Magenta torus around the y axis +tv, ti, tn = torus(3.0, 0.6) +# Rotate so it sits around the y axis +rot_torus = Rotation.from_euler("x", 90, degrees=True).as_matrix().astype(np.float32) +tv_rotated = (rot_torus @ tv.T).T +tn_rotated = (rot_torus @ tn.T).T +scene.set_object( + "/transparent/torus", + slamd.geom.Mesh( + tv_rotated, + np.full_like(tv, [0.8, 0.2, 0.7]), + ti, + tn_rotated, + alpha=0.35, + ), +) + +# Small offset green sphere +sv2, si2, sn2 = uv_sphere(1.2) +sv2 += np.array([2.0, 1.5, 0.0], dtype=np.float32) +scene.set_object( + "/transparent/green_sphere", + slamd.geom.Mesh( + sv2, + np.full_like(sv2, [0.2, 0.9, 0.3]), + si2, + sn2, + alpha=0.25, + ), +) + +time.sleep(10) diff --git a/examples/tree_stress_test.py b/examples/tree_stress_test.py index dd2746b..4d5b6f5 100644 --- a/examples/tree_stress_test.py +++ b/examples/tree_stress_test.py @@ -2,6 +2,7 @@ import slamd import numpy as np +import time def make_transform(x: float, y: float, z: float) -> np.ndarray: @@ -45,7 +46,7 @@ def populate_scene_deep(scene: slamd.Scene): for cat in categories: for j in range(5): path = f"/{cat}/unit_{j}/geometry" - scene.set_object(path, slamd.geom.Box()) + scene.set_object(path, slamd.geom.Box(np.array([1.5, 1.5, 1.5], dtype=np.float32), np.array([0.3, 0.6, 0.9], dtype=np.float32))) scene.set_transform( path, make_transform(j * 2.0, -5.0 - categories.index(cat) * 3.0, 0), @@ -83,6 +84,7 @@ def main(): populate_scene_deep(scene1) populate_scene_wide(scene2) + time.sleep(10) if __name__ == "__main__": diff --git a/examples/two_windows.py b/examples/two_windows.py index 7129eef..cd453eb 100644 --- a/examples/two_windows.py +++ b/examples/two_windows.py @@ -2,6 +2,7 @@ import slamd import numpy as np +import time vis = slamd.Visualizer("Two Windows") @@ -42,3 +43,4 @@ scene2.set_object("/cloud", slamd.geom.PointCloud(positions, colors, 0.08, 0.6)) scene2.set_object("/origin", slamd.geom.Triad()) +time.sleep(10) diff --git a/flatbuffers/geometry.fbs b/flatbuffers/geometry.fbs index fb863b4..cfbe0fd 100644 --- a/flatbuffers/geometry.fbs +++ b/flatbuffers/geometry.fbs @@ -28,7 +28,10 @@ table Image { normalized: bool; } -table Box {} +table Box { + dims: Vec3; + color: Vec3; +} table Sphere { radius: float; diff --git a/pyproject.toml b/pyproject.toml index 33a150c..790c25b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "scikit_build_core.build" [project] name = "slamd" -version = "3.0.1" +version = "3.1.0" description = "A 3D visualiztion library" authors = [{ name = "Robert Leo", email = "robert.leo.jonsson@gmail.com" }] readme = "README.md" diff --git a/slamd/CMakeLists.txt b/slamd/CMakeLists.txt index 5a89d7d..a84590b 100644 --- a/slamd/CMakeLists.txt +++ b/slamd/CMakeLists.txt @@ -107,6 +107,7 @@ target_sources( src/window/frame_timer.cpp src/window/run_window.cpp src/window/tree_overlay.cpp + src/window/controls_hint.cpp src/window/state_manager.cpp src/window/connection.cpp src/window/message.cpp diff --git a/slamd/flatb/flatb/geometry_generated.h b/slamd/flatb/flatb/geometry_generated.h index c27f34b..33d4534 100644 --- a/slamd/flatb/flatb/geometry_generated.h +++ b/slamd/flatb/flatb/geometry_generated.h @@ -453,8 +453,20 @@ inline ::flatbuffers::Offset CreateImage( struct Box FLATBUFFERS_FINAL_CLASS : private ::flatbuffers::Table { typedef BoxBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_DIMS = 4, + VT_COLOR = 6 + }; + const slamd::flatb::Vec3 *dims() const { + return GetStruct(VT_DIMS); + } + const slamd::flatb::Vec3 *color() const { + return GetStruct(VT_COLOR); + } bool Verify(::flatbuffers::Verifier &verifier) const { return VerifyTableStart(verifier) && + VerifyField(verifier, VT_DIMS, 4) && + VerifyField(verifier, VT_COLOR, 4) && verifier.EndTable(); } }; @@ -463,6 +475,12 @@ struct BoxBuilder { typedef Box Table; ::flatbuffers::FlatBufferBuilder &fbb_; ::flatbuffers::uoffset_t start_; + void add_dims(const slamd::flatb::Vec3 *dims) { + fbb_.AddStruct(Box::VT_DIMS, dims); + } + void add_color(const slamd::flatb::Vec3 *color) { + fbb_.AddStruct(Box::VT_COLOR, color); + } explicit BoxBuilder(::flatbuffers::FlatBufferBuilder &_fbb) : fbb_(_fbb) { start_ = fbb_.StartTable(); @@ -475,8 +493,12 @@ struct BoxBuilder { }; inline ::flatbuffers::Offset CreateBox( - ::flatbuffers::FlatBufferBuilder &_fbb) { + ::flatbuffers::FlatBufferBuilder &_fbb, + const slamd::flatb::Vec3 *dims = nullptr, + const slamd::flatb::Vec3 *color = nullptr) { BoxBuilder builder_(_fbb); + builder_.add_color(color); + builder_.add_dims(dims); return builder_.Finish(); } diff --git a/slamd/include/slamd/geom/box.hpp b/slamd/include/slamd/geom/box.hpp index 3847cfa..d49eee2 100644 --- a/slamd/include/slamd/geom/box.hpp +++ b/slamd/include/slamd/geom/box.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -7,14 +8,19 @@ namespace geom { class Box : public Geometry { public: - Box(); + Box(glm::vec3 dims, glm::vec3 color); flatbuffers::Offset serialize( flatbuffers::FlatBufferBuilder& builder ) override; + + private: + glm::vec3 dims; + glm::vec3 color; }; -std::shared_ptr box(); +std::shared_ptr +box(glm::vec3 dims = glm::vec3(1.0f), glm::vec3 color = glm::vec3(0.8f, 0.2f, 0.0f)); } // namespace geom } // namespace slamd diff --git a/slamd/include/slamd/net/client_set.hpp b/slamd/include/slamd/net/client_set.hpp index abaff43..2950a31 100644 --- a/slamd/include/slamd/net/client_set.hpp +++ b/slamd/include/slamd/net/client_set.hpp @@ -12,6 +12,7 @@ class ClientSet { void add(std::shared_ptr conn); void broadcast(std::vector&& msg); void broadcast(std::shared_ptr> msg); + void clear(); private: void clean(); diff --git a/slamd/include/slamd/visualizer.hpp b/slamd/include/slamd/visualizer.hpp index 2588e6a..a88e1d9 100644 --- a/slamd/include/slamd/visualizer.hpp +++ b/slamd/include/slamd/visualizer.hpp @@ -35,6 +35,8 @@ class Visualizer : public std::enable_shared_from_this { void hang_forever(); + void stop(); + void broadcast(std::shared_ptr> message_buffer); private: @@ -61,7 +63,6 @@ class Visualizer : public std::enable_shared_from_this { const uint16_t port; const std::string name; std::thread server_thread; - std::atomic stop_requested = false; std::mutex view_map_mutex; diff --git a/slamd/include/slamd_window/controls_hint.hpp b/slamd/include/slamd_window/controls_hint.hpp new file mode 100644 index 0000000..9da9f16 --- /dev/null +++ b/slamd/include/slamd_window/controls_hint.hpp @@ -0,0 +1,13 @@ +#pragma once +#include + +namespace slamd { + +class ControlsHint { + public: + /// Renders a small controls hint in the bottom-right of the current ImGui + /// window. + void render(); +}; + +} // namespace slamd diff --git a/slamd/include/slamd_window/frame_buffer.hpp b/slamd/include/slamd_window/frame_buffer.hpp index 31ec42a..a208293 100644 --- a/slamd/include/slamd_window/frame_buffer.hpp +++ b/slamd/include/slamd_window/frame_buffer.hpp @@ -5,16 +5,35 @@ namespace slamd { class FrameBuffer { private: + static const int MAX_PEEL_LAYERS = 4; + // Resolve target (for ImGui) uint32_t frame_buffer_object_id = 0; uint32_t texture_id = 0; uint32_t render_buffer_object_id = 0; - // MSAA framebuffer + // MSAA framebuffer (opaque pass) uint32_t msaa_framebuffer_id = 0; uint32_t msaa_color_buffer_id = 0; uint32_t msaa_depth_buffer_id = 0; - int samples = 4; // Multisample level (can tweak later) + int samples = 4; + + // Depth peeling resources + uint32_t peel_msaa_fbo_id = 0; + uint32_t peel_msaa_color_id = 0; // MSAA renderbuffer + uint32_t peel_msaa_depth_id = 0; // MSAA renderbuffer + + uint32_t peel_depth_fbo_id[2] = {}; // non-MSAA, for depth resolve + uint32_t peel_depth_tex_id[2] = {}; // non-MSAA depth textures (ping-pong) + + uint32_t peel_resolve_fbo_id = 0; // non-MSAA, for color resolve + uint32_t peel_layer_tex_id[MAX_PEEL_LAYERS] = {}; // resolved RGBA per layer + + // Fullscreen quad for compositing + uint32_t quad_vao_id = 0; + uint32_t quad_vbo_id = 0; + + int current_peel_read = 0; size_t current_height; size_t current_width; @@ -23,11 +42,17 @@ class FrameBuffer { FrameBuffer(size_t width, size_t height); ~FrameBuffer(); - uint32_t frame_texture(); // Resolved texture ID + uint32_t frame_texture(); bool rescale(size_t width, size_t height); - void bind(); // Bind MSAA FBO for rendering - void unbind(); // Unbind - void resolve(); // Blit MSAA → resolved texture + void bind(); + void unbind(); + void resolve(); + + // Depth peeling interface + void bind_peel_pass(); + uint32_t prev_peel_depth_texture() const; + void end_peel_pass(int layer); + void composite_peel(int num_layers); double aspect() const; size_t width() const; diff --git a/slamd/include/slamd_window/gen/shader_sources.hpp b/slamd/include/slamd_window/gen/shader_sources.hpp index c7e9ae0..26027a1 100644 --- a/slamd/include/slamd_window/gen/shader_sources.hpp +++ b/slamd/include/slamd_window/gen/shader_sources.hpp @@ -95,8 +95,15 @@ out vec4 FragColor; uniform mat4 view; uniform float min_brightness; uniform float alpha; +uniform bool peel_enabled; +uniform sampler2D peel_depth_tex; void main() { + if (peel_enabled) { + float prev_depth = texelFetch(peel_depth_tex, ivec2(gl_FragCoord.xy), 0).r; + if (gl_FragCoord.z <= prev_depth) discard; + } + vec3 norm = normalize(Normal); // Camera-relative lighting: extract camera forward and right from view matrix @@ -188,6 +195,32 @@ void main() { } )"; } // namespace mono_instanced +namespace peel_composite { +inline const std::string vert = R"( #version 330 core + +layout(location = 0) in vec2 a_pos; +layout(location = 1) in vec2 a_uv; + +out vec2 uv; + +void main() { + uv = a_uv; + gl_Position = vec4(a_pos, 0.0, 1.0); +} + )"; +inline const std::string frag = R"( #version 330 core + +in vec2 uv; +out vec4 FragColor; + +uniform sampler2D layer_texture; + +void main() { + FragColor = texture(layer_texture, uv); +} + )"; +} // namespace peel_composite + namespace point_cloud { inline const std::string vert = R"( #version 330 core @@ -263,5 +296,5 @@ void main() { )"; } // namespace xy_grid -} // namespace shader_source +} // namespace shaders } // namespace slamd \ No newline at end of file diff --git a/slamd/include/slamd_window/geom/box.hpp b/slamd/include/slamd_window/geom/box.hpp index d5736ae..2aa55b1 100644 --- a/slamd/include/slamd_window/geom/box.hpp +++ b/slamd/include/slamd_window/geom/box.hpp @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -7,7 +8,7 @@ namespace _geom { class Box : public Geometry { public: - Box(); + Box(glm::vec3 dims, glm::vec3 color); void render(glm::mat4 model, glm::mat4 view, glm::mat4 projection) override; static std::shared_ptr deserialize(const slamd::flatb::Box* box_fb); @@ -17,4 +18,4 @@ class Box : public Geometry { }; } // namespace _geom -} // namespace slamd \ No newline at end of file +} // namespace slamd diff --git a/slamd/include/slamd_window/geom/geometry.hpp b/slamd/include/slamd_window/geom/geometry.hpp index 0b9c1cc..b2b5315 100644 --- a/slamd/include/slamd_window/geom/geometry.hpp +++ b/slamd/include/slamd_window/geom/geometry.hpp @@ -13,6 +13,8 @@ class Geometry { virtual void render(glm::mat4 model, glm::mat4 view, glm::mat4 projection) = 0; + virtual bool is_transparent() const { return false; } + virtual ~Geometry() = default; virtual std::optional bounds(); diff --git a/slamd/include/slamd_window/geom/mesh.hpp b/slamd/include/slamd_window/geom/mesh.hpp index 17bf5f7..4741d2c 100644 --- a/slamd/include/slamd_window/geom/mesh.hpp +++ b/slamd/include/slamd_window/geom/mesh.hpp @@ -32,6 +32,9 @@ class Mesh : public Geometry { static std::shared_ptr deserialize(const slamd::flatb::Mesh* mesh_fb); void render(glm::mat4 model, glm::mat4 view, glm::mat4 projection) override; + bool is_transparent() const override; + + static void set_peel_state(bool enabled, uint32_t depth_tex_id = 0); void update_positions(const std::vector& positions); void update_colors(const std::vector& colors); diff --git a/slamd/include/slamd_window/geom/plane.hpp b/slamd/include/slamd_window/geom/plane.hpp index 7d57221..110c2cd 100644 --- a/slamd/include/slamd_window/geom/plane.hpp +++ b/slamd/include/slamd_window/geom/plane.hpp @@ -15,6 +15,7 @@ class Plane : public Geometry { ); void render(glm::mat4 model, glm::mat4 view, glm::mat4 projection) override; + bool is_transparent() const override { return true; } static std::shared_ptr deserialize( const slamd::flatb::Plane* plane_fb diff --git a/slamd/include/slamd_window/tree/tree.hpp b/slamd/include/slamd_window/tree/tree.hpp index f84c1c3..0b1b8cc 100644 --- a/slamd/include/slamd_window/tree/tree.hpp +++ b/slamd/include/slamd_window/tree/tree.hpp @@ -21,6 +21,8 @@ class Tree { set_object(const TreePath& path, std::shared_ptr<_geom::Geometry> object); void render(const glm::mat4& view, const glm::mat4& projection) const; + void render_opaque(const glm::mat4& view, const glm::mat4& projection) const; + void render_transparent(const glm::mat4& view, const glm::mat4& projection) const; static std::shared_ptr deserialize( const slamd::flatb::Tree* serialized, @@ -42,7 +44,8 @@ class Tree { const Node* node, const glm::mat4& current_transform, const glm::mat4& view, - const glm::mat4& projection + const glm::mat4& projection, + bool transparent_pass ) const; std::optional diff --git a/slamd/include/slamd_window/view/scene_view.hpp b/slamd/include/slamd_window/view/scene_view.hpp index d0e8d6e..08ce243 100644 --- a/slamd/include/slamd_window/view/scene_view.hpp +++ b/slamd/include/slamd_window/view/scene_view.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace slamd { @@ -20,12 +21,14 @@ class SceneView { bool _dirty = true; std::shared_ptr tree; TreeOverlay tree_overlay; + ControlsHint controls_hint; private: FrameBuffer frame_buffer; Arcball arcball; Camera camera; FrameTimer frame_timer; + bool show_grid = true; _geom::GridXYPlane xy_grid; _geom::ArcballIndicator arcball_indicator; diff --git a/slamd/shaders/mesh/fragment_shader.frag b/slamd/shaders/mesh/fragment_shader.frag index 2c9b221..e37d1b4 100644 --- a/slamd/shaders/mesh/fragment_shader.frag +++ b/slamd/shaders/mesh/fragment_shader.frag @@ -8,8 +8,15 @@ out vec4 FragColor; uniform mat4 view; uniform float min_brightness; uniform float alpha; +uniform bool peel_enabled; +uniform sampler2D peel_depth_tex; void main() { + if (peel_enabled) { + float prev_depth = texelFetch(peel_depth_tex, ivec2(gl_FragCoord.xy), 0).r; + if (gl_FragCoord.z <= prev_depth) discard; + } + vec3 norm = normalize(Normal); // Camera-relative lighting: extract camera forward and right from view matrix diff --git a/slamd/shaders/peel_composite/fragment_shader.frag b/slamd/shaders/peel_composite/fragment_shader.frag new file mode 100644 index 0000000..8520bcd --- /dev/null +++ b/slamd/shaders/peel_composite/fragment_shader.frag @@ -0,0 +1,10 @@ +#version 330 core + +in vec2 uv; +out vec4 FragColor; + +uniform sampler2D layer_texture; + +void main() { + FragColor = texture(layer_texture, uv); +} diff --git a/slamd/shaders/peel_composite/vertex_shader.vert b/slamd/shaders/peel_composite/vertex_shader.vert new file mode 100644 index 0000000..d7499b6 --- /dev/null +++ b/slamd/shaders/peel_composite/vertex_shader.vert @@ -0,0 +1,11 @@ +#version 330 core + +layout(location = 0) in vec2 a_pos; +layout(location = 1) in vec2 a_uv; + +out vec2 uv; + +void main() { + uv = a_uv; + gl_Position = vec4(a_pos, 0.0, 1.0); +} diff --git a/slamd/src/slamd/geom/box.cpp b/slamd/src/slamd/geom/box.cpp index bb55ce1..76c789a 100644 --- a/slamd/src/slamd/geom/box.cpp +++ b/slamd/src/slamd/geom/box.cpp @@ -1,15 +1,20 @@ #include #include +#include namespace slamd { namespace geom { -Box::Box() {} +Box::Box(glm::vec3 dims, glm::vec3 color) + : dims(dims), + color(color) {} flatbuffers::Offset Box::serialize( flatbuffers::FlatBufferBuilder& builder ) { - auto box_fb = flatb::CreateBox(builder); + auto dims_fb = gmath::serialize(this->dims); + auto color_fb = gmath::serialize(this->color); + auto box_fb = flatb::CreateBox(builder, &dims_fb, &color_fb); return flatb::CreateGeometry( builder, this->id.value, @@ -18,10 +23,8 @@ flatbuffers::Offset Box::serialize( ); } -std::shared_ptr box() { - auto box = std::make_shared(); - // _global::geometries.add(box->id, box); - return box; +std::shared_ptr box(glm::vec3 dims, glm::vec3 color) { + return std::make_shared(dims, color); } } // namespace geom diff --git a/slamd/src/slamd/net/client_set.cpp b/slamd/src/slamd/net/client_set.cpp index b27a1be..b7139fb 100644 --- a/slamd/src/slamd/net/client_set.cpp +++ b/slamd/src/slamd/net/client_set.cpp @@ -45,5 +45,10 @@ void ClientSet::broadcast( } } +void ClientSet::clear() { + std::scoped_lock l(this->client_mutex); + this->clients.clear(); +} + } // namespace _net } // namespace slamd \ No newline at end of file diff --git a/slamd/src/slamd/net/connection.cpp b/slamd/src/slamd/net/connection.cpp index 3f3dd04..b7bc21f 100644 --- a/slamd/src/slamd/net/connection.cpp +++ b/slamd/src/slamd/net/connection.cpp @@ -1,4 +1,5 @@ #include +#include #include #include @@ -11,6 +12,13 @@ bool Connection::is_alive() { Connection::~Connection() { this->stop_requested = true; + // Use OS-level shutdown to unblock any pending asio::write on the worker + // thread. We avoid calling ASIO socket methods here because they are not + // thread-safe with concurrent operations on the worker. + auto handle = this->socket.native_handle(); + if (handle >= 0) { + ::shutdown(handle, SHUT_RDWR); + } if (this->worker.joinable()) { this->worker.join(); } diff --git a/slamd/src/slamd/visualizer.cpp b/slamd/src/slamd/visualizer.cpp index 2292bdd..5e0b61d 100644 --- a/slamd/src/slamd/visualizer.cpp +++ b/slamd/src/slamd/visualizer.cpp @@ -270,36 +270,32 @@ void Visualizer::server_job() { std::function accept_loop = [&]() { acceptor.async_accept([&](std::error_code ec, asio::ip::tcp::socket socket) { - if (!ec) { - auto conn = - std::make_shared<_net::Connection>(std::move(socket)); - conn->write(this->get_state()); - this->client_set->add(conn); + if (ec) { + return; } - accept_loop(); // keep accepting + auto conn = + std::make_shared<_net::Connection>(std::move(socket)); + conn->write(this->get_state()); + this->client_set->add(conn); + accept_loop(); }); }; accept_loop(); - // Spin a thread to cancel io when stop is requested - std::thread stop_watcher([&]() { - while (!this->stop_requested) { - std::this_thread::sleep_for(std::chrono::milliseconds(100)); - } - this->io_context.stop(); - }); - this->io_context.run(); - - stop_watcher.join(); } Visualizer::~Visualizer() { - this->stop_requested = true; + this->stop(); +} + +void Visualizer::stop() { + this->io_context.stop(); if (this->server_thread.joinable()) { this->server_thread.join(); } + this->client_set->clear(); } void Visualizer::hang_forever() { diff --git a/slamd/src/window/controls_hint.cpp b/slamd/src/window/controls_hint.cpp new file mode 100644 index 0000000..d1235d1 --- /dev/null +++ b/slamd/src/window/controls_hint.cpp @@ -0,0 +1,82 @@ +#include +#include + +namespace slamd { + +void ControlsHint::render() { + const float margin = 8.0f; + + struct HintLine { + const char* key; + const char* action; + }; + + const HintLine lines[] = { + {"WASDQE / Right-drag", "Pan"}, + {"Left-drag", "Rotate"}, + {"Scroll", "Zoom"}, + {".", "Re-center"}, + {"G", "Toggle grid"}, + }; + const int n_lines = sizeof(lines) / sizeof(lines[0]); + + const ImGuiStyle& style = ImGui::GetStyle(); + const float col_gap = 16.0f; + const float pad = 6.0f; + + // Measure column widths. + float key_w = 0.0f; + float action_w = 0.0f; + for (int i = 0; i < n_lines; i++) { + key_w = std::max(key_w, ImGui::CalcTextSize(lines[i].key).x); + action_w = std::max(action_w, ImGui::CalcTextSize(lines[i].action).x); + } + + float line_h = ImGui::CalcTextSize("X").y; + float total_h = n_lines * line_h + (n_lines - 1) * style.ItemSpacing.y; + + float content_w = key_w + col_gap + action_w; + float child_w = content_w + pad * 2.0f + 2.0f; + float child_h = total_h + pad * 2.0f + 2.0f; + + // Position in the bottom-right of the current ImGui window. + ImVec2 win_pos = ImGui::GetWindowPos(); + ImVec2 cr_max = ImGui::GetWindowContentRegionMax(); + + ImVec2 overlay_pos( + win_pos.x + cr_max.x - child_w - margin, + win_pos.y + cr_max.y - child_h - margin + ); + + ImVec2 saved_cursor = ImGui::GetCursorScreenPos(); + + ImGui::PushID("##controls_hint"); + ImGui::SetCursorScreenPos(overlay_pos); + + ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0, 0, 0, 0.55f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6, 6)); + + ImGui::BeginChild( + "##controls_child", + ImVec2(child_w, child_h), + true, + ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoInputs + ); + + for (int i = 0; i < n_lines; i++) { + ImGui::TextUnformatted(lines[i].key); + ImGui::SameLine(key_w + col_gap + pad); + ImGui::TextUnformatted(lines[i].action); + } + + ImGui::EndChild(); + + ImGui::PopStyleVar(); + ImGui::PopStyleColor(); + ImGui::PopID(); + + ImGui::SetCursorScreenPos(saved_cursor); +} + +} // namespace slamd diff --git a/slamd/src/window/frame_buffer.cpp b/slamd/src/window/frame_buffer.cpp index 68f0d1e..b22ed87 100644 --- a/slamd/src/window/frame_buffer.cpp +++ b/slamd/src/window/frame_buffer.cpp @@ -1,47 +1,39 @@ +#include + #include #include #include +#include +#include #include namespace slamd { void FrameBuffer::initialize() { - // === MSAA FBO === + // === MSAA FBO (opaque pass) === gl::glGenFramebuffers(1, &msaa_framebuffer_id); gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, msaa_framebuffer_id); gl::glGenTextures(1, &msaa_color_buffer_id); gl::glBindTexture(gl::GL_TEXTURE_2D_MULTISAMPLE, msaa_color_buffer_id); gl::glTexImage2DMultisample( - gl::GL_TEXTURE_2D_MULTISAMPLE, - samples, - gl::GL_RGB8, - current_width, - current_height, - gl::GL_TRUE + gl::GL_TEXTURE_2D_MULTISAMPLE, samples, gl::GL_RGB8, + current_width, current_height, gl::GL_TRUE ); gl::glFramebufferTexture2D( - gl::GL_FRAMEBUFFER, - gl::GL_COLOR_ATTACHMENT0, - gl::GL_TEXTURE_2D_MULTISAMPLE, - msaa_color_buffer_id, - 0 + gl::GL_FRAMEBUFFER, gl::GL_COLOR_ATTACHMENT0, + gl::GL_TEXTURE_2D_MULTISAMPLE, msaa_color_buffer_id, 0 ); gl::glGenRenderbuffers(1, &msaa_depth_buffer_id); gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, msaa_depth_buffer_id); gl::glRenderbufferStorageMultisample( - gl::GL_RENDERBUFFER, - samples, - gl::GL_DEPTH24_STENCIL8, - current_width, - current_height + gl::GL_RENDERBUFFER, samples, gl::GL_DEPTH24_STENCIL8, + current_width, current_height ); gl::glFramebufferRenderbuffer( - gl::GL_FRAMEBUFFER, - gl::GL_DEPTH_STENCIL_ATTACHMENT, - gl::GL_RENDERBUFFER, - msaa_depth_buffer_id + gl::GL_FRAMEBUFFER, gl::GL_DEPTH_STENCIL_ATTACHMENT, + gl::GL_RENDERBUFFER, msaa_depth_buffer_id ); if (gl::glCheckFramebufferStatus(gl::GL_FRAMEBUFFER) != @@ -49,70 +41,158 @@ void FrameBuffer::initialize() { throw std::runtime_error("MSAA framebuffer not complete"); } - // === Resolve FBO === + // === Resolve FBO (final output for ImGui) === gl::glGenFramebuffers(1, &frame_buffer_object_id); gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, frame_buffer_object_id); gl::glGenTextures(1, &texture_id); gl::glBindTexture(gl::GL_TEXTURE_2D, texture_id); gl::glTexImage2D( - gl::GL_TEXTURE_2D, - 0, - gl::GL_RGB, - current_width, - current_height, - 0, - gl::GL_RGB, - gl::GL_UNSIGNED_BYTE, - nullptr + gl::GL_TEXTURE_2D, 0, gl::GL_RGB, + current_width, current_height, 0, + gl::GL_RGB, gl::GL_UNSIGNED_BYTE, nullptr ); gl::glTexParameteri( - gl::GL_TEXTURE_2D, - gl::GL_TEXTURE_MIN_FILTER, - gl::GL_LINEAR + gl::GL_TEXTURE_2D, gl::GL_TEXTURE_MIN_FILTER, gl::GL_LINEAR ); gl::glTexParameteri( - gl::GL_TEXTURE_2D, - gl::GL_TEXTURE_MAG_FILTER, - gl::GL_LINEAR + gl::GL_TEXTURE_2D, gl::GL_TEXTURE_MAG_FILTER, gl::GL_LINEAR ); gl::glFramebufferTexture2D( - gl::GL_FRAMEBUFFER, - gl::GL_COLOR_ATTACHMENT0, - gl::GL_TEXTURE_2D, - texture_id, - 0 + gl::GL_FRAMEBUFFER, gl::GL_COLOR_ATTACHMENT0, + gl::GL_TEXTURE_2D, texture_id, 0 ); gl::glGenRenderbuffers(1, &render_buffer_object_id); gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, render_buffer_object_id); gl::glRenderbufferStorage( - gl::GL_RENDERBUFFER, - gl::GL_DEPTH24_STENCIL8, - current_width, - current_height + gl::GL_RENDERBUFFER, gl::GL_DEPTH24_STENCIL8, + current_width, current_height ); gl::glFramebufferRenderbuffer( - gl::GL_FRAMEBUFFER, - gl::GL_DEPTH_STENCIL_ATTACHMENT, - gl::GL_RENDERBUFFER, - render_buffer_object_id + gl::GL_FRAMEBUFFER, gl::GL_DEPTH_STENCIL_ATTACHMENT, + gl::GL_RENDERBUFFER, render_buffer_object_id ); - if (glCheckFramebufferStatus(gl::GL_FRAMEBUFFER) != + if (gl::glCheckFramebufferStatus(gl::GL_FRAMEBUFFER) != gl::GL_FRAMEBUFFER_COMPLETE) { throw std::runtime_error("Resolve framebuffer not complete"); } + // === Peel FBO (non-MSAA — exact depth for peeling) === + gl::glGenFramebuffers(1, &peel_msaa_fbo_id); + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, peel_msaa_fbo_id); + + gl::glGenRenderbuffers(1, &peel_msaa_color_id); + gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, peel_msaa_color_id); + gl::glRenderbufferStorage( + gl::GL_RENDERBUFFER, gl::GL_RGBA8, + current_width, current_height + ); + gl::glFramebufferRenderbuffer( + gl::GL_FRAMEBUFFER, gl::GL_COLOR_ATTACHMENT0, + gl::GL_RENDERBUFFER, peel_msaa_color_id + ); + + gl::glGenRenderbuffers(1, &peel_msaa_depth_id); + gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, peel_msaa_depth_id); + gl::glRenderbufferStorage( + gl::GL_RENDERBUFFER, gl::GL_DEPTH24_STENCIL8, + current_width, current_height + ); + gl::glFramebufferRenderbuffer( + gl::GL_FRAMEBUFFER, gl::GL_DEPTH_STENCIL_ATTACHMENT, + gl::GL_RENDERBUFFER, peel_msaa_depth_id + ); + + if (gl::glCheckFramebufferStatus(gl::GL_FRAMEBUFFER) != + gl::GL_FRAMEBUFFER_COMPLETE) { + throw std::runtime_error("Peel framebuffer not complete"); + } + + // === Peel depth textures (ping-pong, non-MSAA, for shader comparison) === + gl::glGenTextures(2, peel_depth_tex_id); + gl::glGenFramebuffers(2, peel_depth_fbo_id); + for (int i = 0; i < 2; i++) { + gl::glBindTexture(gl::GL_TEXTURE_2D, peel_depth_tex_id[i]); + gl::glTexImage2D( + gl::GL_TEXTURE_2D, 0, gl::GL_DEPTH24_STENCIL8, + current_width, current_height, 0, + gl::GL_DEPTH_STENCIL, gl::GL_UNSIGNED_INT_24_8, nullptr + ); + gl::glTexParameteri( + gl::GL_TEXTURE_2D, gl::GL_TEXTURE_MIN_FILTER, gl::GL_NEAREST + ); + gl::glTexParameteri( + gl::GL_TEXTURE_2D, gl::GL_TEXTURE_MAG_FILTER, gl::GL_NEAREST + ); + + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, peel_depth_fbo_id[i]); + gl::glFramebufferTexture2D( + gl::GL_FRAMEBUFFER, gl::GL_DEPTH_STENCIL_ATTACHMENT, + gl::GL_TEXTURE_2D, peel_depth_tex_id[i], 0 + ); + } + + // === Peel layer color textures + resolve FBO === + gl::glGenTextures(MAX_PEEL_LAYERS, peel_layer_tex_id); + for (int i = 0; i < MAX_PEEL_LAYERS; i++) { + gl::glBindTexture(gl::GL_TEXTURE_2D, peel_layer_tex_id[i]); + gl::glTexImage2D( + gl::GL_TEXTURE_2D, 0, gl::GL_RGBA8, + current_width, current_height, 0, + gl::GL_RGBA, gl::GL_UNSIGNED_BYTE, nullptr + ); + gl::glTexParameteri( + gl::GL_TEXTURE_2D, gl::GL_TEXTURE_MIN_FILTER, gl::GL_LINEAR + ); + gl::glTexParameteri( + gl::GL_TEXTURE_2D, gl::GL_TEXTURE_MAG_FILTER, gl::GL_LINEAR + ); + } + + gl::glGenFramebuffers(1, &peel_resolve_fbo_id); + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, peel_resolve_fbo_id); + gl::glFramebufferTexture2D( + gl::GL_FRAMEBUFFER, gl::GL_COLOR_ATTACHMENT0, + gl::GL_TEXTURE_2D, peel_layer_tex_id[0], 0 + ); + + // === Fullscreen quad === + // clang-format off + float quad_verts[] = { + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, -1.0f, 1.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, -1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 1.0f, 1.0f, + -1.0f, 1.0f, 0.0f, 1.0f, + }; + // clang-format on + gl::glGenVertexArrays(1, &quad_vao_id); + gl::glGenBuffers(1, &quad_vbo_id); + gl::glBindVertexArray(quad_vao_id); + gl::glBindBuffer(gl::GL_ARRAY_BUFFER, quad_vbo_id); + gl::glBufferData( + gl::GL_ARRAY_BUFFER, sizeof(quad_verts), quad_verts, gl::GL_STATIC_DRAW + ); + gl::glEnableVertexAttribArray(0); + gl::glVertexAttribPointer( + 0, 2, gl::GL_FLOAT, gl::GL_FALSE, 4 * sizeof(float), (void*)0 + ); + gl::glEnableVertexAttribArray(1); + gl::glVertexAttribPointer( + 1, 2, gl::GL_FLOAT, gl::GL_FALSE, 4 * sizeof(float), + (void*)(2 * sizeof(float)) + ); + gl::glBindVertexArray(0); + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, 0); gl::glBindTexture(gl::GL_TEXTURE_2D, 0); gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, 0); } -FrameBuffer::FrameBuffer( - size_t width, - size_t height -) +FrameBuffer::FrameBuffer(size_t width, size_t height) : current_height(height), current_width(width) { initialize(); @@ -126,6 +206,19 @@ FrameBuffer::~FrameBuffer() { gl::glDeleteFramebuffers(1, &msaa_framebuffer_id); gl::glDeleteTextures(1, &msaa_color_buffer_id); gl::glDeleteRenderbuffers(1, &msaa_depth_buffer_id); + + gl::glDeleteFramebuffers(1, &peel_msaa_fbo_id); + gl::glDeleteRenderbuffers(1, &peel_msaa_color_id); + gl::glDeleteRenderbuffers(1, &peel_msaa_depth_id); + + gl::glDeleteFramebuffers(2, peel_depth_fbo_id); + gl::glDeleteTextures(2, peel_depth_tex_id); + + gl::glDeleteFramebuffers(1, &peel_resolve_fbo_id); + gl::glDeleteTextures(MAX_PEEL_LAYERS, peel_layer_tex_id); + + gl::glDeleteVertexArrays(1, &quad_vao_id); + gl::glDeleteBuffers(1, &quad_vbo_id); } uint32_t FrameBuffer::frame_texture() { @@ -145,24 +238,94 @@ void FrameBuffer::resolve() { gl::glBindFramebuffer(gl::GL_READ_FRAMEBUFFER, msaa_framebuffer_id); gl::glBindFramebuffer(gl::GL_DRAW_FRAMEBUFFER, frame_buffer_object_id); gl::glBlitFramebuffer( - 0, - 0, - current_width, - current_height, - 0, - 0, - current_width, - current_height, - gl::GL_COLOR_BUFFER_BIT, - gl::GL_NEAREST + 0, 0, current_width, current_height, + 0, 0, current_width, current_height, + gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT, gl::GL_NEAREST ); gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, 0); } -bool FrameBuffer::rescale( - size_t width, - size_t height -) { +void FrameBuffer::bind_peel_pass() { + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, peel_msaa_fbo_id); + gl::glViewport(0, 0, current_width, current_height); + + gl::glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + gl::glClearDepth(1.0); + gl::glClear(gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT); + + gl::glEnable(gl::GL_DEPTH_TEST); + gl::glDepthMask(gl::GL_TRUE); + gl::glDisable(gl::GL_BLEND); +} + +uint32_t FrameBuffer::prev_peel_depth_texture() const { + return peel_depth_tex_id[current_peel_read]; +} + +void FrameBuffer::end_peel_pass(int layer) { + // Resolve MSAA color → layer texture + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, peel_resolve_fbo_id); + gl::glFramebufferTexture2D( + gl::GL_FRAMEBUFFER, gl::GL_COLOR_ATTACHMENT0, + gl::GL_TEXTURE_2D, peel_layer_tex_id[layer], 0 + ); + + gl::glBindFramebuffer(gl::GL_READ_FRAMEBUFFER, peel_msaa_fbo_id); + gl::glBindFramebuffer(gl::GL_DRAW_FRAMEBUFFER, peel_resolve_fbo_id); + gl::glBlitFramebuffer( + 0, 0, current_width, current_height, + 0, 0, current_width, current_height, + gl::GL_COLOR_BUFFER_BIT, gl::GL_NEAREST + ); + + // Resolve MSAA depth → non-MSAA depth texture for next pass comparison + int write_idx = 1 - current_peel_read; + gl::glBindFramebuffer(gl::GL_DRAW_FRAMEBUFFER, peel_depth_fbo_id[write_idx]); + gl::glBlitFramebuffer( + 0, 0, current_width, current_height, + 0, 0, current_width, current_height, + gl::GL_DEPTH_BUFFER_BIT | gl::GL_STENCIL_BUFFER_BIT, gl::GL_NEAREST + ); + + current_peel_read = write_idx; + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, 0); +} + +static thread_local std::optional composite_shader; + +void FrameBuffer::composite_peel(int num_layers) { + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, frame_buffer_object_id); + gl::glViewport(0, 0, current_width, current_height); + + gl::glDisable(gl::GL_DEPTH_TEST); + gl::glEnable(gl::GL_BLEND); + gl::glBlendFunc(gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA); + + if (!composite_shader.has_value()) { + composite_shader.emplace( + shader_source::peel_composite::vert, + shader_source::peel_composite::frag + ); + } + auto& shader = composite_shader.value(); + shader.use(); + shader.set_uniform("layer_texture", 0); + + gl::glBindVertexArray(quad_vao_id); + + // Back-to-front: furthest layer first + for (int i = num_layers - 1; i >= 0; i--) { + gl::glActiveTexture(gl::GL_TEXTURE0); + gl::glBindTexture(gl::GL_TEXTURE_2D, peel_layer_tex_id[i]); + gl::glDrawArrays(gl::GL_TRIANGLES, 0, 6); + } + + gl::glBindVertexArray(0); + gl::glEnable(gl::GL_DEPTH_TEST); + gl::glBindFramebuffer(gl::GL_FRAMEBUFFER, 0); +} + +bool FrameBuffer::rescale(size_t width, size_t height) { if (width == current_width && height == current_height) { return false; } @@ -170,47 +333,58 @@ bool FrameBuffer::rescale( current_width = width; current_height = height; - // Realloc MSAA targets + // MSAA targets gl::glBindTexture(gl::GL_TEXTURE_2D_MULTISAMPLE, msaa_color_buffer_id); gl::glTexImage2DMultisample( - gl::GL_TEXTURE_2D_MULTISAMPLE, - samples, - gl::GL_RGB8, - width, - height, - gl::GL_TRUE + gl::GL_TEXTURE_2D_MULTISAMPLE, samples, gl::GL_RGB8, + width, height, gl::GL_TRUE ); - gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, msaa_depth_buffer_id); gl::glRenderbufferStorageMultisample( - gl::GL_RENDERBUFFER, - samples, - gl::GL_DEPTH24_STENCIL8, - width, - height + gl::GL_RENDERBUFFER, samples, gl::GL_DEPTH24_STENCIL8, width, height ); - // Realloc resolve target + // Resolve target gl::glBindTexture(gl::GL_TEXTURE_2D, texture_id); gl::glTexImage2D( - gl::GL_TEXTURE_2D, - 0, - gl::GL_RGB, - width, - height, - 0, - gl::GL_RGB, - gl::GL_UNSIGNED_BYTE, - nullptr + gl::GL_TEXTURE_2D, 0, gl::GL_RGB, width, height, 0, + gl::GL_RGB, gl::GL_UNSIGNED_BYTE, nullptr ); - gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, render_buffer_object_id); gl::glRenderbufferStorage( - gl::GL_RENDERBUFFER, - gl::GL_DEPTH24_STENCIL8, - width, - height + gl::GL_RENDERBUFFER, gl::GL_DEPTH24_STENCIL8, width, height ); + + // Peel targets + gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, peel_msaa_color_id); + gl::glRenderbufferStorage( + gl::GL_RENDERBUFFER, gl::GL_RGBA8, width, height + ); + gl::glBindRenderbuffer(gl::GL_RENDERBUFFER, peel_msaa_depth_id); + gl::glRenderbufferStorage( + gl::GL_RENDERBUFFER, gl::GL_DEPTH24_STENCIL8, width, height + ); + + // Peel depth textures + for (int i = 0; i < 2; i++) { + gl::glBindTexture(gl::GL_TEXTURE_2D, peel_depth_tex_id[i]); + gl::glTexImage2D( + gl::GL_TEXTURE_2D, 0, gl::GL_DEPTH24_STENCIL8, + width, height, 0, + gl::GL_DEPTH_STENCIL, gl::GL_UNSIGNED_INT_24_8, nullptr + ); + } + + // Peel layer textures + for (int i = 0; i < MAX_PEEL_LAYERS; i++) { + gl::glBindTexture(gl::GL_TEXTURE_2D, peel_layer_tex_id[i]); + gl::glTexImage2D( + gl::GL_TEXTURE_2D, 0, gl::GL_RGBA8, + width, height, 0, + gl::GL_RGBA, gl::GL_UNSIGNED_BYTE, nullptr + ); + } + return true; } diff --git a/slamd/src/window/geom/box.cpp b/slamd/src/window/geom/box.cpp index c3918c1..669e1af 100644 --- a/slamd/src/window/geom/box.cpp +++ b/slamd/src/window/geom/box.cpp @@ -1,3 +1,4 @@ +#include #include #include @@ -5,7 +6,8 @@ namespace slamd { namespace _geom { // clang-format off -// 6 faces * 4 vertices = 24 unique verts +// Unit box centered at origin, 6 faces * 4 vertices = 24 unique verts. +// Scaled by dims at construction time. const std::vector box_corners = {{ // Back face {-0.5f, -0.5f, -0.5f}, {0.5f, -0.5f, -0.5f}, {0.5f, 0.5f, -0.5f}, {-0.5f, 0.5f, -0.5f}, @@ -21,72 +23,38 @@ const std::vector box_corners = {{ {-0.5f, -0.5f, 0.5f}, {0.5f, -0.5f, 0.5f}, {0.5f, -0.5f, -0.5f}, {-0.5f, -0.5f, -0.5f}, }}; - -const std::vector vertex_colors = {{ - // Back face - red - {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, - // Front face - green - {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, - // Left face - blue - {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, - // Right face - yellow - {1.0f, 1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}, {1.0f, 1.0f, 0.0f}, - // Top face - magenta - {1.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 1.0f}, {1.0f, 0.0f, 1.0f}, - // Bottom face - cyan - {0.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 1.0f}, {0.0f, 1.0f, 1.0f}, -}}; - const std::vector box_indices = {{ - // Back face (Z-) 0, 2, 1, 0, 3, 2, - // Front face (Z+) 4, 5, 6, 4, 6, 7, - // Left face (X-) 8, 10, 9, 8, 11, 10, - // Right face (X+) 12, 13, 14, 12, 14, 15, - // Top face (Y+) 16, 18, 17, 16, 19, 18, - // Bottom face (Y-) 20, 21, 22, 20, 22, 23 }}; const std::vector vertex_normals = {{ - // Back face (-Z) {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, {0.0f, 0.0f, -1.0f}, - // Front face (+Z) {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, {0.0f, 0.0f, 1.0f}, - // Left face (-X) {-1.0f, 0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}, {-1.0f, 0.0f, 0.0f}, - // Right face (+X) {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, {1.0f, 0.0f, 0.0f}, - // Top face (+Y) {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, {0.0f, 1.0f, 0.0f}, - // Bottom face (-Y) {0.0f, -1.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, {0.0f, -1.0f, 0.0f}, }}; - -auto get_mesh_data() { - return slamd::data::MeshData( - box_corners, - vertex_colors, - box_indices, - vertex_normals, - 1.0 - ); -} - // clang-format on -Box::Box() { - this->box_mesh = std::make_unique(slamd::data::MeshData( - box_corners, - vertex_colors, - box_indices, - vertex_normals, - 1.0 - )); +Box::Box(glm::vec3 dims, glm::vec3 color) { + std::vector scaled(box_corners.size()); + for (size_t i = 0; i < box_corners.size(); i++) { + scaled[i] = box_corners[i] * dims; + } + + auto data = slamd::data::MeshDataBuilder() + .set_positions(scaled) + .set_colors(color) + .set_indices(box_indices) + .set_normals(vertex_normals) + .build(); + this->box_mesh = std::make_unique(std::move(data)); } void Box::render( @@ -100,9 +68,11 @@ void Box::render( std::shared_ptr Box::deserialize( const slamd::flatb::Box* box_fb ) { - (void)box_fb; - return std::make_shared(); + return std::make_shared( + slamd::gmath::deserialize(box_fb->dims()), + slamd::gmath::deserialize(box_fb->color()) + ); } } // namespace _geom -} // namespace slamd \ No newline at end of file +} // namespace slamd diff --git a/slamd/src/window/geom/mesh.cpp b/slamd/src/window/geom/mesh.cpp index d582b0f..139c60b 100644 --- a/slamd/src/window/geom/mesh.cpp +++ b/slamd/src/window/geom/mesh.cpp @@ -9,6 +9,9 @@ namespace slamd { namespace _geom { +static bool s_peel_enabled = false; +static uint32_t s_peel_depth_tex = 0; + std::shared_ptr Mesh::deserialize( const slamd::flatb::Mesh* mesh_fb ) { @@ -213,16 +216,11 @@ void Mesh::render( shader.set_uniform("projection", projection); shader.set_uniform("min_brightness", this->min_brightness); shader.set_uniform("alpha", this->mesh_data.alpha); - - bool needs_blend = this->mesh_data.alpha < 1.0f; - gl::GLboolean blend_was_enabled = gl::GL_FALSE; - gl::GLboolean depth_mask_was_enabled = gl::GL_TRUE; - if (needs_blend) { - gl::glGetBooleanv(gl::GL_BLEND, &blend_was_enabled); - gl::glGetBooleanv(gl::GL_DEPTH_WRITEMASK, &depth_mask_was_enabled); - gl::glEnable(gl::GL_BLEND); - gl::glBlendFunc(gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA); - gl::glDepthMask(gl::GL_FALSE); + shader.set_uniform("peel_enabled", s_peel_enabled); + if (s_peel_enabled) { + gl::glActiveTexture(gl::GL_TEXTURE0); + gl::glBindTexture(gl::GL_TEXTURE_2D, s_peel_depth_tex); + shader.set_uniform("peel_depth_tex", 0); } gl::glDrawElements( @@ -232,16 +230,18 @@ void Mesh::render( 0 ); - if (needs_blend) { - gl::glDepthMask(depth_mask_was_enabled); - if (blend_was_enabled == gl::GL_FALSE) { - gl::glDisable(gl::GL_BLEND); - } - } - gl::glBindVertexArray(0); }; +bool Mesh::is_transparent() const { + return this->mesh_data.alpha < 1.0f; +} + +void Mesh::set_peel_state(bool enabled, uint32_t depth_tex_id) { + s_peel_enabled = enabled; + s_peel_depth_tex = depth_tex_id; +} + Mesh::~Mesh() { gl::glDeleteBuffers(1, &eab_id); gl::glDeleteBuffers(1, &pos_vbo_id); diff --git a/slamd/src/window/geom/poly_line.cpp b/slamd/src/window/geom/poly_line.cpp index 045b42d..1de6c21 100644 --- a/slamd/src/window/geom/poly_line.cpp +++ b/slamd/src/window/geom/poly_line.cpp @@ -18,12 +18,89 @@ std::shared_ptr PolyLine::deserialize( ); } -std::unique_ptr make_poly_line_mesh( +const float BEND_THRESHOLD = 0.85f; // cos(~30°) + +// Insert arc-interpolated points at sharp bends so the tube rounds corners +// instead of pinching or ballooning. +std::vector subdivide_sharp_bends( const std::vector& points, + float radius +) { + size_t n = points.size(); + if (n < 3) { + return points; + } + + // First pass: determine which interior points are sharp bends. + std::vector is_sharp(n, false); + for (size_t i = 1; i < n - 1; i++) { + glm::vec3 incoming = glm::normalize(points[i] - points[i - 1]); + glm::vec3 outgoing = glm::normalize(points[i + 1] - points[i]); + is_sharp[i] = glm::dot(incoming, outgoing) < BEND_THRESHOLD; + } + + // Second pass: compute per-corner pullback so adjacent bends don't + // overlap. Each segment is shared by at most two corners (its start + // and end). Each corner gets at most half of each adjacent segment. + // If both ends of a segment are sharp, each gets a quarter. + std::vector pullback(n, 0.0f); + for (size_t i = 1; i < n - 1; i++) { + if (!is_sharp[i]) continue; + + float seg_before = glm::length(points[i] - points[i - 1]); + float seg_after = glm::length(points[i + 1] - points[i]); + + float budget_before = + seg_before * (is_sharp[i - 1] ? 0.25f : 0.5f); + float budget_after = + seg_after * ((i + 1 < n - 1 && is_sharp[i + 1]) ? 0.25f : 0.5f); + + pullback[i] = + glm::min(radius * 2.0f, glm::min(budget_before, budget_after)); + } + + // Third pass: emit points with Bezier arcs at sharp bends. + std::vector result; + result.push_back(points[0]); + + for (size_t i = 1; i < n - 1; i++) { + if (!is_sharp[i]) { + result.push_back(points[i]); + continue; + } + + glm::vec3 incoming = glm::normalize(points[i] - points[i - 1]); + glm::vec3 outgoing = glm::normalize(points[i + 1] - points[i]); + float cos_angle = glm::dot(incoming, outgoing); + + glm::vec3 p_in = points[i] - incoming * pullback[i]; + glm::vec3 p_out = points[i] + outgoing * pullback[i]; + + int steps = glm::clamp( + static_cast((1.0f - cos_angle) * 5.0f), 2, 8 + ); + + for (int s = 0; s <= steps; s++) { + float t = static_cast(s) / static_cast(steps); + glm::vec3 a = glm::mix(p_in, points[i], t); + glm::vec3 b = glm::mix(points[i], p_out, t); + result.push_back(glm::mix(a, b, t)); + } + } + + result.push_back(points.back()); + return result; +} + +std::unique_ptr make_poly_line_mesh( + const std::vector& input_points, float thickness, const glm::vec3& color, float min_brightness ) { + const auto points = + subdivide_sharp_bends(input_points, thickness * 0.5f); + std::vector verts; std::vector indices; @@ -42,7 +119,9 @@ std::unique_ptr make_poly_line_mesh( for (size_t i = 0; i < points.size(); i++) { glm::vec3 forward; - if (i == points.size() - 1) { + if (i == 0) { + forward = glm::normalize(points[1] - points[0]); + } else if (i == points.size() - 1) { forward = glm::normalize(points[i] - points[i - 1]); } else { forward = glm::normalize(points[i + 1] - points[i]); diff --git a/slamd/src/window/geom/xy_grid.cpp b/slamd/src/window/geom/xy_grid.cpp index 36616b1..6d6ca26 100644 --- a/slamd/src/window/geom/xy_grid.cpp +++ b/slamd/src/window/geom/xy_grid.cpp @@ -141,6 +141,7 @@ GridXYPlane::~GridXYPlane() { gl::glDeleteVertexArrays(1, &this->vao_id); } + void GridXYPlane::set_arcball_zoom( float zoom ) { diff --git a/slamd/src/window/run_window.cpp b/slamd/src/window/run_window.cpp index 49332ef..92fb402 100644 --- a/slamd/src/window/run_window.cpp +++ b/slamd/src/window/run_window.cpp @@ -96,6 +96,7 @@ void run_window( if (scene->tree_overlay.render(scene->tree)) { scene->mark_dirty(); } + scene->controls_hint.render(); ImGui::End(); ImGui::PopStyleVar(); diff --git a/slamd/src/window/tree/tree.cpp b/slamd/src/window/tree/tree.cpp index 8be9949..c42965d 100644 --- a/slamd/src/window/tree/tree.cpp +++ b/slamd/src/window/tree/tree.cpp @@ -103,7 +103,22 @@ void Tree::render( const glm::mat4& view, const glm::mat4& projection ) const { - this->render_recursive(this->root.get(), glm::mat4(1.0), view, projection); + this->render_recursive(this->root.get(), glm::mat4(1.0), view, projection, false); + this->render_recursive(this->root.get(), glm::mat4(1.0), view, projection, true); +} + +void Tree::render_opaque( + const glm::mat4& view, + const glm::mat4& projection +) const { + this->render_recursive(this->root.get(), glm::mat4(1.0), view, projection, false); +} + +void Tree::render_transparent( + const glm::mat4& view, + const glm::mat4& projection +) const { + this->render_recursive(this->root.get(), glm::mat4(1.0), view, projection, true); } void Tree::set_transform( @@ -142,7 +157,8 @@ void Tree::render_recursive( const Node* node, const glm::mat4& current_transform, const glm::mat4& view, - const glm::mat4& projection + const glm::mat4& projection, + bool transparent_pass ) const { if (!node->glob_matches.has_value() && !node->checked) { return; @@ -158,11 +174,14 @@ void Tree::render_recursive( const auto node_object = node->get_object(); if (node_object.has_value() && node->glob_matches.value_or(true)) { - node_object.value()->render(next_transform, view, projection); + bool is_transparent = node_object.value()->is_transparent(); + if (is_transparent == transparent_pass) { + node_object.value()->render(next_transform, view, projection); + } } for (auto& [_, child] : node->children) { - this->render_recursive(child.get(), next_transform, view, projection); + this->render_recursive(child.get(), next_transform, view, projection, transparent_pass); } } diff --git a/slamd/src/window/view/scene_view.cpp b/slamd/src/window/view/scene_view.cpp index bccc24c..49f13cd 100644 --- a/slamd/src/window/view/scene_view.cpp +++ b/slamd/src/window/view/scene_view.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include namespace slamd { @@ -22,7 +23,7 @@ SceneView::SceneView( ) : tree(std::move(tree)), frame_buffer(500, 500), - camera(45.0, 0.001, 100.0), + camera(45.0, 0.05, 100.0), xy_grid(1000.0) { this->xy_grid.set_arcball_zoom(this->arcball.radius); this->arcball_indicator.set_arcball_zoom(this->arcball.radius); @@ -53,43 +54,73 @@ void SceneView::render_to_imgui() { } void SceneView::render_to_frame_buffer() { - this->frame_buffer.bind(); - auto view = this->arcball.view_matrix(); auto background_color = make_background_color(view); - - gl::glEnable(gl::GL_BLEND); - gl::glBlendFunc(gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA); - - gl::glClear(gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT); - gl::glEnable(gl::GL_DEPTH_TEST); - - gl::glClearColor( - background_color.r, - background_color.g, - background_color.b, - 1.0f - ); - gl::glClear(gl::GL_COLOR_BUFFER_BIT); - auto projection = this->camera.get_projection_matrix( this->frame_buffer.aspect(), this->arcball.radius ); - this->tree->render(view, projection); + // === Pass 1: Opaque geometry to MSAA FBO === + this->frame_buffer.bind(); + gl::glEnable(gl::GL_DEPTH_TEST); + gl::glDepthMask(gl::GL_TRUE); + gl::glDisable(gl::GL_BLEND); + gl::glClearColor( + background_color.r, background_color.g, background_color.b, 1.0f + ); + gl::glClear(gl::GL_COLOR_BUFFER_BIT | gl::GL_DEPTH_BUFFER_BIT); - this->xy_grid.render(glm::mat4(1.0), view, projection); + this->tree->render_opaque(view, projection); this->arcball_indicator.render(this->arcball.center, view, projection); + // Grid renders in MSAA pass with its own alpha blending + if (this->show_grid) { + gl::glEnable(gl::GL_BLEND); + gl::glBlendFunc(gl::GL_SRC_ALPHA, gl::GL_ONE_MINUS_SRC_ALPHA); + this->xy_grid.render(glm::mat4(1.0), view, projection); + gl::glDisable(gl::GL_BLEND); + } + this->frame_buffer.unbind(); this->frame_buffer.resolve(); + + // === Pass 2: Depth peeling for transparent geometry === + int num_layers = 0; + for (int layer = 0; layer < 4; layer++) { + this->frame_buffer.bind_peel_pass(); + + // Re-render opaques depth-only to establish occlusion + gl::glColorMask(gl::GL_FALSE, gl::GL_FALSE, gl::GL_FALSE, gl::GL_FALSE); + this->tree->render_opaque(view, projection); + gl::glColorMask(gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE, gl::GL_TRUE); + + // Set peel discard for layers > 0 + if (layer > 0) { + uint32_t prev_depth = this->frame_buffer.prev_peel_depth_texture(); + _geom::Mesh::set_peel_state(true, prev_depth); + } + + this->tree->render_transparent(view, projection); + + _geom::Mesh::set_peel_state(false); + + this->frame_buffer.end_peel_pass(layer); + num_layers = layer + 1; + } + + // === Pass 3: Composite layers back-to-front === + this->frame_buffer.composite_peel(num_layers); } void SceneView::handle_input() { if (ImGui::IsWindowFocused()) { this->handle_mouse_input(); this->handle_translation_input(); + if (ImGui::IsKeyPressed(ImGuiKey_G, false)) { + this->show_grid = !this->show_grid; + this->mark_dirty(); + } if (ImGui::IsKeyPressed(ImGuiKey_Period, false)) { this->arcball.reset(); this->xy_grid.set_arcball_zoom(this->arcball.radius); diff --git a/src/main.cpp b/src/main.cpp index 631e314..d99aaa5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -346,7 +346,13 @@ void define_private_geom( void define_geom( py::module_& m ) { - m.def("Box", &slamd::geom::box, "Create a Box geometry"); + m.def( + "Box", + &slamd::geom::box, + py::arg("dims") = glm::vec3(1.0f), + py::arg("color") = glm::vec3(0.8f, 0.2f, 0.0f), + "Create a Box geometry" + ); m.def( "Arrows", &slamd::geom::arrows, @@ -491,6 +497,31 @@ void define_geom( "Create a SimpleMesh geometry from raw data" ); + m.def( + "Mesh", + [](const std::vector& positions, + const std::vector& vertex_colors, + const std::vector& triangle_indices, + const std::vector& normals, + float alpha) { + slamd::data::MeshData data = slamd::data::MeshDataBuilder() + .set_positions(positions) + .set_colors(vertex_colors) + .set_indices(triangle_indices) + .set_normals(normals) + .set_alpha(alpha) + .build(); + + return slamd::geom::mesh(std::move(data)); + }, + py::arg("vertices"), + py::arg("vertex_colors"), + py::arg("triangle_indices"), + py::arg("vertex_normals"), + py::arg("alpha"), + "Create a SimpleMesh geometry from raw data with transparency" + ); + m.def( "Sphere", &slamd::geom::sphere, @@ -560,7 +591,8 @@ PYBIND11_MODULE( py::arg("scene") ) .def("scene", &slamd::_vis::Visualizer::scene, py::arg("name")) - .def("delete_scene", &slamd::_vis::Visualizer::delete_scene); + .def("delete_scene", &slamd::_vis::Visualizer::delete_scene) + .def("stop", &slamd::_vis::Visualizer::stop); m.def( "spawn_window", diff --git a/src/slamd/__init__.py b/src/slamd/__init__.py index caf4f62..f7b9dff 100644 --- a/src/slamd/__init__.py +++ b/src/slamd/__init__.py @@ -1,6 +1,8 @@ from __future__ import annotations from pathlib import Path +import atexit import threading +import weakref import subprocess from sys import argv from . import geom @@ -41,6 +43,16 @@ def __init__(self, name: str, spawn=True, port: int = 5555) -> None: if spawn: spawn_window(port) + impl_ref = weakref.ref(self._impl) + def _stop_if_alive(): + impl = impl_ref() + if impl is not None: + impl.stop() + atexit.register(_stop_if_alive) + + def __del__(self): + self._impl.stop() + def hang_forever(self): """Block execution forever (used to keep the visualizer alive).""" threading.Event().wait() diff --git a/src/slamd/bindings/__init__.pyi b/src/slamd/bindings/__init__.pyi index 20e143b..6084bac 100644 --- a/src/slamd/bindings/__init__.pyi +++ b/src/slamd/bindings/__init__.pyi @@ -22,6 +22,7 @@ class Visualizer: def add_scene(self, name: str, scene: Scene) -> None: ... def delete_scene(self, arg0: str) -> None: ... def scene(self, name: str) -> Scene: ... + def stop(self) -> None: ... def spawn_window( port: typing.SupportsInt | typing.SupportsIndex = 5555, diff --git a/src/slamd/bindings/geom.pyi b/src/slamd/bindings/geom.pyi index bb094bd..838cfc2 100644 --- a/src/slamd/bindings/geom.pyi +++ b/src/slamd/bindings/geom.pyi @@ -27,7 +27,9 @@ def Arrows( Create an Arrows geometry """ -def Box() -> slamd.bindings._geom_types.Box: +def Box( + dims: numpy.ndarray = ..., color: numpy.ndarray = ... +) -> slamd.bindings._geom_types.Box: """ Create a Box geometry """ @@ -68,6 +70,20 @@ def Mesh( Create a SimpleMesh geometry from raw data """ +@typing.overload +def Mesh( + vertices: numpy.ndarray, + vertex_colors: numpy.ndarray, + triangle_indices: collections.abc.Sequence[ + typing.SupportsInt | typing.SupportsIndex + ], + vertex_normals: numpy.ndarray, + alpha: typing.SupportsFloat | typing.SupportsIndex, +) -> slamd.bindings._geom_types.Mesh: + """ + Create a SimpleMesh geometry from raw data with transparency + """ + def Plane( normal: numpy.ndarray, point: numpy.ndarray, diff --git a/src/slamd/geom/__init__.py b/src/slamd/geom/__init__.py index aaf4daf..0372d2c 100644 --- a/src/slamd/geom/__init__.py +++ b/src/slamd/geom/__init__.py @@ -1,10 +1,15 @@ -from ..bindings.geom import ( +from .overrides import ( + Arrows, Box, CameraFrustum, Mesh, + Plane, + PointCloud, + PolyLine, + Sphere, + Spheres, Triad, ) -from .overrides import PointCloud, PolyLine, Sphere, Arrows, Plane, Spheres __all__ = [ diff --git a/src/slamd/geom/overrides.py b/src/slamd/geom/overrides.py index 64e3f70..afec2f6 100644 --- a/src/slamd/geom/overrides.py +++ b/src/slamd/geom/overrides.py @@ -1,5 +1,8 @@ import numpy as np from ..bindings.geom import ( + Box as Box_internal, + CameraFrustum as CameraFrustum_internal, + Mesh as Mesh_internal, PointCloud as PointCloud_internal, Spheres as Spheres_internal, PolyLine as PolyLine_internal, @@ -12,6 +15,61 @@ from .._utils.handle_input import process_color, process_radii, process_single_color +def Box( + dims: np.ndarray, + color: np.ndarray | tuple[int, int, int] = Color.orange, +): + """An axis-aligned box, centered at the node transform. + + Args: + dims: (3,) float32 array, dimensions along x, y, z. + color: Either a (3,) float32 array with RGB in (0, 1), or an RGB tuple (0-255). + """ + return Box_internal(dims, process_single_color(color)) + + +def CameraFrustum( + intrinsics: np.ndarray, + image_width: int, + image_height: int, + image: np.ndarray | None = None, + scale: float = 1.0, +): + """A camera frustum wireframe for visualizing camera poses. + + Args: + intrinsics: (3, 3) float32 camera intrinsics matrix. + image_width: Image width in pixels. + image_height: Image height in pixels. + image: Optional (H, W, 3) uint8 RGB image to display on the frustum. + scale: Size of the rendered frustum. + """ + return CameraFrustum_internal(intrinsics, image_width, image_height, image, scale) + + +def Mesh( + positions: np.ndarray, + colors: np.ndarray, + indices: np.ndarray, + normals: np.ndarray | None = None, + alpha: float = 1.0, +): + """A triangle mesh. + + Args: + positions: (N, 3) float32 vertex positions. + colors: (N, 3) float32 per-vertex RGB colors in (0, 1). + indices: (M,) uint32 triangle indices (M must be a multiple of 3). + normals: Optional (N, 3) float32 per-vertex normals. Auto-computed if omitted. + alpha: Opacity, 0.0 (transparent) to 1.0 (opaque). + """ + if normals is None: + return Mesh_internal(positions, colors, indices) + if alpha == 1.0: + return Mesh_internal(positions, colors, indices, normals) + return Mesh_internal(positions, colors, indices, normals, alpha) + + def PointCloud( positions: np.ndarray, colors: np.ndarray | tuple[int, int, int] = Color.black, @@ -21,15 +79,15 @@ def PointCloud( """A 3D point cloud. Args: - positions: An N x 3 array of the 3D point positions. - colors: The color of the points. Can be one of: - - array of shape N x 3 of RGB colors in (0, 1) - - array of shape 3 with a single RGB color in (0, 1) - - tuple of an RGB value, 0–255 - radii: The radius of each point. Can be: - - array of shape N with a radius per point - - single float for uniform radius - min_brightness: Minimum brightness applied to the points. + positions: (N, 3) float32 point positions. + colors: Per-point or uniform color: + - (N, 3) float32 RGB in (0, 1) + - (3,) float32 single RGB + - RGB tuple (0-255) + radii: Per-point or uniform radius: + - (N,) float32 array + - single float + min_brightness: Minimum brightness floor. """ n = positions.shape[0] colors_np = process_color(colors, n) @@ -44,13 +102,13 @@ def PolyLine( color: np.ndarray | tuple[int, int, int] = Color.red, min_brightness: float = 1.0, ): - """A 3D polyline made of straight segments. + """A 3D polyline tube through a sequence of points. Args: - points: An N x 3 array of points the polyline passes through. - thickness: Thickness of the line. - color: Either a numpy array with values in (0, 1), or an RGB tuple (0–255). - min_brightness: Minimum brightness applied to the line. + points: (N, 3) float32 positions the line passes through. + thickness: Tube diameter. + color: (3,) float32 RGB in (0, 1), or RGB tuple (0-255). + min_brightness: Minimum brightness floor. """ color_np = process_single_color(color) return PolyLine_internal(points, thickness, color_np, min_brightness) @@ -61,7 +119,7 @@ def Sphere(radius: float, color: np.ndarray | tuple[int, int, int] = Color.blue) Args: radius: Radius of the sphere. - color: Either a numpy array with values in (0, 1), or an RGB tuple (0–255). + color: (3,) float32 RGB in (0, 1), or RGB tuple (0-255). """ return Sphere_internal(radius, process_single_color(color)) @@ -75,13 +133,13 @@ def Arrows( """A collection of 3D arrows from start to end points. Args: - starts: N x 3 array of arrow starting points. - ends: N x 3 array of arrow end points. - colors: The color of the arrows. Can be: - - array of shape N x 3 of RGB colors in (0, 1) - - array of shape 3 with a single RGB color in (0, 1) - - tuple of an RGB value, 0–255 - thickness: The thickness of the arrow shafts. + starts: (N, 3) float32 arrow start positions. + ends: (N, 3) float32 arrow end positions. + colors: Per-arrow or uniform color: + - (N, 3) float32 RGB in (0, 1) + - (3,) float32 single RGB + - RGB tuple (0-255) + thickness: Arrow shaft thickness. """ return Arrows_internal( @@ -96,10 +154,26 @@ def Plane( radius: float = 1.0, alpha: float = 0.8, ): + """A flat circular disc in 3D. + + Args: + normal: (3,) float32 plane normal direction. + point: (3,) float32 disc center position. + color: (3,) float32 RGB in (0, 1), or RGB tuple (0-255). + radius: Disc radius. + alpha: Opacity, 0.0 (transparent) to 1.0 (opaque). + """ return Plane_internal(normal, point, process_single_color(color), radius, alpha) -def Triad(pose: np.ndarray | None = None, scale: float = 1.0, thickness: float = 1.0): +def Triad(pose: np.ndarray | None = None, scale: float = 1.0, thickness: float = 0.1): + """An RGB axis triad (X=red, Y=green, Z=blue). + + Args: + pose: Optional 4x4 float32 homogeneous transform matrix. If None, uses identity. + scale: Length of each axis arrow. + thickness: Thickness of the arrow shafts. + """ return Triad_internal(pose, scale, thickness) @@ -109,18 +183,18 @@ def Spheres( radii: np.ndarray | float = 1.0, min_brightness: float = 0.3, ): - """3D spheres with per-point color and radius. + """A collection of 3D spheres. Args: - positions: An N x 3 array of the 3D positions. - colors: The color of the spheres. Can be one of: - - array of shape N x 3 of RGB colors in (0, 1) - - array of shape 3 with a single RGB color in (0, 1) - - tuple of an RGB value, 0–255 - radii: The radius of each sphere. Can be: - - array of shape N with a radius per sphere - - single float for uniform radius - min_brightness: Minimum brightness applied to the spheres. + positions: (N, 3) float32 sphere center positions. + colors: Per-sphere or uniform color: + - (N, 3) float32 RGB in (0, 1) + - (3,) float32 single RGB + - RGB tuple (0-255) + radii: Per-sphere or uniform radius: + - (N,) float32 array + - single float + min_brightness: Minimum brightness floor. """ n = positions.shape[0] colors_np = process_color(colors, n) diff --git a/tools/build.py b/tools/build.py index e505be4..cb93385 100644 --- a/tools/build.py +++ b/tools/build.py @@ -67,6 +67,14 @@ def main(): check=True, ) + stubs_dir = REPO_DIR / "src" / "slamd" / "bindings" + stub_files = list(stubs_dir.glob("*.pyi")) + if stub_files: + print("\n=== Formatting and fixing stubs with ruff ===") + stub_paths = [str(f) for f in stub_files] + subprocess.run(["ruff", "check", "--fix"] + stub_paths, cwd=REPO_DIR, check=True) + subprocess.run(["ruff", "format"] + stub_paths, cwd=REPO_DIR, check=True) + print("\n=== Done ===") diff --git a/uv.lock b/uv.lock index facb8e6..88b89b1 100644 --- a/uv.lock +++ b/uv.lock @@ -448,7 +448,7 @@ wheels = [ [[package]] name = "slamd" -version = "3.0.1" +version = "3.1.0" source = { editable = "." } dependencies = [ { name = "numpy" },