Game Development Community

dev|Pro Game Development Curriculum

Mount images on images!

by Daniel Buckmaster · 06/23/2008 (7:26 am) · 12 comments

Developed and tested on TGE 1.5.2.
Updated for T3D 1.1 final.

Thanks to bank for supplying some code fixes where I apparently fell asleep at the keyboard! :)

The aim of this resource is to make it easy to do things like mount a bayonet, ammo clip, or scope to a weapon image. These things could be included in the weapon model itself, but it would quickly become a pain to model and manage a system like that with any variety of weapons and addons.

My solution is, simply:
-Mount the weapon image. ex., %player.mountImage(CrossbowImage,0);
-Mount the image of the piece of equipment. ex., %player.mountImage(BayonetImage,1);
-Mount the bayonet image to the crossbow image. ex., %player.mountImageImage(1,0,"bayonetMount");
The BayonetImage's position is then modified to attach to the 'bayonetMount' node in the CrossbowImage (assuming your CrossbowImage model has a node named 'bayonetMount').

Notes:
-Each piece of equipment, you can see, still takes up an image slot. By default, characters are only allowed 4 images. This can quickly be increased to 8, but more than that will, I think, require code changes. This resource by Derk Adams fixes that, but I will base this resource off stock TGE code.
-Each image is still a proper image, retaining its own states and all. There is no connection between an image and the image it is mounted to, except in position.
-I heartily reccomend Xavier Amado's mount point resource as a nice companion for this one, but it's not necessary.

All right, here are the code changes. I'll deal with all the header file changes first, then all the cc changes, to avoid having to skip between files.

First thing we do is add a member to ShapeBaseImageData. This new member will tell the image what node it is supposed to mount to whenever it's mounted to another image, sort of the same way mountPoint tells the image what point it is mounted to by default.

Open up shapeBase.h and find:
U32               mountPoint;     ///< Mount point for the image.
In the definition of ShapeBaseImageData. Beneath it, add:
//Dan's mods ->
   const char*       imageMountNode; ///< Node to mount to if we're attached to an existing image
   //<- Dan

Next, we have to add two fields to MountedImage, the struct in ShapeBase that stores the data for an image mounted to the shape. We want each image to remember two things: which image it is mounted to, and which node on the image.
To do the first, we'll store the slot of the 'parent' image. For the second, we'll store the ID of the node, rather than its name.

Find this:
struct MountedImage {
Then after this line:
StringHandle nextSkinNameHandle;
Add the following:
//Dan's mods ->
      S32 imageMountNode;
      S32 imageMountSlot;
      //<- Dan

Now we need to add the mountImageImage method I mentioned earlier. Don't worry - I'll explain its parameters later. For now, find this piece of code:
/// Unmount an image from a slot
   /// @param   imageSlot   Mount point
   virtual bool unmountImage(U32 imageSlot);
And add beneath it:
//Dan's mods ->
   /// Mount the image to another image
   /// @param  imageSlot  Mount point
   /// @param  otherSlot  Target mount point
   /// @param  node       Target image node
   virtual bool mountImageImage(U32 imageSlot,U32 otherSlot,const char* node = "");
   //<- Dan

Now, that's it for shapeBase.h. Open up shapeImage.cc.

First things first, we need to initialise the field we added to ShapeBaseImageData.

Find this, the constructor of ShapeBaseImageData:
ShapeBaseImageData::ShapeBaseImageData()
{
   emap = false;

   mountPoint = 0;
And immediately after that line, add:
//Dan's mods ->
   imageMountNode = "";
   //<- Dan

Next, we'll allow the member to be set in scripts (quite useful, really...). This is done in initPersistFields.

Find:
addField("mountPoint", TypeS32, Offset(mountPoint,ShapeBaseImageData));
And add on the next line:
//Dan's mods ->
   addField("imageMountNode",TypeCaseString,Offset(imageMountNode,ShapeBaseImageData));
   //<- Dan

And now, we need to make sure this member is being sent across the network. It's not as scary as it sounds:

Find:
stream->write(mountPoint);
And add after:
//Dan's mods ->
   stream->writeString(imageMountNode);
   //<- Dan
Then find:
stream->read(&mountPoint);
And add:
//Dan's mods ->
   imageMountNode = stream->readSTString();
   //<- Dan

Now we can get back to those fields we added to MountedImage. MountedImage has its own constructor, so we'll head there now.

Find:
ShapeBase::MountedImage::MountedImage()
{
   shapeInstance = 0;
   state = 0;
   dataBlock = 0;
And add beneath it:
//Dan's mods ->
   imageMountNode = -1;
   imageMountSlot = -1;
   //<- Dan

-1 is a default value for 'this field contains nothing', since node IDs and image slot indices start at 0, but never go negative.

Now we get to define that mountImageImage method we declared. This method takes two or three arguments, imageSlot, otherSlot, and, optionlly, node.
imageSlot refers to the slot of the image you want to mount to something else
otherSlot refers to the slot of the image you want to mount [i]onto[/b]
node, if set, gives the name of the node you want the image to be mounted to. If left blank, the datablock value will be used.

Find this method:
bool ShapeBase::unmountImage(U32 imageSlot)
{
   ...
}
And beneath it, add:
(Thanks to bank for fixing my logic in this method!)
//Dan's mods ->
bool ShapeBase::mountImageImage(U32 imageSlot,U32 otherSlot,const char* node)
{
   //Get the image
   MountedImage& image = mMountedImageList[imageSlot];
   //If it's not there, bail out
   if(!image.dataBlock)
      return false;

   //Get the target image
   //If it's not there, bail out
   if(!mMountedImageList[otherSlot].dataBlock)
      return false;

   //Find the node
   const char* n = (dStricmp(node,""))? node : image.dataBlock->imageMountNode;
   S32 ni = mMountedImageList[otherSlot].dataBlock->shape->findNode(n);
   if (ni == -1)
   {
      Con::errorf("Error: node %s not found on image on target slot %d!", n, otherSlot);
      return false;
   }

   //If it's there, set the node number
   image.imageMountNode = ni;
   //Set the slot
   image.imageMountSlot = otherSlot;

   //Update ghosts on what's goin' DOWN!
   setMaskBits(ImageMaskN << imageSlot);

   return true;
}
//<- Dan

Basically, what's happening is this: the first thing to do is get a reference to a MountedImage from the array. If it's dataBlock member is not valid, then there's no image there, so we stop trying. Same goes for the target image, but we don't bother getting a MountedImage reference.
Next, we get the index of the node in the shape with the name we want. If the parameter passed to the function is empty, we use the datablock value.
This is quitesimple - just setting some values. These values are then used later in a few key methods that determine where an image is mounted.

So that we don't end up breaking our getImageTransform functions (which we'll change later...), we need to insert some logic into unmountImage. Basically, if an image is unmounted, we check to see whether any other images were mounted on it. If so, we unmount them as well.

Find the method:
bool ShapeBase::unmountImage(U32 imageSlot)
{
   bool returnValue = false;
   MountedImage& image = mMountedImageList[imageSlot];
And add immediately afterwards:
//Dan's mods ->
   //Is anything mounted to us?
   for(U32 i = 0; i < MaxMountedImages; i++)
      if(mMountedImageList[i].imageMountSlot == imageSlot)
         unmountImage(i);
   //<- Dan
And there's one more thing to take care of. The next line of the function should be:
if (image.dataBlock)
   {
And append:
//Dan's mods ->
      image.imageMountNode = -1;
      image.imageMountSlot = -1;
      //<- Dan

This just resets the image values so the next image mounted in this slot doesn't get messed up.

Now, here come the critical changes. getImageTransform and getRenderImageTransform are the two functions that decide where your images are placed and rendered. If you, for fun, hacked this function to return an identity or zero matrix, all images would be rendered at the origin. Images don't store their own positions and transform like other objects; they are simply rendered in the position obtained by calling this function.
We need to modify it to, if the image is mounted to another image, adjust the image's position to correspond to the mount's node.

Find the method:
void ShapeBase::getImageTransform(U32 imageSlot,MatrixF* mat)
{
And find this part of the code:
else {
         getMountTransform(image.dataBlock->mountPoint,&nmat);
         mat->mul(nmat,data.mountTransform);
      }
Now, change it to look like this:
else {
         //Dan's mods ->
         if((image.imageMountSlot == -1) && (image.imageMountNode != -1))
            getImageTransform(image.imageMountSlot,image.imageMountNode,&nmat);
         else
            getMountTransform(data.mountPoint,&nmat);
         //<- Dan
         mat->mul(nmat,data.mountTransform);
      }
Note: if you're using Xavier Amado's mountPoint resource, change 'data.mountPoint' to 'image.mountPoint'.

This simply says: if we've got no parent image, then find the mount transform on the object we're mounted to. If, on the other hand, we do, then get the transform of the appropriate node on the appropriate image. Then apply our mountTransform to it, either way.

There's another function we have to change in the same way: getRenderImageTransform. It's the same change, but the method calls are different:

Find:
void ShapeBase::getRenderImageTransform(U32 imageSlot,MatrixF* mat)
{
And within it, find:
else {
         getRenderMountTransform(data.mountPoint,&nmat);
         mat->mul(nmat,data.mountTransform);
      }
And change it to:
else {
         //Dan's mods ->
         if((image.imageMountSlot != -1) && (image.imageMountNode != -1))
            getRenderImageTransform(image.imageMountSlot,image.imageMountNode,&nmat);
         else
            getRenderMountTransform(data.mountPoint,&nmat);
         //<- Dan
         mat->mul(nmat,data.mountTransform);
      }
And again, if you're using Xavier Amado's mountPoint resource, change 'data.mountPoint' to 'image.mountPoint'.

Now, finally we're done with shpeImage.cc. A few last things to add, in shapeBase.cc.

The next thing to do is 'networkify' the members we added to MountedImage, way back when. It's the same sort of thing we did before, with the one member in ShapeBaseImageData. Quite simple:

Find:
stream->writeInt(image.dataBlock->getId() - DataBlockObjectIdFirst,
                             DataBlockObjectIdBitSize);
And add beneath:
//Dan's mods ->
         if(stream->writeFlag(image.imageMountSlot != -1))
         {
            stream->writeInt(image.imageMountSlot,3);
            stream->writeInt(image.imageMountNode,6);
         }
         //<- Dan
And then find:
MountedImage& image = mMountedImageList[i];
            ShapeBaseImageData* imageData = 0;
            if (stream->readFlag()) {
               SimObjectId id = stream->readInt(DataBlockObjectIdBitSize) +
                  DataBlockObjectIdFirst;
               if (!Sim::findObject(id,imageData)) {
                  con->setLastError("Invalid packet (mounted images).");
                  return;
               }
            }
And again, add beneath:
//Dan's mods ->
         if(stream->readFlag())
         {
            image.imageMountSlot = stream->readInt(3);
            image.imageMountNode = stream->readInt(6);
         }
         //<- Dan

And, last thing, we've got to declare a ConsoleMethod so our new mountImageImage method will be available in scripts. This isn't too tough.

Find:
ConsoleMethod( ShapeBase, unmountImage, bool, 3, 3, "(int slot)")
{
   ...
}

And add beneath:
(Thanks to bank for supplying a fix to this method, which originally would have allowed you to pass garbage into the third argument by accident.)
//Dan's mods ->
ConsoleMethod(ShapeBase,mountImageImage,bool,4,5,"(slot, targslot [,nodeName=""] ) Set the image from 'slot' to be mounted to nodeName on 'targslot' image")
{
   int imageSlot = dAtoi(argv[2]);
   int otherSlot = dAtoi(argv[3]);
   const char* node = (argc == 5? argv[4] : "");
   return object->mountImageImage(imageSlot,otherSlot,node);
}
//<- Dan

And that's all! Compile and run!

About the author

Studying mechatronic engineering and computer science at the University of Sydney. Game development is probably my most time-consuming hobby!


#1
06/19/2008 (1:42 pm)
Very nice Dan, this is one of those long term needs. Have you tested it over a network yet?
#2
06/20/2008 (5:25 am)
Haven't had the chance, but I don't see why it shouldn't work. As long as the updates get sent right, all the work is done in getImageTransform.
#3
08/07/2008 (5:40 pm)
Great addition!.
Quite interesting workaround, but a good working solution.
I've made some changes:
in bool ShapeBase::mountImageImage(U32 imageSlot,U32 otherSlot,const char* node)
you need to put:
S32 ni = mMountedImageList[otherSlot].dataBlock->shape->findNode(n);
   if (ni==-1)
   {
      Con::errorf("Error: node %s not found on image on target slot %d!", n, otherSlot);
      return false;
   }
instead of
S32 ni = image.dataBlock->shape->findNode(n);
As we want to find a node on the shape we are mounting TO. Assuming I'm attaching bayonet to my weapon, and "bayonetPoint" is on weapon, then your current code with this:
%player.mountImage(CrossbowImage,0);
   %player.mountImage(BayonetImage,1);
   %player.mountImageImage(1,0,"bayonetMount");
will try to find the "bayonetMount" on the datablock of wrong image.

Another addition I would like to recommend is to change the console method "mountImageImage" to this:
ConsoleMethod(ShapeBase,mountImageImage,bool,4,5,"(slot, targslot [,nodeName=""] ) Set the image from 'slot' to be mounted to nodeName on 'targslot' image")
{
   return object->mountImageImage(dAtoi(argv[2]), dAtoi(argv[3]), (argc==5) ? argv[4] : "" );
}
I've added checking for existence of last parameter, so it will default to "", otherwise it would pass some garbage from memory if you don't specify nodeName.

Thanks for sharing, amazing solution!
#4
08/20/2008 (9:21 am)
Glad you liked the resource - and thanks for these fixes! If it's all right, I'll go ahead and put them into the main body of the resource, so people don't have to read all the comments to get better code. Your changes make a lot of sense, and nice catch with the mount node thing! Thanks again :)
And sorry I took a while to catch up on this - I should really monitor my resources more carefully ;P
#5
10/04/2009 (8:24 pm)
Must have resource for any FPS!

Took a while of searching before I found this... which is exactly what I need right now!!!
#6
07/10/2010 (10:27 pm)
Odd issue, not sure if this is my fault or not. The attachments only stick to the weapon when I'm viewing it from the third person. They don't animate with the weapon in first person...Any thoughts?
#7
07/11/2010 (1:21 am)
Hmm... it could be that the get[...]Transform functions don't account for animation. Does animating your weapons affect things like the muzzle vector (could be seen when firing projectiles). Torque does some strange things with animations when in first person, so I should have experimented a bit more before posting this up.
To be honest, it's saved me a lot of work to use Jacob Dankovchik's more realistic first person resource, and just animate characters properly all the time. Well... saved me work in some areas and made more work in others. But I just prefer the more simulated approach. (On second thoughts, this discrepancy between 3rd and 1st person views sounds like exactly what Jacob's resource aims to fix. You might look there for inspiration, even if you don't use it in its entirety.)
Also, slightly related, I'm no longer actually running this code - I've basically done away with Images and now mount everything as an object. Makes hierarchical mounting a bit easier on the head :P.
#8
07/12/2010 (6:32 am)
Got it working, just needed to add some code to the isFirstPerson() sections of the image transform functions.

Incredible resource, one of my favorites by far. Quite pleased with my SOPMOD M4 :)

img413.imageshack.us/img413/7223/sopmod.jpg
#9
07/13/2010 (11:21 pm)
Awesome, glad to hear it wasn't too complicated.
#10
07/01/2011 (12:43 am)
Has anyone been able to get this working with T3D1.1 yet, I've tried and have seen three areas that obviously need modifcation the hardest being the ConsoleDefine but I've had no success.
#11
08/07/2011 (1:58 pm)
Same here, would love to have this in T3D pro.