Game Development Community

TGE Multiple Material Reskinning

by Dave Young · 03/06/2008 (11:38 am) · 7 comments

This is part of code being developed for the RPG Series: Character Creation for AFX Course on TorqueSchool.com. It was gleaned from the idea presented here:
Reskinning

This allows us to change a skin by saying "replace the base material with this new material". It's a bit more effective to track things that way in a multiplayer environment. Stock TGE gives us one skinnamehandle, and that simply won't do for multiple textured meshes.

The code to pack and unpack the multiple skin updates was prototyped, working well, and then scrapped once a neat and clean code segment was found in a post made by Orion Elenzil, and he gets credit for contribution there as a pioneer.

First, apply the changes found here:
Server Side Material List

Including the example usage code as it is a predecessor to this functionality.
This enables the server to have knowledge of material names when we need it most.

Now let's add in the basic reskinning changes.

In TSShapeInstance.h, change:
void reSkin(StringHandle& newBaseHandle);

to:

void reSkin(StringHandle& oldMaterial,StringHandle& newMaterial);

In TSShapeInstance.cc, replace the reSkin function with this one:

void TSShapeInstance::reSkin(StringHandle& oldMaterial,StringHandle& newMaterial)
{
   #define NAME_BUFFER_LENGTH 256
   static char pathName[NAME_BUFFER_LENGTH];
   const char* newBaseName;
   const char* resourcePath = hShape.getFilePath();
   
   const char* MatName = NULL;
   const char* newMatName = NULL;
      if (oldMaterial.isValidString() && newMaterial.isValidString()) {
      	MatName = oldMaterial.getString();
	newMatName = newMaterial.getString();
      	}
   if (MatName == NULL || newMatName == NULL){return;}
   if (ownMaterialList() == false) {cloneMaterialList();}
   // Cycle through the materials.
   TSMaterialList* pMatList = getMaterialList();
   for (S32 j = 0; j < pMatList->mMaterialNames.size(); j++)
  {
      const char* pName = pMatList->mMaterialNames[j];
      if (pName == NULL){continue;}
      if(!dStricmp(pName,MatName))
      {
	     //Make the new texture handle. First the path, then the handle
		 // Start with the resource path:
		if (resourcePath != NULL) {
			dStrcpy(pathName, resourcePath);
			dStrcat(pathName, "/");
		}
		else {
			pathName[0] = '[[4fc767d121536]]';
		}
	    dStrcat(pathName, newMatName);

		// OK, it is a skin texture.  Get the handle.
		TextureHandle skinHandle = TextureHandle(pathName, MeshTexture, false);
		// Do a sanity check; if it fails, use the original skin instead.
		if (skinHandle.getGLName() != 0) {
			pMatList->mMaterials[j] = skinHandle;
		}
		else {
			makeSkinPath(pathName, NAME_BUFFER_LENGTH, resourcePath, pName, NULL, NULL);
			pMatList->mMaterials[j] = TextureHandle(pathName, MeshTexture, false);
		}
      }
   }
}

This function is a combination of the old reskin + makeskinpath functions, plus some of the magic from the resource noted above for TGEA reskinning.

Now that we've changed our basic reskin definition, there are a few places in the engine we need to also change, as they were expecting the old reskin function with a different number of arguments.

You can recompile and let the errors find you, or do a search for reSkin.
In shapeImage.cc, you will find two instances of:
image.shapeInstance->reSkin(skinNameHandle);

Which need to be changed to:
image.shapeInstance->reSkin(skinNameHandle,skinNameHandle);

In shapebase.cc, you will find one instance of:
mShapeInstance->reSkin(mSkinNameHandle);

Which you will change to:
mShapeInstance->reSkin(mSkinNameHandle,mSkinNameHandle);

That will get you to compile cleanly.

For the final part, we add the reskinning support to shapebase and the ability to preserve and reference a set of base textures and a set of changed textures.

In ShapeBase.h
Under:
StringHandle     mSkinNameHandle;

Add:
StringHandle     mNewSkinNameHandle;

Under:
const char* getShapeName();

Add:
void setSkinName(const char*,const char*);
const char* getSkinName();
void listSkins();


In shapebase.cc:
In onNewDatablock:
After:
if (!mDataBlock)
      return false;

Add:
mSkinNameHandles.clear();
mNewSkinNameHandles.clear();

In packUpdate, find the block inside
if (stream->writeFlag(mask & SkinMask)) {

And change it to:
if (!mShapeInstance)
         {
            stream->writeInt(0, 7);  //7 bits means max 127 skins
         }
         else
         {
            stream->writeInt(mNewSkinNameHandles.size(), 7);
            for (int n = 0; n < mNewSkinNameHandles.size(); n++)
            {
	        StringHandle oldtmp((mSkinNameHandles)[n]);
                StringHandle tmp((mNewSkinNameHandles)[n]);
		con->packStringHandleU(stream, oldtmp);
		con->packStringHandleU(stream, tmp);
            }
         }

What we are doing here is spitting out the list of old and new skin handles.
Note! Be careful inside this codeblock, many people use the SkinMask for other things. You can safely add the above block of code as long as you replace the previous con->packStringHandleU(stream, mSkinNameHandle);

Now for the client section, the unpackUpdate.
Find the code block inside:
if (stream->readFlag())   // SkinMask
{
}

And change it to:

int numSkins = stream->readInt(7);
      StringHandle shold;
      StringHandle shnew;

         for (int n = 0; n < numSkins; n++) {
            shold = con->unpackStringHandleU(stream);
	    shnew = con->unpackStringHandleU(stream);
	    if(shold!=shnew)
               mShapeInstance->reSkin(shold,shnew);
         }

Look how simple and elegant that is! Thanks Orion Elenzil for that :)

Now you're wondering.. gee, how did the reskin get initiated?
Glad you asked. You remembered to add that Server Side Material Resource Right? Good!

Still in shapebase.cc, jump down to the old setSkinName engine and console functions. Comment them out for safe keeping, and replace them with the following, and the additional optional utility functions for listing skins. I could have delete all the debugging statements, but I left them to help you with debugging your own changes if you need em!

void ShapeBase::setSkinName(const char *name,const char *newSkinName)
{
   
   if (!isGhost()) {
	   //Con::errorf("Coming in as %s - %s",name,newSkinName);
      	   StringTableEntry oldname = StringTable->insert(name);
	   StringTableEntry newname = StringTable->insert(newSkinName);
      
         //Store this decision if there is a change to be made
	  bool skinChanged;
	  bool skinFound;
	  skinFound = false;
	  skinChanged = false;
	  //Check for this assignment in skin handles already
	  for(int x=0; x < mSkinNameHandles.size(); x++)
	  {
		  //Do we have a matching for this handle already?
		  if(mSkinNameHandles[x]==oldname)
		  {
			  //Con::errorf("Original Skin %s was found",name);
			  //Found the handle in the list already
			  skinFound = true;
			  //Con::errorf("Comparing %s and %s",mNewSkinNameHandles[x],newname);
			  if(mNewSkinNameHandles[x]!=newname)
			  {
				 //Con::errorf("The remapped skin %s is not what was here before %s",mNewSkinNameHandles[x],newname);
			        //The new name previously there is not the one being given to us now
				 //It's a new one
				 mNewSkinNameHandles[x] = StringTable->insert(newSkinName);
				 skinChanged = true;
			  } else {
				 //Con::errorf("The remapped skin is the same as what was here before %s",newSkinName);
			  }
			  break;
		  }
	  }

	  //We didn't find the original material, highly unlikely and means trouble
	  if(!skinFound)
	  {
		 Con::errorf("******Original Skin %s was not found",name);
	  }

	  //Somewhere above, it was determined that we made a change, network it!
	  if(skinChanged)
	  {
               setMaskBits(SkinMask);
	  }
   }
}

ConsoleMethod( ShapeBase, setSkinName, void, 4, 4, "(oldMaterialName,newMaterialName)")
{
   object->setSkinName(argv[2],argv[3]);
}

//Function to list base and changed texture names
void ShapeBase::listSkins()
{
	 for(int y=0; y < mSkinNameHandles.size(); y++)
	  {
            Con::errorf("Original Skin %d %s",y,mSkinNameHandles[y]);
	  }

	  for(int z=0; z < mNewSkinNameHandles.size(); z++)
	  {
            Con::errorf("New Skin %d %s",z,mNewSkinNameHandles[z]);
	  }

}

ConsoleMethod( ShapeBase, listSkins, void, 2, 2, "(listSkins)")
{
   object->listSkins();
}

All this code together has the awesome effect of perfectly managing multiple skin changes in a single pack/unpack. It's network safe and was well worth the effort.

This code is fully integrated into the upcoming:
RPG Series: Character Creation for AFX Course

Similar code was developed for the AFXA version already, albeit much simpler thanks to the awesome material system in TGEA.

#1
03/07/2008 (7:28 am)
This is awesome work!! My question is: How is this different than the following resource??
www.garagegames.com/index.php?sec=mg&mod=resource&page=view&qid=12197
Which we have been using with great success. It allows us to change any texture we want on a player.
#2
03/07/2008 (7:47 am)
Shon, it's a different can of worms. Leslie's work enables a whole other level of texture layering.

This resource is just for changing textures, any texture. Name doesn't need to begin with "base" or follow a naming convention, and the changes are stored and sent to the client. I suspect a problem with Leslie's resource and many others is that it works well for a single-texture model, where you are overlaying textures as opposed to having body, hands, feet, head, hair, etc skinned with different textures.

So the main difference is that this demonstrates persistence and networking for multiple textures, something most of the other retexturing resources need. They all work great for existing clients receiving progressive reskinning commands, but when a new client joins he will probably only see the last change. With this resource, if a new client joins the game, he will get the full changes. What's more, the multiple changes come down at the same time.

The other main difference is the reskinning method. I built off a fine resource someone made for TGEA (linked at the top of the resource here) which lets you specify an original texture name and the texture which should take its place. It's less limiting to me.
#3
03/07/2008 (2:20 pm)
@Dave: Thanks for your quick and informative response. So I will ask a couple more questions, if you don't mind?
1. Can I continue to overlay png's as I can with Leslie's resource. That is the main reason we are using it, as we can overlay Tatoos, add paint on the body and other weird things.
2. Can I reskin any Shapebase item with this resource or is it only for characters. It looks like I can, if so you are right. That does make it a different can of worms.
3. I am using TGE/AFX. Does it matter?

Pardon me for all the questions, but if I can change the skin on any shapebase item, on any area, as easy as it sounds it will solve several major hoops for me and I thank you a lot for doing it.
After going over the code it looks like all of the changes are in shapebase.cc and doesn't affect the player.cc file at all. I will try to implement both and see what happens. Unless you already know of any reason not to, I will give it a try.
Once again thanks!
#4
04/16/2008 (8:10 am)
Excellent! Solves some problems I was having with multiple materials and multiple meshes. A question: if I use %obj.setSkinName("base.skinRed","notbase.skinRed"); and then I want to change back to the normal skin, I try %obj.setSkinName("notbase.skinRed","base.skinRed"); It gives me an error that it can't find the texture 'notbase.skinRed'.
#5
04/17/2008 (5:00 pm)
Hi Daniel,
Just try:
%obj.setSkinName("base.skinRed", "base.skinRed");

Consider the first name as a handle for the texture in the original model.

Luck!
Guimo
#6
09/29/2008 (9:11 am)
Thanks for the response :) I can't test this right now, but as soon as I've reintegrated the resource, I will :)
#7
10/10/2008 (1:46 pm)
does it works for TGEA ?


i cant find this in TGEA
TSMaterialList * materialList;

Vector<const char *> mBaseTextureNames;

that you mentioned, that update first