Scene-Graph.html (17528B)
1 <div id="content"> 2 <h1 id="content-title">Scene Graph</h1> 3 <h1 id="content-url" style='display:none;'>Guest-Articles/2021/Scene/Scene-Graph</h1> 4 <p> 5 In this article, we will talk about the <def>scene graph</def>. 6 A scene graph is not a class or an object, it's more like a pattern that allows you to create inheritance. 7 This pattern is used a lot in game engines. 8 For example, it is used in animation to manage bones. 9 If I move my arm, my hand needs to move too. 10 To obtain this result, I need a hierarchy with parents and children. 11 Thanks to it, my hand will be a child of my arm that is also a child of my body. 12 This hierarchy has the consequence of representing objects in global and local space. 13 Global space is based on position/rotation and scale based on the world and local space is based on its parent. 14 So, if I only move my hand, its local and global position will change but not the local and global position of my arm. 15 However, if I move my arm, its local and global position will change but also the global position of my hand. 16 Scene graphs can take a lot of forms: register or not the parent, list or contiguous buffer to store the children, flags... 17 Firstly, we will see a quick example of it. 18 In a second time, we will see simple optimizations to improve it. 19 So, talk less, code more ! ;D 20 </p> 21 22 <div class="box"> 23 <img src="/img/guest/2021/scene_graph/graph.png" width="400" style="display: inline" alt="Graph example"/> 24 <img src="/img/guest/2021/scene_graph/sceneGraph.png" width="400" style="display: inline" alt="Scene graph example in editor"/> 25 </div> 26 27 <p> 28 First of all, let’s create a transform. 29 Transform will contain local and global space information about our entities 30 </p> 31 32 <pre><code> 33 struct Transform 34 { 35 /*SPACE INFORMATION*/ 36 //Local space information 37 glm::vec3 pos = { 0.0f, 0.0f, 0.0f }; 38 glm::vec3 eulerRot = { 0.0f, 0.0f, 0.0f }; 39 glm::vec3 scale = { 1.0f, 1.0f, 1.0f }; 40 41 //Global space information concatenate in matrix 42 glm::mat4 modelMatrix = glm::mat4(1.0f); 43 }; 44 </code></pre> 45 46 <note> 47 In this example, we use the Euler angle system. 48 The Euler system is easy to use and understand but it contains a lot of undesirable effects: <a href="https://en.wikipedia.org/wiki/Gimbal_lock" target="_blank">Gimbal lock</a>, shortest angle between to angle thanks to lerp is wrong, cost and size... 49 To avoid it, we need to use <strong>quaternion</strong> but it needs to be hidden because it's hard to represent a rotation thanks to it. 50 To have a better understanding of this tutorial we will use the Euler angle but I invite you to do some research on quaternion. 51 </note> 52 53 <p> 54 Now we can represent local and global space information about an object. 55 We will make create a class called <funct>Entity</funct>. 56 This class will simply wrap the space information with a model for the visual demonstration. 57 </p> 58 59 <pre><code> 60 class Entity : public Model 61 { 62 public: 63 [...] 64 65 Transform transform; 66 67 // constructor, expects a filepath to a 3D model. 68 Entity(string const& path, bool gamma = false) : Model(path, gamma) 69 {} 70 }; 71 </code></pre> 72 <p> 73 The scene graph is still missing. So, let's add it into the Entity class. 74 </p> 75 <pre><code> 76 /*SCENE GRAPH*/ 77 std::list<std::unique_ptr<Entity>> children; 78 Entity* parent = nullptr; 79 </code></pre> 80 <p> 81 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... 82 In our example we used std::list. 83 We don't want our entity address to change at all. 84 My parents’ address always needs to be valid. 85 Then, I used std::unique_ptr because it's the cleanest approach to avoid a leak. 86 Memory leak appears happens if you allocate memory thanks to new or malloc and forget to call delete or free when datas disappears. 87 Information is still in memory but you cannot have access to it. 88 If you want more information about it, many tutorials talk about it more clearly. 89 </p> 90 91 <p> 92 <dl> 93 <dt> 94 We need now to create a function to add a child to our entity. 95 This function will be easy: 96 </dt> 97 <dd> 98 1: Add entity 99 </dd> 100 <dd> 101 2: Register parent of this new entity as the current entity 102 </dd> 103 </dl> 104 </p> 105 106 107 <pre><code> 108 template<typename... TArgs> 109 void addChild(const TArgs&... args) 110 { 111 children.emplace_back(std::make_unique<Entity>(args...)); 112 children.back()->parent = this; 113 } 114 </code></pre> 115 116 <note> 117 A variadic template allows us to create an entity with any constructor that you want to use without overloading the addChild function. 118 I used it in this example to awake your curiosity and show you a simple use of it. 119 </note> 120 121 <p> 122 Ok, perfect! It's almost done, courage! 123 Now we have scene graphs and space information but something is still missing. 124 When we want to send space information to our shader, we need a model matrix. 125 However, we don't see how to compute it thanks to our parents and our local information. 126 First, we need a function to create a local model matrix that represents an object in space based on its parent. 127 This function will be added to the transform class. 128 </p> 129 130 <pre><code> 131 glm::mat4 getLocalModelMatrix() 132 { 133 const glm::mat4 transformX = <function id='57'>glm::rotate</function>(glm::mat4(1.0f), 134 <function id='63'>glm::radians</function>(eulerRot.x), 135 glm::vec3(1.0f, 0.0f, 0.0f)); 136 const glm::mat4 transformY = <function id='57'>glm::rotate</function>(glm::mat4(1.0f), 137 <function id='63'>glm::radians</function>(eulerRot.y), 138 glm::vec3(0.0f, 1.0f, 0.0f)); 139 const glm::mat4 transformZ = <function id='57'>glm::rotate</function>(glm::mat4(1.0f), 140 <function id='63'>glm::radians</function>(eulerRot.z), 141 glm::vec3(0.0f, 0.0f, 1.0f)); 142 143 // Y * X * Z 144 const glm::mat4 roationMatrix = transformY * transformX * transformZ; 145 146 // translation * rotation * scale (also know as TRS matrix) 147 return <function id='55'>glm::translate</function>(glm::mat4(1.0f), pos) * 148 roationMatrix * 149 <function id='56'>glm::scale</function>(glm::mat4(1.0f), scale); 150 } 151 </code></pre> 152 153 <p> 154 To combine multiple model matrices, we need to multiply them. 155 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! 156 So, let's implement a function in entity class that will do it for us: 157 </p> 158 159 <pre><code> 160 void updateSelfAndChild() 161 { 162 if (parent) 163 modelMatrix = parent->modelMatrix * getLocalModelMatrix(); 164 else 165 modelMatrix = getLocalModelMatrix(); 166 167 for (auto&& child : children) 168 { 169 child->updateSelfAndChild(); 170 } 171 } 172 </code></pre> 173 174 <p> 175 Now, if I update the world, all of my entities contained will also be updated and our global model matrix will be computed. 176 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. 177 </p> 178 179 <pre><code> 180 /*BEFOR THE MAIN LOOP*/ 181 182 // load entities 183 // ----------- 184 const char* pathStr = "resources/objects/planet/planet.obj"; 185 Entity ourEntity(FileSystem::getPath(pathStr)); 186 ourEntity.transform.pos.x = 10; 187 const float scale = 0.75; 188 ourEntity.transform.scale = { scale, scale, scale }; 189 190 { 191 Entity* lastEntity = &ourEntity; 192 193 for (unsigned int i = 0; i < 10; ++i) 194 { 195 lastEntity->addChild(FileSystem::getPath(pathStr)); 196 lastEntity = lastEntity->children.back().get(); 197 198 //Set tranform values 199 lastEntity->transform.pos.x = 10; 200 lastEntity->transform.scale = { scale, scale, scale }; 201 } 202 } 203 ourEntity.updateSelfAndChild(); 204 205 /*IN THE MAIN LOOP*/ 206 207 // draw our scene graph 208 Entity* lastEntity = &ourEntity; 209 while (lastEntity->children.size()) 210 { 211 ourShader.setMat4("model", lastEntity->transform.modelMatrix); 212 lastEntity->Draw(ourShader); 213 lastEntity = lastEntity->children.back().get(); 214 } 215 216 ourEntity.transform.eulerRot.y += 20 * deltaTime; 217 ourEntity.updateSelfAndChild(); 218 </code></pre> 219 220 <p> 221 We can see this result thanks to this code. 222 </p> 223 224 <img src="/img/guest/2021/scene_graph/result.png" alt="Graph example"/> 225 226 <h2>Optimization</h2> 227 228 <p> 229 Being pragmatic is essential to implement a new feature but now let's see how to optimize it. 230 Firstly, in the main loop, we always update the model matrix even if it doesn't move. 231 In programming, a pattern called a dirty flag can help us. 232 A dirty flag is a simple boolean called "isDirty" that allows us to know if an entity was moved during the previous frame. 233 So, if the user scales, rotates or translates it, we need to set this flag on. 234 Don't forget, the model matrix is based on the parent model matrix. 235 So if I change it, I also need to compute all its children's matrix. 236 This flag will be set off in the update function when the model matrix was recomputed. 237 </p> 238 239 <warning> 240 This pattern is very powerful but can be integrated easily only if you encapsulate your class correctly. 241 I haven’t done it to give you an example of rigid code without the possibility to evolve and change easily. 242 This code requires the programmer to code its functionality perfectly and is difficult to change, improve or optimize. 243 But in production, time is a precious resource and you sometimes need to implement the feature without optimizing. 244 One day, if your feature becomes the performance bottleneck, you need to change it easily. 245 Encapsulation, private/protected, getter/setter is C++ advantage that allows you to do it. 246 Encapsulation has its downsides too. 247 It can increase the size of your class and reduce its visibility. 248 It can also be laborious if you create a getter/setter for all your members. 249 An illarouse <a href="https://www.youtube.com/watch?v=-AQfQFcXac8" target="_blank">video</a> show the c++ limit but keep in mind this example and make do the balance in function of your situation. 250 </warning> 251 252 <p> 253 Let's remake do again transform function with encapsulation and dirty flag 254 </p> 255 256 <pre><code> 257 class Transform 258 { 259 protected: 260 //Local space information 261 glm::vec3 m_pos = { 0.0f, 0.0f, 0.0f }; 262 glm::vec3 m_eulerRot = { 0.0f, 0.0f, 0.0f }; //In degrees 263 glm::vec3 m_scale = { 1.0f, 1.0f, 1.0f }; 264 265 //Global space information concatenate in matrix 266 glm::mat4 m_modelMatrix = glm::mat4(1.0f); 267 268 //Dirty flag 269 bool m_isDirty = true; 270 271 protected: 272 glm::mat4 getLocalModelMatrix() 273 { 274 const glm::mat4 transformX = <function id='57'>glm::rotate</function>(glm::mat4(1.0f), 275 <function id='63'>glm::radians</function>(m_eulerRot.x), 276 glm::vec3(1.0f, 0.0f, 0.0f)); 277 const glm::mat4 transformY = <function id='57'>glm::rotate</function>(glm::mat4(1.0f), 278 <function id='63'>glm::radians</function>(m_eulerRot.y), 279 glm::vec3(0.0f, 1.0f, 0.0f)); 280 const glm::mat4 transformZ = <function id='57'>glm::rotate</function>(glm::mat4(1.0f), 281 <function id='63'>glm::radians</function>(m_eulerRot.z), 282 glm::vec3(0.0f, 0.0f, 1.0f)); 283 284 // Y * X * Z 285 const glm::mat4 roationMatrix = transformY * transformX * transformZ; 286 287 // translation * rotation * scale (also know as TRS matrix) 288 return <function id='55'>glm::translate</function>(glm::mat4(1.0f), m_pos) * 289 roationMatrix * 290 <function id='56'>glm::scale</function>(glm::mat4(1.0f), m_scale); 291 } 292 public: 293 294 void computeModelMatrix() 295 { 296 m_modelMatrix = getLocalModelMatrix(); 297 } 298 299 void computeModelMatrix(const glm::mat4& parentGlobalModelMatrix) 300 { 301 m_modelMatrix = parentGlobalModelMatrix * getLocalModelMatrix(); 302 } 303 304 void setLocalPosition(const glm::vec3& newPosition) 305 { 306 m_pos = newPosition; 307 m_isDirty = true; 308 } 309 310 [...] 311 312 const glm::vec3& getLocalPosition() 313 { 314 return m_pos; 315 } 316 317 [...] 318 319 const glm::mat4& getModelMatrix() 320 { 321 return m_modelMatrix; 322 } 323 324 bool isDirty() 325 { 326 return m_isDirty; 327 } 328 }; 329 330 331 class Entity : public Model 332 { 333 public: 334 //Scene graph 335 std::list<std::unique_ptr<Entity>> children; 336 Entity* parent = nullptr; 337 338 //Space information 339 Transform transform; 340 341 // constructor, expects a filepath to a 3D model. 342 Entity(string const& path, bool gamma = false) : Model(path, gamma) 343 {} 344 345 //Add child. Argument input is argument of any constructor that you create. 346 //By default you can use the default constructor and don't put argument input. 347 template<typename... TArgs> 348 void addChild(const TArgs&... args) 349 { 350 children.emplace_back(std::make_unique<Entity>(args...)); 351 children.back()->parent = this; 352 } 353 354 //Update transform if it was changed 355 void updateSelfAndChild() 356 { 357 if (!transform.isDirty()) 358 return; 359 360 forceUpdateSelfAndChild(); 361 } 362 363 //Force update of transform even if local space don't change 364 void forceUpdateSelfAndChild() 365 { 366 if (parent) 367 transform.computeModelMatrix(parent->transform.getModelMatrix()); 368 else 369 transform.computeModelMatrix(); 370 371 for (auto&& child : children) 372 { 373 child->forceUpdateSelfAndChild(); 374 } 375 } 376 }; 377 </code></pre> 378 379 <p> 380 Perfect! Another solution to improve performance can be memorizing the local Model matrix. 381 Thanks to it, we avoid recomputing all child local model matrices if the parent is moved. 382 This solution improves performance but transforms become heavier. 383 This size is really important for your hardware. 384 We will see why in the limitation subchapter. 385 You can find the code <a href="https://learnopengl.com/code_viewer_gh.php?code=src/8.guest/2021/1.scene/1.scene_graph/scene_graph.cpp" target="_blank">here</a>. 386 </p> 387 388 <h2>Limitation</h2> 389 390 <p> 391 Do you ever hear about <def>data oriented design</def> ? No ? Let's talk brevely about it ! 392 </p> 393 <p> 394 This design is based on how your hardware works. 395 In your computer, data is aligned into your memory. 396 When you use data like a variable, this data is sent to the cache. 397 The cache is a very fast memory but without a lot of space. 398 This memory is localized into your CPU. 399 For example, if you want to make a cake, you need to go to a supermarket (hard disk) to buy ingredients. 400 A supermarket is very big but is also very far from your home. 401 When you buy your ingredients, you store them in your fridge (RAM). 402 Your fridge contains ingredients that you need to live but I hope for you that you don't live only with cake ^^ 403 It is also small but near to your kitchen worktop. 404 And finally, your kitchen worktop (the cache) is small and can't contain all your ingredients but is very near to your preparation. 405 It's the same for the PC! 406 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). 407 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. 408 std::list is a noncontiguous array. 409 std::map will probably make the job better but inheritance cannot be aligned into your memory because it's a tree. 410 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. 411 </p> 412 413 <note> 414 std::map is tree and is aligned in memory but scene graph will jump into this memory and will be slower than don't use inheritance. 415 </note> 416 417 <p> 418 You know now what a Scene graph is and how to use it with its limitations. 419 In the next chapter, we will talk about frustum culling with a scene graph. 420 </p> 421 422 <h2>Additional resources</h2> 423 424 <ul> 425 <li><a href="https://walterkuppens.com/post/wtf-is-a-scene-graph/" target="_blank"> 426 walterkuppens article about scene graph</a>: An article by Walter Kuppens with another approach of the scene graph.</li> 427 428 <li><a href="https://webglfundamentals.org/webgl/lessons/webgl-scene-graph.html" target="_blank"> 429 webglfundamentals article and demonstration about scene graph</a>: An article with animated webGL code and animated demonstration</li> 430 </ul> 431 432 <author> 433 <strong>Article by: </strong>Six Jonathan<br> 434 <strong>Contact: </strong><a href="Six-Jonathan@orange.fr" target="_blank">e-mail</a><br> 435 <strong>Date: </strong> 09/2021<br> 436 <div> 437 <a href="https://github.com/Renardjojo"> 438 <svg height="32" aria-hidden="true" viewBox="0 0 16 16" version="1.1" width="32" data-view-component="true" class="octicon octicon-mark-github v-align-middle"> 439 <path fill-rule="evenodd" d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"></path> 440 </svg> 441 </a> 442 <a href="https://www.linkedin.com/in/jonathan-six-4553611a9/"> 443 <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 34 34" class="global-nav__logo"> 444 <path d="M34,2.5v29A2.5,2.5,0,0,1,31.5,34H2.5A2.5,2.5,0,0,1,0,31.5V2.5A2.5,2.5,0,0,1,2.5,0h29A2.5,2.5,0,0,1,34,2.5ZM10,13H5V29h5Zm.45-5.5A2.88,2.88,0,0,0,7.59,4.6H7.5a2.9,2.9,0,0,0,0,5.8h0a2.88,2.88,0,0,0,2.95-2.81ZM29,19.28c0-4.81-3.06-6.68-6.1-6.68a5.7,5.7,0,0,0-5.06,2.58H17.7V13H13V29h5V20.49a3.32,3.32,0,0,1,3-3.58h.19c1.59,0,2.77,1,2.77,3.52V29h5Z" fill="currentColor"></path> 445 </svg> 446 </a> 447 </div> 448 </author> 449 450 451 452 453 </div> 454