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:
GeometryRendererrenders geometry (internally used byGeometryResource, mentioned in the previous tutorial T1: Introduction to HIRO GUI)LineFieldRendererrenders many lines as when visualizing vector fieldsBillboardRendererrenders 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.