Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 8 additions & 30 deletions .github/workflows/pip_publish.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: Upload Python Package to PyPI when a Release is Created
name: Build Python Wheels

on:
pull_request:
release:
types: [created]

Expand All @@ -13,16 +14,6 @@ jobs:
with:
submodules: recursive

- name: Copy stuff
run: |
rm -rf python_bindings/vendored_deps
rm -rf python_bindings/slamd_src
rm -rf python_bindings/README.md

cp -r vendored_deps python_bindings/vendored_deps
cp -r slamd python_bindings/slamd_src
cp README.md python_bindings/README.md

- name: Set up Python
uses: actions/setup-python@v4
with:
Expand All @@ -49,15 +40,13 @@ jobs:
libxkbcommon-devel \
mesa-libEGL-devel \
mesa-libGLES-devel
run: |
cd python_bindings
cibuildwheel --output-dir dist
run: cibuildwheel --output-dir dist

- name: Upload Linux artifact
uses: actions/upload-artifact@v4
with:
name: dist-linux
path: python_bindings/dist
path: dist

build-macos:
name: Build wheels on macOS
Expand All @@ -67,16 +56,6 @@ jobs:
with:
submodules: recursive

- name: Copy stuff
run: |
rm -rf python_bindings/vendored_deps
rm -rf python_bindings/slamd_src
rm -rf python_bindings/README.md

cp -r vendored_deps python_bindings/vendored_deps
cp -r slamd python_bindings/slamd_src
cp README.md python_bindings/README.md

- name: Set up Python
uses: actions/setup-python@v4
with:
Expand All @@ -88,20 +67,19 @@ jobs:
pip install cibuildwheel

- name: Build wheels with cibuildwheel
run: |
cd python_bindings
cibuildwheel --output-dir dist
run: cibuildwheel --output-dir dist

- name: Upload macOS artifact
uses: actions/upload-artifact@v4
with:
name: dist-macos
path: python_bindings/dist
path: dist

publish:
name: Publish to PyPI
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs: [build-linux, build-macos] # 🔥 waits for both builds
needs: [build-linux, build-macos]
environment:
name: pypi
permissions:
Expand Down
59 changes: 0 additions & 59 deletions .github/workflows/test_python_build.yml

This file was deleted.

6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ build/
**/*.so
**/*__pycache__
.cache/
.testvenv/
.testvenv/
dist/
*.egg-info/
_skbuild/
src/slamd/slamd_window
7 changes: 3 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{
"clangd.arguments": [
"--compile-commands-dir=python_bindings/build",
// "--compile-commands-dir=examples/build",
"--compile-commands-dir=build/cp312-cp312-linux_x86_64",
"--function-arg-placeholders=false",
"--tweaks=-Wall",
"--tweaks=-Wextra",
Expand Down Expand Up @@ -119,8 +118,8 @@
"C_Cpp.inlayHints.autoDeclarationTypes.showOnLeft": false,
"C_Cpp.inlayHints.parameterNames.enabled": true,
"C_Cpp.inlayHints.referenceOperator.enabled": true,
"python.analysis.extraPaths": ["python_bindings/src/"],
"cmake.sourceDirectory": "/home/robert/projects/slam_dunk/examples",
"python.analysis.extraPaths": ["src/"],
"cmake.sourceDirectory": "${workspaceFolder}",
"python.analysis.exclude": [
"**/build/",
"**/.venv/",
Expand Down
8 changes: 4 additions & 4 deletions python_bindings/CMakeLists.txt → CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
cmake_minimum_required(VERSION 3.14)
project(myviz_bindings LANGUAGES CXX)
project(slamd LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
set(LIB_DIR vendored_deps)
set(LIB_DIR ${CMAKE_CURRENT_SOURCE_DIR}/vendored_deps)

set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(BUILD_SHARED_LIBS OFF)
Expand Down Expand Up @@ -36,7 +36,7 @@ add_compile_definitions(SPDLOG_ACTIVE_LEVEL=SPDLOG_LEVEL_OFF)

# EXCLUDE_FROM_ALL prevents vendored deps from installing their own
# files (static archives, examples, tools) into the wheel.
add_subdirectory(slamd_src EXCLUDE_FROM_ALL)
add_subdirectory(slamd EXCLUDE_FROM_ALL)

find_package(Python REQUIRED COMPONENTS Interpreter Development.Module REQUIRED)

Expand Down Expand Up @@ -82,7 +82,7 @@ target_link_libraries(bindings
fmt::fmt
)

# slamd_window is excluded from ALL due to EXCLUDE_FROM_ALL on slamd_src,
# slamd_window is excluded from ALL due to EXCLUDE_FROM_ALL on slamd,
# so we need to make bindings depend on it to ensure it gets built.
add_dependencies(bindings slamd_window)

Expand Down
144 changes: 58 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,125 +1,97 @@
![](./images/logo.png)
<img src="./images/logo.png" width="100%">

---

SlamDunk is a powerful and user-friendly Python library for making 3D and 2D visualizations for prototyping, data exploration, and algorithm development.
<img src="./images/galaxy.gif" width="49%"> <img src="./images/lorenz_attractor.gif" width="49%">

It is lightweight, built using OpenGL and ImGui.
<img src="./images/double_pendulum.gif" width="49%"> <img src="./images/moving_spheres.gif" width="49%">

# Examples
`slamd` is a 3D visualization library for Python. `pip install`, write a few lines, and you have a GPU-accelerated interactive 3D viewer. No event loops, no boilerplate — just set geometry and it shows up.

## Hello world

Here is a simple "hello world" program for a SlamDunk visualization.

```python
import slamd

if __name__ == "__main__":
vis = slamd.Visualizer("Hello world")

scene = vis.scene("scene")

scene.set_object("/origin", slamd.geom.Triad())

vis.hang_forever()
```bash
pip install slamd
```

Running this program results in the following interactive visualization:
![](./images/hello_world.png)

This example highlights the main components of SlamDunk.

The `Visualizer` object maintains the state of the visualization, and starts a TCP server that the visualization window connects to.
## Why slamd?

By default, it spawns a window process that reads from it and displays the visualizations. You can opt out of this with the `spawn` argument, and control the port with the `port` argument.
In this case, you can start a window with the `slamd-window` executable:
Most 3D visualization in Python is either painfully slow (matplotlib's 3D mode), or requires you to learn a massive framework. `slamd` is neither.

```
slamd-window --port [port] --ip [ip]
```
- **Dead simple** — create a visualizer, set geometry, done. The viewer window spawns automatically in a separate process. No event loop, no main thread hijacking, no callbacks.
- **Real 3D rendering** — GPU-accelerated OpenGL. Handles millions of points, animated meshes, and real geometry at interactive framerates. This is not a plot library pretending to do 3D.
- **Pose tree** — objects live in a transform tree (like ROS TF or a scene graph). Set a parent transform and everything underneath moves. Makes articulated and hierarchical scenes trivial.
- **The right primitives** — point clouds, meshes, camera frustums, triads, arrows, polylines, spheres, planes. The stuff you actually need when working with 3D data, robotics, or SLAM.

This client-server architecture allows launching a visualizer a remote server, and connecting to it on your local machine.
## How is this different from Rerun?

A `Scene` object represents and contains a tree of 3D objects, accessed by paths like `/comp1/comp2/comp3`. 3D poses and `Geometry` objects can be assigned with the `set_transform` and `set_object` methods.
`slamd` is a **stateful viewer**, not a logging database. There's no append-only log, no timeline, no timestamps forced onto your data. You have a tree of geometry — you set objects, move them, delete them, and what you see is what's there right now.

`Geometry` objects represent the objects that are displayed in the scene.
Rerun is powerful, but it's a big tool with a lot of concepts. `slamd` does one thing: show your geometry, right now, with minimal API surface. If you want a data recording platform with time-series scrubbing, use Rerun. If you want to throw some geometry on screen and look at it, use `slamd`.

## Multiple scenes

SlamDunk uses ImGui to allow multiple sub-windows with floating and docking support inside the SlamDunk viewer. The following example illustrates creating two windows, each showing its own scene.
## Quick Start

```python
import slamd
import numpy as np

if __name__ == "__main__":
vis = slamd.Visualizer("two windows")

scene1 = vis.scene("scene 1")
scene2 = vis.scene("scene 2")

scene1.set_object("/box", slamd.geom.Box())

scene2.set_object("/origin", slamd.geom.Triad())

scene2.set_object("/ball", slamd.geom.Sphere(2.0))

sphere_transform = np.identity(4, dtype=np.float32)
sphere_transform[:, 3] = np.array([5.0, 1.0, 2.0, 1.0])

scene2.set_transform("/ball", sphere_transform)

vis.hang_forever()

vis = slamd.Visualizer("Hello world")
scene = vis.scene("scene")
scene.set_object("/origin", slamd.geom.Triad())
```

The resulting window looks like this:

![](./images/two_scenes.png)
![](./images/hello_world.gif)

The windows are fully controllable - you can drag them around, make tabs, use them in floating mode, or dock them to the sides like you see in the screenshot. All of this is supported by [ImGui](https://github.com/ocornut/imgui).
That's it. A window opens with an interactive 3D view.

Here is a slightly more elaborate example of something you can do with SlamDunk:
Objects live in a transform tree — move a parent and children follow:

![](./images/moving_mesh.gif)

Or this one:
```python
scene.set_object("/robot/camera/frustum", slamd.geom.CameraFrustum(K, w, h, scale=0.2))
scene.set_object("/robot/lidar/cloud", slamd.geom.PointCloud(pts, colors, point_size))

![](./images/spiral.gif)
# Move the whole robot — camera and lidar come with it
scene.set_transform("/robot", pose)
```

## Supported geometry primitives
## Multiple Windows

### 3D
Create multiple scenes — each gets its own sub-window with ImGui docking. Drag, tab, float, or dock them however you like:

- Camera Frustums (with optional image) (`slamd.geom.CameraFrustum`)
- Arrows/Vectors (`slamd.geom.Arrows`)
- Arbitrary meshes (`slamd.geom.Mesh`)
- Planes (`slamd.geom.Plane`)
- Point Clouds (`slamd.geom.PointCloud`)
- Piecewise linear curves (`slamd.geom.PolyLine`)
- Spheres (`slamd.geom.Sphere`)
- Triads/reference frames (`slamd.geom.Triad`)
```python
vis = slamd.Visualizer("multi-view")
scene1 = vis.scene("RGB camera")
scene2 = vis.scene("point cloud")

## 2D
scene1.set_object("/frustum", slamd.geom.CameraFrustum(K, w, h, img, 1.0))
scene2.set_object("/cloud", slamd.geom.PointCloud(pts, colors, 0.3, 0.5))
```

- Images (`slamd.geom2d.Image`)
- Points (`slamd.geom2d.Points`)
- Piecewise linear curves (`slamd.geom2d.PolyLine`)
- Circles (`slamd.geom2d.Circles`)
![](./images/two_windows.gif)

## Further reading
## Geometry

The examples in `python_examples` showcase some more features of SlamDunk. Some examples are canvases for 2D visualizations and lots of additional geometry primitives such as point clouds, meshes, camera frustums, etc.
- Point clouds — `slamd.geom.PointCloud`
- Meshes — `slamd.geom.Mesh`
- Camera frustums (with image) — `slamd.geom.CameraFrustum`
- Arrows — `slamd.geom.Arrows`
- Polylines — `slamd.geom.PolyLine`
- Spheres — `slamd.geom.Sphere` / `slamd.geom.Spheres`
- Triads — `slamd.geom.Triad`
- Planes — `slamd.geom.Plane`
- Boxes — `slamd.geom.Box`

# Installation
## Installation

Wheels are available on [PyPi](https://pypi.org/project/slamd/), so you can simply
Wheels on [PyPI](https://pypi.org/project/slamd/) for Linux and macOS (Python >= 3.11):

```bash
pip install slamd
```

# Contributions
Only runtime dependency is `numpy >= 1.23`.

## Examples

See [examples/](./examples) for the full set.

## License

All contributions and feedback are welcome and appreciated!
Apache 2.0 — see [LICENSE](./LICENSE).
Loading
Loading