Relationship between ISceneNode and IRenderable

Started by
6 comments, last by JasonBlochowiak 17 years, 5 months ago
Okay I'm sure everyone's getting pretty bored of scene management questions by now but I've trawled the forums and not really found a solution that fits my needs. I think I have a come up with a reasonable solution of my own, but I want to run it by a few people to make sure I'm heading in the right direction. Basically I want to be able to traverse my scene graph and create a list of renderable objects. Once the traversal is complete the render list should be sorted by material and in order of distance from the camera. My plan is to create IRenderable and ISceneNode interface classes.

class IRenderable
{
	virtual void Render() = 0;
	
	Material	*pMaterial;
	
	Matrix	*pTransform;
};

class ISceneNode
{
	virtual void Render();

	// Name, transform, parent / child links, bounding box etc. You know the drill, no point listing then all here.
};


Objects that inherit from ISceneNode (e.g. CMesh) may contain several renderable objects (inherited from IRenderable of course). When traversing the scene graph I will call each visible scene node's render function, which should in turn add the relevant renderable objects to the render list.

class CMeshAtomic : public IRenderable
{
	void Render()
	{
		// Render geometry
	}
};

class CMesh : public ISceneNode
{
	void Render()
	{
		// Add all atomics to list of renderable objects
	}

	std::list<CMeshAtomic>	atomics;
};


The biggest reservation I have is that the ISceneNode::Render function may be a bit misleading and could easily be confused with IRenderable::Render. Also, I'm struggling to find a good way of adding objects rendered using immediate mode (e.g. bounding boxes for debugging) to a scene? What do you think? Any help and or constructive criticism would be greatly appreciated.
Advertisement
ISceneNode::Render seems accurate - it renders things, even if it forks the responsibility off on it's children. (The rest of my post is based on this assumption, ignore it if I've misinterpreted what ISceneNode::Render and clarify what exactly does it do)

That not all Render()ables are IRenderable stinks of a naming flaw, though - ISceneNode should be IRenderable, since it is Render()able. Since material isn't shared by all Render()ables, that probably belongs in a seperate class.

Refactoring, I'd end up with something like:

class IRenderable {public:    virtual ~IRenderable() {}    virtual void Render() const = 0;    virtual const Matrix& Transformation() const = 0;};class ISceneNode : public IRenderable {    std::list< IRenderable* > children; //just for synopsispublic:    virtual void Render(); //std::for_each( children.begin() , children.end() , std::mem_fun( & IRenderable::Render ) );    //name, bounding box, transformation implementation    //children management accessors    //etc};class AtomicMesh : public IRenderable {    Material* material; //if this is used in more classes, we could refactor this into IMaterialedRenderablepublic:    virtual void Render();};class Mesh : public ISceneNode {    //no more "atomics" list - that's been pulled up into SceneNodepublic:    //mesh-specific children management accessors here - e.g.:    //   LoadMesh( filename ) would call:    //   SceneNode::Add( new AtomicMesh( ... ) ) as needed [or equivilant].};

Don't know if it will help you, but I use a different approach.
I have a ISceneNode interface, like you. But I don't use a IRenderable method. Instead, I use a SRenderable structure which contains everything needed to render something (texture ids, vb, ib ids, etc.) and in the ISceneNode::Render() method, I send those structures to a Renderer, instead of rendering things.

Then, this renderer can sort the SRenderables, and render them. I prefer this approach : you have only one rendering entry point, and the advantage is that the renderer has a global view of everything that is rendered. For the moment, I don't really need this, but I usually prefer the most open methods ^^

struct SRenderable{   unsigned int  _vb;    ///< id of the vertex buffer   unsigned int  _ib;    ///< id of the index buffer   // ...};class ISceneNode{protected:   ISceneNode(void);            ///< protected to avoid instanciation of ISceneNode   virtual ~ISceneNode(void);public:   // default implementation of OnRender :   virtual void OnRender(void)   {      for (RenderableIterator iter = m_Renderables.begin(); iter != m_Renderables.end(); ++iter)         CRenderer::AddRenderable(*iter);   }private:   std::vector<SRenderable *>                    m_Renderables;   typedef std::vector<SRenderable *>::iterator  RenderableIterator;};
Thanks for the input guys, much appreciated.

MaulingMonkey, your assumption is correct. In most situations ISceneNode::Render() will pass responsibility on to a set of IRenderable objects, although I have tried to leave the functionality open and generic so that it would be easy for other programmers to render debugging info (such as bounding boxes) for a scene node by simply calling immediate mode functions from within it's Render() function (rather than having to create and setup a new IRenderable object specifically for this purpose). Hope that makes sense??? I agree that if ISceneNode is to have a Render() function it should really be a renderable itself, but this seems a little unnecessary. Perhaps what I should really be doing is re-naming ISceneNode::Render() to ISceneNode::PrepRenderables() or something similar, that way I could keep the material variable within IRenderable, which would make sorting renderables by material much easier.

Paic, your suggestion is something I had already considered and I am warming to it now I see that I'm not the only one to have thought of it. The only reservation I have is regarding the lack of flexibility (when compared to using IRenderables), but I think perhaps that's just me trying to over complicate things and really it provides more than enough flexibility.

I plan to go with Paic's method because it sounds a little more straight forward. There are so many other things to consider with scene management and rendering that I want to keep things reasonably simple, a least for now. I may re-name SRenderable to something like SPrimitiveBatch because if I have something named renderable, it suggests that all objects that can be rendered should be renderables. But that's not really important, it's just what makes sense to me.
class ISceneNode{	virtual std::vector<IRenderable*> getRenderables();};


How does this sound? I don't think a scene node should care about a Render method, becasue to a scene node that doesn't make sense. At least to me.

Disclaimer - I'm no scene expert!
---------------------http://www.stodge.net
In the theory, you're right ... but in practice (in an editor in my case) almost every scene node needs something to be drawn (the cameras need to draw a dummy on screen so the user know where is is, same for lights, etc.) So I decided to go this way ^^
But the principle is the same so the exact implementation is just a matter of taste :)
Quote:
class ISceneNode
{
virtual std::vector<IRenderable*> getRenderables();
};

How does this sound? I don't think a scene node should care about a Render method, becasue to a scene node that doesn't make sense. At least to me.

Disclaimer - I'm no scene expert!


I had also considered this but it would mean an ISceneNode object would have no way of determining how it's IRenderable objects are rendered, something which would be useful for level of detail meshes amongs other things.

class CMesh : public ISceneNode{	void Render()	{		if(distanceFromCamera > lodDistance)		{			// Render lod1Atomics		}		else		{			// Render lod2Atomics		}	}	std::list<CMeshAtomic>	lod1Atomics, lod2Atomics;};


Of course if there's a better way of doing this I'd be willing to give it a try.
In my last engine, ISceneNode had-a IRenderable. In my current engine, IRenderable refers to ISceneNode, and I prefer it this way. Basically, it came down to "Why should a scene node know anything about drawing? It's purely a positional thing." The renderable thing cares about position and orientation, so it asks the scene node, but the scene node is completely ignorant about drawing anything.

Similarly, last time around, my cameras were derived from scene nodes, which I consider in retrospect to be a mistake. Now, cameras refer to scene nodes, but don't derive from them.

So, also, in my way of doing things, multiple renderables can refer to the same scene node.

As for LOD, I'd recommend that you simply treat things generically (ignoring any code or logic flaws - I just typed this in):

class IRenderable{public: IRenderable(INode &node) : mNode(&node) {} virtual tSphere BoundingSphere(void) const = 0; virtual void Render(void) const = 0;protected: ptr<INode> mNode;};class ILODRenderable : public IRenderable{public: typedef pair<float, ptr<IRenderable> > tLOD; virtual void Render(void) const {  if (mLODs.empty())   return;  float coverage = mNode->ComputeCoverage(BoundingSphere());  tLOD::const_iterator it;  for (it = mLODs.begin(); it != mLODs.end(); ++it)  {   const tLOD &lod = *it;   if (lod.first > coverage)   {    lod.second->Render();    return;   }  }  (mLODs.end() - 1).second->Render(); }private: vector<tLOD> mLODs;};

This topic is closed to new replies.

Advertisement