Getting started with morph::Visual
morph::Visual
is the graphics scene class. It bundles up all the complexities of OpenGL rendering, including that of managing fonts for text into a single header-only include. It allows you to pan and zoom the environment to view 3D visualisations from any angle. It bundles fonts and text-handling code to allow you to render anti-aliased text in your models. It provides shaders that give you lighting and alpha-blend effects. It also provides the facilities to save an image of the scene, or save the graphical portions of the scene into a glTF file (so that you could open them in Blender). If you want to make a custom visualisation for a simulation, the only job you have to do is to describe what shapes will make up the graphical model. And because it’s modern OpenGL, the graphics rendering is really fast, so you can visualise your simulations in realtime.
Creating a scene
Here’s a “Helloworld” example that creates a morph::Visual
object, adds a label containing some text at coordinates (0,0,0) in the scene, and renders it in a window.
#include <morph/Visual.h>
int main()
{
// Visual is templated with an int parameter that defines the OpenGL version
morph::Visual<morph::gl::version_4_1> v(600, 400, "Hello World!");
v.addLabel ("Hello World!", {0,0,0});
v.keepOpen(); // Renders the scene and polls for mouse/keyboard events
return 0;
}
This program is in morphologica’s examples, so you can compile and run it with:
cd morphologica
mkdir build
cd build
cmake ..
make helloworld
./examples/helloworld
It’s an empty Visual scene with a text label and nothing else. However, try pressing ‘Ctrl-c’ in the window, and you’ll see the 3D coordinate system arrows appear. Press ‘Ctrl-q’ to exit.
Adding a visual model to the scene
A Visual scene is nothing without some objects inside it. We’ll use an example VisualModel called RodVisual
to add an object. Add this include to the helloworld program:
#include <morph/RodVisual.h>
Then before v.keepOpen() you can add a simple VisualModel:
morph::vec<float, 3> pos = { 0.0f, 0.0f, 0.0f }; // model position in scene
morph::vec<float, 3> start = { 0.0f, 0.0f, 0.0f }; // start coordinate of rod (model frame of reference)
morph::vec<float, 3> end = { 0.25f, 0.0f, 0.0f }; // end coordinate of rod
morph::vec<float, 3> colour1 = { 1.0f, 0.0f, 0.0f }; // Colour for the rod
// This will return a std::unique_ptr<morph::VisualModel<>> into rvm. VisualModels
// have to have the gl version template arg.
auto rvm = std::make_unique<morph::RodVisual<morph::gl::version_4_1>> (pos, start, end, 0.1f, colour1, colour1);
v.bindmodel (rvm); // boilerplate for all VisualModels
// Any model specific settings, such as rvm->setWidth(0.4f); would go here
rvm->finalize(); // Required. Causes the OpenGL vertices to be computed
auto rvm_ptr = v.addVisualModel (rvm); // Transfer ownership of the model into the morph::Visual
The pattern is to:
- create a
std::unique_ptr
to the VisualModel withstd::make_unique
- call Visual’s
bindmodel()
method, passing in the VisualModel unique_ptr. This sets some runtime callbacks - set any features of the model (colour, dimensions, associated data etc)
finalize()
the model, which causes the vertices to be computed (The final derived version ofVisualModel::initalizeVertices()
is called to do this)- add it to the
morph::Visual
withaddVisualModel()
, transferring ownership of the unique_ptr.
Note that addVisualModel
returns a non-owning pointer to the model, which can be used to interact with the model after it has been added to the Visual. In the example above, the auto
type will be determined to be morph::RodVisual<morph::gl::version_4_1>*
.
Render the scene and poll for events
The Visual::render
method calls render() for each object in the scene and re-draws the entire window. You can call render() as often as is necessary in your program, but be sure to include a call to waitevents() so that your keyboard/mouse input can be processed. A common pattern would be:
MyModel model;
while (!v.readyToFinish) {
// Do computations on your model object
model.step(); // This may have nothing to do with morphologica
if ((model.stepnum % 100) == 0) { // Render every 100 steps
// Calls to update VisualModels and then...
v.render();
v.waitevents (0.001); // A very short wait
}
}
There is also v.poll()
if you want to poll without any timeout or v.wait(double timeout)
if you want to wait a full timeout (waitevents
returns as soon as there is a keyboard, window resize or mouse event or when the timeout elapses).
If you don’t need to change the scene you’re viewing, then
v.keepOpen();
is equivalent to render()
followed by waitevents (0.018)
in an infinite loop.
The scene view
The scene will render VisualModel objects at different coordinates. You may add several graphs on a grid, and when you’ve completed the scene, you will want the new window to appear with a view that places all the graphs within a appropriately sized window. You can set the window size in the Visual constructor, but to move the ‘view’ of the scene, you need Visual::setSceneTrans
. This translates the entire scene by a 3D vector. By moving the scene in the z coordinate, you can zoom.
v.setSceneTrans (morph::vec<float,3>({0.0f, 0.0f, -5.0f}));
To get the right numbers for the offset, Visual has a neat trick which you can try with any of the example programs. With the mouse, pan and zoom the content until it looks right. Press ‘Ctrl-z’ and then look in the stdout for the program which should look something like:
Scenetrans setup code:
v.setSceneTrans (morph::vec<float,3>{ float{-0.140266}, float{0.237435}, float{-3.5} });
v.setSceneRotation (morph::Quaternion<float>{ float{0.910055}, float{0.402162}, float{0.100329}, float{0} });
Writing scene trans/rotation into /tmp/Visual.json... Success.
It will give the setSceneTrans() call you need. Add it after the Visual constructor:
morph::Visual<morph::gl::version_4_1> v(600, 400, "Hello World!");
v.setSceneTrans (morph::vec<float,3>{ float{-0.140266}, float{0.237435}, float{-3.5} });
Background colour and lighting
You can set the background colour to anything by changing Visual::bgcolour which is of type std::array<float, 4> (RGBA). To set white or black backgrounds, it’s just
v.backgroundWhite();
v.backgroundBlack();
Often in a visualization you don’t want any kind of lighting effects, because an exact colour conveys information, so any kind of lighting effect distorts the information perceived by the viewer. However, to add a simple diffuse light source (which switches morph::Visual to use a slightly different set of shaders) you can call
v.lightingEffects (true);
Perspective
The default is to render with perspective. However, it’s possible to choose an orthgraphic projection by changing the Visual::ptype
attribute
// Change from default of morph::perspective_type::perspective;
v.ptype = morph::perspective_type::orthographic;
Saving
You can save a PNG image of the view with
v.saveImage ("file_path.png");
or a glTF file that describes the graphical elements of your scene (excluding text) with
v.savegltf ("file_path.gltf");
Callbacks
morph::Visual contains a scheme for setting mouse and keyboard callbacks so that you can add custom key press events to your programs. See Custom callbacks in extended Visual classes for more details.
Removing a VisualModel
You may need to remove a VisualModel from your scene. Use removeVisualModel
:
auto rvm = std::make_unique<morph::RodVisual<morph::gl::version_4_1>> (pos, start, end, 0.1f, colour1, colour1);
auto rvm_ptr = v.addVisualModel (rvm);
// Stuff happens
v.removeVisualModel (rvm_ptr);
rvm_ptr = nullptr; // Don't use it again until it points to something valid