Developing new basic visualization features for Cogs.Core can in most cases be done following the pattern described here.
When developing a feature there are of course several aspects of said feature to evaluate and reason about before starting the implementation. Understanding the anatomy of the feature is crucial for choosing the right implementation strategy.
Common attributes to evaluate are:
For the purposes of this guide we will study a relatively simple feature visualizing sharks at locations around the scene. First we consider the required input data from the application side:
We will have meshes created for the different kinds of sharks, 4 in total. These are named GreatWhite.fbx, Blue.fbx, Whale.fbx and Tiger.fbx.
At each sharks location we should display an arrow (just a line segment) pointing in the direction of the sharks travel, with magnitude based on the speed of the shark.
We can count on the number of sharks being <= 100.
This kind of scenario is quite similar to many basic visualizations performed in our software today. When considering this feature it should be readily apparent that no very special GPU or parallel techniques are necessary to implement the basics.
We can use existing functionality of the engine to handle
What we have to do in our new code is:
We will design the feature using a component and component system pair to handle data and processing.
First we add our component files to the project, SharkComponent.h and SharkComponent.cpp are placed in Source/Components.
We also add files to contain our new system, SharkSystem.h and SharkSystem.cpp to Source/Systems.
Update the project files and we are ready to implement.
We first implement the component, responsible for receiving and storing shark data from application code. We implement a registration method to be able to use reflection to populate the speed and direction fields.
// SharkComponent.h
#include "ComponentBase.h"
// We use an enum mapped to integer to represent different types of shark.
struct SharkType
{
enum ESharkTypes : int
{
GreatWhite,
Blue,
Whale,
Tiger
}
};
/*! Remember Component documentation */
struct SharkComponent : public ComponentModel::ComponentBase
{
public SharkComponent() :
direction(0),
speed(0),
type(SharkType::GreatWhite)
{}
static void registerType();
/*! Remember field documentation */
float direction;
float speed;
int type;
};
// This is to make reflecting the name of the type possible.
template<> inline std::string getName() { return "SharkComponent"; }
// SharkComponent.cpp
#include "SharkComponent.h"
#include "Types.h"
using namespace Cogs::Reflection;
void SharkComponent::registerTypes()
{
// Create information about all fields we want to be publicly available.
Field fields[] = {
Field(Name("direction"), &SharkComponent::direction),
Field(Name("speed"), &SharkComponent::speed),
Field(Name("type"), &SharkComponent::type),
};
// This makes our type available to the type system as "SharkComponent"
ComponentBase::registerType().setFields(fields);
}
We now have a component ready to be managed by our component system. We will first take a look at the definition of the system, the implementation comes later.
// SharkSystem.h
// Defines the ComponentSystem template
#include "Systems/ComponentSystem.h"
// Our new component
#include "Components/SharkComponent.h"
// To support mesh construction and handling
#include "Resources/MeshManager.h"
class Context;
class SharkSystem : public ComponentSystem
{
public:
// This will be called every frame before rendering.
void update(Context * context) override;
private:
// Initialize first time in each context.
bool first = true;
// This will hold handles for our shark meshes, to be shared between actual shark instances
MeshHandle sharkMeshes[4];
// Handle to a mesh where we will produce geometry for all shark arrows.
MeshHandle arrowMeshHandle;
// Entity to create arrow mesh in
EntityPtr entity;
};
We now have the definition of our system ready and we can integrate into other parts of the engine. First we modify Types.cpp Context.cpp to initialize our component type and system:
// SharkSystem.cpp
... other include statements
#include "Systems/SharkSystem.h"
void initializeTypes()
{
...
SharkComponent::registerType();
}
// In Context.cpp
...
void initialize()
{
...
LOG_DEBUG(logger, "Initialized static SharkSystem.");
// We add our shark system here, this instantiates the system and ensures the update method
// is called every frame. After registering the system, the engine knows how to create components
// with the type name "SharkComponent".
engine->registerSystem(SystemPriority::Geometry, nullptr, 128);
}
We now need to create an actual entity type for sharks. We can load the following JSON snippet in SharkExtension.cpp:
constexpr const char* cSharkEntityDefinition = R"(
{
"name": "Shark",
"components": [ "TransformComponent", "SharkComponent", "MeshComponent", "MeshRenderComponent" ]
})";
readEntityDefinition(cSharkEntityDefinition, context->store);
into the engine in EntityDefinitions.cpp. This ensures that when someone calls createEntity with type "Shark" all of the above components will be instantiated and added to the entity. The component names are resolved to types and the systems responsible for the given types allocate and manage the component instances for the engine.
We can now move on to the implementation of our shark system:
// SharkSystem.cpp
#include "SharkSystem.h"
#include "Context.h"
#include "Components/TransformComponent"
#include "Components/MeshComponent"
// Elided for space
MeshHandle loadSimpleModel();
void SharkSystem::update(Context * context)
{
// We want to do some initialization the first time we run with an available context.
if (first) {
sharkMeshes[0] = loadSimpleModel("GreatWhite.fbx");
sharkMeshes[1] = loadSimpleModel("Blue.fbx");
sharkMeshes[2] = loadSimpleModel("Whale.fbx");
sharkMeshes[3] = loadSimpleModel("Tiger.fbx");
entity = context->store->createEntity("Arrows", "Default");
arrowMeshHandle = context->meshManager->create();
entity->getComponent()->meshHandle = arrowMeshHandle;
first = false;
}
// Retrieve a pointer to the actual Mesh instance from the model manager. The Mesh holds
// vertex and index data in addition to information about the primitive type etc.
auto arrowMesh = context->meshManager->get(arrowMeshHandle);
// Clear out existing data, making room for our new arrows.
arrowMesh->clear();
// We want to draw the arrows as simple lines.
arrowMesh->primitiveType = PrimitiveType::LineSet;
// Create vectors to hold the vertexes (position only) and indexes for our mesh.
std::vector arrowPositions;
std::vector arrowIndexes;
for (auto & shark : pool) {
if (!shark.hasChanged()) continue;
auto meshComponent = shark.getComponent();
// We select the mesh to use based on the shark type. Setting the mesh handle here
// will cause the mesh to be rendered with the transform and other parameters of this
// instance.
meshComponent->meshHandle = sharkMeshes[shark.type];
// We fetch the transform component to get the position of the shark in 3D space.
auto transformComponent = shark.getComponent();
// Use the position of the shark as starting position for the arrow geometry.
auto basePosition = transformComponent->position;
auto direction = glm::normalize(glm::transform(glm::fromAxisAngle(glm::vec3(0, 0, 1), direction), glm::vec3(0, 1, 0)));
arrowPositions.push_back(basePosition);
// Extend the arrow in the sharks direction of travel.
arrowPositions.push_back(basePosition + direction * speed);
// Add indexes drawing a single line between the vertexes used for this shark instance.
arrowIndexes.push_back(arrowPositions.size() - 2);
arrowIndexes.push_back(arrowPositions.size() - 1);
}
// Set our newly generated vertex and index data on the mesh. The mesh is already set on our
// arrow entity's mesh component, so the next time it renders will be using this data.
arrowMesh->setPositions(arrowPositions);
arrowMesh->setIndexes(arrowIndexes);
}