|
||||||||||||||||||
| Myopic Rhino | Comments for the article Object-Oriented Scene Management |
| snk_kid | Quote: And who said that? i haven't read or seen this post until today and some of what i've read i don't agree with certain "individuals" what they believe a SG is how its usually implementated anyways moving on... A thew commerical scene graphs do you use DAGs or restricted form of DAGs where only leaf nodes are allowed to be shared (this is easy to do with composite design pattern). So why not use a full-blown DAG?, well its much more diffcult to deal/maintain for certain things/operations. Take for example you decide to maintain bounding volumes in all node types (just stick it in base node type), this will give a structure simillar to bounding volume hierarchy (BVH) except that the tree is an n-ary tree, you can perform hierarchy frustum culling with this set-up. When you update bounds of object in a hierarchy you need to do it from leafs to root, with a tree this is trivial (update transforms on the downwards and update bounds on the way back) but think how much more complicated it will be for a full-blown DAGs. Trees have useful & desirable properties that can be exploited. Quote: Well if you think about it more carefully you can still keep the footprint small, as they say you can ""have your cake and eat it too" with a tree by "externalizing state" instead of containing meshes you reference them via a reference/(smart) pointer, e.g. say 2 leaf nodes in a tree refer to the same geometry but in the SG it looks there are two but there is only really one instance. Also you may think about pooling nodes & node links. Quote: Well it is if you think about it carefully, for example reference counted smart pointers. Quote: This is nothing new & you would be silly not to really, have a base class (say called Object) that implements embedded reference counting system, make your base node type derive from it and your pretty much sorted. Quote: I doubt anybody would make such queries i might be wrong, anyways the way i see it is you don't want to be dealing with "grouping & sorting" directly on the scene graph you only want to deal with a list of potentially visiable set of objects, something like: Cull set -> Group & Sort potentially visiable set -> render. This is very simplified view, for grouping & sorting you would be using something like render queues (most likely priority queues) or render bins/buckets. More stuff on state management/sorting here and here The topic of efficent & extendable shader integration is complicated and beyond the scope of this thread (heh just joking), and no i don't have the anwsers to it either ![]() [ begin pun ] but if anyone is interested in talking about it come chat in #graphicsdev chat room .[ end pun ] Quote: When you try to look at the whole the world looks like choas, dealing with parts/views the world becomes digestible things begin to look ordered. Then again physics on the large tell us that things are ordered and on the quantum level things are at choas, something to think about... NOTES: Some things to consider looking into when designing SGs, composite, visitor design patterns there are most definetely more but those are the main ones to give you good ideas. [Edited by - snk_kid on January 30, 2005 2:31:24 PM] |
| johdex | I'm in the process of designing my first scene graph, so I have a few thoughts and suggestions that may well be irrealistic. Please let me know if that's the case: 1) Using a tree Why not a Directed Acyclic Graph instead? This should help keep the memory footprint of your scene graph smaller in the case where you have many objects sharing the same mesh, for instance. Consider a racing simulation, where all cars have the same geometry:
Car1 and Car2 would hold information about the textures to use, the local transformations, physics state... 2) Object destruction I may have missed something, but I think that objects should never ever destroy themselves. An object should be destroyed only after no other object still has references to it. The idea behind parent-ownership is that the parent is always the last one to hold references (or pointers) to an object. That's not necessarily easy to enforce in your code. Why not use reference-counting garbage collection instead? I've been using this technique for a while now, and I don't see why anyone would like to avoid it. 3) State changes For efficiency reasons, I think that polygons with identical textures should be drawn in a row, in order to minimize texture switches. Same thing with vertex and pixel shaders. How does one design a scene graph to cope with that? More generaly, the database querying suggested above is very appealing. Typical queries would be: "select polygons p such that p is visible, order by textureOf(p)" "select polygons p such that p is opaque" "select polygons p such that p is transparent, order by distanceFromViewer(p)" Drawing the scene seems more complex than just drawing objects individually. |
| hplus0603 | Quote: It can easily cost thousands of cycles. However, you really don't want a deep OR wide inheritance hierarchy, and you don't want a hierarchical tree sorted on some specific attribute (like parentage, or position). That only allows you to query for a single attribute kind. Instead, you want a SCENE DATABASE, and you want to run QUERIES on this DATABASE. Sound similar to some other technology we all know? I'm fully convinced that the scene "graphs" of the future will have relations and a query language, and use customized index classes for high performance queries (like "what intersects this sphere and this frustum"). |
| dmikesell | If you're using RTTI and dynamic_cast, chances are your inheritance is broken. Do a web search on the "Liskov Substitution Principle" to read about proper inheritance. I believe it's on Object Mentor's homepage. The whole idea behind polymorphism is so that client code can work solely with abstractions, not having to know anything about concrete classes. RTTI is a hack to fix bad inheritance hierarchies and should be used only as a last resort, and hopefully only temporarily until you can fix your inheritance. -- Dave Mikesell d.mikesell@computer.org http://davemikesell.com |
| pierg | Hi, I am new to c++ templates and I cannot understand how to write a tree where each node is a different class, derived from the same base node class. I would like to understand how to have trees containing more types of derived nodes. The code posted by eli_pulsifer seems to do something like this, but I don't know how it works. Is it possible to write something like: Class Derived1 : public TNode {...} Class Derived2 : public TNode {...} and then in the main program: Derived1 foo1; Derived2 foo2; foo1.attach_parent(foo2); Or am I completely misled? If so, should I read more about templates or is there another way to reach my goal (any suggestion about readings, ecc...)? Thank you. [edited by - pierg on January 21, 2003 12:00:28 PM] [edited by - pierg on January 22, 2003 4:23:38 AM] |
| JeffKershner | The point is that if you want to detach a child from the scene, you usually know what child you want as it is the subject of the conversation. It is like this, if I fire a missle into a wall, the wall is the FFObject that needs to be deleted from the scene graph. // FFObject* pObject; Pointer that the collision system // returned for the wall assert (pObject); assert (pObject->GetParent()); pObject->GetParent()->DeleteChild(pObject); |
| MMK | Well, finding the parent of a child won't be a problem, however, a problem appears when the parent tries to locate the child in its list to delete its pointer and free its memory. For example if an object is owned by the whole world such as a bullet, when it wants to get destroyed, it will tell its parent "Kill me" and pass a reference to itself (or some other sort of identification). Since a great number of childs may be attached to the same parent, the search in its child list could consume some time. Of course this search could be optimized by modifying the objects system a little. But that's mainly Hargrove's point in this area. -Mokhtar M. Khorshid |
| Qatal | Chris, your comment on when a node (projectile example) wants to destroy itself is invalid. Provided that the scene graph is constructed carefully, the node already KNOWS what its parent is. Recall the FFObject::setParent() function? All that is needed is for the node to delete itself from the parent's child list like obj->parent->m_childlist.popObj(obj) or something like that. (STL is not my thing) And then the node frees the memory it's allocated to. nothing more to it. If i've missed something, please point it out. Qatal die or be died...i think |
| nias | Hi, I really enjoyed the discussion on scene graphs. I have gotten some good ideas from it, which I will use in my own projects. What I'm wanted to ask is the following: is a scenegraph like this combinable with a BSP/ oct-tree solution ? I'm trying to write a basic 3d renderer in opengl myself, and sofar I've stuck to this simple scenegraph. But in a later version I'd also like to integrate BSP, without losing the simpler structure. So, is this possible ? Or should I seperate the two, and provide a seperate structure for BSP ? I've been looking for articles on scenegraphs in general, but haven't been very lucky. So any help/hints are very much appreciated. Thanks, Nias |
| eli_pulsifer | Well, I had a few hours tonight to play around so I coded up this node thing. I haven’t exactly tested this yet but it looks good Here is my implementation:
template <class _TDerived>
class TNode
{
protected:
template <class _TNode>
struct TLink
{ // link for circular node linked list
typedef TLink<_TNode> _MyType;
TLink() : m_pParent(NULL), m_pChild(NULL)
, m_pNext(NULL), m_pPrev(NULL)
{}
void Init(_TNode *pParent, _TNode *pChild, _MyType *pNext, _MyType *pPrev)
{ // initialize node and link pointers
m_pParent = pParent; m_pChild = pChild;
m_pNext = pNext; m_pPrev = pPrev;
}
_TNode *m_pParent;
_TNode *m_pChild;
_MyType *m_pNext;
_MyType *m_pPrev;
};
typedef TNode<_TDerived> _MyType;
typedef TLink<_MyType>* _Linkptr;
typedef TLink<_MyType>*& _Linkpref;
typedef _TDerived& _Vref;
typedef _TDerived* _Vptr;
protected:
static _Linkpref _NextSibling(_Linkptr pLink)
{ // return reference to successor pointer in node
return ((_Linkpref)(*pLink).m_pNext);
}
static _Linkpref _PrevSibling(_Linkptr pLink)
{ // return reference to predecessor pointer in node
return ((_Linkpref)(*pLink).m_pPrev);
}
static _Vref _Myval(_Linkptr pLink)
{ // return links owner node
return (_Vref)(*(*pLink).m_pChild);
}
public:
class iterator;
friend class iterator;
class iterator
{ // iterator for sibling nodes
public:
iterator()
: m_pLink(0)
{ // construct with null node pointer
}
iterator(_Linkptr pLink)
: m_pLink(pLink)
{ // construct with node pointer _Pnode
}
_Vref operator*() const
{ // return designated value
return _Myval(m_pLink);
}
_Vptr operator->() const
{ // return pointer to class object
return (&**this);
}
iterator& operator++()
{ // preincrement
m_pLink = _NextSibling(m_pLink);
return (*this);
}
iterator operator++(int)
{ // postincrement
iterator iTmp = *this;
++*this;
return (iTmp);
}
iterator& operator--()
{ // predecrement
m_pLink = _PrevSibling(m_pLink);
return (*this);
}
iterator operator--(int)
{ // postdecrement
iterator iTmp = *this;
--*this;
return (iTmp);
}
bool operator==(const iterator& iRight) const
{ // test for iterator equality
return (m_pLink == iRight.m_pLink);
}
bool operator!=(const iterator& iRight) const
{ // test for iterator inequality
return (!(*this == iRight));
}
_Linkptr _Mynode() const
{ // return node pointer
return (m_pLink);
}
protected:
_Linkptr m_pLink; // pointer to node
};
public:
TNode()
{ // initialize links
m_lnkParent.Init(NULL, this, &m_lnkParent, &m_lnkParent);
m_lnkChild.Init(this, NULL, &m_lnkChild, &m_lnkChild);
}
virtual ~TNode()
{ }
void attach_parent(_MyType *pParent)
{ // link to parent
m_lnkParent.m_pParent = pParent;
m_lnkParent.m_pPrev = pParent->m_lnkChild.m_pPrev;
m_lnkParent.m_pNext = &pParent->m_lnkChild;
pParent->m_lnkChild.m_pPrev->m_pNext = &m_lnkParent;
pParent->m_lnkChild.m_pPrev = &m_lnkParent;
}
void detach_parent()
{ // un-link from parent
m_lnkParent.m_pParent = NULL;
m_lnkParent.m_pPrev->m_pNext = m_lnkParent.m_pNext;
m_lnkParent.m_pNext->m_pPrev = m_lnkParent.m_pPrev;
m_lnkParent.m_pPrev = m_lnkParent.m_pNext = &m_lnkParent;
}
_MyType* parent()
{ // return parent node
return m_lnkParent.m_pParent;
}
iterator child_begin()
{ // return begin iterator for children
return iterator(m_lnkChild.m_pNext);
}
iterator child_end()
{ // return end iterator for children
return iterator(&m_lnkChild);
}
iterator begin()
{ // return begin iterator for siblings
return (NULL == m_lnkParent.m_pParent) ? iterator(m_lnkChild.m_pNext) :
iterator(m_lnkParent.m_pParent->m_lnkChild.m_pNext);
}
iterator end()
{ // return end iterator for siblings
return (NULL == m_lnkParent.m_pParent) ? iterator(&m_lnkChild) :
iterator(&m_lnkParent.m_pParent->m_lnkChild);
}
protected:
TLink<_MyType> m_lnkParent; // link to parent node and sibling list
TLink<_MyType> m_lnkChild; // link to child node list
}; There are probably bugs in there, but I figured someone may find this useful. [edited by - eli_pulsifer on May 10, 2002 3:00:12 AM] |
| Chris Hargrove | The class of the node that inherits from the template (as given in the example) is your base class. The use of subclasses in the tree is perfectly fine, since the links use pointers. As long as all the tree nodes are inherited from the base node class (the one passed into the template) then you're fine. The node functions (like GetParent(), the iterator functions, etc) will all return the base node type and not a specific subclass of course... but a good abstract interface in the base class and/or a reasonably thought-out RTTI mechanism will cover that situation (no different than any other type of scene graph). - Chris |
| eli_pulsifer | quote: Unless I misunderstand how this works, wouldn’t there need to be a non-template base class that would be used by the link object. Otherwise you could only have trees containing one type of derived node. I guess having only one node type is fine as long as nodes are simply containers and don’t store any actual data, just a pointer to the data. Edited to remove extra quote tag [edited by - eli_pulsifer on May 8, 2002 10:05:30 PM] |
| Chris Hargrove | quote: You can do that, but there are a number of drawbacks. For example, it's not as easily self-cleaning, since having only a single child pointer means that if that first child decides to delete itself it needs to clean up the reference in the parent to point to another sibling, etc. Another example is the added complexity of making an iterator. If you start the iterator at a particular child and want it to know when to stop automatically, it will either need to hold on to that starting child (to check when it's reached again), or do similar checks by jumping up to the parent and then comparing the result against the first child pointer and seeing if they match. Unfortunately, this can become complicated if a node deletes itself during iteration, since it can easily throw off the state of the iterator. In contrast, the quad-linked approach never loses its stopping condition (due to the always-null child pointer of the parent's ChildLink), so it only needs to track the next node for iteration-with-deletion purposes and the rest takes care of itself. That said, the tree form you're talking about does only have half the overhead, so the additional complexity may be justified in certain situations where the node count is very high and memory is tight. I never intended to imply that the quad-linked tree is the only way to do a decent tree structure (there are tons of ways to make trees). I just find it to be a simple approach which, for only a marginal additional cost in memory, takes care of a lot of issues automatically. - Chris |
| Infinite Monkey | Is there a reason why the parent and child links need to be seperate from the node? Can we not just create a node class that has previous, next, parent and child pointers, and then do without the parentlinka and child link objects? |
| Countach | Thanks for the info, Chris. Much appreciated. I'll adopt your system (I was using linked lists in the parent to manage the siblings, much like in this tutorial). |
| Chris Hargrove | quote: The basic idea uses links within circular doubly-linked lists, and embedding a pair of these links into each node of the tree. Each link contains four pointers: The previous and next link pointers (for doubly-linked list behavior), and parent and child node pointers (which may be null in specific situations, as described below). Each node has two of these links contained in the node, the ParentLink and the ChildLink (along with whatever other data you want in the node of course). At node creation, the links are initially constructed where they are self-linked (previous and next point back to the link itself). In addition, the ParentLink's child pointer points to the node, as does the ChildLink's parent pointer. The ParentLink's parent pointer and the ChildLink's child pointer are both set to null. After this construction, the node will be independent (not connected to any tree), but will be ready for tree functionality. The node implements a "SetParent" method that takes another node it wants to be its parent. When this happens, the node's ParentLink's parent pointer is set to the new parent node, and the ParentLink is linked into the circular list of the parent node's ChildLink. This constant-time process automatically handles all sibling connections since they're part of this circular list. A similar process goes for unlinking, where the node sets its ParentLink's parent pointer to null and unlinks the ParentLink from whatever list it was in. Iteration of all the children of a node is merely the iteration of all the links connected to the ChildLink of that node. This iteration can be performed starting with the ChildLink's next or previous link (depending on the direction you want), where each link's child node pointer will end up pointing to the child node you want (since these links are actually the ParentLinks of the children, and their child pointers were initialized at construction to point to the node). The iteration ends when you reach a child pointer of null, which only occurs in the original ChildLink. All of this can be wrapped with relative ease into a few templates along the lines of Coplien's "Curiously Recurring Template" pattern, where the tree node template class is inherited from and the name of the subclass is passed into the template. For example:
class CMyTreeNode
: public TTreeNode<CMyTreeNode>
{
// regular node contents go here
};
In this case, CMyTreeNode would inherit all the basic functionality of a tree item (like TreeGetParent(), TreeGetFirstChild() or whatnot), with all the pointers being returned of the proper type. In addition, you can build an iterator template class to automatically go through the children of a node, which usage like:
CMyTreeNode* parent; // passed in from somewhere else
for (TTreeIterator<CMyTreeNode> iter(parent); iter; ++iter)
{
// overloads operator * and -> to use node pointer
CMyTreeNode* child = *iter;
iter->DoSomething(); // same as child->DoSomething()
}
A quad-linked tree has the nice benefit of being entirely built up by connection data embedded directly in the nodes themselves, without the need for any kind of dynamic allocation (like internal child pointer arrays). The only things that need to be allocated are the nodes themselves, making memory management much simpler, and alleviating a lot of fragmentation concerns. In addition, you can do a large number of operations in constant time, since its built on circular linked lists (which have a lot of constant-time operations). Data access isn't an issue since every node knows its parent, its siblings, and its children at all times. At runtime the tree is very malleable, but with a slightly higher cost for iteration. The only other real additional cost is a bit more memory, due to the two links per node (on current machines this is generally 16 bytes per link, so you get an additional 32 bytes per node of size overhead). In many situations this cost is trivial, and the benefits of using the structure outweigh the drawbacks. As always, whether this structure is appropriate depends on the situation at hand. Scene graphs may very well be one such situation. [Editing to put the code into source blocks] - Chris [edited by - Chris Hargrove on May 7, 2002 5:09:17 PM] |
| eli_pulsifer | quote: Chris, would you care to elaborate a bit on this quad linked tree data structure. |
| eli_pulsifer | Ecko, the two main problems I have with using the build in RTTI are the following. First, (I may be wrong here) every single object will get RTTI information associated with it which is unneeded overhead when you only really need RTTI for a select few objects. When working with console systems memory foot print is a big issue, well it used to be and still is to some extent. Second, C++ RTTI is not portable. Example: VC 6.0 typeid(pMyClass).name() returns "class MyClass" CodeWarrior 8 typeid(pMyClass).name() returns "MyClass" That said, I still use the build in RTTI. It provides all the functionality I need and I've been able to over come all the portability issues so far with minimal effort. I’m sure other people have reasons not to use the built in RTTI too, but I've not run into any others personally. [edited by - eli_pulsifer on May 6, 2002 11:35:28 AM] |
| Jeff Kershner | The link to Gil's paper got lost in the new version of my web page. I will try to get it back, but here at shiny we are going through a serious crunch time. If anyone needs me to send it sooner, shoot me an email... |
| yzzid | Where is the paper you discussed located? I looked on your web page as stated in the article but couldn't locate it. -Dizzy |
| Ecko | How bad is it to use dynamic_cast for RTTI? |
| Chris Hargrove | Glad you didn't take my comments too personally, Jeff. Like I said, I understand that it's a tutorial and it's really hard to capture all the issues of a large topic (like scene graphs) in only a couple pages of web text. So I totally know where you're coming from there. My key concern was not so much the simplicity with regards to the issues mentioned above, but rather the fact that they weren't addressed more in the sense of "this is just an example; you wouldn't really want to do it this way; look at my For Further Reading section for the proper way to set this up in a production situation". Even though it's unrealistic to expect a small primer tutorial to address all the major issues, a big step in that direction is just the indication that the issues exist. Anyway, sorry if I sounded too harsh; I just wanted to get the thoughts out there. - Chris |
| Jeff Kershner | Thanks for your thoughts on my article. You are correct that the article does have a lot to be desired. The conundrum that I face is that I needed to explain an incredibly complicated topic in a short space that is renderer non-specific. The article really needs its own four chapters in a large book of engine creation. Here is a clip from Chris's post (verbatim): Your example uses a mesh object directly as a node in the scene graph, when in a real engine this is something you would never EVER want to do. Scene graphs are always supposed to use nodes representing instances of assets, NEVER the assets themselves. It is far to easy for the nodes to be moved around, duplicated where they refer to the same asset (such as with mirrors or other portals), and so on. What you can't see is that the mesh would be loaded from a resource manager that allows instancing of resources. Since this article wasn't about resource management, that was left out. Chris's second point talks about objects being owned by parents and being referenced by direct pointers. This can be a horrible thing and I understand your concerns. But deleting a child is just a recursive search for the node, a call to the the parent, and having the parent delete the child. Again this article is not a game engine article but a scene graph article. If a person wants to set up a smart pointer system like I have to assure valid pointers and automatic deletion, that is fine; again not the focus of the article. About Chris's third point on the RTTI: It is true that the system has it's problems; using the built in RTTI has even more. The point is that this system is quick to set up. There are a plethora of different ways of doing this. Having hardcoded enumerated types will cause the rebuilding of the library, but the frequency of doing this is quite small. And there are way around this as well. I appreciate the comments, /jk |
| Void | Chris, in the real world, you really see code like this in production. Sad but true. For an nary STL container, take a look at this http://www.damtp.cam.ac.uk/user/kp229/tree/#overview It beats using a vector of pointers because STL containers are designed for storing copies, not pointers. A vector of pointers is not exception safe. My main comments is about the RTTI implementation too. Each time you add a new type, you have to recompile the whole library. Take a look at Modern C++ Design by Andrei Alexendrascu for a RTTI scheme that deals with C++'s "defective" RTTI. If your base objects interfaces are well designed and you need to find out the type of object at runtime using RTTI, chances are you require a double dispatch scheme. Check out MC++D again. |
| Chris Hargrove | I hate to sound like a bastard, especially to someone who's made a tutorial (which I generally consider a Very Good Thing), but I'm sorry, there are so many issues in this tutorial it's not even funny. The key problem is that in the process of giving the basic description and intro of scene graphs, you are introducing other elements (as if they're directly related) in a trivialized manner that is more likely to set an engine project up for failure in the long term rather than success. If you're going to bring up big issues like good management of inheritance and use of custom RTTI solutions, you should either A) indicate that your examples are highly simplified and not suitable to a production environment, or B) provide examples that are suitable to such an environment. The lack of either of these two things is likely to introduce reader confusion and make peoples' code worse rather than better. Three key examples: 1) Scene graph object inheritance. Your example uses a mesh object directly as a node in the scene graph, when in a real engine this is something you would never EVER want to do. Scene graphs are always supposed to use nodes representing instances of assets, NEVER the assets themselves. It is far to easy for the nodes to be moved around, duplicated where they refer to the same asset (such as with mirrors or other portals), and so on. This may seem like a triviality for a tutorial example, but the people who read this tutorial may not know a thing about scene graphs, and if you lead them down the wrong path with things like this it could be extremely difficult for them to repair their engine later to fix the problems. 2) Scene graph object references and management. You're using direct pointers to parent and child objects, and forcing all child object destruction to be managed by the parent? Umm, no. This is unrealistic, and prone to loads of problems, or at the very least, completely ridiculous overhead. If you want to use the scene graph as a parallel to your actor graph, what happens when a simple transient actor (like a projectile) wants to destroy itself (like when it explodes)? Does your system actually have to get the world's permission to destroy a projectile? And how do you intend to tell the world what projectile to destroy? Pass in a pointer or handle and force it to find the right entry in its child list so it can delete it and clear the reference? There'd be no other way to clean up the parent's pointer to the projectile child, and that means tons of unnecessary child list searches when you already KNOW what child you want to delete. In other words, you are using an inappropriate data structure for your tree. A properly designed scene graph would allow node insertions and removals in constant time with all references cleaned up automatically. This is easy to do with a simple quad-linked tree structure, and it avoids all these problems. If a node chooses to move or otherwise delete itself, the parent and all siblings would be updated automatically without any cleanup searches required. It would also avoid the use of using std::vectors for your child lists, since std::vector is very suboptimal for small allocations that change in size frequently. 3) Hardcoded RTTI types. No offense, but why in the hell would you ever want to hardcode your RTTI type identifiers like this? There are a ton of patterns in the factory family that avoid this process and allow adaptive behavior with regards to the object types available to the system. I posted one here in gdnet's patterns section last year (called Reflective Factory), and it's far from the only solution. Hardcoding your RTTI types in an enumeration generally means that this enumeration would have to be provided in a header used by anyone needing to use even one of the RTTI types (which generally means a large number of files). That makes it not only non-adaptive, but completely uninsulated adding a lot of needless compile-time dependency. In a large project this would be completely unacceptable. There is absolutely no benefit to doing it this way when the same identification can be given from a runtime class ID or pointer that is created with the associated class automatically. I know it may sound like I'm nitpicking with this kind of stuff, and if any one of these things was present on its own I wouldn't bother complaining about it (it is a tutorial after all, and doesn't have to be perfect). But there are so many larger-scale design flaws in this tutorial, and novice readers who are first learning about scene graphs (i.e. your target audience) may not recognize these problems until they've already built a scene graph architecture around your examples only to find out later that they have to refactor the entire thing. I know your intentions are good Jeff, but you know as well as I do that your examples won't fly that easily in a real production codebase (or at least not a codebase that isn't fraught with problems). The concepts from this article are good, but the context they're provided in has a lot to be desired, and I hope readers don't take it too literally. - Chris |