Scene Graph
Guest-Articles/2021/Scene/Scene-Graph
In this chapter, we will talk about the
First of all, let’s create a transform. Transform will contain local and global space information about our entities
struct Transform
{
/*SPACE INFORMATION*/
//Local space information
glm::vec3 pos = { 0.0f, 0.0f, 0.0f };
glm::vec3 eulerRot = { 0.0f, 0.0f, 0.0f };
glm::vec3 scale = { 1.0f, 1.0f, 1.0f };
//Global space information concatenate in matrix
glm::mat4 modelMatrix = glm::mat4(1.0f);
};
Now we can represent local and global space information about an object.
We will make create a class called
class Entity : public Model
{
public:
[...]
Transform transform;
// constructor, expects a filepath to a 3D model.
Entity(string const& path, bool gamma = false) : Model(path, gamma)
{}
};
The scene graph is still missing. So, let's add it into the Entity class.
/*SCENE GRAPH*/
std::list<std::unique_ptr<Entity>> children;
Entity* parent = nullptr;
As we said, scene graphs can take a lot of forms: use std::vector instead of std::list, don't register its parent to simplify the size of the class... In our example we used std::list. We don't want our entity address to change at all. My parents’ address always needs to be valid. Then, I used std::unique_ptr because it's the cleanest approach to avoid a leak. Memory leak appears happens if you allocate memory thanks to new or malloc and forget to call delete or free when datas disappears. Information is still in memory but you cannot have access to it. If you want more information about it, many tutorials talk about it more clearly.
- We need now to create a function to add a child to our entity. This function will be easy:
- 1: Add entity
- 2: Register parent of this new entity as the current entity
template<typename... TArgs>
void addChild(const TArgs&... args)
{
children.emplace_back(std::make_unique<Entity>(args...));
children.back()->parent = this;
}
Ok, perfect! It's almost done, courage! Now we have scene graphs and space information but something is still missing. When we want to send space information to our shader, we need a model matrix. However, we don't see how to compute it thanks to our parents and our local information. First, we need a function to create a local model matrix that represents an object in space based on its parent. This function will be added to the transform class.
glm::mat4 getLocalModelMatrix()
{
const glm::mat4 transformX = glm::rotate (glm::mat4(1.0f),
glm::radians (eulerRot.x),
glm::vec3(1.0f, 0.0f, 0.0f));
const glm::mat4 transformY = glm::rotate (glm::mat4(1.0f),
glm::radians (eulerRot.y),
glm::vec3(0.0f, 1.0f, 0.0f));
const glm::mat4 transformZ = glm::rotate (glm::mat4(1.0f),
glm::radians (eulerRot.z),
glm::vec3(0.0f, 0.0f, 1.0f));
// Y * X * Z
const glm::mat4 roationMatrix = transformY * transformX * transformZ;
// translation * rotation * scale (also know as TRS matrix)
return glm::translate (glm::mat4(1.0f), pos) *
roationMatrix *
glm::scale (glm::mat4(1.0f), scale);
}
To combine multiple model matrices, we need to multiply them. So if I multiply the local model matrix of my hand with the model matrix of the world with arm and arm with hand, I will obtain the global matrix of my hand! So, let's implement a function in entity class that will do it for us:
void updateSelfAndChild()
{
if (parent)
modelMatrix = parent->modelMatrix * getLocalModelMatrix();
else
modelMatrix = getLocalModelMatrix();
for (auto&& child : children)
{
child->updateSelfAndChild();
}
}
Now, if I update the world, all of my entities contained will also be updated and our global model matrix will be computed. Finally, we just need to add some code in the main function to add multiple moons with the same local distance and scale and code in the main loop to move the first entity.
/*BEFOR THE MAIN LOOP*/
// load entities
// -----------
const char* pathStr = "resources/objects/planet/planet.obj";
Entity ourEntity(FileSystem::getPath(pathStr));
ourEntity.transform.pos.x = 10;
const float scale = 0.75;
ourEntity.transform.scale = { scale, scale, scale };
{
Entity* lastEntity = &ourEntity;
for (unsigned int i = 0; i < 10; ++i)
{
lastEntity->addChild(FileSystem::getPath(pathStr));
lastEntity = lastEntity->children.back().get();
//Set tranform values
lastEntity->transform.pos.x = 10;
lastEntity->transform.scale = { scale, scale, scale };
}
}
ourEntity.updateSelfAndChild();
/*IN THE MAIN LOOP*/
// draw our scene graph
Entity* lastEntity = &ourEntity;
while (lastEntity->children.size())
{
ourShader.setMat4("model", lastEntity->transform.modelMatrix);
lastEntity->Draw(ourShader);
lastEntity = lastEntity->children.back().get();
}
ourEntity.transform.eulerRot.y += 20 * deltaTime;
ourEntity.updateSelfAndChild();
We can see this result thanks to this code.
Optimization
Being pragmatic is essential to implement a new feature but now let's see how to optimize it. Firstly, in the main loop, we always update the model matrix even if it doesn't move. In programming, a pattern called a dirty flag can help us. A dirty flag is a simple boolean called "isDirty" that allows us to know if an entity was moved during the previous frame. So, if the user scales, rotates or translates it, we need to set this flag on. Don't forget, the model matrix is based on the parent model matrix. So if I change it, I also need to compute all its children's matrix. This flag will be set off in the update function when the model matrix was recomputed.
Let's remake do again transform function with encapsulation and dirty flag
class Transform
{
protected:
//Local space information
glm::vec3 m_pos = { 0.0f, 0.0f, 0.0f };
glm::vec3 m_eulerRot = { 0.0f, 0.0f, 0.0f }; //In degrees
glm::vec3 m_scale = { 1.0f, 1.0f, 1.0f };
//Global space information concatenate in matrix
glm::mat4 m_modelMatrix = glm::mat4(1.0f);
//Dirty flag
bool m_isDirty = true;
protected:
glm::mat4 getLocalModelMatrix()
{
const glm::mat4 transformX = glm::rotate (glm::mat4(1.0f),
glm::radians (m_eulerRot.x),
glm::vec3(1.0f, 0.0f, 0.0f));
const glm::mat4 transformY = glm::rotate (glm::mat4(1.0f),
glm::radians (m_eulerRot.y),
glm::vec3(0.0f, 1.0f, 0.0f));
const glm::mat4 transformZ = glm::rotate (glm::mat4(1.0f),
glm::radians (m_eulerRot.z),
glm::vec3(0.0f, 0.0f, 1.0f));
// Y * X * Z
const glm::mat4 roationMatrix = transformY * transformX * transformZ;
// translation * rotation * scale (also know as TRS matrix)
return glm::translate (glm::mat4(1.0f), m_pos) *
roationMatrix *
glm::scale (glm::mat4(1.0f), m_scale);
}
public:
void computeModelMatrix()
{
m_modelMatrix = getLocalModelMatrix();
m_isDirty = false;
}
void computeModelMatrix(const glm::mat4& parentGlobalModelMatrix)
{
m_modelMatrix = parentGlobalModelMatrix * getLocalModelMatrix();
m_isDirty = false;
}
void setLocalPosition(const glm::vec3& newPosition)
{
m_pos = newPosition;
m_isDirty = true;
}
[...]
const glm::vec3& getLocalPosition()
{
return m_pos;
}
[...]
const glm::mat4& getModelMatrix()
{
return m_modelMatrix;
}
bool isDirty()
{
return m_isDirty;
}
};
class Entity : public Model
{
public:
//Scene graph
std::list<std::unique_ptr<Entity>> children;
Entity* parent = nullptr;
//Space information
Transform transform;
// constructor, expects a filepath to a 3D model.
Entity(string const& path, bool gamma = false) : Model(path, gamma)
{}
//Add child. Argument input is argument of any constructor that you create.
//By default you can use the default constructor and don't put argument input.
template<typename... TArgs>
void addChild(const TArgs&... args)
{
children.emplace_back(std::make_unique<Entity>(args...));
children.back()->parent = this;
}
//Update transform if it was changed
void updateSelfAndChild()
{
if (transform.isDirty())
{
forceUpdateSelfAndChild();
return;
}
for (auto&& child : children)
{
child->updateSelfAndChild();
}
}
//Force update of transform even if local space don't change
void forceUpdateSelfAndChild()
{
if (parent)
transform.computeModelMatrix(parent->transform.getModelMatrix());
else
transform.computeModelMatrix();
for (auto&& child : children)
{
child->forceUpdateSelfAndChild();
}
}
};
Perfect! Another solution to improve performance can be memorizing the local Model matrix. Thanks to it, we avoid recomputing all child local model matrices if the parent is moved. This solution improves performance but transforms become heavier. This size is really important for your hardware. We will see why in the limitation subchapter. You can find the code here.
Limitation
Do you ever hear about
This design is based on how your hardware works. In your computer, data is aligned into your memory. When you use data like a variable, this data is sent to the cache. The cache is a very fast memory but without a lot of space. This memory is localized into your CPU. For example, if you want to make a cake, you need to go to a supermarket (hard disk) to buy ingredients. A supermarket is very big but is also very far from your home. When you buy your ingredients, you store them in your fridge (RAM). Your fridge contains ingredients that you need to live but I hope for you that you don't live only with cake ^^ It is also small but near to your kitchen worktop. And finally, your kitchen worktop (the cache) is small and can't contain all your ingredients but is very near to your preparation. It's the same for the PC! When you need to process data, your system will copy your data but also all datas after in cache (depending on your cache line size). So, if all your datas are not contiguous in your memory, you will be as slow as if you need to take eggs one by one in your fridge to make your cake. std::list is a noncontiguous array. std::map will probably make the job better but inheritance cannot be aligned into your memory because it's a tree. So, if you want to make RTS for example, with an independent unit, you probably don't need or don't want a scene graph to manage your entities.
You know now what a Scene graph is and how to use it with its limitations. In the next chapter, we will talk about frustum culling with a scene graph.
Additional resources
- walterkuppens article about scene graph: An article by Walter Kuppens with another approach of the scene graph.
- webglfundamentals article and demonstration about scene graph: An article with animated webGL code and animated demonstration
Autor
Contact: e-mail
Date: 09/2021