Interactive visualization¶
DEMVisualizer is a native OpenGL 3.3 simulation inspector with a Dear ImGui
sidebar and raylib window/input support. It is enabled in ordinary C++ builds
and Python wheels by default. Existing visualizer calls remain supported.
The viewer does not advance simulation time: each call to Render
synchronously captures and displays the solver state at that moment.
Recommended scope¶
Use the interactive viewer for proof-of-concept (PoC) work and small-scale simulations: checking geometry placement, inspecting motion, and exploring solver settings. It is not recommended as an always-on viewer for large production runs or throughput benchmarks. For those workloads, run without interactive rendering, write output at a suitable interval, and inspect it later in ParaView. There is no universal particle-count cutoff: mesh complexity, sphere detail, hardware, and display frequency all affect the cost.
Sphere components use a shared instanced mesh. Triangle geometry stays in
persistent indexed buffers in owner-local coordinates. Each display frame
uploads owner poses and colors, rather than reconstructing component positions
and triangle vertices on the CPU. Geometry uploads occur after initialization,
Update(), mesh deformation (including wear), or a sphere-detail change.
Writing a responsive visualization loop¶
Render() handles mouse and keyboard input as well as drawing. The target FPS
is a frame-rate limit, not a background event loop: it cannot make the window
respond while your application is inside a blocking solver call or file write.
A simulation-time interval such as DoDynamicsThenSync(1.0 / 60.0) can take
seconds of wall time to compute; it does not imply a 60 Hz display.
Use short dynamics advances and a wall-clock display deadline, independently
of simulation-time file-output intervals. The examples below advance one solver
timestep per call. They check the deadline between calls, keep drawing while
paused, and consume ShouldStep() exactly once per intended advance. A paused
single-step therefore advances one timestep. Check for window closure again
after rendering so closing the viewer does not advance another step.
Schedule the next deadline after rendering finishes. The renderer’s FPS limiter may sleep for a whole frame; scheduling from the start can make every subsequent timestep immediately trigger another render, leaving little time for physics. A 16 ms work interval below is a starting point, not a guaranteed 60 FPS rate. Increase it to favor simulation throughput. Keep your physical timestep unchanged when tuning the display frequency.
C++ usage¶
After configuring and initializing solver:
#include <chrono>
#include "DEM/API.h"
#include "DEM/utils/DEMVisualizer.h"
deme::DEMVisualizer visualizer(solver);
visualizer.SetTargetFPS(60);
visualizer.Initialize();
using Clock = std::chrono::steady_clock;
auto next_display = Clock::now();
const auto work_interval = std::chrono::milliseconds(16);
// Match the solver's internal float timestep; a slightly larger double
// duration could request an extra step. This example uses a fixed timestep.
const float dt = static_cast<float>(solver.GetTimeStepSize());
while (visualizer.Run()) {
if (visualizer.IsPaused() || Clock::now() >= next_display) {
visualizer.Render();
next_display = Clock::now() + work_interval;
}
if (!visualizer.Run())
break;
if (visualizer.ShouldStep())
solver.DoDynamics(dt);
}
solver.DoDynamicsThenSync(0.0); // Join both workers at the end.
visualizer.Close();
Python usage¶
After configuring and initializing solver:
import time
import numpy as np
import deme
visualizer = deme.DEMVisualizer(solver)
visualizer.SetTargetFPS(60)
visualizer.Initialize()
next_display = time.monotonic()
work_interval = 0.016
# Round to the internal float timestep before passing a Python float.
# This example uses a fixed timestep.
dt = float(np.float32(solver.GetTimeStepSize()))
try:
while visualizer.Run():
if visualizer.IsPaused() or time.monotonic() >= next_display:
visualizer.Render()
next_display = time.monotonic() + work_interval
if not visualizer.Run():
break
if visualizer.ShouldStep():
solver.DoDynamics(dt)
finally:
solver.DoDynamicsThenSync(0.0)
visualizer.Close()
DoDynamics returns after the dynamic worker finishes the requested advance;
these loops capture its state on the same application thread afterwards. They
do not need to reset both workers with DoDynamicsThenSync at every display
refresh. Do not run Render concurrently with dynamics from another thread.
For a finite simulation, also add your simulation-end condition to the loop.
If you change the solver timestep at runtime, refresh dt accordingly.
Spheres and triangles are both rendered by default. They can be controlled independently before or during the visualization loop:
visualizer.SetRenderSpheres(False)
visualizer.SetRenderTriangles(True)
Performance tradeoffs¶
Persistent geometry reduces rebuilding work, but the viewer is not free:
Each
Rendersynchronously transfers owner state to the CPU, prepares colors, and uploads frame data to OpenGL. Speed coloring and inspecting a selected owner also request velocity data. Paused redraws still capture state.Sphere detail and mesh facet counts affect drawing cost and GPU memory use. Geometry changes, including mesh deformation and wear, trigger geometry uploads. Hiding geometry reduces drawing work but does not eliminate the frame capture for all owners.
Frequent calls across the solver boundary, particularly from Python, add overhead. Drawing, state transfers, and the FPS limiter share the application’s time with simulation. More responsive interaction can mean a substantially slower overall run, even with few particles.
A long individual solver step, expensive output operation, or large frame transfer can still stall input. The wall-clock deadline is checked only when control returns to the application; rendering is not asynchronous.
Measure representative runs with and without calls to Render using the
same physics and output settings. Compare wall time per simulated second,
not just the viewer’s displayed FPS. In one development run,
SingleSphereCollide took about 287 seconds with output-frame-only rendering
and 521 seconds with frequent interactive updates (about 1.8 times as long).
That illustrates the tradeoff, not a portable benchmark or expected slowdown
for other scenes or machines.
For PoC work, start with a small scene, reduce sphere detail, and lengthen the work interval if throughput matters more than camera responsiveness. Pausing is useful for inspection, but still incurs redraw costs. For large or long runs, omit the viewer loop and use file output and postprocessing instead. Removing the viewer at build time is optional; see the headless build option below. Moving rendering to a background thread is not a supported shortcut.
Inspection controls¶
Left drag: orbit around the camera target. Middle drag: pan. Wheel: zoom.
F: frame visible geometry.Shift+F: frame the selected owner.Right click: select a sphere component or mesh triangle using depth-tested geometry IDs. The sidebar reports its owner, family, position, orientation, and linear velocity. Hidden geometry cannot be selected.
Space: pause/resume..: request one application-defined dynamics advance while paused (one solver timestep in the loops above). The application must honorShouldStep()as shown above. It consumes one pending step request; call it once per intended advance.F12or the screenshot button: savedeme-screenshot.pngin the current directory.RequestScreenshot(path)saves the next rendered frame elsewhere.Esc: close the window. The three existing visualizer demos continue their simulation/output after closing the window.
The sidebar controls sphere/mesh visibility, wireframe, the XY reference grid, sphere detail, per-family visibility/colors, and coloring by owner height or linear speed. Scalar modes include an automatic or manually specified range and a color legend. Height uses the owner center, not individual component centers. Values are in the simulation’s length/time units (the labels assume SI). Analytical boundaries currently remain outside this viewer’s geometry interface; use analytical VTK output for those surfaces.
These controls also have C++/Python entry points: SetPaused, RequestStep,
SetFamilyVisible, SetColorMode, FrameAll, FrameSelected, and
PickAt. Python color modes are deme.VisualizerColorMode.FAMILY, HEIGHT,
and SPEED; C++ uses deme::DEMVisualizerColorMode. PickAt uses logical
window coordinates and the last rendered frame. GetSelectedOwner returns
NULL_BODYID (the maximum 32-bit unsigned value) when nothing is selected;
GetSelectedGeometryID returns SIZE_MAX in that case. Selected geometry
indices are global sphere or triangle indices, distinguished by
IsSelectedSphere().
Camera settings supplied before initialization are preserved; otherwise the
first frame automatically fits the scene. The camera, frame buffers, and GUI
must be used on the thread owning the window. Only one native viewer window
may be open in a process. Pause does not block Render; keep rendering to
handle input while paused. The existing visualizer demos illustrate display
updates between solver calls, independently of file-output intervals. For
custom applications, use the loop pattern above and check ShouldStep()
before every advance, including advances between display deadlines.
Scene/frame interface¶
GetVisualizationScene() returns local sphere offsets/radii, local triangle
vertices, owner mappings, and a geometry revision. Cache it until the revision
in GetVisualizationFrame(frame, include_velocities=false) changes.
DEMVisualizationFrame contains reusable vectors indexed by owner ID:
positions, quaternions (x, y, z, w), families, and optional velocities. Python
uses deme.VisualizationFrame() and the same solver methods. Python field
access converts vectors to Python values; the native viewer uses the reusable
C++ buffers directly.
Capture scene/frame data only at a synchronized solver boundary. Runtime family
or rigid-pose changes update the frame without invalidating geometry. Runtime
mesh changes through solver/tracker APIs invalidate the scene even when the
facet count is unchanged. GetVisualizationSnapshot remains available for
consumers needing expanded world-space geometry.
This implementation uses synchronous host state transfer. CUDA/OpenGL interop, asynchronous rendering, sphere impostors, contact overlays, and force dragging are not part of this viewer version.
Verification¶
DEMTest_VisualizationScene checks local-to-world transforms, combined-owner
members, family/velocity updates, deformation invalidation, and insertion.
DEMTest_Visualizer additionally requires CUDA and a display; it exercises
rendering, occlusion-aware picking, filters, stepping requests, window resize,
and close/reopen. Pass a PNG filename to save a frame for inspection:
./build/bin/DEMTest_Visualizer /tmp/deme-visualizer.png
The existing DEMdemo_SingleSphereCollide, DEMdemo_MeshCollide, and
DEMdemo_MeshFalling continue to use the viewer and honor pause/single-step.
Linux build requirements¶
Dear ImGui v1.91.9b is vendored with its MIT license; builds require no additional download for the UI. The source build uses raylib’s bundled GLFW backend and requires the standard X11 and OpenGL development headers. On Debian or Ubuntu:
sudo apt-get install libx11-dev libxrandr-dev libxinerama-dev \
libxcursor-dev libxi-dev libgl1-mesa-dev
The GitHub wheel workflow installs the equivalent packages in its manylinux
container. At runtime, a graphical display and OpenGL implementation must be
available. WSL users should run under WSLg or configure DISPLAY to an X
server.
The visualizer can be omitted for a headless source build with:
cmake -S . -B build -DDEME_BUILD_VISUALIZER=OFF
Postprocessing with ParaView¶
Load output .vtk mesh files directly. For component-sphere CSV output from
WriteSphereFile, apply Table To Points using the X, Y, and Z
columns. Then apply Glyph, choose Sphere, select r as the scale
array, set the scale factor to 2 (radii become diameters), and choose
All Points. Reduce sphere resolution when visualizing large datasets.