Game Development Community

dev|Pro Game Development Curriculum

Hiding nodes in a model

by Justin Mette · 02/06/2002 (11:31 am) · 48 comments

Hiding nodes in a model
Implemented and tested with Torque 1.1.1

Overview
This tutorial describes one way in which you can programmatically (C++ or script) hide nodes in a model. This is useful in creating many different effects for your game. For example, in Myrmidon, we use this technique to alter the mesh of the player model based on armor they are wearing. The Myrmidon model has all the armor meshes (each with their own skin) positioned/animation with the rest of the model and the code simply hides all the armor meshes but the one being worn. Another use of this technique might be to blow off the limb of an opponent, leaving only a stump behind.

Background
I had a few design points in mind when implementing this feature. First, all shapes in the game should have this capability not just a player as I can envision many other uses for this technique in future phases of Myrmidon. Second, hiding nodes on a model should only affect that instance of the model (which requires keeping all connected clients up to date). Finally, the engine coding should be generic leaving the scripts to do all game-specific implementations.

I decided that the ShapeBase class was the appropriate place for the new feature, based on the design points stated above. You may choose a different class, but for this tutorial to work, the class needs to at least be derived from ShapeBase.

Using this Tutorial
In the different code snippets throughout the tutorial, bold font is used to show new code. Also, I often leave out code that is not important which is denoted by a comment in italics reading "irrelevant code not shown for brevity".

Step 1 Storing hidden nodes
The approach here is to store the numeric index of the nodes to hide in the model. We can use a simple array for this data. Add the following data member to the ShapeBase class:

engine/game/ShapeBase.h
class ShapeBase : public GameBase
{
   [i]// irrelevant code not shown for brevity[/i]

[b]protected: 
   Vector<S32> mHiddenNodes;  // stores an array of nodes to hide[/b]

   [i]// irrelevant code not shown for brevity[/i]
};
Step 2 Updating ghosts
Torque is a client/server engine and it "ghosts" instances of the ShapeBase object from the server to the client. Simply put, the server has the "master" list of ShapeBase instances for the world. The client also has a list of corresponding ShapeBase instances, called "ghosts", that are updated with network packets from the server on a regular basis. For more information on "ghosts", refer to the Torque documentation.

To help keep network updates to a minimum, Torque uses a bitmask to keep track of changes to a ShapeBase class. The packUpdate() method on the server version of a ShapeBase instance uses this bitmask to determine if an update is required every 32 milliseconds.

So, first let's create our bit in the update bitmask of ShapeBase for tracking changes to the hidden node list, called HideNodeMask. I just inserted it before SoundMaskN as shown below:

engine/game/ShapeBase.h
class ShapeBase : public GameBase
{
   [i]// irrelevant code not shown for brevity[/i]

// Network state masks
enum ShapeBaseMasks {
   NameMask        = Parent::NextFreeMask,
   DamageMask      = Parent::NextFreeMask << 1,
   NoWarpMask      = Parent::NextFreeMask << 2,
   MountedMask     = Parent::NextFreeMask << 3,
   CloakMask       = Parent::NextFreeMask << 4,
   ShieldMask      = Parent::NextFreeMask << 5,
   InvincibleMask  = Parent::NextFreeMask << 6,
   [b]HideNodeMask    = Parent::NextFreeMask << 7,[/b]
   SoundMaskN      = Parent::NextFreeMask << [b]8[/b],
   ThreadMaskN     = SoundMaskN << MaxSoundThreads,
   ImageMaskN      = ThreadMaskN << MaxScriptThreads,
   NextFreeMask    = ImageMaskN << MaxMountedImages
   };  

   [i]// irrelevant code not shown for brevity[/i]
};
Note that our new code will set this HideNodeMask bit when it changes the contents of the hidden node array. That will signal to the packUpdate() method that it should send a packet with the changes to the corresponding "ghost" objects. We will now modify ShapeBase::packUpdate() to detect the change and send an update.

The first step is to protect against the first call to packUpdate at which point every bit in the mask is set. We simply check the size of our array and if there aren't any hidden nodes, we strip off the HideNodeMask from the update bitmask, as follows:

engine/game/ShapeBase.cc
U32 ShapeBase::packUpdate(NetConnection *con, U32 mask, BitStream *stream)
{
   U32 retMask = Parent::packUpdate(con, mask, stream);

   if (mask & InitialUpdateMask) {
      // mask off sounds that aren't playing
      S32 i;
      for (i = 0; i < MaxSoundThreads; i++)
         if (!mSoundThread[i ].play)
            mask &= ~(SoundMaskN << i);

      // mask off threads that aren't running
      for (i = 0; i < MaxScriptThreads; i++)
         if (mScriptThread[i ].sequence == -1)
            mask &= ~(ThreadMaskN << i);

      // mask off images that aren't updated
      for(i = 0; i < MaxMountedImages; i++)
         if(!mMountedImageList[i ].dataBlock) 
            mask &= ~(ImageMaskN << i);

      [b]// mask off node visibility if nothing to update
      if(mHiddenNodes.size() == 0)
	   mask &= ~HideNodeMask;[/b]
   }

   [i]// irrelevant code not shown for brevity[/i]
}
Now, in that same method let's add our code to handle a change to the hidden node list. I chose to add the code to the "uncommon stuff" that existed in the packUpdate() method already, as follows:

engine/game/ShapeBase.cc
U32 ShapeBase::packUpdate(NetConnection *con, U32 mask, BitStream *stream)
{
   [i]// irrelevant code not shown for brevity[/i]

   if(!stream->writeFlag(mask & (NameMask | DamageMask | SoundMask | ThreadMask |
      ImageMask | CloakMask | MountedMask | [b]HideNodeMask[/b])))
      return retMask;

   [i]// irrelevant code not shown for brevity[/i]

   // Group some of the uncommon stuff together.
   if (stream->writeFlag(mask & (NameMask | ShieldMask | CloakMask | InvincibleMask | [b]HideNodeMask[/b]))) {
      if (stream->writeFlag(mask & CloakMask)) {
         // cloaking
         stream->writeFlag( mCloaked );

         // piggyback control update
         stream->writeFlag(bool(getControllingClient()));
         
         // fading
         if(stream->writeFlag(mFading && mFadeElapsedTime >= mFadeDelay)) {
            stream->writeFlag(mFadeOut);
            stream->write(mFadeTime);
         }
         else
            stream->writeFlag(mFadeVal == 1.0f);
      }
      if (stream->writeFlag(mask & NameMask)) {
         stream->writeInt(mNameTag,NetStringTable::StringIdBitSize);
         con->checkString(mNameTag);
      }
      if (stream->writeFlag(mask & ShieldMask)) {
         stream->writeNormalVector(mShieldNormal, ShieldNormalBits);
         stream->writeFloat( getEnergyValue(), EnergyLevelBits );
      }
      if (stream->writeFlag(mask & InvincibleMask)) {
         stream->write(mInvincibleTime);
         stream->write(mInvincibleSpeed);
      }
      [b]if (stream->writeFlag(mask & HideNodeMask)) {
         stream->writeInt(mHiddenNodes.size(), 8);
         for(int x = 0; x < mHiddenNodes.size(); x++)
            stream->writeInt(mHiddenNodes[x], 8);
      }[/b]
   }

   [i]// irrelevant code not shown for brevity[/i]
}
Of course, now we need the client "ghost" objects to receive this packet and update their local copy of the hidden node array. This takes place in the unpackUpdate() method, as you would expect. The reading of the packet must be in the same order it was written in so our new code is dropped in just after the invincible info:

engine/game/ShapeBase.cc
void ShapeBase::unpackUpdate(NetConnection *con, BitStream *stream)
{
   [i]// irrelevant code not shown for brevity[/i]

   if (stream->readFlag()) {  // InvincibleMask
      F32 time, speed; 
      stream->read(&time);
      stream->read(&speed);
      setupInvincibleEffect(time, speed);
      }

   [b]if (stream->readFlag()) { // HideNodeMask
      mHiddenNodes.clear();
      int count = stream->readInt(8);
      for(int x = 0; x < count; x++)
         mHiddenNodes.push_back(stream->readInt(8));
      updateHiddenNodes();
   }[/b]

   [i]// irrelevant code not shown for brevity[/i]
}
Optimization opportunities probably exist in the above packUpdate() and unpackUpdate() methods. Note that I chose to rewrite the entire array each update. You could extend the code to only write the changes to the array - if this feature is to be used a lot at runtime.

Step 3 Hiding nodes

You may have noticed that in the last code blurb, we introduced a new method called updateHiddenNodes(). This is the method that actually hides the nodes listed in mHiddenNodes. Let's add the declaration for our new method first in ShapeBase.h:

engine/game/ShapeBase.h
class ShapeBase : public GameBase
{
   [i]// irrelevant code not shown for brevity[/i]

protected: 
   [b]void updateHiddenNodes();  // hides nodes in the model using mHiddenNodes[/b]

   [i]// irrelevant code not shown for brevity[/i]
};
Now for the fun part. Following is the code that actually walks the nodes in the models, setting the visibility flag to 0.0 (for invisible) based on the contents of the mHiddenNodes array. Note that we aren't sure what changed in the mHiddenNodes array since the last call so we for nodes that aren't in our array, we set the visibility to 1.0. This assumes that all mesh objects in your model are visible at startup which is usually the case.

engine/game/ShapeBase.cc
[b]void ShapeBase::updateHiddenNodes()
{
   if(!mShapeInstance) return;
   int s = mShapeInstance->mMeshObjects.size();
   for(int x = 0; x < s; x++)
   {
      S32 nodeIndex = mShapeInstance->mMeshObjects[x].object->nodeIndex;

      F32 visible = 1.0;
      if(isHiddenNode(nodeIndex))
         visible = 0.0;

      mShapeInstance->mMeshObjects[x].visible = visible;
      mShapeInstance->mMeshObjects[x].forceHidden = (visible == 0.0);
   }
}[/b]
Another support function was introduced in the last code snipper called isHiddenNode(). Let's add that in:

engine/game/ShapeBase.h
class ShapeBase : public GameBase
{
   [i]// irrelevant code not shown for brevity[/i]

public: 
   [b]bool isHiddenNode(S32 node);  // checks mHiddenNodes array[/b]

   [i]// irrelevant code not shown for brevity[/i]
};
engine/game/ShapeBase.cc
[b]bool ShapeBase::isHiddenNode(S32 node)
{
   for(int x = 0; x < mHiddenNodes.size(); x++)
      if(mHiddenNodes[x] == node)
         return true;
   return false;
}[/b]
Finally, there is reference to a 'forceHidden' property on the MeshObjectInstance class that was introduced in this last snippet. This property is used to ensure the animation code doesn't 'unhide' the node each tick of the game during visibility animation routines (which it does by default). So, we have to add the following:

engine/ts/tsShapeInstance.h
[i]// irrelevant code not shown for brevity[/i]

struct MeshObjectInstance : ObjectInstance
{
   TSMesh * const * meshList; // one mesh per detail level...Null entries allowed
   const TSObject * object;
   S32 frame;
   S32 matFrame;
   F32 visible;
   [b]bool forceHidden;[/b]

   [i]// irrelevant code not shown for brevity[/i]
};
Now lets initialize that variable.

engine/ts/tsShapeInstance.cc
void TSShapeInstance::buildInstanceData(TSShape * _shape, bool loadMaterials)
{
   [i]// irrelevant code not shown for brevity[/i]

   // add objects to trees
   S32 numObjects = mShape->objects.size();
   mMeshObjects.setSize(numObjects);
   for (i=0; i<numObjects; i++)
   {
      const TSObject * obj = &mShape->objects[i ];
      MeshObjectInstance * objInst = &mMeshObjects[i ];

      // call objInst constructor
      constructInPlace(objInst);

      // hook up the object to it's node
      objInst->nodeIndex = obj->nodeIndex;

      // set up list of meshes
      if (obj->numMeshes)
         objInst->meshList = &mShape->meshes[obj->startMeshIndex];
      else
         objInst->meshList = NULL;

      objInst->object = obj;
      [b]objInst->forceHidden = false;[/b]
   }

   [i]// irrelevant code not shown for brevity[/i]
};
Now let's use this new variable to force a node hidden during the visibility animation routine:

engine/ts/tsAnimate.cc
void TSShapeInstance::animateVisibility(S32 ss)
{
   [i]// irrelevant code not shown for brevity[/i]

   // set defaults   
   S32 a = mShape->subShapeFirstObject[ss];
   S32 b = a + mShape->subShapeNumObjects[ss];
   [b]for (i=a; i<b; i++)
   {
      if(mMeshObjects[i ].forceHidden)
         mMeshObjects[i ].visible = 0.0;
      else if (beenSet.test(i))
         mMeshObjects[i ].visible = 1.0;
   }[/b]

   [i]// irrelevant code not shown for brevity[/i]
};
Step 4 Accessing from script
Whew, we are almost done. Now we want to write some methods in ShapeBase that actually hide and unhide nodes. Because the user of these methods don't really know the internal numeric index for a mesh/node, lets make it easy on them and let them specify the node name instead. Let's start with the method declaration:

engine/game/ShapeBase.h
class ShapeBase : public GameBase
{
   [i]// irrelevant code not shown for brevity[/i]

public: 
   [b]void hideNode(const char* nodeName);  // hides the specified node 
   void unhideNode(const char* nodeName);[/b]

   [i]// irrelevant code not shown for brevity[/i]
};
First, we have the hideNode() method to implement. This method will look for the specified node name in the mesh and add the corresponding node index to the mHiddenNodes array. Note that the HideNodeMask bit is set to let the engine know that the array has changed.

engine/game/ShapeBase.cc
[b]void ShapeBase::hideNode(const char* nodeName)
{
   if(!mShapeInstance) return;

   // find the node in the shape
   S32 node = mShapeInstance->getShape()->findNode(nodeName);
   if(node == -1) return;

   // make sure the node isn't already hidden
   for(int x = 0; x < mHiddenNodes.size(); x++)
      if(mHiddenNodes[x] == node)
         return;

   // add the node to our hidden nodes array
   // and flag that the array has changed
   mHiddenNodes.push_back(node);
   setMaskBits(HideNodeMask);
}[/b]
Next is the unhideNode() method which simply removes a node from the mHiddenNodes array.

engine/game/ShapeBase.cc
[b]void ShapeBase::unhideNode(const char* nodeName)
{
   if(!mShapeInstance) return;

   // find the node in the shape
   S32 node = mShapeInstance->getShape()->findNode(nodeName);
   if(node == -1) return;

   // look for the node in our hidden nodes array
   // if we find it, erase it and flag that the array has changed
   for(int x = 0; x < mHiddenNodes.size(); x++)
      if(mHiddenNodes[x] == node)
      {
         mHiddenNodes.erase(x);
         setMaskBits(HideNodeMask);
         return;
      }
}[/b]
Finally, to make these methods accessible to the script, we add the following static methods and update the consoleInit() method as follows (note I'm still old school and do this without ConsoleFunction for some reason):

engine/game/ShapeBase.cc
[b]static void cShapeBaseHideNode(SimObject * obj, S32, const char **argv)
{
   ShapeBase * shape = static_cast<ShapeBase*>(obj);
   shape->hideNode(argv[2]);
}

static void cShapeBaseUnhideNode(SimObject * obj, S32, const char **argv)
{
   ShapeBase * shape = static_cast<ShapeBase*>(obj);
   shape->unhideNode(argv[2]);
}

void ShapeBase::consoleInit()
{
   // irrelevant code not shown for brevity

   Con::addCommand("ShapeBase", "hideNode", cShapeBaseHideNode, "obj.hideNode(nodeName)", 2, 3);
   Con::addCommand("ShapeBase", "unhideNode", cShapeBaseUnhideNode, "obj.unhideNode(nodeName)", 2, 3);
}[/b]
That's all the engine changes required. It is recommended that you compile all these changes and make sure you don't have any compile or linking errors before continuing.

Step 5 Example implementation
Writing script to use these two new methods, hideNode() and unhideNode(), is really an exercise for the reader as it will most likely be unique to your game needs. However, to prove that this code is working, we can have a little fun add the following line of script to the end of the GameConnection::createPlayer() method:

fps/server/scripts/game.cs
function GameConnection::createPlayer(%this, %spawnPoint)
{
   if (%this.player > 0)  {
      // The client should not have a player currently
      // assigned.  Assigning a new one could result in 
      // a player ghost.
      error( "Attempting to create an angus ghost!" );
   }

   // Create the player object
   %player = new Player() {
      dataBlock = LightMaleHumanArmor;
      client = %this;
   };
   MissionCleanup.add(%player);

   // Player setup...
   %player.setTransform(%spawnPoint);
   %player.setEnergyLevel(60);
   %player.setShapeName(%this.name);
   
   // Update the camera to start with the player
   %this.camera.setTransform(%player.getEyeTransform());

   // Give the client control of the player
   %this.player = %player;
   %this.setControlObject(%player);

   [b]// hide the players head
   %player.hideNode("Bip01 Head");[/b]
}
Conclusion
I have tested this code with Torque 1.1.1 right from the CVS repository. If you have any problems or suggestions with the tutorial, lets discuss them in the feedback.

Enjoy!
Justin Mette
21-6 Productions
Page «Previous 1 2 3 Last »
#1
02/06/2002 (11:40 am)
A good tutorial. What's neat about it is that it covers several different aspects of the engine to add a nifty feature... have you tried it with more than one player in the game? I'm not a TS expert, but isn't the change to the original shape object state affecting all player instances?
#3
02/06/2002 (2:04 pm)
Awesome tutorial. Very nicely done.
#4
02/06/2002 (2:20 pm)
Very nice Justin. Is there any extensions to this to allow for the extra meshes to be stored in seperate shapes and apply them to the model?
#5
02/06/2002 (2:26 pm)
Thanks guys. Chris, we have no plans at this time to make that type of extension. We explored using separate models for the different parts of the character but it ended up being much easier on the artists to animate everything in one MAX file.
#6
02/07/2002 (2:28 pm)
Uh guys? There already is support for adding meshes... :) You could simply mount an object on the player. Sure, it may not animate correctly if you arent careful, but it should work for most instances. At least, I think that would work. I could easily be horribly wrong.... :)

I love the tutorial, thank you VERY much. :)
#7
02/08/2002 (10:52 pm)
Awesome tutorial :)

I followed it to the letter but I can't get it to update in real time (from the console). It works if I put it in the player spawn code.

is that normal?

*Edit: Ahhh, I figured it out. You forgot to include something (or I'm incredibly blind).

There are a few lines that read:

if(!stream->writeFlag(mask & (SkinMask | MaxSpeedMask | GravityMask | NameMask | DamageMask | SoundMask |
         ThreadMask | ImageMask | CloakMask | MountedMask | ShieldMask)))
      return retMask;

You need to add HideNodeMask to that list. Mine looks a little different, I added a couple new functions.
#8
02/09/2002 (5:42 am)
Hi Chris, glad you liked the tutorial. The change you mention is in the 3rd code snippet of Step 2. I wanted to use color to help highlight changes, but only had bold and italics at my disposal. Sorry for the confusion.
#9
02/09/2002 (9:41 am)
No, not that one. There's a second if statement above that that looks similar. Without adding to it, you can only hide/show nodes at startup, or whenever another mask updates.

Notice how it returns if the statement is false:

if(!stream->writeFlag(mask & (SkinMask | MaxSpeedMask | GravityMask | NameMask | DamageMask | SoundMask |
         ThreadMask | ImageMask | CloakMask | MountedMask | ShieldMask)))
      return retMask;// <--Here--<<<
#10
02/09/2002 (9:47 am)
Of course your right Chris, nice catch! The tutorial has been updated.
#11
02/10/2002 (2:24 pm)
My shapeBase.cc file compiled fine, but I keep getting this syntax error in the console:


>>> Advance script error report. Line 389.
>>> Some error context, with ## on sides of error halt:
// hide the players head
%player.hideNode(
#12
02/11/2002 (3:20 am)
I got that same thing at first. It was from copying it off off this page, and pasting it into the script. To get around it try typing it manually.
#13
02/11/2002 (5:44 am)
I copied the hideNode call from here into notepad and the quote characters are whacky and need to be replaced with regular quotes in your code. Sorry about that - so much for pretty formatting...
#14
02/11/2002 (6:54 pm)
I retyped it in manually, and the error is gone! But, the problem is nothing happened :/ He still has his head darnit :) Shapebase.cc compiled no errors. Sorry to be a pest. Darned kids...

Scott
#15
02/13/2002 (6:39 am)
Hi Scott - if you send me the following files, I can take a quick look to see if I can find the error.

engine/game/ShapeBase.cc
engine/game/ShapeBase.h
fps/server/scripts/game.cs
#16
02/13/2002 (2:42 pm)
Sent, thank you very much...

Scott
#17
02/20/2002 (1:13 pm)
This is a very useful ability. Although it might seem like it's wasteful at first (ie, you have to make and skin a very complex initial model, instead of just attaching a generic armor mesh). However, in the long run, this is not only a great approach but a MUST-HAVE for content-rich games that want to go easy on memory. For instance, Dark Age of Camelot uses that approach on many monsters and all player models; for instance, wearing a helmet doesn't just attach a helmet mesh to your head. That generally looks silly. Instead, it hides your head and unhides the helmet portion of the mesh.

What makes this powerful is that one model can have an incredible variety of looks; basically, you find yourself separating your models into various body types/skeleton structures, and then building all of your new models as variations. It does seem kind of counter-intuitive, but it allows you to, for instance, load a single model with textures that has a total memory footprint (model+textures) of around 800K, and you might have a dozen or so completely unique characters as a result, each with a variety of different looks/skins.

It worked pretty well for DAoC, but that's with an MMORPG-customized engine, fine-tuned for that kind of thing. I find it cool that Torque can support this functionality with this little modification.
#18
04/07/2002 (7:13 am)
I just updated the tutorial to fix a bug that occurred in multiplayer games using a dedicated server. The changes are all in Step 3 and have to do with the new "forceHidden" property of the MeshObjectInstance class. If you have previously implemented this tutorial, I suggest going back through Step #3 and updating your code. Sorry for any inconvenience.
#19
04/08/2002 (1:32 pm)
Hm, everything compiles fine, I get no errors, but the head of the default player still is where it's supposed to be... Any idea anyone??
Thanks!
[edit]Nevermind! I didn't realize at first that I have to replace the for-loop in tsAnimate.cc ... [/edit]
#20
04/08/2002 (1:40 pm)
Hi Stefan, what version of Torque are you using? Also, if you want to send me the following files, I can do a quick code review:

engine/game/shapeBase.h
engine/game/shapeBase.cc
engine/ts/tsShapeInstance.h
engine/ts/tsShapeInstance.cc
engine/ts/tsAnimate.cc
fps/server/scripts/game.cs (or wherever you are calling hideNode() from)

justin.mette@21-6.com
Page «Previous 1 2 3 Last »