T2: Creating Custom Modules — HIRO 0.7.0 documentation

# T2: Creating Custom Modules

The true power of HIRO comes with the ability to create custom data modules. However, before learning that, you must understand the basic architecture, which describes how HIRO modules communicate with the engine. This tutorial guides you through this process.

## Architecture

As we saw in the first section, the main window of HIRO can have one or more viewareas, each consisting of a viewport and sidebar. Each viewarea visualizes on its own, but sometimes, it may render the same data as the other viewarea, but with different settings. Therefore, HIRO uses the concept of resource and gadget, existing together as the primary building block of every data module. Since these are only abstract concepts, you can imagine `GeometryResource` and `GeometryGadget` as specific class examples instead.

Object responsibilities.

The user program creates the desired resource object, which is added to HIRO as shown in the previous tutorial. Gadget, however, is never created by the user directly. When the user selects a resource in the resource inspector, the corresponding viewarea requests the resource to create a gadget. This new gadget exists in the context of the current viewarea only. A gadget is never shared between multiple viewareas, while all resources are shared in the whole engine (and user application).

An example of a possible application state.

In the example above, the user program created two resource objects, and the window currently has three viawareas. The first viewarea requested both resource objects to instantiate and therefore manages two gadget objects, while the second viewarea has instanced only resource object B. Third viewarea currently has no gadget object.

## Simplest of Modules

To create a custom data module, the `Resource` and `Gadget` classes must be derived. The following code shows an example of a straightforward data module, which does nothing apart from being shown in the resource inspector.

     1#include <HIRO/Resource.h>
     2
     3class MyGadget : public hiro::Gadget
     4{
     5public:
     6  MyGadget(const hiro::Resource *res) : hiro::Gadget(res) {}
     7};
     8
     9class MyResource : public hiro::Resource
    10{
    11public:
    12  MyResource(const std::string &name)
    13    : hiro::Resource(hiro::ResourceId(name, "MyResource"))
    14  {
    15  }
    16  hiro::PGadget CreateGadget() override
    17  {
    18    return std::make_shared<MyGadget>(this);
    19  }
    20};

The constructor of the derived `Resource` class requires an identifier, specifying the name by which the HIRO should recognize this resource. Abstract method `Resource::CreateGadget` is overridden to define the specific gadget class corresponding to the resource. In this example, for `MyResource, `` we want to create the ``MyGadget` just by making a shared pointer to a new object instance, which you do in most cases.

## Rendering

To render data, HIRO uses the HIRO_DRAW library. It provides several renderers that you can use to render a specific data type. For example:

- `GeometryRenderer` renders geometry (internally used by `GeometryResource`, mentioned in the previous tutorial T1: Introduction to HIRO GUI)

- `LineFieldRenderer` renders many lines as when visualizing vector fields

- `BillboardRenderer` renders 2D textures on 3D planes

These and many other renderers are available to use in your custom module. You can learn more about all provided structures in namespace hiro::draw documentation.

Each renderer has a corresponding style object assigned. The style defines rendering properties, such as a material. To use HIRO_DRAW renderers in the custom module, you must register a specific renderer and style object pair using the `Gadget::AddRenderer` method. It is recommended to do this in the `Gadget::Initialize` method.

     1...
     2#include <HIRO_DRAW\renderers\GeometryRenderer.h>
     3
     4class MyGadget : public hiro::Gadget
     5{
     6public:
     7  MyGadget(const hiro::Resource *res) : hiro::Gadget(res) {}
     8
     9  virtual void Initialize() override
    10  {
    11    Gadget::Initialize();
    12    renderer_ = std::make_shared<hiro::draw::GeometryRenderer>(
    13      hiro::draw::GeometryName::sphere_s1
    14    );
    15    style_ = std::make_shared<hiro::draw::GeometryStyle>();
    16    this->AddRenderer(renderer_, style_);
    17  }
    18
    19private:
    20    hiro::draw::PGeometryRenderer renderer_;
    21    hiro::draw::PGeometryStyle style_;
    22};
    23...

Hint

This piece of code also uses HIRO_DRAW library. You should include it in the project linker input for successful linking. See page T0: Setting up Visual Studio Project for more information.

It is also possible to define a custom renderer by inheriting the `Renderer` class and overriding some of its methods. Optionally, you can inherit from the more specified `ElementRenderer` class if you create a renderer designed to visualize geometry data.

## Resource as Program Interface

This section describes how to create a proper interface between the user program and gadget, using the resource objects. The goal is to be able to create a custom mesh in the user program and visualize it in the HIRO window via our custom module.

main.cpp

     1#include <HIRO/HIRO.h>
     2#include "MyResource.h"
     3
     4cogs::Mesh CreatePyramid()
     5{
     6  cogs::Mesh pyramid;
     7  pyramid.points = std::make_shared<cogs::PointCloud>();
     8  pyramid.points->Resize(5);
     9  auto positions = pyramid.points->GetPositions();
    10  positions[0] = glm::vec3(0, 1, 0);
    11  positions[1] = glm::vec3(0.5, 0, 0.5);
    12  positions[2] = glm::vec3(0.5, 0, -0.5);
    13  positions[3] = glm::vec3(-0.5, 0, 0.5);
    14  positions[4] = glm::vec3(-0.5, 0, -0.5);
    15
    16  pyramid.faces = std::make_shared<cogs::Triangulation>();
    17  pyramid.faces->SetFaces(
    18  {
    19    {0, 1, 2},
    20    {0, 2, 4},
    21    {0, 4, 3},
    22    {0, 3, 1},
    23    {3, 4, 2},
    24    {2, 1, 3},
    25  });
    26  return pyramid;
    27}
    28
    29int main()
    30{
    31  hiro::SetAssetDirectory("./hiro_libs/assets/");
    32  hiro::SetIntermediateDirectory("./temp/");
    33
    34  hiro::Initialize();
    35
    36  auto pyramid = CreatePyramid();
    37  auto pyramid_res = std::make_shared<MyResource>("Pyramid", pyramid);
    38  hiro::AddResource(pyramid_res);
    39
    40  while (hiro::IsOpen())
    41  {
    42    hiro::Update();
    43  }
    44
    45  hiro::Terminate();
    46  return 0;
    47}

To the main file, we add a function `CreatePyramid()` that will initialize `cogs::Mesh` structure by defining proper vertices and faces. The code shows you how to define your own mesh right in the code. The mesh consists of a set of vertices, also known as a pointcloud. Additionally, a vector of triplets is supplied specifying which points (by index) should form triangles, wrapped in the `cogs::Triangulation` class. Alternatively, `cogs::Mesh` class has an `Import` method, supporting several mesh file formats.

Apart from that, we created an instance of object `MyResource`, which now takes the second parameter corresponding to a mesh object we want to visualize. Sure, you can not do this without changing the implementation of class `MyResource` itself, now residing in a separate file.

MyResource.h

     1#pragma once
     2#include <COGS/Mesh.h>
     3#include <HIRO/Resource.h>
     4#include <HIRO_DRAW/renderers/MeshRenderer.h>
     5
     6class MyResource : public hiro::Resource
     7{
     8public:
     9  MyResource(const std::string &name, const cogs::Mesh &mesh);
    10  hiro::PGadget CreateGadget() override;
    11  hiro::draw::PMeshRenderer GetMeshRenderer() const;
    12private:
    13  hiro::draw::PMeshRenderer mesh_renderer_;
    14};

As mentioned, `MyResource` now takes the second parameter that specifies a mesh we want to visualize. The class only creates and stores the `MeshRenderer` object since we do not need the whole mesh object for visualization. The interface of `MyResource` allows getting the `MeshRenderer` object via the method `GetMeshRenderer`.

MyResource.cpp

     1#include "MyGadget.h"
     2#include "MyResource.h"
     3
     4MyResource::MyResource(const std::string &name, const cogs::Mesh &mesh)
     5  : hiro::Resource(hiro::ResourceId(name, "MyResource"))
     6{
     7  mesh_renderer_ = std::make_shared<hiro::draw::MeshRenderer>(mesh);
     8}
     9
    10hiro::PGadget MyResource::CreateGadget()
    11{
    12  return std::make_shared<MyGadget>(this);
    13}
    14
    15hiro::draw::PMeshRenderer MyResource::GetMeshRenderer() const
    16{
    17  return mesh_renderer_;
    18}

Nothing fancy going on in here.

MyGadget.h

     1#pragma once
     2#include <HIRO/Gadget.h>
     3#include <HIRO_DRAW/renderers/MeshRenderer.h>
     4
     5class MyGadget : public hiro::Gadget
     6{
     7public:
     8  MyGadget(const hiro::Resource *res);
     9  void Initialize() override;
    10private:
    11  hiro::draw::PMeshStyle style_;
    12};

Nothing fancy going on in here either.

MyGadget.cpp

     1#include "MyResource.h"
     2#include "MyGadget.h"
     3
     4MyGadget::MyGadget(const hiro::Resource *res) : hiro::Gadget(res)
     5{
     6}
     7
     8void MyGadget::Initialize()
     9{
    10  hiro::Gadget::Initialize();
    11  style_ = std::make_shared<hiro::draw::MeshStyle>();
    12  style_->render_mode = hiro::draw::MeshStyle::RenderMode::wired_faces;
    13  AddRenderer(GetResource<MyResource>()->GetMeshRenderer(), style_);
    14}

Gadget classes remember the resource objects that they were constructed from automatically. The resource can be accessed using the template method `GetResource` in which you can specify the same resource class if known. To get the renderer object stored in `MyResource`, use the method `GetResource<MyResource>` to the resource that created this gadget and call the interface method `GetMeshRenderer` we made earlier.

Following a similar procedure, you can create other setter and getter methods to resource classes and use them to set the data the gadgets.

Hint

To simplify class interfaces, do not store gadget objects created by a resource in the resource object. It may be incredibly tempting when you wish to update some gadget parameters on the resource setter called by the user program. Instead, you can use the method `Resource::ResetGadgets` that effectively destroys and creates gadget objects created by the resource called this method.

Hint

It is an excellent practice to split data storage between resource and gadget objects, as done in this tutorial. The resource is used to store the extensive data, shared among several gadget objects persistently. The gadget stores only parameters that can be different for every viewarea or changed during runtime, such as rendering options. Both efficiency and code readability will benefit.