Scene graph design / survey

Started by
8 comments, last by snk_kid 19 years, 6 months ago
I'm currently working on the design of my scene graph. One of the most critical points is data / renderer separation. 99.9% of the scene graphs out here do not separate their data from the renderer, which makes it very hard to use in a client/server environment. Keeping that in mind, i'm currently facing a few questions and i'd like to get some feedback before making a choice: 1. In my engine, creation and allocation of objects (in the OOP sense) are separate. You allocate an object with "new", and the constructor is always empty. To initialize or create an object, you must call a "create" method. What do you think is better to use:

/// Method A
CObject *obj = new CObject();
obj->create(scene, node, model);
or

/// Method B
CObject *obj = scene->createObject(node, model);
Advantages/drawbacks: - you can manage objects allocations yourself in A, while in B you leave it to the scene manager. This allows you to make optimizations like: allocating an object, creating it, destroying it, creating another one w/o having to allocate a new one. - with A, the create function can return an error code, while B returns a pointer to the created object (or NULL if it failed). A is more powerful in that respect, although B could use exceptions (which i want to avoid) - A is coherent with the rest of the engine, since all objects use that "new"/"create" mechanism. - B is more simple to use, and shorter. What method do you prefer? 2. How to handle methods in a class, that must be kept public, but that are ONLY used internally by the engine ? "friend" is not a good option, as it generates a dependency between the two modules. Ex.: imagine an internal "update" method in the scene graph module. Now, an object in the scene renderer (which is a different module) needs to call the "update" method in the scene graph. If you use a friend declaration, you tie the scene graph to the scene renderer. 3. How far is too far ? Some engines tend to go mad and to use classes for every little things. Let's say i'm designing a "CLight" class. I have two possibilities: A: have a single CLight class, with all possible attributes/parameters. B: have an ILight interface, and create a CPointLight, CSpotLight and CDirectionalLight class, all with their specific attributes. Is B too much ? Where do you draw the line ? 4. Naming conventions: let's say i need a function to retrieve the position of an object. Would you prefer a short name like "getPos()", or a longer (but maybe less ambiguous) name like "getPosition()" ? I want to avoid the DirectX syndrom, but maybe "getPos()" is too extreme on the other side ? Y.
Advertisement
Quote:Original post by Ysaneya
I'm currently working on the design of my scene graph. One of the most critical points is data / renderer separation. 99.9% of the scene graphs out here do not separate their data from the renderer, which makes it very hard to use in a client/server environment. Keeping that in mind, i'm currently facing a few questions and i'd like to get some feedback before making a choice:

1. In my engine, creation and allocation of objects (in the OOP sense) are separate. You allocate an object with "new", and the constructor is always empty. To initialize or create an object, you must call a "create" method.


thats abit of silly idea, constructors are mean't to setup the enviroment for the state invariant, better idea is to sperate allocation and initialization instead, you have a memory manager allocate chunks memory of uninitialized memory then initialize/construct them at the appropriate time, a silly trivial example:

foo* foo_ptr = static_cast<foo*>(::operator new(sizeof(foo))); //<-- uninitialized memory for one foo object//later on, construct/initialize with placement new operatorfoo_ptr = new(foo_ptr) foo;/* later on */foo_ptr->~foo(); //<--- destroydelete foo_ptr; //<-- deallocate memory


Off course clients don't do this the memory manager does it, and you may wont to consider some kind of object pooling scheme to be more effective.

Quote:Original post by Ysaneya
What do you think is better to use:

/// Method ACObject *obj = new CObject();obj->create(scene, node, model);



this looks unmanageable especially when you some kind of memory manager that is mean't take of it.

Quote:Original post by Ysaneya
/// Method BCObject *obj = scene->createObject(node, model);



a little better but it could be improved, you can have your cake & eat too, by combining both approaches.

Some kind of memory/resource manager then overloading operators new/delete for particular type hierracy that use the memory manager, then you have complete control & and a consistent look to clients.

Quote:Original post by Ysaneya
3. How far is too far ? Some engines tend to go mad and to use classes for every little things. Let's say i'm designing a "CLight" class. I have two possibilities:

A: have a single CLight class, with all possible attributes/parameters.

B: have an ILight interface, and create a CPointLight, CSpotLight and CDirectionalLight class, all with their specific attributes.

Is B too much ? Where do you draw the line ?


i don't think B is to much it seems perfectly fine but i don't think Light needs to be an interface, its fine as an abstract class (meaning it could have some implementation that is common to all light types).

Quote:Original post by Ysaneya
4. Naming conventions: let's say i need a function to retrieve the position of an object. Would you prefer a short name like "getPos()", or a longer (but maybe less ambiguous) name like "getPosition()" ? I want to avoid the DirectX syndrom, but maybe "getPos()" is too extreme on the other side ?


I don't think it matters to much, its personal preference at the end of the day just as long as your consistant.

As long as your naming conventions doesn't delude you from the reason to have getters/setters which is to enforce state invariants and/or encapsulating variants/variation with an invariant interface/s.

[Edited by - snk_kid on September 29, 2004 6:53:00 AM]
Quote:Original post by Ysaneya
What do you think is better to use:

I'd use the following:
CObject *obj = new CObject();obj->create(node, model);scene->addObject(obj);

Scene graph's job is exactly that: maintaining a graph of the scene. It is *not* instantiating objects, managing their memory, loading data from resources, etc. The object's job is to initialize itself as necessary and provide appropriate functionality. It is *not* the object's job to know anything about the scene. So why not create the object in a place that's responsible for creating objects and then add it to the scene manager?
Quote:Original post by Ysaneya
2. How to handle methods in a class, that must be kept public, but that are ONLY used internally by the engine ?

I suppose there is a pattern for this but I can't think of it off the top of my head. Do you have the GoF pattern book? I'll try to look this up tonight. With your specific example, you'll probably need to decouple the scene graph from the renderer by adding another class in the middle. This class would be scene graph's friend. Whenever you'd need to access the update() method from the renderer, you'd use the class in the middle. There may be better solutions, it all depends on the specific situation. I have to think about this in more detail.
Quote:Original post by Ysaneya
3. How far is too far ?

Ahh, the never ending problem software engineers face every day [smile] A good rule is

* Don't add abstraction layers if they don't provide a benefit that will be taken advantage of in some reasonable future

What is the purpose of adding a light interface and deriving multiple light types? How different is the functionality of the classes? If every one of them will simply call an API function with different parameters, having five light classes is stupid. If your classes will actually provide some sort of reasonably different functionality with a set of common features or some basic uniform interface, then yeah, add the abstraction. Ask yourself this question, what do you expect to achieve by adding multiple light classes? How will the time taken to add multiple files benefit your architecture?
Quote:Original post by Ysaneya
4. Naming conventions

I like my functions to be reasonably verbose but not overly so. Will typing extra five characters ("ition") kill you? Just be reasonable. Don't create functions like "getObjectPositionPleaseThankYouVeryMuch()" but be reasonably verbose and don't be afraid of a few extra characters: memory is cheap nowdays. Here at work someone decided to abbreviate the word "thread" and used "thrd". They saved nothing but added confusion. At this point people simply laugh at that piece of code.
Quote:Original post by snk_kid
[thats abit of silly idea, constructors are mean't to setup the enviroment for the state invariant, better idea is to sperate allocation and initialization instead,


But in my method A, allocation and initialization are separate:

CObject *obj = new CObject();obj->create(...);obj->destroy(...);obj->create(...);obj->destroy(...);delete obj;

... will have the object allocated once, but used twice.

I see your idea with placement new, but quite frankly i don't like it much. It's okay for code inside a memory manager, but i want a consistant way to allocate all kind of objects in my engine, not only with the scene graph. And sometimes, some objects are just too simple to bother with a memory manager at all. Let's imagine a CTriangle class that only contains 3 vertices. Wouldn't you prefer to simply do a "new" to allocate the triangle; or would you bother calling a manager/factory in order to allocate one ?

Quote:Original post by snk_kid
this looks unmanageable especially when you some kind of memory manager that is mean't take of it.


But only specific objects really need a memory manager. What should i do for all the other "common" objects in the engine ?

Quote:Original post by snk_kid
i don't think B is to much it seems perfectly fine but i don't think Light needs to be an interface, its fine as an abstract class (meaning it could have some implementation that is common to all light types).


Agreed, i wrote "interface" but i really had an abstract class in mind.

Quote:Original post by CoffeeMug
Scene graph's job is exactly that: maintaining a graph of the scene. It is *not* instantiating objects, managing their memory, loading data from resources, etc.


That's a very interesting thought. What about this:

CObject *obj = sceneFactory->newObject();obj->create(model);scene->getRoot()->addObject(obj);

Although i'm not quite sure about the 3rd line. It would take a node of the scene graph (getRoot()) and add a child object to it. On the other hand it might also want to do some additional work on the object, that is specific to the scene manager; so this approach could also work:

scene->addObject(scene->getRoot(), obj);

Or i can also separate adding the object to the scene graph, and linking an object to a node, like:

scene->addObject(obj);scene->getRoot()->addObject(obj);

In the context of a scene graph tree, which solution do you prefer?

Quote:Original post by CoffeeMug
you'll probably need to decouple the scene graph from the renderer by adding another class in the middle.


Hum, that's going to add another layer of complexity to an engine that is already pretty complex.. i don't really like the sound of it :(

Y.
Quote:Original post by Ysaneya
But in my method A, allocation and initialization are separate:

CObject *obj = new CObject();obj->create(...);obj->destroy(...);obj->create(...);obj->destroy(...);delete obj;

... will have the object allocated once, but used twice.


it doesn't separate it, when you call new the constructor is called even if your constructor doesn't do anything its initialized, but i was thinking along the lines of other consequences such as copy construction.

Quote:Original post by Ysaneya
I see your idea with placement new, but quite frankly i don't like it much. It's okay for code inside a memory manager, but i want a consistant way to allocate all kind of objects in my engine, not only with the scene graph. And sometimes, some objects are just too simple to bother with a memory manager at all. Let's imagine a CTriangle class that only contains 3 vertices. Wouldn't you prefer to simply do a "new" to allocate the triangle; or would you bother calling a manager/factory in order to allocate one ?


Well for other types thats not managed, having a 2 phase construction is a bad idea by that i mean creating an instance then forcing clients to call some init method, the time between construction and that init method it's in an undefined state & puts a burden on clients.
Quote:Original post by Ysaneya
CObject *obj = sceneFactory->newObject();

What purpose does sceneFactory serve? Why make this abstraction instead of just using "new"?
Quote:Original post by Ysaneya
In the context of a scene graph tree, which solution do you prefer?

Well, first of all, most objects *are* nodes. You attach a human to the world and you add a sword to the human. The world, the human and the sword are essentially nodes. You could do something like this:
World.AttachObject(Human);Human.AttachObject(Sword);

You could then write some algorithms to traverse the graph. Depending on your requirements, you may not necessarily need a "SceneGraph" class (the algorithms could be global).
Quote:Original post by Ysaneya
Hum, that's going to add another layer of complexity to an engine that is already pretty complex.. i don't really like the sound of it :(

Well, you want to specify access restrictions in some class for a subset of your code but you don't want that class to know anything about the subset. Essentially, you have no choice but to create a facade class that has special access to your "priviledged" methods and expose that facade to the appropriate subset of your engine.
Quote:Original post by snk_kid
it doesn't separate it, when you call new the constructor is called even if your constructor doesn't do anything its initialized, but i was thinking along the lines of other consequences such as copy construction.


No copy constructor - to copy an object, you'd call obj->create(another_obj);

I'm basically replacing the usual constructor by a "create" method - which gives two advantages: an object can be reused without having to allocate some new memory; and the create function can return an error code, which i find much more simple and clean than using exceptions all over the place.

Quote:Original post by snk_kid
Well for other types thats not managed, having a 2 phase construction is a bad idea by that i mean creating an instance then forcing clients to call some init method, the time between construction and that init method it's in an undefined state & puts a burden on clients.


The constructor still set the memory to "0" (ie. integers to 0, floats to 0.0, pointers to NULL, etc.. ). I don't really see how it's different than in your "placement new" suggestion. In your example, you get a pointer to an object whose memory has been allocated, but that hasn't been "constructed" yet. Your object is also in an undefined state (ie. you can't use it or you'll make your program crash) until you call the constructor. There isn't a lot of difference with my approach, except the initializations are not done in the constructor but in the "create" method. I fail to see what is the fundamental difference between the two.

Quote:Original post by CoffeeMug
What purpose does sceneFactory serve? Why make this abstraction instead of just using "new"?


Most of the time, the scene factory would simply call the new operator. However sometimes, a scene factory could implement transparently an optimized memory manager; for example, a pool of preallocated objects?

Do you think i should not bother with that, and simply use "new" ?

Quote:Original post by CoffeeMug
Well, first of all, most objects *are* nodes. You attach a human to the world and you add a sword to the human. The world, the human and the sword are essentially nodes. You could do something like this:


That's exactly what i described in my last post. With a difference though; you must first add the object to the scene, so that it gets assigned a unique object ID in a manager, or other operations.

By doing this:
scene->addObject(obj);scene->getRoot()->addObject(obj);


You add the object to the scene manager (first call), but the object is not yet linked to a parent node. The second call assigns the object has a child of the scene's root node (or any other node for the matter).

It's possible to detect that the object is not yet part of the scene manager when you attach the object to a parent node (which would implicitely do the "scene->addObject(obj)" operation), but i'm wondering if it wouldn't be better for clarity to let the user do it explicitely (hence the first line in the code above)..?

Y.
Quote:Original post by Ysaneya
Quote:Original post by snk_kid
Well for other types thats not managed, having a 2 phase construction is a bad idea by that i mean creating an instance then forcing clients to call some init method, the time between construction and that init method it's in an undefined state & puts a burden on clients.


The constructor still set the memory to "0" (ie. integers to 0, floats to 0.0, pointers to NULL, etc.. ). I don't really see how it's different than in your "placement new" suggestion. In your example, you get a pointer to an object whose memory has been allocated, but that hasn't been "constructed" yet. Your object is also in an undefined state (ie. you can't use it or you'll make your program crash) until you call the constructor. There isn't a lot of difference with my approach, except the initializations are not done in the constructor but in the "create" method. I fail to see what is the fundamental difference between the two.


You didn't quite understand what i'm talking about, i wasn't talking about placement new and like said anyways clients wouldn't be dealing with this and at this level. I was actually talking about types that your not going to manage like your triangle example 2 phase construction is generally a bad idea.
I would add the objects using

/// Method B
CObject *obj = scene->createObject(node, model);

since the objects are "part of" the scene.
although I would leave the "node" part as automatic, because otherwise you would have to specify

CNode *node = new CNode();
CModel *model = new CModel();
...etc

a good reason I can think of in favor of 2 phase construction would be in large systems where the memory for a number of objects is allocated before the actual data - in the case where a scene file header has a variable for the number of objects, with filenames for the actual mesh data - the mesh would have to be loaded separately but the scene manager would have to contain the number of meshes required.
Quote:Original post by ray_intellect
a good reason I can think of in favor of 2 phase construction would be in large systems where the memory for a number of objects is allocated before the actual data - in the case where a scene file header has a variable for the number of objects, with filenames for the actual mesh data - the mesh would have to be loaded separately but the scene manager would have to contain the number of meshes required.


I know what your saying but thats not exactly 2 phase construction, when a user-defined type is created properly thats like creating a number of instances all in the same default valid state that conforms to the state invariant and then changing the state of each instance by the values stored in the file.

There are ways to get pass this immediate step of all elements of a container from being created in a default valid state such that they are constructed with the values from the file straight away.

If you had a real explicit 2 phase construction for a user-defined type such that the first phase construct's an instance in an invalid state it might be all good for when your loading from a file but all other times your forcing clients to go through a 2 step procedure just to get the instance in a valid state, i wouldn't consider that a well designed user-defined type.

[Edited by - snk_kid on September 29, 2004 6:06:12 PM]

This topic is closed to new replies.

Advertisement