Skip to main content
gamedev.net
🔒 Locked

Time based animation

Started by Endar Nov 30, 2005 at 6:21 PM 11 replies 1k views
Original Post
Endar
Endar
In order to get a model animating at the right speed, I need to use time to calculate the duration between the last rendered frame as to interpolate the right amount. I just added a pre-render and post-render function to my ISceneGraphNode object, and a timer to the scene graph.

/**
 * All scene graph nodes will be derived from this class.
 */
class ISceneGraphNode	:	public IMBase
{
protected:

	util::list<ISceneGraphNode*> m_childList;	///< Holds all the child nodes of this node
	scene::ISceneManager* m_sceneManager;		///< Pointer to the scene manager for rendering and other

public:

	/**
	 * Constructor
	 * \param smgr A pointer to the scene manager
	 */
	ISceneGraphNode(scene::ISceneManager* smgr)
		:	m_sceneManager(smgr)
	{
		// We are not calling smgr->get() because we want the graph to be destroyed when
		// smgr->drop() is called.
	}

	/**
	 * Copy constructor
	 * \param i A const reference to an object derived from ISceneGraphNode
	 */
	ISceneGraphNode(const ISceneGraphNode& i)
		:	m_sceneManager(i.m_sceneManager)
	{
		// We are not calling smgr->get() because we want the graph to be destroyed when
		// smgr->drop() is called.
	}

	/**
	 * Destructor
	 */
	virtual ~ISceneGraphNode()
	{
		util::list<ISceneGraphNode*>::iterator i = m_childList.begin();

		for( ; i != m_childList.end() ; i++)
			(*i)->drop();	// reduce the ref_count for all child nodes

		m_sceneManager->drop();
	}

	/**
	 * Add a node to the child list.
	 * \param n A pointer to a ISceneGraphNode derived object
	 */
	void addSceneGraphNode(scene::ISceneGraphNode* n)
	{
		m_childList.push_back( n );		// add n to the list
	}

	/**
	 * Render all the child nodes.
	 */
	virtual void render()
	{
		util::list<ISceneGraphNode*>::iterator i = m_childList.begin();

		for( ; i != m_childList.end(); i++)
			(*i)->render();	// render child node
	}

	/**
	 * Things to do before rendering, like updating.
	 * \param delta_time The time (in seconds) since the last rendering frame
	 */
	virtual void preRender(float delta_time)
	{
		util::list<ISceneGraphNode*>::iterator i = m_childList.begin();

		for( ; i != m_childList.end(); i++)
			(*i)->preRender(delta_time);	// render child node

	}

	/**
	 * Things to do after rendering.
	 * \param delta_time The time (in seconds) since the last rendering frame
	 */
	virtual void postRender(float delta_time)
	{
		util::list<ISceneGraphNode*>::iterator i = m_childList.begin();

		for( ; i != m_childList.end(); i++)
			(*i)->postRender(delta_time);	// render child node

	}

};


/**
 * Render all objects in the scene graph.
 */
void CSceneGraph::render()
{
	util::list<ISceneGraphNode*>::iterator i = m_nodeList.begin();

	for( ; i != m_nodeList.end(); i++){
		(*i)->preRender( m_hiResTimer.getElapsedSeconds() );
		(*i)->render();
		(*i)->postRender( m_hiResTimer.getElapsedSeconds() );
	}
}

This was in order to animate an md2 model properly:

/**
 * Update the animation.
 * \param delta_time The time since the last update.
 */
void update(float delta_time)
{
	m_modelTime += delta_time;
	if ( m_modelTime >= 1.0f / MD2AnimationList[m_animState].fps )
		m_modelTime = 0.0f;

	updateInterpolation( m_modelTime / ( 1.0f / MD2AnimationList[m_animState].fps ) );
}

/**
 * Pre-render things, like updating the animation.
 */
void preRender(float delta_time)
{
	update(delta_time);	// updates the animation

	ISceneGraphNode::preRender(delta_time);
}

/**
 * Render the model using indexed vertex arrays.
 */
void render()
{
	if(m_texture)
		m_texture->apply();	// apply the texture

	// draw the indexed triangle list
	m_sceneManager->getVideoDriver()->drawIndexedTriangleList(
				m_displayVertexList.getPtr(), m_displayVertexList.getSize(),
				m_displayIndexList.getPtr(), m_displayIndexList.getSize() );

	ISceneGraphNode::render();	// render all the children
}

How does this all look? Or am I way off on the way it should be?
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Endar
Endar
Also, to restrict the rending to a specific fps, I've just written this:

/** * Render all objects in the scene graph. */void CSceneGraph::render(){	util::list<ISceneGraphNode*>::iterator i = m_nodeList.begin();	// the time between frames to ensure that it doens't run above 100 fps	float time_between_frames = 1.0f / 100.0f;	float time = 0.0f;	while( (time += m_hiResTimer.getElapsedSeconds()) < time_between_frames );	for( ; i != m_nodeList.end(); i++){		(*i)->preRender( m_hiResTimer.getElapsedSeconds() );		(*i)->render();		(*i)->postRender( m_hiResTimer.getElapsedSeconds() );	}}


I mean, it could theoretically be right, but, with dividing by 100 intended to be 100 fps, it runs at 80. But, that could be because my md2 model isn't animating properly.
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Trienco
Trienco
There are two ways to limit it to a certain fps. Just wasting time in an empty loop is the one I like to consider pointless. You could use it in a somewhat better way if you do something like:

if (time_passed > physics_update_delay) do_physics (or ai, or rendering).

Unless you set all the delays the same you can offset the different things a bit, do less important stuff less frequently and most of all only draw a new frame when the scene was actually updated.

Of course you will still get in trouble when the machine can't keep up, if you use fixed time steps to make calculations easier and more consistent they might add up and make the app get behind more and more unless you start dropping updates. If you still use the actual time that has passed you should be safe from that (but end up with uglier math).

Advantage: non-linear movement can be broken down in small and fixed linear movements. Results will always be the same on all machines that are fast enough. Achieving the same with arbitrary time steps requires more complex math. Much more comfortable for input recording or networking (ask the poor guys on X-Wing vs. Tie-Fighter)

Disadvantage: Slower machines can't keep up and require switching to less but larger time steps, creating different results from fast machines. Or they run slower for a while whenever they are drowned in accumulated updates, though it would be hard to predict if the complexity of the updates won't stay high, meaning it can never catch up again. Basically the result will be that an update is done every single frame (or tick, loop, whatever might be a good name). So it will look
f@dzhttp://festini.device-zero.de
Endar
Endar
Changed it a little.

/** * Render all objects in the scene graph. */void CSceneGraph::render(){	util::list<ISceneGraphNode*>::iterator i = m_nodeList.begin();	// the time between frames to ensure that it doens't run above 100 fps	float time_between_frames = 1.0f / 290.0f;	static float time = 0.0f;	while( (time += m_hiResTimer.getElapsedSeconds()) < time_between_frames );	for( ; i != m_nodeList.end(); i++){		// use these three to get the constant frame rate going		(*i)->preRender( time );		(*i)->render();		(*i)->postRender( time );	}	// reset time to 0.0f because we have just drawn a frame	time = 0.0f;}


Here's my question for the moment (keeping in mind the previous two still hold): While I would like to get rid of the while loop, because this is intended to be a graphics engine, if I just have an if statement and a 'return' if it is not ready to render yet, it goes back to the main loop and resets the modelview matrix, so I obviously get such an absurd amount of flickering, I can't see the model at all.

How should I fix this? Should I not be resetting the modelview matrix every iteration of the loop?
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Trienco
Trienco
Are you really sure that's even the problem? If you don't render anything then it shouldn't matter if you change the matrix, wipe it, reset it, sneeze on it or lock it up with the neighbours pitbull. As long as it will have the right value before you draw anything again. Now, clearing the color buffer with every iteration and only drawing every once in a while WOULD be a really bad idea (unless you at least don't swap the buffers every time.. then it would just be like me.. useless, but harmless).

Another small thing is depending on what you are doing in your pre and post render functions. In a perfect world all preparation would be happening in one big block right after having sent all the rendering calls for the last frame to make them work in parallel as much as possible. Ie. collecting all visible models should be a block, updating ALL animations of visible objects should be one and rendering ALL visible objects should be a single block. Interleaving all that may easily mean you sent a little bit of work to the gpu and it's long done before you get around to send the next piece of work, meaning it will spend a lot of the time not doing anything.

That's more optimizing than making it work, but it's the kind that requires rearranging quite a bit.

About that 80 instead of 100 issue you could try a few things. What are you using to get the time? Did you try double instead of float (just in case) or getsomeTime()-lastTime > delay to avoid potential weirdness with really small numbers?
f@dzhttp://festini.device-zero.de
Endar
Endar
Well, am I really interleaving it? I mean, I only do one call to CSceneGraph::render, but each ISceneGraphNode::preRender calls preRender on its children, etc, and the same for postRender.

Or should I have seperate calls to CSceneGraph::preRender and CSceneGraph::postRender?
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Trienco
Trienco
Now that you mention it. I must have lost my orientation somewhere and missed that the scene graph is a container and not just the root node.
f@dzhttp://festini.device-zero.de
Jernej.L
Jernej.L
try this formula (i got it in this forum but cannot remember who it was):

[source="pascal"]// livefor is time the current animation time length// framecount is number of frames in animations// age is current time - the time when animation has started (age:= runningtime - born)calc:= livefor div framecount;calc:= age div calc;spritewanted:= calc;


Projects: Top Down City: http://mathpudding.com/
Endar
Endar
Quote:
Original post by Trienco
Now that you mention it. I must have lost my orientation somewhere and missed that the scene graph is a container and not just the root node.


It's an easy mistake to make considering I only gave you a single function and nothing else from the class. [smile]
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Endar
Endar
Previously, the md2 animation was going too fast when I was running this code:

/** * Update the animation. * \param delta_time The time since the last update. */void update(float delta_time){	m_modelTime += delta_time;	if( m_modelTime >= 1.0f / MD2AnimationList[m_animState].fps )		m_modelTime = 0.0f;	updateInterpolation( m_modelTime / ( 1.0f / MD2AnimationList[m_animState].fps ) );}


and that was before I had the fps restrictions. Was the reason it wasn't working because I wasn't restricting the fps? Because it looks okay now, but I'm just not sure.
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Trienco
Trienco
Hm, the only part that could make it somewhat unsmooth seems to be:
m_modelTime = 0.0f;
instead of m_modelTime-=...

But that should make it go too slow, not too fast. Unless the timer does strange things. Though, if
float time_between_frames = 1.0f / 290.0f;
is what you need to make it run at 100fps then either the timer or the code to calculate the fps must be screwed up somewhere.
f@dzhttp://festini.device-zero.de
Endar
Endar
No, that was a screwed up comment.

This code here:
void CSceneGraph::render(){	util::list<ISceneGraphNode*>::iterator i = m_nodeList.begin();	// the time between frames to ensure that it doens't run above whatever fps	float time_between_frames = 1.0f / 182.0f;	static float time = 0.0f;	while( (time += m_hiResTimer.getElapsedSeconds()) < time_between_frames );	//if( (time += m_hiResTimer.getElapsedSeconds()) < time_between_frames )	//	return;	for( ; i != m_nodeList.end(); i++){		// use these three to get the constant frame rate going		(*i)->preRender( time );		(*i)->render();		(*i)->postRender( time );	}	// reset time to 0.0f because we have just drawn a frame	time = 0.0f;}


Where is has "182.0f", I can set that to anything slower than 400-500, which seems to be its normal non-restrictive limit, and it works perfectly as far as I can tell.

My main concern was that the value I was passing to the updateInterpolation function wasn't right, so the animation wasn't playing at the right speed.

/** * Update the animation. * \param delta_time The time since the last update. */void update(float delta_time){	m_modelTime += delta_time;	if( m_modelTime >= 1.0f / MD2AnimationList[m_animState].fps )		m_modelTime = 0.0f;	updateInterpolation( m_modelTime / ( 1.0f / MD2AnimationList[m_animState].fps ) );}


I mean, I'm not really sure what 25fps or 60fps is really supposed to look like, or how fast the animations are usually played, but parts of it just look to be going to fast. Actually most of the animations after the stand look to be going too fast.

[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper
Endar
Endar
bump
[size="2"][size=2]Mort, Duke of Sto Helit: NON TIMETIS MESSOR -- Don't Fear The Reaper

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.