Game Development Community

Multiple skins on a single model

by Chris Robertson · 02/10/2005 (7:18 am) · 25 comments

They say the best things come in small packages, and this tiny modification to tsShapeInstance.cc gives you the ability to apply an arbitrary number of skins to a single model. Skins can also be grouped, and switched by group.

For example, let's say you have a model with the following textures:

base.armor.human
red.armor.human
blue.armor.human
green.armor.human

base.clothes.shirt.human
red.clothes.shirt.human
blue.clothes.shirt.human
green.clothes.shirt.human

base.clothes.pants.human
red.clothes.pants.human
blue.clothes.pants.human
green.clothes.pants.human

Normally, using the %obj.setSkinName() command would affect all textures. But not any more......

Where "string" is the setSkinName parameter, we now get the following behaviour:

"blue" matches base.armor.human and base.clothes.shirt.human and base.clothes.pants.human
"blue.clothes" only matches base.clothes.shirt.human and base.clothes.pants.human
"blue.clothes.shirt" only matches base.clothes.shirt.human

eg. %obj.setSkinName("red") would give:

red.armor.human
red.clothes.shirt.human
red.clothes.pants.human

if we then apply %obj.setSkinName("blue.armor"):

blue.armor.human
red.clothes.shirt.human
red.clothes.pants.human

if we then apply %obj.setSkinName("green.clothes"):

blue.armor.human
green.clothes.shirt.human
green.clothes.pants.human

if we then apply %obj.setSkinName("red.clothes.shirt"):

blue.armor.human
red.clothes.shirt.human
green.clothes.pants.human

Pretty neat, huh? In tsShapeInstance, in the reSkin function, just make the changes in bold:

void TSShapeInstance::reSkin(StringHandle& newBaseHandle)
{
#define NAME_BUFFER_LENGTH 256
   static char pathName[NAME_BUFFER_LENGTH];
   [b]char defaultBaseName[NAME_BUFFER_LENGTH] = "base";[/b]
   const char* newBaseName;

   if (newBaseHandle.isValidString()) {
      newBaseName = newBaseHandle.getString();
      if (newBaseName == NULL) {
         return;
      }
      [b]// Append any skin groupings to the end of the default base name. This
      // allows us to reskin materials that match the search pattern without
      // affecting any others.
      // eg newBaseHandle = "red" or "red.armor" or "red.clothes.shirt" would
      // only affect materials that contain "base.", "base.armor." or
      // "base.clothes.shirt." respectively.
      const char *p = dStrchr(newBaseName, '.');
      if(p)
      {
         AssertFatal(dStrlen(p) < (NAME_BUFFER_LENGTH - dStrlen(defaultBaseName)),
                                                 "reSkin: newBaseHandle too long");
         dStrcat(defaultBaseName, p);[/b]
      }
   }
   else {
      newBaseName = defaultBaseName;
   }

   // Make our own copy of the materials list from the resource
   // if necessary.
	[b]bool clonedList = false;[/b]
   if (ownMaterialList() == false) {
		[b]clonedList = true;[/b]
      cloneMaterialList();
   }

   const char* resourcePath = hShape.getFilePath();

   // Cycle through the materials.
   TSMaterialList* pMatList = getMaterialList();
   for (S32 j = 0; j < pMatList->mMaterialNames.size(); j++) {
      // Get the name of this material.
      const char* pName = pMatList->mMaterialNames[j];
      // Bail if no name.
      if (pName == NULL) {
         continue;
      }
      // Make a texture file pathname with the new root if this name
      // has the old root in it; otherwise just make a path with the
      // original name.
      bool replacedRoot = makeSkinPath(pathName, NAME_BUFFER_LENGTH, resourcePath,
                          pName, defaultBaseName, newBaseName);

      if (!replacedRoot) {
         // If this wasn't in the desired format, set the material's
         // texture handle (since that wasn't copied over in the
         // cloning) and continue.
         [b]if(clonedList)[/b]
				pMatList->mMaterials[j] = TextureHandle(pathName, MeshTexture, false);
         continue;
      }

      // 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);
      }
   }
}

Note that this modification does not alter the original behaviour at all, so no changes need to be made to existing models or scripts!
Page «Previous 1 2
#1
02/10/2005 (8:27 am)
Nice, I was looking for something like this. Thanks for sharing.
#2
02/10/2005 (8:53 am)
Thanks, I needed that.
#3
02/10/2005 (9:21 am)
thanks this is the answer to my latest post question! Or I believe it should work if not I can probably tweek it so it does.
#4
02/10/2005 (10:00 am)
I wonder if this would work well with my mesh hiding resource? I always meant for that to include skins, but I ran into snags that Im sure you found. I'll have to try this one out!
#5
02/10/2005 (10:22 am)
@Eric: The only problem I had was transmitting multiple skins from server to client. When you call %obj.setSkinName(), it sets the SkinMask bit, stores the new skin string, then sends the new skin next client update.

Unfortunately, this means that if you issue two setSkinName commands in a row, only the second will be applied, as it will overwrite the skin to be transmitted before it has made it to the client. You need to wait for a client update between each skin change, or modify shapebase to handle multiple skin changes in one update.

This resource is only about making multiple skins _possible_, which it does.
#6
02/13/2005 (5:48 am)
Good resource Chris. I was just updating the EGTGE chapter about this (mentioning that currently using multiple skins resets the other skins on set). This is definitely useful.

Thanks!

www.hallofworlds.com/how.ico Hall Of Worlds, LLC
EdM|EGTGE
#7
02/25/2005 (5:37 am)
Interesting bug...

When I first compiled I deleted my ~/.garagegames folder there by forcing a recompile of all of my .dso files (I'm in linux)

Then I copied over the bin to my examples dir and fired it up.
segfault while loading mission.


Ok try again
segfault while loading mission.

So I use gdb from within the terminal window
gdb ./torqueDemo_DEBUG.bin
start
Runs great! No problems!
quit

So I start again this time without gdb
Loads mission fine
Runs ok for until I go to press the fire button
NULL GHOST OBJECT window pops up.
Dismiss
segfault

This DOES NOT Happen under gdb at all

FYI I have the Server Side Melee code working with this same codebase, and it did not segfault after 30 hours of testing.
Applying this patch appears to be causing the segfault.

My console log is located at....
http://www.freewebs.com/creativedge/

Please take a look and see if you can get any ideas... Thanx!
#8
02/25/2005 (9:21 am)
@Creative Edge Computing

I couldn't see anything in the log file.

Are you applying the setSkinName command using the new naming convention? If you only pass a single group, there should be no difference in behaviour.

eg. %obj.setSkinName("red") will operate exactly the same with the patch as without it.
#9
02/27/2005 (5:27 am)
I've only applied the patch I haven't yet tried to use it.
However I've applied the patch to a clean 1.3 and there are no Null Ghosts, no troubles at all actually I have noticed that using Server Side Melee in conjunction with AIPlayer extensions also causes this behvior.
Therefore I now believe that the problem stems from a change caused by Server Side Melee that is not being accounted for, by other patches.

The only question I have now is, what the heck is a Null Ghost and what can I do to fix and/or prevent them so I do not segfault whenever they are encountered.
#10
04/16/2005 (7:33 am)
Would this mean that you export the various copies of the model with the skin or do you apply a multi-sub object. I don't fully see how the model have multiple skins.

Toby.
#11
04/16/2005 (12:24 pm)
@Toby: Skins are implemented by exporting a single instance of the model with specially named textures (see naming convention described above).
#12
04/18/2005 (9:26 am)
So this is more switching models rather than actual textures for the model.
Just to be clear.
Toby.
#13
09/17/2005 (4:02 am)
Ok first great resource this is exactly what i have been looking for :)

Now the problems :( (new to tge sorry)
I have a model with 6 textures arms, legs, body, feet, hands, head
my default textures are same name with base infront ie: base.legs.png etc.

so i made a new texture and called it fred.legs.png
and i am trying to call like this %obj,setSkinName("fred.legs"); (only want to change the legs)

nothing seems to happen, im trying to call from PlayerShape::onAdd
just for testing not sure if this is the problem or not?

any help would be great
#14
09/20/2005 (9:40 pm)
I think this is very usefull for all. Because this is a great feature.
#15
11/28/2005 (9:22 am)
Here is a small modification to this which some folks may find useful.

This change is to tsShapeInstance.cc's makeSkinPath() function.
The change allows skin names to match without a trailing "human" type component in the name.

For example,
the original version needs the textures to be named thus:

base.armor.human.png
red.armor.human.png
base.skin.human.png
red.skin.human.png

and would not have worked without the ".human" in there.
this change lets you just use filenames like this:

base.armor.png
red.armor.png
base.skin.png
red.skin.png

tsShapeInstance.cc
static bool makeSkinPath(char* buffer, U32 bufferLength, const char* resourcePath,
                         const char* oldSkin, const char* oldRoot, const char* newRoot)
{
   bool replacedRoot = true;[b]
   dsize_t extraLen  = 0;[/b]

   dsize_t oldRootLen = 0;
   char* rootStart = NULL;

   if (oldRoot == NULL) {
      // Not doing any replacing.
      replacedRoot = false;
   }
   else {
      // See if original name has the old root in it.
      oldRootLen = dStrlen(oldRoot);
      AssertFatal((oldRootLen + 1) < bufferLength, "makeSkinPath: Error, skin root name too long");
      dStrcpy(buffer, oldRoot);[b]
      if (dStricmp(oldSkin, buffer)) {
         dStrcat(buffer, ".");
         extraLen = 1;
      }[/b]
      rootStart = dStrstr(oldSkin, buffer);
      if (rootStart == NULL) {
         replacedRoot = false;
      }
   }

   // Find out how long the total pathname will be.
   const dsize_t oldLen = dStrlen(oldSkin);
   dsize_t pathLen = 0;
   if (resourcePath != NULL) {
      pathLen = dStrlen(resourcePath);
   }
   if (replacedRoot) {
      const dsize_t newRootLen = dStrlen(newRoot);[b]
      AssertFatal((pathLen + extraLen + oldLen + newRootLen - oldRootLen) < bufferLength, "makeSkinPath: Error, pathname too long");[/b]
   }
   else {[b]
      AssertFatal((pathLen + extraLen + oldLen) < bufferLength, "makeSkinPath: Error, pathname too long");[/b]
   }

   // OK, now make the pathname.

   // Start with the resource path:
   if (resourcePath != NULL) {
      dStrcpy(buffer, resourcePath);
      dStrcat(buffer, "/");
   }
   else {
      buffer[0] = '[[4c88f18969195]]';
   }
   if (replacedRoot) {
      // Then the pre-root part of the old name:
      dsize_t rootStartPos = rootStart - oldSkin;
      if (rootStartPos != 0) {
         dStrncat(buffer, oldSkin, rootStartPos);
      }
      // Then the new root:
      dStrcat(buffer, newRoot);[b]
      if (extraLen > 0)
         dStrcat(buffer, ".");[/b]
      // Then the post-root part of the old name:[b]
      dStrcat(buffer, oldSkin + rootStartPos + oldRootLen + extraLen);[/b]
   }
   else {
      // Then the old name:
      dStrcat(buffer, oldSkin);
   }

   return replacedRoot;
}
#16
11/29/2005 (5:38 am)
More additions to this resource -

If you want to send multiple reskinning commands in one packUpdate,
a simple way which seems to work is just to keep an array parallel to mSkinNameHandle which accumulates skinNameHandles and which gets emptied in packUpdate().

edit: oops, this worked great on single-player, but i'm having trouble getting it properly networked. will post when it's working.

edit: .. two years later, arright!
don't have the time to post 100% of the code,
but here's the shape of the parts in ShapeBase::packUpdate() and unpackUpdate(), which should provide the outline for the other parts needed.
if (stream->writeFlag(mask & SkinMask)) {
         if (!mShapeInstance) {
            stream->writeInt(0, 7); // 127 skins maximum 
         }
         else {
            Vector<const char*> *list = mShapeInstance->getReplacedSkinNames();
            stream->writeInt(list->size(), 7);
            for (int n = 0; n < list->size(); n++) {
               StringHandle tmp((*list)[n]);
               con->packStringHandleU(stream, tmp);
            }
         }

.. and unpackUpdate()
if (stream->readFlag()) {  // SkinMask
         int numSkins = stream->readInt(7);
         StringHandle sh;

         for (int n = 0; n < numSkins; n++) {
            sh = con->unpackStringHandleU(stream);
            mShapeInstance->reSkin(sh);
         }

         makeSkinHash(mSkinHash);
      }
#17
12/07/2005 (2:03 am)
What is the appropriate way to change texture for the models if I am going to make a MMO game...? Different player can change their skin, hair color, etc...

I have already put required texture files (png format, my texture file name : base.player.png & brown.player.png) to the same folder, however, when I input the following command in the World Editor

1677.setSkinName(brown.player);

it said "Could not locate texture: tutorial.base/data/shapes/Boo/player" & "Mapping String: green to index 13"

Boo - my directory to save the texture files, dts files and dsq files

I also tried to create a folder of "player" under "Boo", but it outputs the same message...

The model I tested is the green player which come with TGE.

Besides, what is the function of mCloakTexture in ShapeBase.cc? How can I load the texture and save it to mCloakTexture () in World Editor (script)?

From the above article, in order to change the texture, we have to modify source code of tsShapeInstance.cc, however, why don't we simply modify tsMesh.cc?

glBindTexture(GL_Texture_2D, tex.getGLName());

Could I change texture for player by using glBindTexture...?

Thanks!!!
#18
12/19/2005 (1:56 am)
@Chan:
Should it not be:

1677.setSkinName('brown.player'); (single quotes)
#19
09/14/2006 (3:01 am)
Nice resource. Thank you
#20
10/05/2006 (5:25 pm)
This is great! I've been serching for something like this. I am quite new to the TGE and was wondering if multiple skins were possible. I've also been searching the Resources and Forums for something similar. Is it possible to do this with a DIF file.
Page «Previous 1 2