Objective
I’m rendering N
instances of a sphere within a bounding box using vtkGlyph3DMapper
. The positions are generated procedurally to fill the box, and this part works well.
Problem
When N
becomes large (e.g., tens of thousands), performance drops significantly — especially during interaction. To prevent extreme cases, I compute the box’s capacity and limit the number of spheres accordingly. However, even within this capped number, rendering can be slow due to the volume of geometry.
I want to implement Level of Detail (LOD) to improve interaction performance while still showing all glyphs, just at a lower resolution when necessary.
What I’ve Tried
- I noticed
vtkGlyph3DMapper
has methods for enabling LOD (SetLODEnabled
, etc.), but when I use them, only a subset of the glyphs are visible, and the rest disappear. My expectation was to see all glyphs, but with simpler geometry. - I also tried using
vtkLODActor
and ‘vtkLODProp3D’, providing a low-resolution version of the sphere as an additional mapper. But during interaction, I see no change in visual fidelity or performance, and it seems the LOD is never triggered — even when rendering becomes slow.
Questions
- Is
vtkGlyph3DMapper
’s built-in LOD system meant to be used together withvtkLODActor
, or are they separate approaches? - When using
vtkLODActor
, is there a way to control the threshold at which it switches to lower-res mappers? TheSetLODMapperRenderTime()
method exists, but its documentation suggests it shouldn’t be used, and render time is hard to estimate accurately. - Is there a recommended way to efficiently render a very large number of identical glyphs with dynamic LOD switching in VTK?
I’ll attach code to demonstrate the setup I’m using. Any suggestions on best practices or alternatives would be greatly appreciated.
#include <QApplication>
#include <QSurfaceFormat>
#include <QVTKOpenGLNativeWidget.h>
#include <vtkGlyph3DMapper.h>
#include <vtkInformation.h>
#include <vtkInformationVector.h>
#include <vtkInteractorStyleTrackballActor.h>
#include <vtkGenericOpenGLRenderWindow.h>
#include <vtkPolyDataAlgorithm.h>
#include <vtkPolyData.h>
#include <vtkRenderer.h>
#include <vtkSphereSource.h>
#include <vtkSmartPointer.h>
#include <random>
class SphereLocationSource : public vtkPolyDataAlgorithm
{
public:
static SphereLocationSource *New();
vtkTypeMacro(SphereLocationSource, vtkPolyDataAlgorithm);
~SphereLocationSource() = default;
vtkSetMacro(NumSpheres, int);
protected:
SphereLocationSource() { this->SetNumberOfInputPorts(0); }
int RequestData(vtkInformation *vtkNotUsed(request), vtkInformationVector **vtkNotUsed(inputVector), vtkInformationVector *outputVector) override
{
vtkInformation *outInfo = outputVector->GetInformationObject(0);
vtkPolyData *output = vtkPolyData::SafeDownCast(outInfo->Get(vtkDataObject::DATA_OBJECT()));
vtkNew<vtkPolyData> polyData;
auto points = vtkSmartPointer<vtkPoints>::New();
// Ideal spacing between centers (2 * radius + some space between particles)
double minSpacing = 2.25;
int nx = std::cbrt(NumSpheres);
int ny = nx;
int nz = nx;
int nTotal = nx * ny * nz;
double dx = 100 / nx;
double dy = 50 / ny;
double dz = 40 / nz;
double offsetX = dx / 2;
double offsetY = dy / 2;
double offsetZ = dz / 2;
int count = 0;
for (int i = 0; i < nx; ++i)
{
for (int j = 0; j < ny; ++j)
{
for (int k = 0; k < nz; ++k)
{
if (count >= nTotal)
break;
double x = offsetX + i * dx;
double y = offsetY + j * dy;
double z = offsetZ + k * dz;
points->InsertNextPoint(x, y, z);
++count;
}
}
}
for (int i = 0; count < nTotal; ++i)
{
int xi = i % (nx - 1);
int yj = (i / (nx - 1)) % (ny - 1);
int zk = (i / ((nx - 1) * (ny - 1))) % (nz - 1);
double x = offsetX + (xi + 0.5) * dx;
double y = offsetY + (yj + 0.5) * dy;
double z = offsetZ + (zk + 0.5) * dz;
points->InsertNextPoint(x, y, z);
++count;
}
// Adding some jitter to the locations so the randomization is implied.
std::mt19937 rng(42); // Fixed seed = reproducible
std::uniform_real_distribution<double> jitter(-2 * minSpacing, 2 * minSpacing);
for (vtkIdType i = 0; i < points->GetNumberOfPoints(); ++i)
{
double p[3];
points->GetPoint(i, p);
p[0] += jitter(rng);
p[1] += + jitter(rng);
p[2] += jitter(rng);
points->SetPoint(i, p);
}
polyData->SetPoints(points);
output->ShallowCopy(polyData);
return 1;
}
private:
int NumSpheres = 10;
};
vtkStandardNewMacro(SphereLocationSource);
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QApplication app(argc, argv);
QSurfaceFormat::setDefaultFormat(QVTKOpenGLNativeWidget::defaultFormat());
vtkNew<vtkSphereSource> sphere;
vtkNew<vtkSphereSource> lowResSphere;
lowResSphere->SetPhiResolution(4);
lowResSphere->SetThetaResolution(4);
vtkNew<SphereLocationSource> locations;
locations->SetNumSpheres(100000);
vtkNew<vtkGlyph3DMapper> mapper;
mapper->ScalarVisibilityOff();
mapper->SetInputConnection(locations->GetOutputPort());
mapper->SetSourceConnection(sphere->GetOutputPort());
// mapper->SetCullingAndLOD(true);
// mapper->SetLODColoring(true);
// mapper->SetNumberOfLOD(2);
// mapper->SetLODDistanceAndTargetReduction(0, 5.0, 0.0);
// mapper->SetLODDistanceAndTargetReduction(1, 10.0, 0.8);
vtkNew<vtkGlyph3DMapper> lowResMapper;
lowResMapper->ScalarVisibilityOff();
lowResMapper->SetInputConnection(locations->GetOutputPort());
lowResMapper->SetSourceConnection(lowResSphere->GetOutputPort());
// Actor trial 1
vtkNew<vtkActor> actor;
actor->SetMapper(mapper);
// Actor trial 2
// vtkNew<vtkLODActor> glyphActor;
// actor->SetMapper(mapper);
// glyphActor->AddLODMapper(lowResMapper);
// Actor trial 3
// vtkNew<vtkLODProp3D> glyphActor;
// glyphActor->AddLOD(mapper, 0.1);
// glyphActor->AddLOD(lowResMapper, 0.0001);
// glyphActor->AutomaticLODSelectionOn();
vtkNew<vtkRenderer> renderer;
renderer->AddActor(actor);
renderer->SetBackground(0.1, 0.2, 0.4);
renderer->ResetCamera();
vtkNew<vtkGenericOpenGLRenderWindow> renderWindow;
renderWindow->AddRenderer(renderer);
QVTKOpenGLNativeWidget vtkWidget;
vtkWidget.setRenderWindow(renderWindow);
// vtkWidget.renderWindow()->GetInteractor()->SetStillUpdateRate(0.001);
// vtkWidget.renderWindow()->GetInteractor()->SetDesiredUpdateRate(60);
vtkWidget.resize(800, 600);
vtkWidget.show();
return app.exec();
}