Processing math: 0%
DGtal 2.0.0
Display3D: a stream mechanism for displaying 3D DGtal objects

This part of the manual describes how to visualize 3D objects and how to import them from binary file (.obj or pgm3d)

Author
Bertrand Kerautret, Martial Tola, Aline Martin, David Coeurjolly, Bastien Doignies

Display3D: a stream mechanism from abstract class Display3D

The semi-abstract template class Display3D defines the stream mechanism to display 3d primitive (like PointVector, DigitalSetBySTLSet, Object ...). The PolyscopeViewer class permits an interactive visualization (based on Polyscope (OpenGL backend)).

Display3D have two template parameters which correspond to the digital space and the Khalimsky space used to put the figures. From the Digital Space and Khalimsky Space, we use the associated embedding mechanism to convert digital objects to structures in \mathbb{R}^n.

Interactive visualization from PolyscopeViewer

The class PolyscopeViewer is a wrapper around the Polyscope library ( https://polyscope.run/ ).
It permits displaying geometrical data such as Point, Lines and (volumetric) meshes. The library handles 3D scenes movement and allows to attributes of displayed objects (such as colors and colormaps). The graphical interface is based on ImGUI ( https://github.com/ocornut/imgui )which can be further extended.

First to use the PolyscopeViewer stream, you need to include the following headers:

#include "DGtal/io/3dViewers/PolyscopeViewer.h"A

The following code snippet defines three points and a rectangular domain in Z3. It then displays them in a PolyscopeViewer object. The full code is in viewer3D-1-points.cpp.

The first step to draw objects is to create an instance of the PolyscopeViewer class:

using namespace DGtal;
using namespace Z3i;
PolyscopeViewer<> viewer;
Z3i this namespace gathers the standard of types for 3D imagery.
DGtal is the top-level namespace which contains all DGtal functions and types.

Then we can display some 3D primitives:

Point p1( 0, 0, 0 );
Point p2( 5, 5 ,5 );
Point p3( 2, 3, 4 );
Domain domain( p1, p2 );
viewer << domain;
viewer << p1 << p2 << p3;
viewer.show();
void show() override
Starts the event loop and display of elements.
MyPointD Point
Domain domain
HyperRectDomain< Space > Domain

The show() method enters the visualization loop and should be call after all object are drawn with the viewer. You should obtain the following visualization:

Digital point visualization with PolyscopeViewer.

Interactive change of rendering


Polyscope has its own default rendering settings. Many parameters can be changed either programatically or at runtime directly within the viewer. This includes camera parameters (up direction, far/near plane, ...), materials of objects, lighting and many more. See polyscope documentation for available options ( https://polyscope.run/ ).

Note
Some rendered image of this documentation may have altered parameters to improve visibility such as scene extents or transparency mode.

Visualization of DigitalSet and digital objects

The PolyscopeViewer class also allows to display directly a DigitalSet. The first step is to create a DigitalSet for example from the Shape class.

PolyscopeViewer v;
Point p1( 0, 0, 0 );
Point p2( 10, 10 , 10 );
Domain domain( p1, p2 );
v << p1 << p2 << domain;
DigitalSet shape_set( domain );
Shapes<Domain>::addNorm1Ball( shape_set, Point( 5, 5, 5 ), 2 );
Shapes<Domain>::addNorm2Ball( shape_set, Point( 3, 3, 3 ), 2 );
shape_set.erase(Point(3,3,3));
shape_set.erase(Point(6,6,6));
v << shape_set;
v.show();

You should obtain the following visualization (see example: viewer3D-2-sets.cpp ):

Digital point visualization with PolyscopeViewer.

Drawmode selection: the example of digital objects in 3D

As for Board2D, a mode can be chosen to display elements. The viewer has several methods to adjust how objects are drawn.

viewer.drawAdjacencies(true /* or false to disable*/);
void drawAdjacencies(bool toggle=true)

or change the pair of adjacencies

Object6_18 shape1( dt6_18, shape_set );
Object18_6 shape2( dt18_6, shape_set );
viewer << shape1;
viewer << shape2;

You should obtain the two following visualizations (see example: viewer3D-3-objects.cpp ):

6-18 digital Adjacencies visualization with PolyscopeViewer.
18-6 digital Adjacencies visualization with PolyscopeViewer.

Note that digital set was displayed with transparency by setting a custom color within the viewer.

Useful modes for several 3D drawable elements

Listing of different modes

As for Board2D the object can be displayed with different possible mode:

Available modes Description Supported objects Method
DEFAULT Default rendering Any viewer.defaultStyle()
PAVING Draws points as cubes Point-based objects (Point, Domain, Object, DigitalSet, ...) viewer.drawAsPaving()
BALL Draws points as spheres Point-based objects (Point, Domain, Object, DigitalSet, ...) viewer.drawAsBalls()
GRID Draws object with gridlines Domains viewer.drawAsGrid(true | false)
ADJACENCIES Draws adjacencies relations Objects viewer.drawAdjacencies(true | false)
SIMPLIFIED Draws quad instead of prism 2D Signed KCells viewer.drawAsSimplified(true | false)

Examples with Objet modes

The file viewer3D-4-modes.cpp illustrates several possible modes to display these objects:

We can display the set of point and the domain

Point p1( -1, -1, -2 );
Point p2( 2, 2, 3 );
Domain domain( p1, p2 );
Point p3( 1, 1, 1 );
Point p4( 2, -1, 3 );
Point p5( -1, 2, 3 );
Point p6( 0, 0, 0 );
Point p0( 0, 2, 1 );

without mode change (see image (a)):

viewer << p1 << p2 << p3<< p4<< p5 << p6 << p0;
viewer << domain;

We can change the mode for displaying the domain (see image (b)):

viewer << p1 << p2 << p3<< p4<< p5 << p6 << p0;
viewer.drawAsGrid();
viewer << domain;
void drawAsGrid(bool toggle=true)

(Note that to avoid transparency displaying artifacts, we need to display the domain after the voxel elements included in the domain)

It is also possible to change the mode for displaying the voxels: (see image (c))

// Domain AND points are drawn as balls
// Grid mode was not removed, domain grid are also drawn
viewer.drawAsBalls();
viewer << domain;
viewer << p1 << p2 << p3<< p4<< p5 << p6 << p0;

(Note to enhance visibility, domain nodes are colored in black)

we obtain the following visualizations:

(a) Default visualization of a digital point sets with the associated domain
(b) visualization using Paving and Grid mode for the domain.
(c) visualization using Balls mode for the voxels.

Changing the style for displaying drawable elements.

As for Board2D, it is possible to custom the way to display 3D elements. By default, the colors of elements are given by the viewer and can cycle through some of them. They can be changed directly in the user interface of the viewer. Colors can also be set programatically;

viewer << Color(250, 0,0); // Alternatively: viewer.drawColor(color);
viewer << p4 << p5 ;
viewer.setDefaultColors(); // Reset to default colors
Structure representing an RGB triple with alpha component.
Definition Color.h:77
void setDefaultColors()

The example viewer3D-5-custom.cpp illustrates some possible customs :

Example of several custom display .

If colors depend on values, please see section about adding properties to object instead of manually setting colors.

Adding clipping planes

It also possible through the stream mechanism to add a clipping plane thanks to the object ClippingPlane. We just have to add the real plane equation and add, as for displaying, an element. The file viewer3D-6-clipping.cpp gives a simple example.

From displaying a digital set defined from a Norm2 ball,

Point p1( 0, 0, 0 );
Point p2( 20, 20, 20 );
Domain domain(p1, p2);
DigitalSet shape_set( domain );
Shapes<Domain>::addNorm2Ball( shape_set, Point( 10, 10, 10 ), 7 );
viewer << shape_set;
viewer << Color(250, 200,0, 20);
static void addNorm2Ball(TDigitalSet &aSet, const Point &aCenter, UnsignedInteger aRadius)
Z2i::DigitalSet DigitalSet

we can add, for instance, two different clipping planes:

viewer << ClippingPlane(1,0,0,-4.9);
viewer << ClippingPlane(0,1,0.3,-10);
Clipping plane.
Definition Display3D.h:299
(a) visualization of the initial set.


(b) visualization after adding the first clipping plane (0,1,0.3,-10).
(c) visualization after adding a second clipping plane (1,0,0,-4.9) .

It also possible to edit the clipping plane directly in the viewer (under View/Slice Planes) which allows to change colors, positions and if they are drawn or not.

Adding 2D image visualization in 3D

With the PolyscopeViewer class it is possible to display 2D slice image from a volume one. It can be done in a few steps (see example of io/viewers/viewer3D-8-2DSliceImages.cpp) :

// Extracting the 2D images from the 3D one and from a given dimension.
// First image the teenth Z slice (dim=2)
Image3D::Value, DGtal::functors::Identity > MySliceImageAdapter;
// Define the functor to recover a 2D domain from the 3D one in the Z direction (2):
DGtal::functors::Projector<DGtal::Z2i::Space> transTo2DdomainFunctorZ; transTo2DdomainFunctorZ.initRemoveOneDim(2);
DGtal::Z2i::Domain domain2DZ(transTo2DdomainFunctorZ(imageVol.domain().lowerBound()),
transTo2DdomainFunctorZ(imageVol.domain().upperBound()));
// Define the functor to associate 2D coordinates to the 3D one by giving the direction Z (2) and the slide numnber (10):
DGtal::functors::Projector<DGtal::Z3i::Space> aSliceFunctorZ(10); aSliceFunctorZ.initAddOneDim(2);
// We can now obtain the slice image (a ConstImageAdapter):
const auto identityFunctor = DGtal::functors::Identity();
MySliceImageAdapter aSliceImageZ(imageVol, domain2DZ, aSliceFunctorZ, identityFunctor );
// Second image the fiftieth Y slice (dim=1)
// Define the functor to recover a 2D domain from the 3D one in the Y direction (1):
DGtal::functors::Projector<DGtal::Z2i::Space> transTo2DdomainFunctorY; transTo2DdomainFunctorY.initRemoveOneDim(1);
DGtal::Z2i::Domain domain2DY(transTo2DdomainFunctorY(imageVol.domain().lowerBound()),
transTo2DdomainFunctorY(imageVol.domain().upperBound()));
// Define the functor to associate 2D coordinates to the 3D one by giving the direction Y (1) and the slide numnber (50):
DGtal::functors::Projector<DGtal::Z3i::Space> aSliceFunctorY(50); aSliceFunctorY.initAddOneDim(1);
// We can now obtain the slice image (a ConstImageAdapter):
MySliceImageAdapter aSliceImageY(imageVol, domain2DY, aSliceFunctorY, identityFunctor );
Example of 2D image visualization

Adding 3D image visualization

In the same way a 3D image can be displayed. By following the same stream operator, you will obtain such examples of display:

Example of 3D image visualization with also digital sets.

See more details in the example: io/viewers/viewer3D-9-3Dimages.cpp

Object names and groups

Each object that is drawn with the Display3D class is given a name as a string. This serves as a unique identifier and may also be used by viewers as user interface elements. It is possible to specify the name of an object with the draw function (internally, the stream operator will call this function).

Point p1( 0, 0, 0 );
viewer.draw(p1, "Point name");
std::string draw(const Point &p, const std::string &uname="Point_{i}")

In order to ensure uniqueness, the given may be altered if necessary. For example:

Point p1( 0, 0, 0 );
Point p2( 1, 1, 1 );
std::string name1 = viewer.draw(p1, "Point name"); // name1 = "Point name"
std::string name2 = viewer.draw(p2, "Point name"); // name2 = "Point name_1"

It is possible to specify where the counter will be placed with the "{i}" token (only first occurence is replaced). For KCell, the token "{d}" can also be used for dimension.

Point p1( 0, 0, 0 );
Point p2( 1, 1, 1 );
std::string name1 = viewer.draw(p1, "Point_{i}_name"); // name1 = "Point_1_name"
std::string name2 = viewer.draw(p1, "Point_{i}_name"); // name2 = "Point_2_name"

A common pattern when using the DGtal library is to iterate over a set of object, and then perform a computation and draw them (see tests/geometry/surfaces/testLocalConvolutionNormalVectorEstimator.cpp). However, this would imply having one object for each draw command, which can bloat the user interface and would make the viewer slow. It also limits the ability to have colormaps, since each value would be scattered across multiple objects. For this reason, a special variable can be set:

viewer.allowReuseList = true;

This will group new objects into the current list. However, this might fail if rendered objects change types (eg. alternating drawing lines and points). Therefore, the lists where the new objects should be pushed must be set manually with:

viewer.setCurrentList(name);
bool setCurrentList(const std::string &name)
Set the current group for further updates.

Adding quantities to objects

The class Display3D supports adding properties to objects. These properties can be colors, scalar values or vector information. These can be useful to display curvature information and/or computed normals. There are two ways to add these quantities. The first one is with the stream API, which is most useful for quick display of a couple of quantities:

// Option 1: stream operator (Scell_1_2d in viewer)
// Most usefull when adding single quantity, otherwise
// it should be nested.
viewer << WithQuantity(
surfels, "Mean Curv", mean_curvs
),
"Gauss Curve", gauss_curvs
),
"Normal", normals
);

The second approach, as powerful as the first one, requires the name of the object, but is more comfortable when the quantities are obtained after drawing.

// Option 2: draw first then add quantities (Surfels 1 in viewer)
// This requires to obtain or set object name
std::string objectName = "Surfels 1";
viewer.draw(surfels, objectName); // Draws the object independantly
viewer.addQuantity(objectName, "Mean Curv", mean_curvs);
viewer.addQuantity(objectName, "Gauss Curv", gauss_curvs);
viewer.addQuantity(objectName, "Normal", normals);

Both methods can be used together and within loops:

std::string objectName2 = "Surfels 3";
viewer.newVolumetricList(objectName2); // Signed cells are drawn as volumetric meshes
viewer.allowReuseList = true; // Allows for automatic groupping
auto surfIt = surfels.begin();
auto mcurveIt = mean_curvs.begin();
auto gcurveIt = gauss_curvs.begin();
auto ncurveIt = normals.begin();
for (; surfIt != surfels.end(); ++surfIt, ++mcurveIt, ++gcurveIt, ++ncurveIt) {
// Both options 1 and 2 can be used together, as long as we know the name for sure
viewer << WithQuantity(*surfIt, "Mean Curv", *mcurveIt);
viewer.addQuantity(objectName2, "Gauss Curve", *gcurveIt);
viewer.addQuantity(objectName2, "Normals", *ncurveIt);
}

These properties can be shown or hidden and their appearance modified directly in the graphical user interface.

Example of adding quantities to an object. Objects were shifted and quantities hidden to enhance visibility.

It is also possible to add a last parameter to WithQuantity and addQuantity which is the element targeted by the quantity. For example, for a surface mesh, we may want to set the quantities at the vertices rather than on the faces when solving PDEs. However, the correct numbers of values should be supplied (eg. one per vertex and not one per face). Unfortunately, not every combination is supported (see table below). By default, it will use the scale corresponding to the dimension of the element (ie. right-most side in the "Supported Scale" column).

Geometric display Supported Scale Name DGtal object examples
Point VERTEX QuantityScale::VERTEX (Real)Points in BALL mode, dim 0 KCell
Lines VERTEX / EDGE QuantityScale::VERTEX, QuantityScale::EDGE Adjacencies, dim 1 KCell
Surface mesh VERTEX / EDGE (Scalar)/ FACE QuantityScale::VERTEX, QuantityScale::EDGE (Scalar), QuantityScale::FACE dim 2 KCell in simplified mode
Volume mesh VERTEX / CELL QuantityScale::VERTEX, QuantityScale::CELL (Real)Points, Objects, Domains, dim 2/3 KCell

Extending the viewer

The viewer supports extensions that allow us to interact with the viewer at runtime. By default, PolyscopeViewer shows what object is clicked; but one may want to add custom UI, or enhance displayed information when an element is clicked. For this purpose, it is possible to set a callback that will be triggered upon three events: attach (only once), UI and clicks.

class MyCustomCallback : public typename Display3D<Space, KSpace>::Callback {
void OnAttach(void* viewer) {
// Called when the callback is attached to the viewer
}
void OnUI(void* data) {
// Called within the event loop and allows to draw some UI
}
void OnClick(const std::string& name, size_t index, const DisplayData<RealPoint>& data, void* viewerData) {
// Called when an element is clicked
}
};
Base class for viewing DGtal objects.
Definition Display3D.h:377
Data required to display an object.
Definition Display3D.h:247
unsigned int index(DGtal::uint32_t n, unsigned int b)
Definition testBits.cpp:44
MyViewer::Callback Callback

A useful method is Display3D::renderNewData that renders to the screen any newly added data. Another method, Display3D::renderAll can be used to (re)render every object; but run-time settings might be lost.

For PolyscopeViewer, user inputs and UI are managed with ImGUI. Also note that the viewer displays "by default", the name of the clicked polyscope structure.