Creating visualizations: Van der Waals visual model#
This tutorial shows you how to create a new visual model that shows the van der Waals representation of a group of atoms.
Note
SAMSON already has a Van der Waals visual model which is installed by default.
The source code for an Extension created in this tutorial can be found at SAMSON-Developer-Tutorials.
Generating an Extension#
We will start by creating a new SAMSON Extension called VanDerWaals thanks to the SAMSON Extension Generator (please, refer to the SAMSON Extension Generator tutorial for a reminder on how to use it):
- launch SAMSON and run the SAMSON Extension Generator;
- specify the path to a folder where you want to develop your SAMSON Extensions;
- name the SAMSON Extension as VanDerWaals and add some description;
- add a Visual Model class (called
SEVanDerWaalsVisualModel
); - generate the SAMSON Extension.
The SAMSON Extension Generator generates a visual model class (derived from the SBMVisualModel
class) and a property widget class for the visual model.
Now, try building the SAMSON Extension.
Setting up the visual model#
A visual model is applied to a group of nodes passed to the constructor of the visual model. Since we want to apply a van der Waals visual model to a group of atoms, we add a member to the visual model to remember the list of atoms that we were passed.
Open the SEVanDerWaalsVisualModel.hpp file and include the SBAtom.hpp header:
And add the following member inside the declaration of the SEVanDerWaalsVisualModel
class in the SEVanDerWaalsVisualModel.hpp file:
class SEVanDerWaalsVisualModel : public SBMVisualModel {
// ...
private:
SBPointerIndexer<SBAtom> atomIndexer;
};
A pointer indexer is similar to a node indexer, thus assigns indices to pointed objects, but automatically removes references from the indexer when a pointed object is deleted (in the C++ sense) so that it can never point to deleted memory.
Then, in the SEVanDerWaalsVisualModel.cpp file, modify the second constructor as follows:
SEVanDerWaalsVisualModel::SEVanDerWaalsVisualModel(
const SBNodeIndexer& nodeIndexer) {
SBNodeIndexer temporaryIndexer;
SB_FOR(SBNode* node, nodeIndexer)
node->getNodes(temporaryIndexer, SBNode::IsType(SBNode::Atom));
SB_FOR(SBNode* node, temporaryIndexer)
atomIndexer.addReferenceTarget(node);
temporaryIndexer.clear();
SAMSON::setStatusMessage(QString("Van der Waals visual model: added for ") +
QString::number(atomIndexer.size()), 0);
}
First, we collect atoms among the nodes passed to the constructor of the visual model (possibly among their descendants), then we add the atoms to the atom indexer atomIndexer
.
Disabling the property window#
The property window of a visual model is mainly used to display the possible parameters of the visual model to the user such that they can be modified.
Note
This is also can be done via the introspection mechanism by exposing visual model attributes. In this case they could be modified via the Inspector.
If enabled, the property window appears when the visual model was created or can be invoced from the context menu of the visual model node in the data graph. For simplicity, in this tutorial we will not be using the property window. To disable the property window, please open the SEVanDerWaalsDescriptor.cpp file that describes the SAMSON Extension and comment the following string:
You can also change the SAMSON Extension category to Visualization:
Displaying spheres#
To display something in the viewport, we overload the display
function of the visual model. For convenience, SAMSON provides many functions to ease rendering objects (instead of having to use OpenGL functions).
The display
function of the visual model receives SBNode::RenderingPass
according to which the geometry is to be rendered. In this tutorial we will consider three of them:
SBNode::RenderingPass::OpaqueGeometry
- the pass where opaque geometry is rendered;SBNode::RenderingPass::ShadowingGeometry
- the pass where shadowing geometry is rendered;SBNode::RenderingPass::SelectableGeometry
- the pass where selectable geometry is rendered.
In this tutorial, we are going to use the displaySpheres function, to draw one sphere per atom, with a radius equal to the van der Waals radius of the atom.
Let's now code inside the display
function of the visual model. Please note that the code demonstrated below is not optimized and is shown for the tutorial purposes. For the optimized code please see the source code for this Extension in SAMSON-Developer-Tutorials.
First, we allocate arrays needed to transfer data to SAMSON for rendering spheres and initialize them to zeros:
void SEVanDerWaalsVisualModel::display(RenderingPass renderingPass) {
const unsigned int numberOfAtoms = atomIndexer.size();
// allocate arrays and initialize them to zeros
float* positionData = new float[3 * numberOfAtoms]();
float* radiusData = new float[numberOfAtoms]();
float* colorData = new float[4 * numberOfAtoms]();
unsigned int* flagData = new unsigned int[numberOfAtoms]();
unsigned int* nodeIndexData = new unsigned int[numberOfAtoms]();
Observe the different array sizes above: we need 3 float
per atom for the position, 1 float
for the radius, 4 float
for the color (red, green, blue, and opacity), and 2 unsigned int
for flags used to alter the rendering colors (e.g. the selection flag and the highlighting flag) and for node indices used for selections (see the Enabling selection section).
We also need to know whether a color scheme is applied to the visual model, so we retrieve the pointer to the material applied to the visual model:
// retreive the pointer to the material applied to the visual model
SBNodeMaterial* material = getMaterial();
We can now fill the arrays by traversing the list of atoms:
// fill in the arrays
for (unsigned int i = 0; i < numberOfAtoms; i++) {
SBPointer<SBAtom> currentAtom = atomIndexer[i];
// check if the atom is not null
if (!currentAtom.isValid()) continue;
// check if the atom is not erased
if (currentAtom->isErased()) continue;
// fill the position array
SBPosition3 position = currentAtom->getPosition();
positionData[3 * i + 0] = (float)position.v[0].getValue();
positionData[3 * i + 1] = (float)position.v[1].getValue();
positionData[3 * i + 2] = (float)position.v[2].getValue();
// fill the radius array
radiusData[i] = (float)currentAtom->getVanDerWaalsRadius().getValue();
// fill the color array based on the material
// applied to the visual model, if any
if (material) {
// if a material is applied to the visual model
// use its color scheme
material->getColorScheme()->getColor(colorData + 4 * i,
currentAtom(), currentAtom()->getPosition());
}
else if (currentAtom->getMaterial()) {
// else if a material is applied to the current atom
// use its color scheme
currentAtom->getMaterial()->getColorScheme()->getColor(
colorData + 4 * i, currentAtom(), currentAtom()->getPosition());
}
else {
// else set the default color
colorData[4 * i + 0] = 1.0f;
colorData[4 * i + 1] = 1.0f;
colorData[4 * i + 2] = 1.0f;
colorData[4 * i + 3] = 1.0f;
}
// fill the flag array based on the combination of the flags
// for the visual model and the current atom
flagData[i] = currentAtom->getInheritedFlags() | getInheritedFlags();
// fill the node index array
nodeIndexData[i] = currentAtom->getNodeIndex();
}
Here, first we check whether the atom has been deleted or not. Thus, we will display spheres only for atoms, present in the data graph.
Then, we fill the position array and the radius array based on atom's position and the van der Waals radius, respectively. Both physical quantities have length units (SBQuantity::length
), so we extract their numerical value with the getValue
function, and cast the resulting double
as a float
, which is expected by the renderer.
Then, we fill the color array based on the material applied to the visual model, if any. Precisely, if a material is applied to the visual model, we charge it of filling the color array, based on the current atom we're examining. Else, we check whether the atom itself has a color scheme in order to use it. If it has no color scheme, we set the color to white (red, green, blue, and opacity are set to their maximum value: 1.0f).
Finally, we fill the flag array based on an "or" combination of the flags of the visual model and the flags of the current atom, to help the user have visual feedback when nodes are highlighted or selected.
We can now ask SAMSON to display these spheres when rendering opaque geometry:
if (renderingPass == SBNode::RenderingPass::OpaqueGeometry) {
// display spheres for atoms
SAMSON::displaySpheres(
numberOfAtoms, positionData, radiusData,
colorData, flagData);
}
And we complete the display function by cleaning up memory:
delete[] positionData;
delete[] radiusData;
delete[] colorData;
delete[] flagData;
delete[] nodeIndexData;
Now, if you build and launch SAMSON, and apply this visual model (Ctrl/Cmd + Shift+V) to a molecule without any material applied, it will show white spheres. Let's make it more appealing with default color set based on the CPK color scheme. For that, add the <SBElementTable.hpp header file in the SEVanDerWaalsVisualModel.cpp file:
and change the setting of the default color to the following line:
// else set the default color based on CPK color
memcpy(&colorData[4 * i],
SBElementTable::getElement(currentAtom->getElementType()).getColorCPK(),
4 * sizeof(float));
Here we get the CPK color for a current atom element type.
Now, if you build and launch SAMSON, and apply this visual model to a molecule without any material applied:
it will show spheres with CPK color:
You can see that the visual model does not cast any shadows, and the only the shadows you see are casted by the molecule itself. Learn how to make you visual model to cast shadows in the next section.
Casting shadows#
SAMSON uses a shadow map algorithm in order to render shadows. This algorithm performs a hidden rendering pass from the point of view of the light and uses this render to determine whether a point is shadowed during the main rendering pass. In order to cast shadows, we thus need to render spheres again, but in the SBNode::RenderingPass::ShadowingGeometry
pass. Shadow pass only cares to render geometry and to determine the depth of objects and it doesn't need colors.
In the display
function of the visual model, add the following pass after the SBNode::RenderingPass::OpaqueGeometry
:
if (renderingPass == SBNode::RenderingPass::ShadowingGeometry) {
// display for shadows
SAMSON::displaySpheres(
numberOfAtoms, positionData, radiusData,
colorData, flagData, true);
}
The visual model then casts shadows:
Enabling selection#
In SAMSON, picking objects in the viewport is also implemented with rendering functions. However, instead assigning colors to each pixel of the viewport, we assign the index of the node being rendered at this location (see nodeIndexData
). Visual models can thus become selectable if they implement the SBNode::RenderingPass::SelectableGeometry
pass in their display
function.
In the display
function of the visual model, add the following pass after the SBNode::RenderingPass::ShadowingGeometry
:
if (renderingPass == SBNode::RenderingPass::SelectableGeometry) {
// display spheres for atoms
SAMSON::displaySpheresSelection(
numberOfAtoms, positionData, radiusData, nodeIndexData);
}
Note that in the SelectableGeometry
pass we don't need to deal with colors but we need to fill the index array with the unique node index that SAMSON has assigned to the atom. As a result, when the user attempts to pick a sphere in the visual model, the atom is selected:
Modifying the properties#
Let's now make it possible for the user to change the size of the spheres.
For that we introduce a dimensionless variable radiusFactor
in the SEVanDerWaalsVisualModel
class (file SEVanDerWaalsVisualModel.hpp) together with according getter and setter functions:
class SEVanDerWaalsVisualModel : public SBMVisualModel {
SB_CLASS
public:
// ...
/// \name Getter/setter functions
//@{
const float& getRadiusFactor() const;
void setRadiusFactor(const float& r);
//@}
private:
SBPointerIndexer<SBAtom> atomIndexer;
float radiusFactor; ///< The radius factor
};
And implementation of these functions in the SEVanDerWaalsVisualModel.cpp file:
const float& SEVanDerWaalsVisualModel::getRadiusFactor() const {
return radiusFactor;
}
void SEVanDerWaalsVisualModel::setRadiusFactor(const float& r) {
if (r < 0.0f) return;
radiusFactor = r;
// request re-rendering of the viewport
SAMSON::requestViewportUpdate();
}
In the second constructor of the SEVanDerWaalsVisualModel
class (file SEVanDerWaalsVisualModel.cpp) we set the default value for the radius factor to 1.0:
SEVanDerWaalsVisualModel::SEVanDerWaalsVisualModel(
const SBNodeIndexer& nodeIndexer) {
radiusFactor = 1.0f; // set a default radius factor
// ...
}
And in both display
and displayForSelection
functions of the visual model modify the computation of the sphere radius by multiplying by the radius factor:
// fill the radius array
radiusData[i] = radiusFactor * currentAtom->getVanDerWaalsRadius().getValue();
There are two main ways to provide a possibility for the user to modify properties of the visual model:
- via the Inspector;
- via the property window of the visual model.
You can use either one of them or both.
You can also specify the range of the radiusFactor
in the Inspector and expose the visual model transparency in the Inspector. To see how to do that, please check the source code for this Extension in SAMSON-Developer-Tutorials.
Inspector#
Now, we expose the radius factor as a read and write attribute thanks to the getter and setter functions and the introspection mechanism. Note that the getter and setter function names should differ only in the get and set parts. Open the SEVanDerWaalsVisualModelDescriptor.hpp file and add the following attribute exposure:
SB_CLASS_BEGIN(SEVanDerWaalsVisualModel);
// ...
SB_INTERFACE_BEGIN;
SB_ATTRIBUTE_READ_WRITE(const float&, SEVanDerWaalsVisualModel, RadiusFactor, "Radius factor", "Geometry");
SB_INTERFACE_END;
SB_CLASS_END(SEVanDerWaalsVisualModel);
Here, we specify the type of the attribute (const float&
), the class, the root name of the getter and setter functions, the description of the attribute, and an attribute's group (a group box in the Inspector window where this attribute will be placed).
Now, the user can modify the radius factor in the Inspector when the visual model is selected:
You can also limit the range of the radiusFactor
in the Inspector using SB_ATTRIBUTE_READ_WRITE_RANGE
and expose the visual model transparency as a slider using SB_ATTRIBUTE_READ_WRITE_RESET_RANGE_SLIDER
. To see how to do that, please check the source code for this Extension in SAMSON-Developer-Tutorials.
Property window#
First, if you disabled the property window of the visual model, enable it back by uncommenting the following string in the Extension's descriptor file (SEVanDerWaalsDescriptor.cpp):
Open the SEVanDerWaalsVisualModelProperties file, modify the default label, and add a QDoubleSpinBox
. Rename this spin box to doubleSpinBoxRadiusFactor
and set its parameters as on an image below (the defaul value to 1.0 and the minimum to 0.0):
Add a slot onRadiusFactorChanged(double)
:
And connect the valueChanged(double)
signal of the spin box to this slot:
Now, add this slot in the SEVanDerWaalsVisualModelProperties
class declaration (SEVanDerWaalsVisualModelProperties.hpp file):
class SEVanDerWaalsVisualModelProperties : public SBGDataGraphNodeProperties {
SB_CLASS
Q_OBJECT
public:
// ...
public slots:
/// \name Properties
//@{
void onRadiusFactorChanged(double radiusFactor);
//@}
private:
// ...
};
And its implementation in the SEVanDerWaalsVisualModelProperties.cpp file:
void SEVanDerWaalsVisualModelProperties::onRadiusFactorChanged(
double radiusFactor) {
if (!visualModel.isValid()) return;
// set the radius factor for the visual model
visualModel->setRadiusFactor(radiusFactor);
// request re-rendering of the viewport
SAMSON::requestViewportUpdate();
}
Here, we set the radius factor of the visual model from the property window and request to force the update of SAMSON's viewport which will render the viewport thus rendering the visual model with new parameters.
Now, the user can modify the radius factor in the property window and it will automaticaly update the visual model representation:
Serializing the visual model#
If you want for your visual model to be copyable and savable it is necessary to implement its serialization. Serialization is used in SAMSON to:
- copy nodes in the data graph,
- save documents in SAMSON format files (.sam, .samx) and further load it from them.
Please, see the serialization section for more information.
By default, the newly created visual model is not serialized.
Open the SEVanDerWaalsVisualModel.cpp file and modify the isSerializable
function to return true
.
This indicates to SAMSON that this class is serializable.
Now, we need to re-implement both serialize
and unserialize
functions.
When SAMSON serializes a group of nodes, it proceeds in two passes:
- First, all nodes that are to be serialized are added to a
nodeIndexer
. This way, each node that's about to be serialized has an index. It's those indices that need to be used to indicate which nodes are referenced by other nodes if the referenced nodes are serialized as well, else we need to use addresses of the referenced nodes. - Then, the
serialize
function of each node is called. This function receives thenodeIndexer
.
During unserialization, SAMSON also proceeds in two steps:
- First, it creates all required nodes and indexes them in a
nodeIndexer
, in the same order as during serialization. - Then, the
unserialize
function of each node is called. This function receives thenodeIndexer
.
The serialize
function allows for serialization of your visual model.
void SEVanDerWaalsVisualModel::serialize(
SBCSerializer* serializer,
const SBNodeIndexer& nodeIndexer,
const SBVersionNumber& sdkVersionNumber,
const SBVersionNumber& classVersionNumber) const {
// Serialization of the parent class
SBMVisualModel::serialize(
serializer, nodeIndexer,
sdkVersionNumber, classVersionNumber);
// Write the radius factor
serializer->writeFloatElement("radiusFactor", radiusFactor);
// Write the number of atoms to which this visual model is applied
serializer->writeUnsignedIntElement("numberOfAtoms", atomIndexer.size());
unsigned int atomIndex = 0; // the index of the atom in the indexer
// Write indices of the atoms to which this visual model is applied
SB_FOR(SBPointer<SBAtom> atom, atomIndexer) {
if (nodeIndexer.getIndex(atom(), atomIndex)) {
// the atom is indexed
serializer->writeBoolElement("atomIsIndexed", true);
serializer->writeUnsignedIntElement("atomIndex", atomIndex);
}
else {
// the atom is not indexed,
// the user must be copying just the visual model
// so we serialize the atom address itself
serializer->writeBoolElement("atomIsIndexed", false);
serializer->writeUnsignedLongLongElement("atomIndex",
(unsigned long long)atom());
}
}
}
First we invoke the serialization function of the parent class. The parent functions do many things (all the way up to SBNode
itself, i.e. SBVisualModel::serialize
calls SBModel::serialize
which calls SBNode::serialize
) and are needed for correct saving and loading of node properties. This allows to save the node's name, visibility flag, a material, etc.
Then we save information on our visual model itself: the radius factor, number of nodes to which the visual model is applied (nodes referenced by the visual model), and the indices of these nodes in the nodeIndexer
or their addresses. For the last part, we check whether the atom itself was serialized or not. Basically, saving of the document leads to the serialization of every node that can be serialized, while copying leads to the serialization only of the copied nodes. If the atom was serialized we save its index in the nodeIndexer
, else we save the atom's address.
The unserialize
function allows for unserialization of your visual model.
void SEVanDerWaalsVisualModel::unserialize(
SBCSerializer* serializer,
const SBNodeIndexer& nodeIndexer,
const SBVersionNumber& sdkVersionNumber,
const SBVersionNumber& classVersionNumber) {
// Unserialization of the parent class
SBMVisualModel::unserialize(
serializer, nodeIndexer,
sdkVersionNumber, classVersionNumber);
// Read the radius factor
radiusFactor = serializer->readFloatElement();
// Read the number of atoms to which this visual model is applied
unsigned int numberOfAtoms = serializer->readUnsignedIntElement();
bool atomIsIndexed;
unsigned int atomIndex = 0; // the index of the atom in the indexer
// Read indices of the atoms to which this visual model is applied and
// add these node into the atom indexer of the visual model
for (unsigned int i = 0; i < numberOfAtoms; ++i) {
atomIsIndexed = serializer->readBoolElement();
if (atomIsIndexed) {
// the atom was serialized too
atomIndex = serializer->readUnsignedIntElement();
atomIndexer.addReferenceTarget(nodeIndexer[atomIndex]);
}
else {
// the atom was not serialized,
// it must still exist in memory
atomIndexer.addReferenceTarget(
(SBAtom*)serializer->readUnsignedLongLongElement());
}
}
}
First, we invoke the unserialization function of the parent class. This will create the node in the data graph. Then we read information on our visual model itself: the radius factor, number of nodes to which the visual model is applied (nodes referenced by the visual model), and the indices of these nodes in the nodeIndexer
or their addresses. We check whether the atom itself was serialized or not. If the atom was serialized we read its index in the nodeIndexer
, else we read the atom's address.
This code allows us to both copy the visual model in the data graph and save it in SAMSON format files or load it from them.