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:
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:
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:
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:
Next, we'll allow the member to be set in scripts (quite useful, really...). This is done in initPersistFields.
Find:
And now, we need to make sure this member is being sent across the network. It's not as scary as it sounds:
Find:
Now we can get back to those fields we added to MountedImage. MountedImage has its own constructor, so we'll head there now.
Find:
-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:
(Thanks to bank for fixing my logic in this method!)
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:
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:
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:
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:
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:
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.)
And that's all! Compile and run!
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;
//<- DanNow 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));
//<- DanAnd 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); //<- DanThen 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;
}
//<- DanBasically, 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);
//<- DanAnd 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;
//<- DanThis 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);
}
//<- DanAnd 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);
}
//<- DanAnd, 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);
}
//<- DanAnd 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!
#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
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:
Another addition I would like to recommend is to change the console method "mountImageImage" to this:
Thanks for sharing, amazing solution!
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 ofS32 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
And sorry I took a while to catch up on this - I should really monitor my resources more carefully ;P
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
Took a while of searching before I found this... which is exactly what I need right now!!!
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
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.
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
Incredible resource, one of my favorites by far. Quite pleased with my SOPMOD M4 :)
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 :)
#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. 
Torque 3D Owner Dave Young
Dave Young Games