VisualModel: The base class for objects

#include <morph/VisualModel.h>

Table of Contents

Overview

morph::VisualModel is the base class for graphical objects in your morph::Visual scene. This page describes the base class and then there is a section dedicated to documenting the derived classes that morphologica provides.

Four VisualModels in a Visual scene. Each model is a geodesic polynomial

A VisualModel holds all the coordinates that define a set of triangles that make up a ‘graphical model’ (in OpenGL we draw almost exclusively with triangles). VisualModel also contains a list of text objects so that your graphical elements can be embellished with text. Text is created by drawing rectangles (made from triangles) to which bitmap ‘texture’ images of character glyphs are applied. The image above shows geodesic polynomials which are constructed from triangles. Here, each triangle is individually coloured so you can see the OpenGL model structure clearly.

Creating an instance

Taking a derived class called GraphVisual as an example, we create an instance of a VisualModel-derived class by using std::make_unique. This allows us to pass ownership of the VisualModel’s memory into a morph::Visual. Our example first needs a morph::Visual instance:

morph::Visual v(1024, 768, "Example");

The new VisualModel (a GraphVisual) must be created with std::make_unique

auto gv = std::make_unique<morph::GraphVisual<float>> (morph::vec<float>({0,0,0}));

Once we have the unique_ptr, we have to call the essential bindmodel function to wire up the callbacks:

v.bindmodel (gv);

After bindmodel, programs will then make any additional function calls to set up the object. Here we might set the data and change what kind of axes should be drawn.

After set up, the VisualModel::finalize() function is the last piece of boilerplate code. VisualModel::finalize is as essential as bindmodel. finalize calls the virtual function VisualModel::initializeVertices which is defined as empty (rather than abstract) in VisualModel, but must be overridden in derived classes. The job of initializeVertices is to create the vertices, normals, colours and indices that fill the OpenGL vertex buffers.

gv->finalize();

Lastly, we transfer memory ownership to the parent Visual with Visual::addVisualModel().

morph::GraphVisual<float>* gv_ptr = v.addVisualModel (gv);

In the example, Visual::bindmodel is used to set up these callback functions in VisualModel:

template <int glver = morph::gl::version_4_1>
class VisualModel
{
    ...
    //! Get all shader progs
    std::function<morph::visgl::visual_shaderprogs(morph::Visual<glver>*)> get_shaderprogs;
    //! Get the graphics shader prog id
    std::function<GLuint(morph::Visual<glver>*)> get_gprog;
    //! Get the text shader prog id
    std::function<GLuint(morph::Visual<glver>*)> get_tprog;
    //! Set OpenGL context. Should call parentVis->setContext()
    std::function<void(morph::Visual<glver>*)> setContext;
    ...
};

Three of the functions ensure that the VisualModel can get access to the OpenGL shader program IDs in a way that avoids a circular header dependency between Visual.h and VisualModel.h. VisualModel::render() needs access to the shader information. The last function allows VisualModel to set the correct OpenGL context in any of its function calls that use the OpenGL library code.

Initializing Vertices

initializeVertices implementations will fill these vector VisualModel members:

//! CPU-side data for indices
std::vector<GLuint> indices;
//! CPU-side data for vertex positions
std::vector<float> vertexPositions;
//! CPU-side data for vertex normals
std::vector<float> vertexNormals;
//! CPU-side data for vertex colours
std::vector<float> vertexColors;

Once initializeVertices has returned, finalize calls VisualModel::postVertexInit, which essentially copies the data in the vectors into the OpenGL context, using the OpenGL attributes VisualModel::vao and VisualModel::vbos:

//! The OpenGL Vertex Array Object
GLuint vao;
//! Vertex Buffer Objects stored in an array
std::unique_ptr<GLuint[]> vbos;

Each triplet of elements in vertexPositions defines a coordinate in model space. There are an equal number of vertexNormal vectors and of vertexColors. indices is filled with triplets of indices into vertexPositions that define triangles. To write an initializeVertices implementation, you simply need to create the vertex coordinates and a suitable sequence of indices. Here’s about as simple an example as possible; a single triangle:

void initializeVertices()
{
    // First clear out vertexPositions, etc:
    this->clear();
    // Now draw a triangle from three vertices
    vec<float> v1 = { 0, 0, 0 };
    vec<float> v2 = { 1, 0, 0 };
    vec<float> v3 = { 0, 1, 0 };
    // Compute the face normal
    vec<float> u1 = v1 - v2;
    vec<float> u2 = v2 - v3;
    vec<float> face_normal = u1.cross (u2);
    face_normal.renormalize();
    // Push three corner vertices
    this->vertex_push (v1, this->vertexPositions);
    this->vertex_push (v2, this->vertexPositions);
    this->vertex_push (v3, this->vertexPositions);
    // Push corresponding colours/normals
    for (size_t i = 0; i < 3; ++i) {
        this->vertex_push (morph::colour::crimson, this->vertexColors);
        this->vertex_push (face_normal, this->vertexNormals);
    }
    // Push indices
    this->indices.push_back (this->idx++); // Pushes 0
    this->indices.push_back (this->idx++); // Pushes 1
    this->indices.push_back (this->idx++); // Pushes 2
    // At end, idx has the value 3
}

You may not need to fill the vertices and indices manually in every case. If you wish to build a model from rods, cones, discs and spheres, then you can use the VisualModel graphics primitives.

If you need to create a custom visualization, then you may well end up needing to write your own initializeVertices function. Consult other examples in the graphics primitives for more inspiration.

Re-initializing a VisualModel

If you need your VisualModel to change then you will need to reinitialize it. There are several reinit functions for different purposes:

void reinit() clears out your vertexPositions (etc) and then calls initilizeVertices afresh. If your initializeVertices function uses data, and the data has changed, the new graphical model will reflect the changes in the data. reinit does not clear out any text labels. For that you need reinit_with_clearTexts().

reinit_colour_buffer allows you to update only the vertexColors. Where you are using only colour to indicate changing values, this can be a very efficient way of updating your visualization.

The VisualModel coordinate frame

When you add vertices to a VisualModel, you do so in the model’s own frame of reference. The VisualModel coordinate frame uses the same arbitrary unit of length as the scene’s coordinate frame.

The VisualModel class holds two transformation matrices (a model view matrix and a scene view matrix) which are applied in the shader when the models are rendered in the scene. The scene view matrix changes as the scene view point is altered (by mouse events). The model view matrix is a way to rotate the model’s coordinate frame independently.

Adding text labels to VisualModels

The VisualModel base class provides addLabel methods to add texts to your models.

visualModel_ptr->addLabel ("Label text", {0, -1, 0}, morph::TextFeatures(0.06f));

Here, the first argument is simply a const std::string& of the text you want to display. The second argument is a coordinate in the model frame which defines where the text should appear and the third argument is a TextFeatures object that defines font size, face, resolution and colour. Usually, you construct TextFeatures in the addLabel arguments.

With the simplest TextFeatures constructor, you set only the font size to 0.06 (vm_ptr is a VisualModel* pointer):

vm_ptr->addLabel ("Label text", {0, -1, 0}, morph::TextFeatures(0.06f));

You can set the text colour with a second argument in a two-argument constructor. Colours can be chosen from the morph::colour namespace.

vm_ptr->addLabel ("Large text", {0, -1, 0}, morph::TextFeatures(0.12f, morph::colour::crimson));

Another two-argument constructor allows you to set the font size and the resolution:

vm_ptr->addLabel ("High-res text", {0, -1, 0}, morph::TextFeatures(0.3f, 128));

There is a three argument constructor to set font size, font resolution and font colour. The font resolution here is the resolution of the bitmap image for the font glyph. You have to adjust this based on the size of the font; it it is too high or too low, your fonts will not look very good on screen.

vm_ptr->addLabel ("Small text", {0, -1, 0}, morph::TextFeatures(0.03f, 48, morph::colour::red));

The font face can be chosen like this with the 5 argument constructor. Available font faces are found in the enum class morph::VisualFont in VisualFace.h

bool no_centering = false;
vm_ptr->addLabel ("Small text", {0, -1, 0}, morph::TextFeatures(0.03f, 48, no_centering,
                                                                morph::colour::blue1,
                                                                morph::VisualFont::DVSansItalic));

When you add a text to a VisualModel is is created as a VisualTextModel and added to a member attribute which is a vector of texts. This is a vector of unique pointers, indicating that the VisualModel owns the memory associated with the texts.

template <int glver = morph::gl::version_4_1>
class VisualModel
{
    ...
    //! A vector of pointers to text models that should be rendered.
    std::vector<std::unique_ptr<morph::VisualTextModel<glver>>> texts;
    ...
};

By default, the text will have its vertical axis aligned with the model coordinate frame’s ‘y’ axis and the horizontal axis is aligned with the ‘x’ axis. It is possible to change this by rotating the text models (see morph::GraphVisual::drawAxisLabels for example code; the coordinate axis labels in CoordArrows also rotate).

Adding text with symbols

You can incorporate a variety of symbols in your text, including Greek characters and mathematical symbols. This is achieved with the help of morph::unicode and the VisualFont::DejaVu that provides a wide range of non-Latin Glyphs.

std::string spc(", ");
std::string greek = "Greek ABC: "
                    + morph::unicode::toUtf8 (morph::unicode::alpha)
                    + spc + morph::unicode::toUtf8 (morph::unicode::beta)
                    + spc + morph::unicode::toUtf8 (morph::unicode::gamma);
vm_ptr->addLabel (greek, {0,0,0});

morph::unicode::alpha is a constexpr char32_t containing the unicode value for the alpha character which is 0x03b1. morph::unicode::toUtf8 is a static function that converts this 32 bit character code into a sequence of 8 bit wide UTF-8 codes. These UTF-8 codes can be appended to your std::string and passed straight into VisualModel::addLabel. You can output the UTF-8 to a modern command line, too.

VisualModel features

There are a number of features built into VisualModel, including the transparency of the model, whether it is currently visible and whether it should be allowed to rotate.

Setting the alpha channel

The setAlpha function allows you to set the transparency of a VisualModel. The single argument is the alpha value, with 0 giving a fully transparent (i.e. invisible) model and 1 giving a fully opaque model (the default).

vm_ptr->setAlpha (0.8f); // valid range: [0, 1]

Hiding a model

It is sometimes useful to hide a model in a scene. This is carried out with setHide(bool) and toggleHide().

vm_ptr->setHide();      // Hide the vm_ptr model
vm_ptr->setHide(false); // Un-hide the model
vm_ptr->toggleHide();   // Toggle hiddenness

Scaling the model

The function VisualModel::setSizeScale(float) sets up a transformation matrix VisualModel::model_scaling which is multiplied by the view matrix on each call to render(). The argument to setSizeScale scales the model equally in all directions by a scalar factor.

Two dimensional models

OpenGL is fundamentally three dimensional. However, some models, such as a GraphVisual are only really useful as two dimensional figures. It’s possible to prevent a model from being rotatable within the scene, even when other models can rotate. Use the Boolean VisualModel::twodimensional attribute to indicate which.

cube_vm_ptr->twodimensional = false; // This cube won't rotate

You can change this attribute at any time, it will be used on each call to render(). In some derived classes, such as GraphVisual, twodimensional is set to true by default.

Graphics primitives

Tubes

Rods or tubes can be created with the computeTube functions:

void computeTube (vec<float> start, vec<float> end,
                  std::array<float, 3> colStart, std::array<float, 3> colEnd,
                  float r = 1.0f, int segments = 12)

Here we specify start and end coordinates for the tube, along with start and end colours, a tube radius and the number of segments to draw the tube. With segments set to 4, you will get a square section tube.

Screenshot of rods/tubes

Example code to generate the image above is in examples/rod.cpp.

Arrows are tube-like (VectorVisual makes use of this, see examples/vectorvis.cpp):

void computeArrow (const vec<float>& start, const vec<float>& end,
                   const std::array<float, 3> clr,
                   float tube_radius = -1.0f,
                   float arrowhead_prop = -1.0f,
                   float cone_radius = -1.0f,
                   const int shapesides = 18)

There is also a flared tube:

void computeFlaredTube (morph::vec<float> start, morph::vec<float> end,
                        std::array<float, 3> colStart, std::array<float, 3> colEnd,
                        float r = 1.0f, int segments = 12, float flare = 0.0f)

Lines

If a tube is the wrong kind of line for your visualization, you may need a ‘flat line’. We have VisualModel::computeFlatLine and friends.

Cones

Use computeCone to draw a cone. Provide coordinates of the centre of the cone base, the tip along with colour, radius, number of segments to draw with and finally a ringoffset, the effect of which is obvious in the image below (it offsets the circle of the cone away from the base so that the cone effectively becomes a double cone with two tips).

void computeCone (vec<float> centre, vec<float> tip,
                  float ringoffset, std::array<float, 3> col, float r = 1.0f, int segments = 12)

Screenshot of the computeCone example

Example computeCone code: examples/cone.cpp

Spheres

There are a couple of different sphere primitives. computeSphere draws a fan of triangles at each end, then fills in the space with rings of triangles. The image below shows also computeSphereGeo which computes an icosahedral geodesic to pattern the triangles.

float x = 1.2f;
this->computeSphere (morph::vec<float>{-x, 0.0f, 0.0f }, morph::colour::royalblue, 1.0f, 12, 12);
// These compute the sphere from a geodesic icosahedron. First with 2 triangulation iterations
this->computeSphereGeo (morph::vec<float>{ x, 0.0f, 0.0f }, morph::colour::maroon, 1.0f, 2);
float y = x * std::tan (morph::mathconst<float>::pi_over_3);
// This one with 3 iterations (meaning more triangles and a smoother sphere) and compile-time geodesic computation
this->computeSphereGeoFast (morph::vec<float>{ 0.0f, y, 0.0f }, morph::colour::cyan3, 1.0f, 3);

Screenshot of spheres

examples/sphere.cpp generated the image above.

Rings

computeRing draws a ring made of flat quads. Example is examples/ring.cpp.

Screenshot of rings

Discs

Thick discs can be made with very short tubes (use computeTube). There is also a function to make 2D polygons: computeFlatPoly.

this->computeFlatPoly (vec<float> vstart,
                       vec<float> _ux, vec<float> _uy,
                       std::array<float, 3> col,
                       float r = 1.0f, int segments = 12, float rotation = 0.0f)

This’ll need some explanation.

Protected attributes

vao, vbos, indices, vertexPositions, vertexNormals, vertexColors

Methods used when saving glTF files