T2: Creating Custom Modules

The true power of HIRO comes with the ability to create custom data modules. However, before you can learn that, you must know 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 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 main building block of every data module. Since these are only abstract concepts, you can imagine GeometryResource and GeometryGadget as specific class examples instead.

_images/architecture_1.svg

Object responsibilities.

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

_images/architecture_2.svg

An example of a possible application state.

In the example above, there were two resource objects created by the user program 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 has currently 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 really simple data module, which does nothing apart from being shown in the resource inspector.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
#include <HIRO/Resource.h>

class MyGadget : public hiro::Gadget
{
public:
  MyGadget(const hiro::Resource *res) : hiro::Gadget(res) {}
};

class MyResource : public hiro::Resource
{
public:
  MyResource(const std::string &name)
    : hiro::Resource(hiro::ResourceId(name, "MyResource"))
  {
  }
  hiro::PGadget CreateGadget() override
  {
    return std::make_shared<MyGadget>(this);
  }
};

The constructor of derived Resource class requires an identifier, specifying how should be this resource recognized in the system. Abstract method Resource::CreateGadget is overridden, to define the specific gadget class that should be created for the resource. In this example, for MyResource we want to create the MyGadget just by creating a shared pointer to a new object instance, which is something you do in most cases.

Rendering

To render data, HIRO uses the HIRO_DRAW library. It provides several renderers that can be used to render a specific type of data. 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 for you 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 specific renderer and style object pair using the Gadget::AddRenderer method. It is recommended to do this in the Gadget::Initialize method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...
#include <HIRO_DRAW\renderers\GeometryRenderer.h>

class MyGadget : public hiro::Gadget
{
public:
  MyGadget(const hiro::Resource *res) : hiro::Gadget(res) {}

  virtual void Initialize() override
  {
    Gadget::Initialize();
    renderer_ = std::make_shared<hiro::draw::GeometryRenderer>(
      hiro::draw::GeometryName::sphere_s1
    );
    style_ = std::make_shared<hiro::draw::GeometryStyle>();
    this->AddRenderer(renderer_, style_);
  }

private:
    hiro::draw::PGeometryRenderer renderer_;
    hiro::draw::PGeometryStyle style_;
};
...

Hint

This piece of code uses also HIRO_DRAW library. It must be included in project linker input for successful linking. See page T0: Setting up Visual Studio Project for more information.

There is also a possibility to define 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 are creating 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 being able to create a custom mesh in the user program and visualize it in the HIRO window via our custom module.

main.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#include <HIRO/HIRO.h>
#include "MyResource.h"

cogs::Mesh CreatePyramid()
{
  cogs::Mesh pyramid;
  pyramid.points = std::make_shared<cogs::PointCloud>();
  pyramid.points->Resize(5);
  auto positions = pyramid.points->GetPositions();
  positions[0] = glm::vec3(0, 1, 0);
  positions[1] = glm::vec3(0.5, 0, 0.5);
  positions[2] = glm::vec3(0.5, 0, -0.5);
  positions[3] = glm::vec3(-0.5, 0, 0.5);
  positions[4] = glm::vec3(-0.5, 0, -0.5);

  pyramid.faces = std::make_shared<cogs::Triangulation>();
  pyramid.faces->SetFaces(
  {
    {0, 1, 2},
    {0, 2, 4},
    {0, 4, 3},
    {0, 3, 1},
    {3, 4, 2},
    {2, 1, 3},
  });
  return pyramid;
}

int main()
{
  hiro::SetAssetDirectory("./hiro_libs/assets/");
  hiro::SetIntermediateDirectory("./temp/");

  hiro::Initialize();

  auto pyramid = CreatePyramid();
  auto pyramid_res = std::make_shared<MyResource>("Pyramid", pyramid);
  hiro::AddResource(pyramid_res);

  while (hiro::IsOpen())
  {
    hiro::Update();
  }

  hiro::Terminate();
  return 0;
}

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. Additionaly, 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 a several mesh file formats.

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

MyResource.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#pragma once
#include <COGS/Mesh.h>
#include <HIRO/Resource.h>
#include <HIRO_DRAW/renderers/MeshRenderer.h>

class MyResource : public hiro::Resource
{
public:
  MyResource(const std::string &name, const cogs::Mesh &mesh);
  hiro::PGadget CreateGadget() override;
  hiro::draw::PMeshRenderer GetMeshRenderer() const;
private:
  hiro::draw::PMeshRenderer mesh_renderer_;
};

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

MyResource.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
#include "MyGadget.h"
#include "MyResource.h"

MyResource::MyResource(const std::string &name, const cogs::Mesh &mesh)
  : hiro::Resource(hiro::ResourceId(name, "MyResource"))
{
  mesh_renderer_ = std::make_shared<hiro::draw::MeshRenderer>(mesh);
}

hiro::PGadget MyResource::CreateGadget()
{
  return std::make_shared<MyGadget>(this);
}

hiro::draw::PMeshRenderer MyResource::GetMeshRenderer() const
{
  return mesh_renderer_;
}

Nothing fancy going on in here.

MyGadget.h

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#pragma once
#include <HIRO/Gadget.h>
#include <HIRO_DRAW/renderers/MeshRenderer.h>

class MyGadget : public hiro::Gadget
{
public:
  MyGadget(const hiro::Resource *res);
  void Initialize() override;
private:
  hiro::draw::PMeshStyle style_;
};

Nothing fancy going on in here either.

MyGadget.cpp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include "MyResource.h"
#include "MyGadget.h"

MyGadget::MyGadget(const hiro::Resource *res) : hiro::Gadget(res)
{
}

void MyGadget::Initialize()
{
  hiro::Gadget::Initialize();
  style_ = std::make_shared<hiro::draw::MeshStyle>();
  style_->render_mode = hiro::draw::MeshStyle::RenderMode::wired_faces;
  AddRenderer(GetResource<MyResource>()->GetMeshRenderer(), style_);
}

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 exact 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 created 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 especially tempting when you wish to update some gadget parameters on resource setter called by the user program. Instead, you can use method Resource::ResetGadgets that effectively destroys and creates gadget objects that were created by the resource that called this method.

Hint

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