A real-time GPU particle simulator in C++20 and raw Vulkan. A million-plus particles are simulated and rendered entirely on the GPU, orbiting gravity wells you place with the mouse, and the whole field reacts live to whatever audio your system is playing.
Dependencies: a Vulkan driver, glslangValidator, GLFW, and GLM. On Fedora:
sudo dnf install cmake gcc-c++ vulkan-loader-devel glslang glfw-devel glm-devel
On Debian/Ubuntu the equivalents are libvulkan-dev glslang-tools libglfw3-dev libglm-dev.
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j$(nproc)
./build/nova
Shaders in shaders/ are compiled to SPIR-V by the build (glslangValidator,
wired up as custom commands in CMake), nothing precompiled is checked in.
Dear ImGui and miniaudio are vendored in third_party/.
- Left click: place a gravity well, or grab and drag a nearby one
- Right click: remove the nearest well
- Tab: switch between gravity and boids mode
- Panel: particle count, gravity strength, well strength, boid rule weights, live audio band meters, reset
Audio capture prefers a monitor/loopback source (PipeWire and PulseAudio expose these), so it reacts to whatever the system is playing. If no monitor source exists it falls back to the default capture device. Band levels are normalized by a slow per-band auto gain, so it works at any system volume.
Measured on an RTX 4060 Laptop GPU (driver 580 series, Vulkan 1.4), 1280x720, release build. GPU times are from Vulkan timestamp queries around the compute and render passes; fps is the uncapped wall-clock rate with mailbox present.
Gravity mode:
| Particles | fps | compute | render |
|---|---|---|---|
| 500k | 1048 | 0.030 ms | 0.554 ms |
| 1M | 641 | 0.062 ms | 0.995 ms |
| 2M | 371 | 0.230 ms | 1.86 ms |
| 4M | 205 | 0.518 ms | 3.68 ms |
Boids mode (spatial grid neighbor search, 64x64 cells):
| Particles | fps | compute | render |
|---|---|---|---|
| 500k | 143 | 6.63 ms | 0.225 ms |
| 1M | 75 | 12.5 ms | 0.340 ms |
Render covers three passes: the trail fade plus particle accumulation into a half-float offscreen target, the tonemapped composite, and a second particle draw for the live layer. At 1M particles gravity mode uses about 1 ms of GPU time per frame, roughly 6% of a 60fps budget. Boids is the expensive mode because every particle surveys its 3x3 grid neighborhood, and it still clears 60fps at 1M.
The CPU does no per-particle work. Per frame it updates push constants (audio bands, delta time), writes at most 16 gravity wells with vkCmdUpdateBuffer, and records the dispatches and one draw. Particle state lives in two device-local ping-pong buffers, one read and one written per dispatch, with a single pipeline barrier covering both the vertex-input read in the same frame and the compute read in the next.
- Vulkan context, swapchain, compute and graphics pipelines by hand, no wrapper libraries
- N-body gravity toward user-placed wells with softened falloff, orbital seeding, framerate-independent drag, and a gentle curl force tuned against the drag so coherent rotation survives and winds into spiral arms while random motion decays
- Boids (separation, alignment, cohesion) over the same particle buffer, using a fixed-capacity uniform grid rebuilt on the GPU each frame
- Audio capture through miniaudio, 1024-point FFT, bass/mid/treble bands with per-band auto gain; bass punches well strength and boid cohesion, mid stirs the swirl and swells point size, treble adds shimmer and brightness
- Persistence trails: particles accumulate into a half-float HDR buffer that fades instead of clearing, then a fullscreen pass tonemaps it (Reinhard plus a black floor and vignette), with the live positions drawn crisp on top
- GPU timestamp profiling (the numbers above), Dear ImGui panel, frame dump
mode (
NOVA_DUMP=dir) for capturing footage
Not implemented: true all-pairs N-body (the attractors are the wells, all-pairs is O(N^2) and dies orders of magnitude before 1M), 3D simulation, a separate bloom blur chain. The glow comes from additive accumulation and the tonemap.
Debug knobs, all environment variables: NOVA_PARTICLES=n initial count,
NOVA_BOIDS=1 start in boids mode, NOVA_PROFILE=1 per-second timing lines on
stderr, NOVA_VALIDATION=1 enable validation layers, NOVA_SELFTEST=1 run a
compute readback self-test at startup, NOVA_DUMP=dir write frames as PPM.
- miniaudio for audio capture
- Dear ImGui for the panel
- GLFW and GLM
- Vulkan Tutorial and Sascha Willems' examples as references for the swapchain dance and the compute-graphics sync
