Errors in tutorial "Make A Character Look At The Camera

Problems building or running the engine, queries about how to use features etc.
Post Reply
runevision
Gnoblar
Posts: 8
Joined: Fri May 12, 2006 1:55 pm
Location: Denmark
Contact:

Errors in tutorial "Make A Character Look At The Camera

Post by runevision »

I've been trying, with great difficulty, to follow the tutorial "Make A Character Look At The Camera", located at http://www.ogre3d.org/wiki/index.php/Ma ... The_Camera . I've now finally gotten it to work, but I had to fix a lot of things which seemed to be wrong in the tutorial.

Update: I've posted complete code in the reply below for a demo that shows the 'looking at the camera' feature correctly.

1) Getting global position of bones

The tutorial gets the global position/orientation of bones this way:

Code: Select all

//we want the head to be facing the camera, along the vector between the camera and the head 
        //this is done in world space for simplicity
	Ogre::Vector3 headBonePosition = headBone->getWorldPosition();
and:

Code: Select all

Ogre::Quaternion neckBoneWorldOrientation = neckBone->getWorldOrientation();
However, according to this thread: http://www.ogre3d.org/phpBB2/viewtopic. ... 6f4e2718c6
... it should be this way instead:

Code: Select all

Ogre::Vector3 headBonePosition = ent->_getParentNodeFullTransform() * headBone->_getDerivedPosition();
and:

Code: Select all

Ogre::Quaternion neckBoneWorldOrientation = ent->getParentSceneNode()->getWorldOrientation() * neckBone->getWorldOrientation();
This seems to actually work, unlike the solution in the tutorial.

2) Testing for pitch and yaw

The tutorial tests for the pitch and yaw of the head rotation using the getRoll() and getPitch() Quaternion methods, even though, according to the thread ( http://www.ogre3d.org/phpBB2/viewtopic. ... 6f4e2718c6 ), those methods are not reliable, since Quaternions don't directly store the yaw, pitch, and roll and actually return some values that may not correspond to the actual yaw, pitch, and roll (though it's not a bug).

Some other way of testing and enforcing restrains of the neck should be implemented, I guess?

3) Adjusting angles

Also, it seems that the .ToAngleAxis(angle,axis) method used in the tutorial might return an angle between 0 and 2*PI rather than between -PI and PI. Can anyone confirm that this is how it works? In either case, the tutorial seems to assume that it's just a positive angle between 0 and PI, and thus does not account for rotation in both directions, which give very odd results sometimes. When instead handling rotation in both directions, the head rotates more naturally in all cases.

I'm just new to much of this stuff and not completely sure that I've got it right, so I haven't updated the wiki tutorial myself. But can someone confirm the observations I've made?

And also: Are the tutorials in the wiki not tested before being put online? It would seem that someone would have spotted that it doesn't actually work if it had been tested (well unless the animated entity is in a scene node with no transformation at all and the camera only looks from certain angles).

Thanks for taking the time to look at this.

Rune
Last edited by runevision on Wed Nov 21, 2007 2:33 pm, edited 1 time in total.
runevision
Gnoblar
Posts: 8
Joined: Fri May 12, 2006 1:55 pm
Location: Denmark
Contact:

Sample code

Post by runevision »

Below is complete SkeletalLookAt.cpp and SkeletalLookAt.h example file, derived from the SkeletalAnimation demo that comes with OGRE. The SkeletalLookAt demo features the same 6 female characters as the original demo; only in this version they are all looking at the camera as they sneak around.

I can post the Visual Studio 2005 project file as well if someone tells me where to post it. There doesn't seem to be a way to attach files here. Anyway, it just use the example application framework like the other demos.

SkeletalLookAt.cpp:

Code: Select all

#include "SkeletalLookAt.h"

#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
#endif

#ifdef __cplusplus
extern "C" {
#endif

#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
INT WINAPI WinMain( HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT )
#else
int main(int argc, char **argv)
#endif
{
    // Create application object
    SkeletalApplication app;

    try {
        app.go();
    } catch( Exception& e ) {
#if OGRE_PLATFORM == OGRE_PLATFORM_WIN32
        MessageBox( NULL, e.getFullDescription().c_str(), "An exception has occured!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
        std::cerr << "An exception has occured: " << e.getFullDescription();
#endif
    }


    return 0;
}

#ifdef __cplusplus
}
#endif
SkeletalLookAt.h:

Code: Select all

#include "ExampleApplication.h"

#define NUM_JAIQUAS 6
AnimationState* mAnimState[NUM_JAIQUAS];
Real mAnimationSpeed[NUM_JAIQUAS];
Vector3 mSneakStartOffset;
Vector3 mSneakEndOffset;

Entity* mEntities[NUM_JAIQUAS];
Quaternion mOrientations[NUM_JAIQUAS];
Vector3 mBasePositions[NUM_JAIQUAS];
SceneNode* mSceneNode[NUM_JAIQUAS];
Degree mAnimationRotation(-60);
Real mAnimChop = 7.96666f;
Real mAnimChopBlend = 0.3f;

ManualObject* myManualObject;


// From Look At Camera Tutorial
void turnHeadToLook(Ogre::Entity *ent, double deltaT, Ogre::Camera * object){
	
	SceneNode* sceneNode = ent->getParentSceneNode();
	SkeletonInstance* skel = ent->getSkeleton();
	Ogre::Bone* headBone = skel->getBone("head");

	//we want the head to be facing the camera, along the vector between the camera and the head 
	//this is done in world space for simplicity

	//Ogre::Vector3 headBonePosition = headBone->getWorldPosition(); // From tutorial, but WRONG
	Ogre::Vector3 headBonePosition = ent->_getParentNodeFullTransform() * headBone->_getDerivedPosition();

	Ogre::Vector3 objectPosition = object->getPosition();
	Ogre::Vector3 between = objectPosition-headBonePosition;
	//what is the unit vector
	Ogre::Vector3 unitBetween = between.normalisedCopy();

	Ogre::Node * neckBone = headBone->getParent();
	//Ogre::Quaternion neckBoneWorldOrientation = neckBone->getWorldOrientation(); // From tutorial, but WRONG
	Ogre::Quaternion neckBoneWorldOrientation = ent->getParentSceneNode()->getWorldOrientation() * neckBone->getWorldOrientation();

	Ogre::Vector3 headForward = unitBetween;
	Ogre::Vector3 neckUp = neckBoneWorldOrientation * Vector3(1.0,0.5,0.0).normalisedCopy();
	Ogre::Vector3 headRight = neckUp.crossProduct(headForward);
	Ogre::Vector3 headUp = headForward.crossProduct(headRight);
	
	Ogre::Quaternion rot(headUp,headForward*-1,headRight*-1);
	rot.normalise(); //might have gotten messed up
	
	// Get ideal head rotation in local neck space:
	rot = neckBoneWorldOrientation.Inverse()*rot;
	
	// Useful variables:
	double PI = Math::PI;//3.1416;
	Ogre::Radian angle;
	Ogre::Vector3 axis;
	Ogre::Real maxAngle;
	Ogre::Real ratio;
	
	// Code to restrain head rotation (different from tutorial)
	rot.ToAngleAxis(angle,axis);
	if (angle.valueRadians()>PI) angle -= Radian(2*PI); // Make sure angle is between -PI and PI
	maxAngle = PI*0.5;
	ratio = 1;
	if(angle.valueRadians() > maxAngle){
		ratio = maxAngle/angle.valueRadians();
	}
	if(angle.valueRadians() < -maxAngle){
		ratio = -maxAngle/angle.valueRadians();
	}
	rot = Ogre::Quaternion::Slerp(ratio,Quaternion(),rot,true);
	
	// Code to make head move smoothly:
	Ogre::Quaternion rotationBetween = rot*headBone->getOrientation().Inverse();
	rotationBetween.ToAngleAxis(angle,axis);
	if (angle.valueRadians()>PI) angle -= Radian(2*PI); // Make sure angle is between -PI and PI
	float lookSpeed = 4.0;
	maxAngle = lookSpeed*deltaT;
	ratio = 1;
	if(angle.valueRadians() > maxAngle){
		ratio = maxAngle/angle.valueRadians();
	}
	if(angle.valueRadians() < -maxAngle){
		ratio = -maxAngle/angle.valueRadians();
	}
	rot = Ogre::Quaternion::Slerp(ratio,headBone->getOrientation(),rot,true);
	
	// Apply rotation:
	headBone->setOrientation(rot);

	// Draw Lines:
	
	// LookAt line
	/*myManualObject->begin("manual1Material", Ogre::RenderOperation::OT_LINE_LIST); 
	myManualObject->position(headBonePosition); 
	myManualObject->position(headBonePosition+(unitBetween*6)); 
	myManualObject->end();*/

	// neckUp vector line
	/*myManualObject->begin("manual1Material", Ogre::RenderOperation::OT_LINE_LIST); 
	myManualObject->position(headBonePosition); 
	myManualObject->position(headBonePosition+(neckUp*8)); 
	myManualObject->end();*/

	// headUp vector line
	/*myManualObject->begin("manual1Material", Ogre::RenderOperation::OT_LINE_LIST); 
	myManualObject->position(headBonePosition); 
	myManualObject->position(headBonePosition+(headUp*4)); 
	myManualObject->end();*/	
}


// Event handler to animate
class SkeletalLookAtFrameListener : public ExampleFrameListener
{
protected:
public:
	SkeletalLookAtFrameListener(RenderWindow* win, Camera* cam, const std::string &debugText)
        : ExampleFrameListener(win, cam)
    {
		mDebugText = debugText;
    }

    bool frameStarted(const FrameEvent& evt)
    {
	if( ExampleFrameListener::frameStarted(evt) == false )
		return false;

		// Clear Lines:
		myManualObject->clear();
        
		for (int i = 0; i < NUM_JAIQUAS; ++i)
        {
			Real inc = evt.timeSinceLastFrame * mAnimationSpeed[i]; 
			if ((mAnimState[i]->getTimePosition() + inc) >= mAnimChop)
			{
				// Loop
				// Need to reposition the scene node origin since animation includes translation
				// Calculate as an offset to the end position, rotated by the
				// amount the animation turns the character
				Quaternion rot(mAnimationRotation, Vector3::UNIT_Y);
				Vector3 startoffset = mSceneNode[i]->getOrientation() * -mSneakStartOffset;
				Vector3 endoffset = mSneakEndOffset;
				Vector3 offset = rot * startoffset;
				Vector3 currEnd = mSceneNode[i]->getOrientation() * endoffset + mSceneNode[i]->getPosition();
				mSceneNode[i]->setPosition(currEnd + offset);
				mSceneNode[i]->rotate(rot);

				mAnimState[i]->setTimePosition((mAnimState[i]->getTimePosition() + inc) - mAnimChop);
			}
			else
			{
				mAnimState[i]->addTime(inc);
			}

			turnHeadToLook(mEntities[i], inc, mCamera);
        }

        return true;
    }
};


class SkeletalApplication : public ExampleApplication
{
public:
    SkeletalApplication() {}

protected:
	std::string mDebugText;

    // Just override the mandatory create scene method
    void createScene(void)
    {
		mSceneMgr->setShadowTechnique(SHADOWTYPE_TEXTURE_MODULATIVE);
		mSceneMgr->setShadowTextureSize(512);
		mSceneMgr->setShadowColour(ColourValue(0.6, 0.6, 0.6));

        // Setup animation default
        Animation::setDefaultInterpolationMode(Animation::IM_LINEAR);
        Animation::setDefaultRotationInterpolationMode(Animation::RIM_LINEAR);

        // Set ambient light
        mSceneMgr->setAmbientLight(ColourValue(0.5, 0.5, 0.5));

		// The jaiqua sneak animation doesn't loop properly, so lets hack it so it does
		// We want to copy the initial keyframes of all bones, but alter the Spineroot
		// to give it an offset of where the animation ends
		SkeletonPtr skel = SkeletonManager::getSingleton().load("jaiqua.skeleton", 
			ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME);
		Animation* anim = skel->getAnimation("Sneak");
		Animation::NodeTrackIterator trackIter = anim->getNodeTrackIterator();
		while (trackIter.hasMoreElements())
		{
			NodeAnimationTrack* track = trackIter.getNext();

			TransformKeyFrame oldKf(0, 0);
			track->getInterpolatedKeyFrame(mAnimChop, &oldKf);

			// Drop all keyframes after the chop
			while (track->getKeyFrame(track->getNumKeyFrames()-1)->getTime() >= mAnimChop - mAnimChopBlend)
				track->removeKeyFrame(track->getNumKeyFrames()-1);

			TransformKeyFrame* newKf = track->createNodeKeyFrame(mAnimChop);
			TransformKeyFrame* startKf = track->getNodeKeyFrame(0);

			Bone* bone = skel->getBone(track->getHandle());
			if (bone->getName() == "Spineroot")
			{
				mSneakStartOffset = startKf->getTranslate() + bone->getInitialPosition();
				mSneakEndOffset = oldKf.getTranslate() + bone->getInitialPosition();
				mSneakStartOffset.y = mSneakEndOffset.y;
				// Adjust spine root relative to new location
				newKf->setRotation(oldKf.getRotation());
				newKf->setTranslate(oldKf.getTranslate());
				newKf->setScale(oldKf.getScale());
			}
			else
			{
				newKf->setRotation(startKf->getRotation());
				newKf->setTranslate(startKf->getTranslate());
				newKf->setScale(startKf->getScale());
			}
		}

        Entity *ent;
		Real rotInc = Math::TWO_PI / (float)NUM_JAIQUAS;
		Real rot = 0.0f;
        for (int i = 0; i < NUM_JAIQUAS; ++i)
        {
			Quaternion q;
			q.FromAngleAxis(Radian(rot), Vector3::UNIT_Y);

			mOrientations[i] = q;
			mBasePositions[i] = q * Vector3(0,0,-20);
			
            ent = mSceneMgr->createEntity("jaiqua" + StringConverter::toString(i), "jaiqua.mesh");

			mEntities[i] = ent;

            // Add entity to the scene node
			mSceneNode[i] = mSceneMgr->getRootSceneNode()->createChildSceneNode();
			mSceneNode[i]->attachObject(ent);
			mSceneNode[i]->rotate(q);
			mSceneNode[i]->translate(mBasePositions[i]);
			
            mAnimState[i] = ent->getAnimationState("Sneak");
            mAnimState[i]->setEnabled(true);
			mAnimState[i]->setLoop(false); // manual loop since translation involved
            mAnimationSpeed[i] = Math::RangeRandom(0.5, 1.5);

			rot += rotInc;
        }
		
		// From Look At Camera Tutorial
		int numAnimations = 0;
		
		// Remove head keyframe track
		Ogre::Bone * headBone;
		headBone = skel->getBone("head");
		numAnimations = skel->getNumAnimations();
		for(int i=0;i<numAnimations;i++){
			Ogre::Animation * anim = skel->getAnimation(i);
			anim->destroyNodeTrack(headBone->getHandle());
		}
		// Set manual control of head for all entities
		for (int i = 0; i < NUM_JAIQUAS; ++i)
        {
			SkeletonInstance* skeleton = mEntities[i]->getSkeleton();
			headBone = skeleton->getBone("head");
			headBone->setManuallyControlled(true);
        }
		
		// Remove neck keyframe track
		Ogre::Bone * neckBone;
		neckBone = skel->getBone("neck");
		numAnimations = skel->getNumAnimations();
		for(int i=0;i<numAnimations;i++){
			Ogre::Animation * anim = skel->getAnimation(i);
			anim->destroyNodeTrack(neckBone->getHandle());
		}

		// Draw Lines:
		myManualObject =  mSceneMgr->createManualObject("manual1"); 
		SceneNode* myManualObjectNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("manual1_node"); 
		
		MaterialPtr myManualObjectMaterial = MaterialManager::getSingleton().create("manual1Material","debugger"); 
		myManualObjectMaterial->setReceiveShadows(false); 
		myManualObjectMaterial->getTechnique(0)->setLightingEnabled(true); 
		myManualObjectMaterial->getTechnique(0)->getPass(0)->setDiffuse(0,0,1,0); 
		myManualObjectMaterial->getTechnique(0)->getPass(0)->setAmbient(0,0,1); 
		myManualObjectMaterial->getTechnique(0)->getPass(0)->setSelfIllumination(0,0,1); 
		
		myManualObject->begin("manual1Material", Ogre::RenderOperation::OT_LINE_LIST); 
		myManualObject->position(0,0,0); 
		myManualObject->position(10,10,10); 
		myManualObject->end();
		
		myManualObjectNode->attachObject(myManualObject);

        // Give it a little ambience with lights
        Light* l;
        l = mSceneMgr->createLight("BlueLight");
		l->setType(Light::LT_SPOTLIGHT);
        l->setPosition(-200,150,-100);
		Vector3 dir(-l->getPosition());
		dir.normalise();
		l->setDirection(dir);
        l->setDiffuseColour(0.5, 0.5, 1.0);

        l = mSceneMgr->createLight("GreenLight");
		l->setType(Light::LT_SPOTLIGHT);
        l->setPosition(0,150,-100);
		dir = -l->getPosition();
		dir.normalise();
		l->setDirection(dir);
        l->setDiffuseColour(0.5, 1.0, 0.5);

        // Position the camera
        mCamera->setPosition(100,20,0);
        mCamera->lookAt(0,10,0);

        // Report whether hardware skinning is enabled or not
        Technique* t = ent->getSubEntity(0)->getMaterial()->getBestTechnique();
        Pass* p = t->getPass(0);
        if (p->hasVertexProgram() && p->getVertexProgram()->isSkeletalAnimationIncluded())
            mDebugText = "Hardware skinning is enabled";
        else
            mDebugText = "Software skinning is enabled";

		Plane plane;
		plane.normal = Vector3::UNIT_Y;
		plane.d = 100;
		MeshManager::getSingleton().createPlane("Myplane",
			ResourceGroupManager::DEFAULT_RESOURCE_GROUP_NAME, plane,
			1500,1500,20,20,true,1,60,60,Vector3::UNIT_Z);
		Entity* pPlaneEnt = mSceneMgr->createEntity( "plane", "Myplane" );
		pPlaneEnt->setMaterialName("Examples/Rockwall");
		pPlaneEnt->setCastShadows(false);
		mSceneMgr->getRootSceneNode()->createChildSceneNode(Vector3(0,99,0))->attachObject(pPlaneEnt);
    }

    // Create new frame listener
    void createFrameListener(void)
    {
        mFrameListener= new SkeletalLookAtFrameListener(mWindow, mCamera, mDebugText);
        mRoot->addFrameListener(mFrameListener);
    }

};
Rune
epha
Halfling
Posts: 51
Joined: Tue Dec 11, 2007 11:34 pm

question

Post by epha »

hi Rune,

thanks a lot for posting this code - I followed your example and it works perfectly with the jaiqua mesh.

I am still trying to understand how to use quaternions sucessfully, and to make my own models' heads follow the camera-
and there is one line of code I am not sure about:

Ogre::Vector3 neckUp = neckBoneWorldOrientation * Vector3(1.0,0.5,0.0).normalisedCopy();

Can you explain how you got this vector- Vector3(1.0, 0.5, 0.0)
and how multiplying the quaternion with it gives neckUp ?

thanks again.
runevision
Gnoblar
Posts: 8
Joined: Fri May 12, 2006 1:55 pm
Location: Denmark
Contact:

Re: question

Post by runevision »

epha wrote:hi Rune,

thanks a lot for posting this code - I followed your example and it works perfectly with the jaiqua mesh.

I am still trying to understand how to use quaternions sucessfully, and to make my own models' heads follow the camera-
and there is one line of code I am not sure about:

Ogre::Vector3 neckUp = neckBoneWorldOrientation * Vector3(1.0,0.5,0.0).normalisedCopy();

Can you explain how you got this vector- Vector3(1.0, 0.5, 0.0)
and how multiplying the quaternion with it gives neckUp ?
Let me see - it is some time since I was working with that code...

I think I found out by trial and error that the head in the jaiqua model is skinned such that up=(1,0,0) forward=(0,-1,0) and right=(0,0,-1) for some reason. I think this varies from model to model.

Then I wanted to define an upwards direction (relative to the local space of the neck) that would be used for the alignment of the head. The simple choice would have been (1,0,0) - however I found that using a vector that goes a bit backwards rather than straight up gives more natural head movements, so I used (1,0.5,0). I got it into the local space of the neck by multiplying with the neck quaternion.

Hope that helps. I'm not sure if I'll be able to provide more specific help, since I've since gone over to using a different engine that makes bone manipulation much simpler, and I have forgotten most of how it works in Ogre...

Rune
epha
Halfling
Posts: 51
Joined: Tue Dec 11, 2007 11:34 pm

thanks

Post by epha »

hi, thanks for replying. yeah that helps - i have been working out my own models' bone axes.
epha.
Post Reply