DTS/Dynamic Skin Modifiers

From TDN

This article/resource or the code on this page needs major improvement.
If you do have enough knowledge on the topics related to this article,
please contribute to the community by making/submitting improvements (in case of source code - optimization).

Introduction

This resource will let you know how to add to your game an ability to dynamically "paint" on any ShapeBase derived objects by specifying mask layers and up to four colors per single mask.

Contents

For reference see this .blog post.

This is a combination of two resources: setSkinSpecific/setSkinMesh and Skin Modifiers with major changes/additions. Also, I've made some improvements on network side.

Based on Stock TGE SDK 1.5, but can be used in any TGE version (1.3, 1.4.x, 1.5.x, AFX, MMOKITs, etc). I can think it also can be used in TGEA, but it's better to use Shaders to use it's advantages.

To be able to use it, you need to have shapes to be modeled/textured with a "skinning support" enabled (see Help with setSkinName forum thread for more info). Also, see Re-Skinning.

Note:
If anyone knows good description on usage of .setSkinName() please add the link.

This resource could be useful to add/change skins on your objects in game. Be that players, cars or any other objects - you can change hair color, add tattoo or scars, paint cars with different colors and textures. The imagination is only limit.

Discussion

To discuss this resource you can use the TDN-based article discussion, or (preferred) post to the dedicated forum thread.

How it works

The basic functionality of this resource is applying custom-painted bitmap (by blending) to the original texture. You can specify up to four colors per one "slot" (layer to use for painting).

In script it's done by calling a function on any ShapeBase-derived object:

%obj.addSkinModifier(%slot, %skinLocation, %skinName, %maskName, %color1, %color2, %color3, %color4);

Where

%slot - the integer from 0 to 29 (can be changed in code to suit your needs). This is the slot that the skin modifier is to occupy. Assigning a second skin modifier to the same slot will override the previous modifier.

%skinLocation - is the name of the model's base skin texture. E.g. if you have base.myobject.jpg as a base texture, if will be myobject. You can have many textures on your model (base.head.jpg, base.torso.jpg, etc).

%skinName - the skin name to use. It's the image to use - red.head.jpg, armor.torso.jpg, etc.

%maskName - the mask image.

%color1, color2, color3, color4 - the colors to use for painting. The color1 will use the red channel of mask, second - green, third - blue and fourth - alpha.

You can specify all four, or any combination of those. You can just call

%obj.addSkinModifier(0, "torso", "armor");

to change the skin (similar to .setSkinName(), but used for multiple parts on object).

Improvement needed

  1. Not sure, but may be it's possible to use OpenGL to do blending and rendering or modified textures to make it faster?
  2. Another possible way of doing covered here - is by using something like RenderToTexture, but I'm not familiar with this, hopefully someone can improve it.

Engine changes description

The good think, that this resource doesn't break anything from Stock SDK. It adds a lot, but actual changes are only in a couple of functions and they are "safe to use".

Affected files:

engine/core/bitStream.*

We are adding new functions to BitStream, to be able transmit colors over the network.
Implemented:

void writeColor3I(const ColorI& sColor);
void readColor3I(ColorI *sColor);
void writeColor3F(const ColorF& sColor, F32 scale = 0.01f);
void readColor3F(ColorF *sColor, F32 scale = 0.01f);
void writeColor4I(const ColorI& sColor);
void readColor4I(ColorI *sColor);
void writeColor4F(const ColorF& sColor, F32 scale = 0.01f);
void readColor4F(ColorF *sColor, F32 scale = 0.01f);

but only *Color4I() pair is used. You can remove the rest from your engine to keep it "clean".

engine/dgl/gBitmap.*

In GBitmap class we need to add some custom functionality. This is a major point when the optimization is needed.

  void fill(const U8 in_r, const U8 in_g, const U8 in_b, const U8 in_a);
  void fill(ColorI fColor);

These two are used to "fill" the new created image with specified color.

  void fillByChannels(const GBitmap& rBlend, U8 channel, ColorI fColor);
  void fillByChannels(const GBitmap& rBlend, U8 channel1, U8 channel2,
     ColorI fColor1, ColorI fColor2);
  void fillByChannels(const GBitmap& rBlend, U8 channel1, U8 channel2, U8 channel3,
     ColorI fColor1, ColorI fColor2, ColorI fColor3);
  void fillByChannels(const GBitmap& rBlend, U8 channel1, U8 channel2, U8 channel3, U8 channel4,
     ColorI fColor1, ColorI fColor2, ColorI fColor3, ColorI fColor4);

The main "magic" is here. You call this function(s) on destination bitmap with specifying rBlend bitmap (the MASK) and channel (R,G,B or A - 0,1,2 or 3) and color(s) to use for filling the destination with.

  void alphaBlend(const GBitmap& rBlend);

This function blends the specified rBlend bitmap on current bitmap instance (applying our "modifications").

engine/game/shapeBase.*

  bool addSkinModifier(U32 slot, const char *skinLocation, const char *skinName, const char *maskName, 
     const ColorI& printColor1, const ColorI& printColor2, 
     const ColorI& printColor3, const ColorI& printColor4 );

Add new skin modifier to the object. It stores parameters in local "storage" and adds network mask, so all clients around will be aware of the change.

  bool removeSkinModifier(U32 slot);

remove skin modifier from object for specified slot.

  void clearSkinModifiers();

clears all skin modifiers on this shapeBase object.

engine/ts/tsShapeInstance.*

  TextureHandle modifySkin(const char *baseSkinName, const char *skinName, U32 matIndex);

This function is substitute to TextureHandle(), so engine will use modified textures instead of "normal" behaviour.

  void reSkin(StringHandle& newBaseHandle);

This one is changed to use modifySkin() function instead of TextureHandle, additionally, it changed to apply skinning by "parts".

  void uploadSkinModifier(U32 slot, StringHandle& skinLocation, StringHandle& skinName, StringHandle& maskName, 
     const ColorI& printColor1, const ColorI& printColor2, 
     const ColorI& printColor3, const ColorI& printColor4 );

uploadSkinModifier() is called from ShapeBase::unpackUpdate - here we adding modifiers to our TSShape instance in game.

  void removeSkinModifier(U32 slot);

clear unneeded skin modifier - same, called from unpack.

  void clearSkinModifiers();

clears all modifiers - this one is called when the TSShapeInstance got destructed (to keep everything clean, freeing memory)

  long whichMaterial(const char *skinLoc);

get the index of speficied material - used for setting modifiers.

  void modifySkins();

force applying the modifiers on skins after all skin modifiers are set.

Code

In this section is listed all the changes needed to be done with Torque to make it work.

core/bitStream.h

In a public section of BitStream class, paste the following (I've put right before writeNormalVector()):

  // colors
  void writeColor3I(const ColorI& sColor);
  void readColor3I(ColorI *sColor);
  void writeColor3F(const ColorF& sColor, F32 scale = 0.01f);
  void readColor3F(ColorF *sColor, F32 scale = 0.01f);
  void writeColor4I(const ColorI& sColor);
  void readColor4I(ColorI *sColor);
  void writeColor4F(const ColorF& sColor, F32 scale = 0.01f);
  void readColor4F(ColorF *sColor, F32 scale = 0.01f);

Actually, the only writeColor4I / readColor4I are used by this resource, you can skin the rest.

core/bitStream.cc

Again, I've put it before before writeNormalVector():

void BitStream::writeColor3I(const ColorI& sColor)
{
   writeInt(sColor.red, 8);
   writeInt(sColor.green, 8);
   writeInt(sColor.blue, 8);
}

void BitStream::readColor3I(ColorI *sColor)
{
   sColor->red = readInt(8);
   sColor->green = readInt(8);
   sColor->blue = readInt(8);
   sColor->alpha = 255;
}

void BitStream::writeColor4I(const ColorI& sColor)
{
   writeInt(sColor.red, 8);
   writeInt(sColor.green, 8);
   writeInt(sColor.blue, 8);
   writeInt(sColor.alpha, 8);
}

void BitStream::readColor4I(ColorI *sColor)
{
   sColor->red = readInt(8);
   sColor->green = readInt(8);
   sColor->blue = readInt(8);
   sColor->alpha = readInt(8);
}

void BitStream::writeColor3F(const ColorF& sColor, F32 scale)
{
   writeCompressedPoint(Point3F(sColor.red, sColor.green, sColor.blue), scale);
}

void BitStream::readColor3F(ColorF *sColor, F32 scale)
{
   Point3F pnt;
   readCompressedPoint(&pnt, scale);
   sColor->red = pnt.x;
   sColor->green = pnt.y;
   sColor->blue = pnt.z;
}

void BitStream::writeColor4F(const ColorF& sColor, F32 scale)
{
   writeCompressedPoint(Point3F(sColor.red, sColor.green, sColor.blue), scale);
   write(sColor.alpha);
}

void BitStream::readColor4F(ColorF *sColor, F32 scale)
{
   Point3F pnt;
   readCompressedPoint(&pnt, scale);
   sColor->red = pnt.x;
   sColor->green = pnt.y;
   sColor->blue = pnt.z;
   read(&sColor->alpha);
}

dgl/gBitmap.h

In public section of class GBitmap, paste the following (around line 100, after getColor/setColor() is okay):

  //skinModifiers
  void fill(const U8 in_r, const U8 in_g, const U8 in_b, const U8 in_a);
  void fill(ColorI fColor);
  void fillByChannels(const GBitmap& rBlend, U8 channel, ColorI fColor);
  void fillByChannels(const GBitmap& rBlend, U8 channel1, U8 channel2, 
     ColorI fColor1, ColorI fColor2);
  void fillByChannels(const GBitmap& rBlend, U8 channel1, U8 channel2, U8 channel3, 
     ColorI fColor1, ColorI fColor2, ColorI fColor3);
  void fillByChannels(const GBitmap& rBlend, U8 channel1, U8 channel2, U8 channel3, U8 channel4, 
     ColorI fColor1, ColorI fColor2, ColorI fColor3, ColorI fColor4);
  void alphaBlend(const GBitmap& rBlend);

dgl/gBitmap.cc

After setColor(), around line 676, paste:

//skinModifiers
//------------------------------------------------------------------------------

void GBitmap::fill(const U8 in_r, const U8 in_g, const U8 in_b, const U8 in_a)
{
   fill(ColorI(in_r, in_g, in_b, in_a));
}

void GBitmap::fill(ColorI fColor)
{
   for (U32 yy=0; (yy<height); yy++)
   {
      for (U32 xx=0; (xx<width); xx++)
      {
         setColor(xx,yy,fColor);
      }
   }
}

// one channel case
void GBitmap::fillByChannels(const GBitmap& rBlend, U8 channel, ColorI fColor)
{
   for (U32 yy=0; (yy<height) && (yy<rBlend.height); yy++)
   {
      for (U32 xx=0; (xx<width) && (xx<rBlend.width); xx++)
      {
         ColorI tempColor;
         rBlend.getColor(xx,yy,tempColor);
         U8 level;
         if (channel==0) level = tempColor.red;
         else if (channel==1) level = tempColor.green;
         else if (channel==2) level = tempColor.blue;
         else level = tempColor.alpha;

         if (level != 0 )
         {
            ColorI fill = ColorI(0,0,0,0);
            F32 t;
            t = fColor.red * level /255;
            fill.red = U8(t);
            t = fColor.green * level /255;
            fill.green = U8(t);
            t = fColor.blue * level /255;
            fill.blue = U8(t);
            t = fColor.alpha * level /255;
            fill.alpha = U8(t);
            setColor(xx, yy, fill);
         }

      }
   }
}

// two channels case
void GBitmap::fillByChannels(const GBitmap& rBlend, 
   U8 channel1, U8 channel2, ColorI fColor1, ColorI fColor2)
{
   for (U32 yy=0; (yy<height) && (yy<rBlend.height); yy++)
   {
      for (U32 xx=0; (xx<width) && (xx<rBlend.width); xx++)
      {
         ColorI tempColor;
         rBlend.getColor(xx,yy,tempColor);
         U8 level1, level2;

         if (channel1==0) level1 = tempColor.red;
         else if (channel1==1) level1 = tempColor.green;
         else if (channel1==2) level1 = tempColor.blue;
         else level1 = tempColor.alpha;

         if (channel2==0) level2 = tempColor.red;
         else if (channel2==1) level2 = tempColor.green;
         else if (channel2==2) level2 = tempColor.blue;
         else level2 = tempColor.alpha;

         ColorI fill1 = ColorI(0,0,0,0);
         ColorI fill2 = ColorI(0,0,0,0);
         F32 t;
         //if (level1 != 0 )
         t = fColor1.red * level1 /255;
         fill1.red = U8(t);
         t = fColor1.green * level1 /255;
         fill1.green = U8(t);
         t = fColor1.blue * level1 /255;
         fill1.blue = U8(t);
         t = fColor1.alpha * level1 /255;
         fill1.alpha = U8(t);
         //if (level2 != 0 )
         t = fColor2.red * level2 /255;
         fill2.red = U8(t);
         t = fColor2.green * level2 /255;
         fill2.green = U8(t);
         t = fColor2.blue * level2 /255;
         fill2.blue = U8(t);
         t = fColor2.alpha * level2 /255;
         fill2.alpha = U8(t);

         if (level1 != 0 || level2 != 0)
         {
            tempColor.red = getMin(fill1.red + fill2.red, 255);
            tempColor.green = getMin(fill1.green + fill2.green, 255);
            tempColor.blue = getMin(fill1.blue + fill2.blue, 255);
            tempColor.alpha = getMin(fill1.alpha + fill2.alpha, 255);
            setColor(xx, yy, tempColor);
         }
      }
   }
}

// three channels case
void GBitmap::fillByChannels(const GBitmap& rBlend, U8 channel1, U8 channel2, U8 channel3, 
   ColorI fColor1, ColorI fColor2, ColorI fColor3)
{
   for (U32 yy=0; (yy<height) && (yy<rBlend.height); yy++)
   {
      for (U32 xx=0; (xx<width) && (xx<rBlend.width); xx++)
      {
         ColorI tempColor;
         rBlend.getColor(xx,yy,tempColor);
         U8 level1, level2, level3;

         if (channel1==0) level1 = tempColor.red;
         else if (channel1==1) level1 = tempColor.green;
         else if (channel1==2) level1 = tempColor.blue;
         else level1 = tempColor.alpha;

         if (channel2==0) level2 = tempColor.red;
         else if (channel2==1) level2 = tempColor.green;
         else if (channel2==2) level2 = tempColor.blue;
         else level2 = tempColor.alpha;

         if (channel3==0) level3 = tempColor.red;
         else if (channel3==1) level3 = tempColor.green;
         else if (channel3==2) level3 = tempColor.blue;
         else level3 = tempColor.alpha;

         ColorI fill1 = ColorI(0,0,0,0);
         ColorI fill2 = ColorI(0,0,0,0);
         ColorI fill3 = ColorI(0,0,0,0);
         F32 t;
         //if (level1 != 0 )
         t = fColor1.red * level1 /255;
         fill1.red = U8(t);
         t = fColor1.green * level1 /255;
         fill1.green = U8(t);
         t = fColor1.blue * level1 /255;
         fill1.blue = U8(t);
         t = fColor1.alpha * level1 /255;
         fill1.alpha = U8(t);
         //if (level2 != 0 )
         t = fColor2.red * level2 /255;
         fill2.red = U8(t);
         t = fColor2.green * level2 /255;
         fill2.green = U8(t);
         t = fColor2.blue * level2 /255;
         fill2.blue = U8(t);
         t = fColor2.alpha * level2 /255;
         fill2.alpha = U8(t);
         //if (level3 != 0 )
         t = fColor3.red * level3 /255;
         fill3.red = U8(t);
         t = fColor3.green * level3 /255;
         fill3.green = U8(t);
         t = fColor3.blue * level3 /255;
         fill3.blue = U8(t);
         t = fColor3.alpha * level3 /255;
         fill3.alpha = U8(t);

         if (level1 != 0 || level2 != 0 || level3 != 0)
         {
            tempColor.red = getMin(fill1.red + fill2.red + fill3.red, 255);
            tempColor.green = getMin(fill1.green + fill2.green + fill3.green, 255);
            tempColor.blue = getMin(fill1.blue + fill2.blue + fill3.blue, 255);
            tempColor.alpha = getMin(fill1.alpha + fill2.alpha + fill3.alpha, 255);
            setColor(xx, yy, tempColor);
         }
      }
   }
}

// four channels case
void GBitmap::fillByChannels(const GBitmap& rBlend, 
   U8 channel1, U8 channel2, U8 channel3, U8 channel4, 
   ColorI fColor1, ColorI fColor2, ColorI fColor3, ColorI fColor4)
{
   for (U32 yy=0; (yy<height) && (yy<rBlend.height); yy++)
   {
      for (U32 xx=0; (xx<width) && (xx<rBlend.width); xx++)
      {
         ColorI tempColor;
         rBlend.getColor(xx,yy,tempColor);
         U8 level1, level2, level3, level4;

         if (channel1==0) level1 = tempColor.red;
         else if (channel1==1) level1 = tempColor.green;
         else if (channel1==2) level1 = tempColor.blue;
         else level1 = tempColor.alpha;

         if (channel2==0) level2 = tempColor.red;
         else if (channel2==1) level2 = tempColor.green;
         else if (channel2==2) level2 = tempColor.blue;
         else level2 = tempColor.alpha;

         if (channel3==0) level3 = tempColor.red;
         else if (channel3==1) level3 = tempColor.green;
         else if (channel3==2) level3 = tempColor.blue;
         else level3 = tempColor.alpha;

         if (channel4==0) level4 = tempColor.red;
         else if (channel4==1) level4 = tempColor.green;
         else if (channel4==2) level4 = tempColor.blue;
         else level4 = tempColor.alpha;

         ColorI fill1 = ColorI(0,0,0,0);
         ColorI fill2 = ColorI(0,0,0,0);
         ColorI fill3 = ColorI(0,0,0,0);
         ColorI fill4 = ColorI(0,0,0,0);
         F32 t;
         //if (level1 != 0 )
         t = fColor1.red * level1 /255;
         fill1.red = U8(t);
         t = fColor1.green * level1 /255;
         fill1.green = U8(t);
         t = fColor1.blue * level1 /255;
         fill1.blue = U8(t);
         t = fColor1.alpha * level1 /255;
         fill1.alpha = U8(t);
         //if (level2 != 0 )
         t = fColor2.red * level2 /255;
         fill2.red = U8(t);
         t = fColor2.green * level2 /255;
         fill2.green = U8(t);
         t = fColor2.blue * level2 /255;
         fill2.blue = U8(t);
         t = fColor2.alpha * level2 /255;
         fill2.alpha = U8(t);
         //if (level3 != 0 )
         t = fColor3.red * level3 /255;
         fill3.red = U8(t);
         t = fColor3.green * level3 /255;
         fill3.green = U8(t);
         t = fColor3.blue * level3 /255;
         fill3.blue = U8(t);
         t = fColor3.alpha * level3 /255;
         fill3.alpha = U8(t);
         //if (level4 != 0 )
         t = fColor4.red * level4 /255;
         fill4.red = U8(t);
         t = fColor4.green * level4 /255;
         fill4.green = U8(t);
         t = fColor4.blue * level4 /255;
         fill4.blue = U8(t);
         t = fColor4.alpha * level4 /255;
         fill4.alpha = U8(t);

         if (level1 != 0 || level2 != 0 || level3 != 0 || level4 != 0)
         {
            tempColor.red = getMin(fill1.red + fill2.red + fill3.red + fill4.red, 255);
            tempColor.green = getMin(fill1.green + fill2.green + fill3.green + fill4.green, 255);
            tempColor.blue = getMin(fill1.blue + fill2.blue + fill3.blue + fill4.blue, 255);
            tempColor.alpha = getMin(fill1.alpha + fill2.alpha + fill3.alpha + fill4.alpha, 255);
            setColor(xx, yy, tempColor);
         }
      }
   }
}

void GBitmap::alphaBlend(const GBitmap& rBlend)
{
   for (U32 yy=0; (yy<height) && (yy<rBlend.height); yy++)
   {
      for (U32 xx=0; (xx<width) && (xx<rBlend.width); xx++)
      {
         ColorI sColor, dColor;
         rBlend.getColor(xx,yy,sColor);

         if (sColor.alpha==255)
            setColor(xx,yy,sColor);
         else if (sColor.alpha>0)
         {
            getColor(xx,yy,dColor);
            dColor.red=((sColor.alpha*(sColor.red-dColor.red))/256)+dColor.red;
            dColor.green=((sColor.alpha*(sColor.green-dColor.green))/256)+dColor.green;
            dColor.blue=((sColor.alpha*(sColor.blue-dColor.blue))/256)+dColor.blue;
            setColor(xx,yy,dColor);
         }
      }
   }
}

game/shapeBase.h

Adding constants:

in public section of ShapeBase class definition, change the end of enum PublicConstants {
from this

       CollisionTimeoutValue = 250     ///< Timeout in ms.
    };

to this:

       CollisionTimeoutValue = 250,     ///< Timeout in ms.
       MaxSkinModifiers = 30            ///< Maximum allowed slots for skin modifiers
    };

In protected section, right after onImpact and before constructor/destructor of ShapeBase, paste (make it to look like this):

   virtual void onImpact(VectorF vec);
   /// @}

   /// @name skinModifiers
   /// @{
   struct SkinMod2
   {
      StringHandle skinLocation;
      StringHandle skinName;
      StringHandle maskName;
      ColorI printColors[4];
      SkinMod2() { skinLocation = StringHandle(); };
   };
   SkinMod2 mSkinModifiers2[MaxSkinModifiers];
   U32 mSkinModMaskBits;
   /// @}

public:
   ShapeBase();
   ~ShapeBase();

Few lines down, in enum ShapeBaseMasks { change

      SkinMask        = Parent::NextFreeMask << 7,
      SoundMaskN      = Parent::NextFreeMask << 8,       ///< Extends + MaxSoundThreads bits
      ThreadMaskN     = SoundMaskN  << MaxSoundThreads,  ///< Extends + MaxScriptThreads bits

to this:

      SkinMask        = Parent::NextFreeMask << 7,
      SkinMaskMod     = Parent::NextFreeMask << 8,
      SoundMaskN      = Parent::NextFreeMask << 9,       ///< Extends + MaxSoundThreads bits
      ThreadMaskN     = SoundMaskN  << MaxSoundThreads,  ///< Extends + MaxScriptThreads bits

(You can use existing SkinMask if you want, then it will not cause to you any problems if you already have all your bits used, then you need to pay attention in the code later, when you do pack/unpack and set masks in functions that change skin modifiers)

Again, few lines down, after the enum BaseMaskConstants { ... };, make your code to look like this:

      ImageMask      = (ImageMaskN << MaxMountedImages) - ImageMaskN
   };

   /// @}

   /// @name skinModifiers
   /// @{
   bool addSkinModifier(U32 slot, const char *skinLocation, const char *skinName, const char *maskName, 
      const ColorI& printColor1, const ColorI& printColor2, 
      const ColorI& printColor3, const ColorI& printColor4 ); ///< set/update skin modifier
   bool removeSkinModifier(U32 slot); ///< removes custom modifier
   void clearSkinModifiers(); ///< clears all modifiers on this shapeBase object
   /// @}

   static bool gRenderEnvMaps; ///< Global flag which turns on or off all environment maps

game/shapeBase.cc

ShapeBase::ShapeBase()

At the end of ShapeBase constructor function - ShapeBase::ShapeBase(), add:

  //skinModifiers
  mSkinModMaskBits = 0;

bool ShapeBase::onNewDataBlock(GameBaseData* dptr)

Further down, in bool ShapeBase::onNewDataBlock(GameBaseData* dptr), after the mSkinNameHandle handling, change (~line 842):

   if (isGhost() && mSkinNameHandle.isValidString() && mShapeInstance) {

      mShapeInstance->reSkin(mSkinNameHandle);

      mSkinHash = _StringTable::hashString(mSkinNameHandle.getString());

   }

to this:

   if (isGhost() && mSkinNameHandle.isValidString() && mShapeInstance) {

      mShapeInstance->reSkin(mSkinNameHandle);

      mSkinHash = _StringTable::hashString(mSkinNameHandle.getString());

   }

   //skinModifiers
   if (isGhost() && mShapeInstance)
   {
      mShapeInstance->modifySkins();
   }

U32 ShapeBase::packUpdate(NetConnection *con, U32 mask, BitStream *stream)

In U32 ShapeBase::packUpdate(NetConnection *con, U32 mask, BitStream *stream) change: In case you are using separate netmask from this:

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

to this:

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

Further down change from this:

   // Group some of the uncommon stuff together.
   if (stream->writeFlag(mask & (NameMask | ShieldMask | CloakMask | InvincibleMask | SkinMask))) {

to this:

   // Group some of the uncommon stuff together.
   if (stream->writeFlag(mask & (NameMask | ShieldMask | CloakMask | InvincibleMask | SkinMask | SkinMaskMod))) {

In the same function, after

      if (stream->writeFlag(mask & SkinMask)) {
         con->packStringHandleU(stream, mSkinNameHandle);
      }

add:

     if (stream->writeFlag(mask & SkinMaskMod)) { //skinModifiers
        Con::errorf("packing skins! %d", mSkinModMaskBits);
        for (U32 ss=0; ss<MaxSkinModifiers; ss++)
        {
           if (stream->writeFlag(mSkinModMaskBits & (1<<ss)))
           {
              con->packStringHandleU(stream, mSkinModifiers2[ss].skinLocation);
              con->packStringHandleU(stream, mSkinModifiers2[ss].skinName);
              con->packStringHandleU(stream, mSkinModifiers2[ss].maskName);
              // we only transmit colors if alpha is not zero, by default - no colouring
              if (stream->writeFlag(mSkinModifiers2[ss].printColors[0].alpha!=0))
                 stream->writeColor4I(mSkinModifiers2[ss].printColors[0]);
              if (stream->writeFlag(mSkinModifiers2[ss].printColors[1].alpha!=0))
                 stream->writeColor4I(mSkinModifiers2[ss].printColors[1]);
              if (stream->writeFlag(mSkinModifiers2[ss].printColors[2].alpha!=0))
                 stream->writeColor4I(mSkinModifiers2[ss].printColors[2]);
              if (stream->writeFlag(mSkinModifiers2[ss].printColors[3].alpha!=0))
                 stream->writeColor4I(mSkinModifiers2[ss].printColors[3]);
           }
        }
     }

(in case you go with using same mask, paste this codeblock right inside the SkinMask.)

void ShapeBase::unpackUpdate(NetConnection *con, BitStream *stream)

For now - we packed data, now we need to unpack it on client.

in void ShapeBase::unpackUpdate(NetConnection *con, BitStream *stream) after:

     if (stream->readFlag()) {  // SkinMask
     ...
     }

code block, paste the following:

     if (stream->readFlag()) {  // SkinMaskMod
        //skinModifiers
        bool modifyFlag=false;
        for (U32 ss=0; ss<MaxSkinModifiers; ss++)
        {
           if (stream->readFlag())
           {
              StringHandle skinLocation = con->unpackStringHandleU(stream);
              StringHandle skinName = con->unpackStringHandleU(stream);
              StringHandle maskName = con->unpackStringHandleU(stream);
              ColorI printColors0 = ColorI(0,0,0,0);
              ColorI printColors1 = ColorI(0,0,0,0);
              ColorI printColors2 = ColorI(0,0,0,0);
              ColorI printColors3 = ColorI(0,0,0,0);
              if (stream->readFlag())
                 stream->readColor4I(&printColors0);
              if (stream->readFlag())
                 stream->readColor4I(&printColors1);
              if (stream->readFlag())
                 stream->readColor4I(&printColors2);
              if (stream->readFlag())
                 stream->readColor4I(&printColors3);
              if ((skinName!=mSkinModifiers2[ss].skinName) ||
                  (maskName!=mSkinModifiers2[ss].maskName) ||
                  (skinLocation!=mSkinModifiers2[ss].skinLocation) ||
                  (printColors0!=mSkinModifiers2[ss].printColors[0]) ||
                  (printColors1!=mSkinModifiers2[ss].printColors[1]) ||
                  (printColors2!=mSkinModifiers2[ss].printColors[2]) ||
                  (printColors3!=mSkinModifiers2[ss].printColors[3]))
              {
                 mSkinModifiers2[ss].skinLocation=skinLocation;
                 mSkinModifiers2[ss].skinName=skinName;
                 mSkinModifiers2[ss].maskName=maskName;
                 mSkinModifiers2[ss].printColors[0] = printColors0;
                 mSkinModifiers2[ss].printColors[1] = printColors1;
                 mSkinModifiers2[ss].printColors[2] = printColors2;
                 mSkinModifiers2[ss].printColors[3] = printColors3;
                 modifyFlag=true;
                 if (mSkinModifiers2[ss].skinName.getString())
                 {
                    mShapeInstance->uploadSkinModifier(ss, mSkinModifiers2[ss].skinLocation, 
                       mSkinModifiers2[ss].skinName, mSkinModifiers2[ss].maskName, 
                       mSkinModifiers2[ss].printColors[0], mSkinModifiers2[ss].printColors[1], 
                       mSkinModifiers2[ss].printColors[2], mSkinModifiers2[ss].printColors[3]);
                 }
                 else
                 {
                    mShapeInstance->removeSkinModifier(ss);
                 }
              }
           }
        }
        if (modifyFlag && (mShapeInstance))
        {
           mShapeInstance->modifySkins();
        }
     }

(again, in case you go with using same mask as for setSkinName, the code above should go inside SkinMask handling)

bool ShapeBase::addSkinModifier( ... )

Now, right after the final closing braket of unpackUpdate add the rest:

//skinModifiers
bool ShapeBase::addSkinModifier(U32 slot, const char *skinLocation, 
                              const char *skinName, const char *maskName, 
                              const ColorI& printColor1, const ColorI& printColor2, 
                              const ColorI& printColor3, const ColorI& printColor4 )
{
   // we can be called only on server!
   if (isGhost())
      return false;
   if (slot<0 || slot>MaxSkinModifiers)
      return false;
   // we do not want to proceed if we don't have location, right?
   if (skinLocation[0] == '\0')
      return false;
   // tag the strings if needed
   StringHandle newSkinLocation;
   if (skinLocation[0] == StringTagPrefixByte)
      newSkinLocation = StringHandle(U32(dAtoi(skinLocation + 1)));
   else
      newSkinLocation = StringHandle(skinLocation);

   // are we setting the name (if empty - resetting to base)
   StringHandle newSkinName;
   if (skinName[0] != '\0')
   {
      if (skinLocation[0] == StringTagPrefixByte)
         newSkinName = StringHandle(U32(dAtoi(skinName + 1)));
      else
         newSkinName = StringHandle(skinName);
   }
   else
      newSkinName = StringHandle();

   // what mask to use?
   StringHandle newMaskName;
   if (maskName[0] != '\0')
   {
      if (skinLocation[0] == StringTagPrefixByte)
         newMaskName = StringHandle(U32(dAtoi(maskName + 1)));
      else
         newMaskName = StringHandle(maskName);
   }
   else
      newMaskName = StringHandle();

   // dont' forget about colors!
   ColorI newPrintColors0 = printColor1;
   ColorI newPrintColors1 = printColor2;
   ColorI newPrintColors2 = printColor3;
   ColorI newPrintColors3 = printColor4;

   //anything changed?
   if ((newSkinLocation != mSkinModifiers2[slot].skinLocation) ||
      (newSkinName != mSkinModifiers2[slot].skinName) ||
      (newMaskName != mSkinModifiers2[slot].maskName) ||
      (newPrintColors0 != mSkinModifiers2[slot].printColors[0]) ||
      (newPrintColors1 != mSkinModifiers2[slot].printColors[1]) ||
      (newPrintColors2 != mSkinModifiers2[slot].printColors[2]) ||
      (newPrintColors3 != mSkinModifiers2[slot].printColors[3]))
   {
      mSkinModifiers2[slot].skinLocation = newSkinLocation;
      mSkinModifiers2[slot].skinName = newSkinName;
      mSkinModifiers2[slot].maskName = newMaskName;
      mSkinModifiers2[slot].printColors[0] = newPrintColors0;
      mSkinModifiers2[slot].printColors[1] = newPrintColors1;
      mSkinModifiers2[slot].printColors[2] = newPrintColors2;
      mSkinModifiers2[slot].printColors[3] = newPrintColors3;
      // flags
      mSkinModMaskBits|=(1<<slot);
   }
   setMaskBits(SkinMaskMod);
   return true;
}

bool ShapeBase::removeSkinModifier(U32 slot)

bool ShapeBase::removeSkinModifier(U32 slot)
{
   // we can be called only on server!
   if (isGhost())
      return false;
   if (slot<0 || slot>MaxSkinModifiers)
      return false;

   if ((mSkinModifiers2[slot].skinLocation.getString() != NULL) ||
      (mSkinModifiers2[slot].skinName.getString() != NULL) ||
      (mSkinModifiers2[slot].maskName.getString() != NULL))
   {
      mSkinModifiers2[slot].skinLocation = StringHandle();
      mSkinModifiers2[slot].skinName = StringHandle();
      mSkinModifiers2[slot].maskName = StringHandle();
      mSkinModifiers2[slot].printColors[0] = ColorI(0,0,0,0);
      mSkinModifiers2[slot].printColors[1] = ColorI(0,0,0,0);
      mSkinModifiers2[slot].printColors[2] = ColorI(0,0,0,0);
      mSkinModifiers2[slot].printColors[3] = ColorI(0,0,0,0);
      // set flags
      mSkinModMaskBits|=(1<<slot);
   }
   setMaskBits(ShapeBase::SkinMask);
   return true;
}

void ShapeBase::clearSkinModifiers()

void ShapeBase::clearSkinModifiers()
{
   // we can be called only on server!
   if (isGhost())
      return;
   for (U32 ss=0; ss<MaxSkinModifiers; ss++)
      removeSkinModifier(ss);
   setMaskBits(SkinMaskMod);
}

Console Methods

// Console methods
ConsoleMethod(ShapeBase, addSkinModifier, bool, 6, 10, "(U32 slot, string skinLocation, string skinName, string maskName, ColorI color1 [,color2,3,4='']) Add a skin modifier.")
{
   ColorI col1 = ColorI(0,0,0,0);
   ColorI col2 = ColorI(0,0,0,0);
   ColorI col3 = ColorI(0,0,0,0);
   ColorI col4 = ColorI(0,0,0,0);
   U32 r, g, b, a;
   char buf[64];

   if (argc>6)
   {
      dStrcpy( buf, argv[6] );
      char* temp = dStrtok( buf, " \0" );
      r = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      g = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      b = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      a = temp ? dAtoi( temp ) : 0;
      col1.set( r, g, b, a );
   }
   //dSscanf(argv[6], "%d %d %d %d", &col1.red, &col1.green, &col1.blue, &col1.alpha);
   if (argc>7)
   {
      dStrcpy( buf, argv[7] );
      char* temp = dStrtok( buf, " \0" );
      r = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      g = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      b = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      a = temp ? dAtoi( temp ) : 0;
      col2.set( r, g, b, a );
   }
   //dSscanf(argv[7], "%d %d %d %d", &col2.red, &col2.green, &col2.blue, &col2.alpha);
   if (argc>8)
   {
      dStrcpy( buf, argv[8] );
      char* temp = dStrtok( buf, " \0" );
      r = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      g = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      b = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      a = temp ? dAtoi( temp ) : 0;
      col3.set( r, g, b, a );
   }
   //dSscanf(argv[8], "%d %d %d %d", &col3.red, &col3.green, &col3.blue, &col3.alpha);
   if (argc>9)
   {
      dStrcpy( buf, argv[9] );
      char* temp = dStrtok( buf, " \0" );
      r = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      g = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      b = temp ? dAtoi( temp ) : 0;
      temp = dStrtok( NULL, " \0" );
      a = temp ? dAtoi( temp ) : 0;
      col4.set( r, g, b, a );
   }
   //dSscanf(argv[9], "%d %d %d %d", &col4.red, &col4.green, &col4.blue, &col4.alpha);
   // I'm using this way of getting color values, as if I use dSscanf()
   // then after object->setCustomSkin() call I get "stack around col1 corrupted" error
   return object->addSkinModifier(dAtoi(argv[2]),argv[3],argv[4],argv[5],col1,col2,col3,col4);
}
ConsoleMethod(ShapeBase, removeSkinModifier, bool, 3, 3, "(U32 slot) Remove a specific skin modifier.")
{
   return object->removeSkinModifier(dAtoi(argv[2]));
}
ConsoleMethod(ShapeBase, clearSkinModifiers, void, 2, 2, "() Clear all skin modifiers.")
{
   object->clearSkinModifiers();
}

ts/tsShapeInstance.h

In public section of TSShapeInstance definition, right before // Misc. comment (line around 236), add:

   /// @name skinModifiers
   /// keep this to be the same with the value that in shapeBase.h file
   /// @{
   enum Constants {
      MaxSkinModifiers = 30
   };
   /// @}

Further down, in protected section, right before public: starts, add:

   /// @name skinModifiers
   /// This is different from shapeBase's one, as we have to perform different tasks
   /// @{
   struct SkinMod2
   {
      StringHandle skinName; ///< the handle of skin to use
      const char * skinPath; ///< the path to the skin
      long matIndex; ///< assiciated material index
      char modTag[32]; ///< unique tag (+[skinHash+maskHash+colors]\0
      GBitmap *modBmp; ///< the modifier bitmap that we apply to the skin

      SkinMod2() {modBmp=NULL; matIndex=-1;};
      ~SkinMod2() {if (modBmp) delete modBmp;};
   };

   SkinMod2 mSkinModifiers2[MaxSkinModifiers];
   TextureHandle modifySkin(const char *baseSkinName, const char *skinName, U32 matIndex);
   ///@}

Few lines down, after void reSkin(StringHandle& newBaseHandle); paste:

   /// @name skinModifiers
   /// @{
   void uploadSkinModifier(U32 slot, StringHandle& skinLocation, StringHandle& skinName, StringHandle& maskName, 
      const ColorI& printColor1, const ColorI& printColor2, 
      const ColorI& printColor3, const ColorI& printColor4 ); ///< set/update skin modifier
   void removeSkinModifier(U32 slot); ///< clear unneeded skin modifier
   void clearSkinModifiers(); ///< clear everything
   long whichMaterial(const char *skinLoc);  ///< get the index of speficied material
   void modifySkins(); ///< force applying the modifiers on skins
   /// @}

ts/tsShapeInstance.cc

Here we add the functions declared in the header, plus we have to modify some of the other member functions, primarily reskin().

TSShapeInstance::~TSShapeInstance()

In TSShapeInstance::~TSShapeInstance(), we have to call clearSkinModifiers(). I placed it before the call to setMaterialList() just to be safe. So, (~line 110) add:

  //skinModidierfs
  clearSkinModifiers();

TSShapeInstance::reSkin(StringHandle& newBaseHandle)

In TSShapeInstance::reSkin(StringHandle& newBaseHandle), we need to check for new skin and change how the skin textures are acquired for the material list. If a skin modifier has been applied to the base skin, the new skin should get the same modifiers.

At the beginning of reSkin change:

#define NAME_BUFFER_LENGTH 256
   static char pathName[NAME_BUFFER_LENGTH];
   const char* defaultBaseName = "base";  
   const char* newBaseName;

to

#define NAME_BUFFER_LENGTH 256
   static char pathName[NAME_BUFFER_LENGTH];
   const char* defaultBaseName = "base";  
   const char* newBaseName;
   const char* saveBaseName;	//skinModifiers

/* Hey Whistle Whistle Guys. I think this already present. shouldn't newBaseName actually be saveBaseName in this following chunk?? */

   //skinModifiers
   if ( newBaseHandle.isValidString())
   {
      newBaseName = newBaseHandle.getString();
      // fail if no skin specified
      if (newBaseName == NULL)
         return;
   }
   else
      newBaseName = defaultBaseName;

Further down, after

     // Bail if no name.
     if (pName == NULL) {
        continue;
     }

add:

     //skinModifiers
     bool updated = false;
     for (S32 k=0; k < MaxSkinModifiers; k++)
     {
        if (j == mSkinModifiers2[k].matIndex)
        {
           if (!mSkinModifiers2[k].skinName.isNull())
           {
              newBaseName = mSkinModifiers2[k].skinName.getString();
              if ((dStrlen(newBaseName)==0) || (dStrlen(newBaseName)>32)) {
                 newBaseName = defaultBaseName;
              }
              updated = true;
           }
           break;
        }
     }
     if (!updated)
        newBaseName = saveBaseName;

Then, change (~line 466):

        pMatList->mMaterials[j] = TextureHandle(pathName, MeshTexture, false);

to this:

        //skinModifiers
        //pMatList->mMaterials[j] = TextureHandle(pathName, MeshTexture, false);
        pMatList->mMaterials[j] = modifySkin(pName,pathName, j);

And change (~line 480):

        makeSkinPath(pathName, NAME_BUFFER_LENGTH, resourcePath, pName, NULL, NULL);
        pMatList->mMaterials[j] = TextureHandle(pathName, MeshTexture, false);

to this:

        makeSkinPath(pathName, NAME_BUFFER_LENGTH, resourcePath, pName, NULL, NULL);
        //skinModifiers
        //pMatList->mMaterials[j] = TextureHandle(pathName, MeshTexture, false);
        pMatList->mMaterials[j] = modifySkin(pName,pathName, j);

void TSShapeInstance::uploadSkinModifier(....)

Right after reSkin function, add the rest:

void TSShapeInstance::uploadSkinModifier(U32 slot, StringHandle& skinLocation, 
      StringHandle& skinName, StringHandle& maskName, 
      const ColorI& printColor1, const ColorI& printColor2, 
      const ColorI& printColor3, const ColorI& printColor4 )
{
   if (skinLocation.isValidString() && skinName.isValidString())
   {
      // We can't go if bad location or skin are given
      if ((skinLocation.getString()==NULL) ||
          (skinName.getString()==NULL))
      {
         return;
      }
   }
   else
      return;
   // fail if we are trying to set non-existing material
   long index = whichMaterial( skinLocation.getString() );
   if (index < 0)
   {
      Con::errorf("Non existant material ( %s )!!!", skinLocation.getString(), index);
      return;
   }
   // Pardon me for this mess. It needs optimizing!
   // Here we assign colors per channels. As it's possible to call like:
   // .(skinLoc, skin, mask, "", "", ColorI, ColorI)
   // to use only blue and alpha channels, we need to handle it
   // we go through all four colors and checking if it's set.
   U8 cnt = 0;
   U8 chn1, chn2, chn3, chn4;
   ColorI clr1, clr2, clr3, clr4;
   if (printColor1.alpha!=0)
   {
      chn1 = 0;
      clr1 = printColor1;
      cnt++;
   }
   if (printColor2.alpha!=0)
   {
      if (cnt==0)
      {
         chn1 = 1;
         clr1 = printColor2;
      }
      else
      {
         chn2 = 1;
         clr2 = printColor2;
      }
      cnt++;
   }
   if (printColor3.alpha!=0)
   {
      if (cnt==0)
      {
         chn1 = 2;
         clr1 = printColor3;
      }
      else if (cnt==1)
      {
         chn2 = 2;
         clr2 = printColor3;
      }
      else
      {
         chn3 = 2;
         clr3 = printColor3;
      }
      cnt++;
   }
   if (printColor4.alpha!=0)
   {
      if (cnt==0)
      {
         chn1 = 3;
         clr1 = printColor4;
      }
      else if (cnt==1)
      {
         chn2 = 3;
         clr2 = printColor4;
      }
      else if (cnt==2)
      {
         chn3 = 3;
         clr3 = printColor4;
      }
      else
      {
         chn4 = 3;
         clr4 = printColor4;
      }
      cnt++;
   }
   // construct modifier bitmap
   // Make our own copy of the materials list from the resource
   // if necessary.
   if (ownMaterialList() == false) {
      cloneMaterialList();
   }

#define NAME_BUFFER_LENGTH 256
   static char skinPath[NAME_BUFFER_LENGTH];
   static char maskPath[NAME_BUFFER_LENGTH];
   const char* defaultBaseName = "base";

   const char* resourcePath = hShape.getFilePath();
   // get the material
   TSMaterialList* pMatList = getMaterialList();
   const char* pName = pMatList->mMaterialNames[index];
   // Bail if no name.
   if (pName == NULL) {
      return;
   }
   
   // This one is a bit hacky. I'm not good with char**-related stuff, so all of this
   // is need only to get rid off of the file extention if present.
   // This is needed as you can have different extensions on base image and skin (e.g. base.smth.jpg & mask.smth.png)
   static char newM[NAME_BUFFER_LENGTH];
   const char* extArray[4] = { "jpg", "png", "gif", "bmp" };
   char * ext;
   Con::errorf("preparing: %s %s", pName, maskName.getString());

   for( U32 i = 0; i < 4; i++ )
   {
      ext = dStrstr(pName, extArray[i]);
      if (ext != NULL)
      {
         U32 len = dStrlen(pName);
         dStrcpy(newM, pName);
         newM[len-4] = '\0';
         break;
      }
   }
   if (ext == NULL)
   {
      U32 len = dStrlen(pName);
      dStrcpy(newM, pName);
      newM[len] = '\0';
   }

   // 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.
   makeSkinPath(skinPath, NAME_BUFFER_LENGTH, resourcePath,
                        pName, defaultBaseName, skinName.getString());
   // If no mask specified we are only changing the skin and getting outta here without anything else
   if (!maskName.isValidString())
   {
      removeSkinModifier(slot);
      mSkinModifiers2[slot].matIndex = index;
      mSkinModifiers2[slot].skinName = skinName;
      mSkinModifiers2[slot].skinPath = skinPath;
      dSprintf(mSkinModifiers2[slot].modTag,32,"+[%d+0]",_StringTable::hashString(skinName.getString()));
      return;
   }
   makeSkinPath(maskPath, NAME_BUFFER_LENGTH, resourcePath,
                        newM, defaultBaseName, maskName.getString());
   GBitmap *maskBmp=TextureManager::loadBitmapInstance(maskPath);
   // fail if no mask found
   if (!maskBmp)
   {
      Con::errorf("Can't open mask! %s %s", newM, maskName.getString());
      return;
   }
   // This one if our bitmap where most "magic" happens
   GBitmap *printBmp=new GBitmap(maskBmp->getWidth(),maskBmp->getHeight(),false,GBitmap::RGBA);
   // Make it "black" transparent first
   printBmp->fill(ColorI(0,0,0,0));
   // Fill & Blend if needed
   if (cnt==1)
      printBmp->fillByChannels(*maskBmp, chn1, clr1);
   else if (cnt==2)
      printBmp->fillByChannels(*maskBmp, chn1, chn2, clr1, clr2);
   else if (cnt==3)
      printBmp->fillByChannels(*maskBmp, chn1, chn2, chn3, clr1, clr2, clr3);
   else if (cnt==4)
      printBmp->fillByChannels(*maskBmp, chn1, chn2, chn3, chn4, clr1, clr2, clr3, clr4);
   else
      Con::errorf("No colors assigned! Will use transparent picture on filling! (why calling with mask and no colors?)");
   // creating modifier
   removeSkinModifier(slot);
   mSkinModifiers2[slot].matIndex = index;
   mSkinModifiers2[slot].skinName = skinName;
   mSkinModifiers2[slot].skinPath = skinPath;
   mSkinModifiers2[slot].modBmp=printBmp;
   dSprintf(mSkinModifiers2[slot].modTag,32,"+[%d+%d+%d+%d+%d+%d]",_StringTable::hashString(skinName.getString()),
                                                          _StringTable::hashString(maskName.getString()), 
                                                          clr1.alpha, clr2.alpha, clr3.alpha, clr4.alpha);
   if (maskBmp)
      delete maskBmp;
}

void TSShapeInstance::removeSkinModifier(U32 slot)

void TSShapeInstance::removeSkinModifier(U32 slot)
{
   mSkinModifiers2[slot].skinName=StringHandle();
   mSkinModifiers2[slot].matIndex=-1;
   mSkinModifiers2[slot].modTag[0]='\0';
   if (mSkinModifiers2[slot].modBmp)
   {
      GBitmap *tmpBmp=mSkinModifiers2[slot].modBmp;
      mSkinModifiers2[slot].modBmp=NULL;
      delete tmpBmp;
   }
}

void TSShapeInstance::clearSkinModifiers()


void TSShapeInstance::clearSkinModifiers()
{
   for (U32 ss=0; ss<MaxSkinModifiers; ss++)
   {
      removeSkinModifier(ss);
   }
}

TextureHandle TSShapeInstance::modifySkin( ... )

TextureHandle TSShapeInstance::modifySkin(const char *baseSkinName, const char *skinName, U32 matIndex)
{
   char modTextureName[512];
   GBitmap *modTextureBmp=NULL;

   // check all skin modifier slots
   for (U32 ss=0; ss<MaxSkinModifiers; ss++)
   {
      if (mSkinModifiers2[ss].matIndex == -1)
         continue;
      if (mSkinModifiers2[ss].matIndex == matIndex && (mSkinModifiers2[ss].modBmp))
      {
         if (!modTextureBmp)
         {
            modTextureBmp=TextureManager::loadBitmapInstance(skinName);
            if (!modTextureBmp)
               break;
            Con::errorf("making skin: %s", skinName);

            dStrcpy(modTextureName,skinName);
         }
         modTextureBmp->alphaBlend(*mSkinModifiers2[ss].modBmp);
         dStrcat(modTextureName,mSkinModifiers2[ss].modTag);
      }
   }
   if (!modTextureBmp)
      return TextureHandle(skinName,MeshTexture,false);
   else
   {
      Con::errorf("returning Texture handle - %s", modTextureName);
      return TextureHandle(modTextureName,modTextureBmp,MeshTexture,false);
   }
}

void TSShapeInstance::modifySkins()

void TSShapeInstance::modifySkins()
{
   char skinName[512];

   // Make our own copy of the materials list from the resource
   // if necessary.
   if (!ownMaterialList()) {
      cloneMaterialList();
   }

   TSMaterialList* pMatList = getMaterialList();
   for (U32 mm=0; mm<pMatList->mMaterialNames.size(); mm++)
   {
      // Get the name of this material.
      const char* baseSkinName=pMatList->mMaterialNames[mm];
      // a different skin could be in use, so use that name instead
      const char* materialName=pMatList->mMaterials[mm].getName();
      if(materialName == NULL)
      {
          Con::errorf("DEBUG: ModifySkins: No existing skin in use!");
          continue;
      }
      dStrcpy(skinName,materialName);
      char *modTag=dStrstr((const char*)skinName,"+[");
      if (modTag)
      {
         skinName[modTag-skinName]='\0';
      }
      // modify skin
      pMatList->mMaterials[mm]=modifySkin(baseSkinName,skinName, mm);
   }
}

long TSShapeInstance::whichMaterial(const char *skinLoc)

long TSShapeInstance::whichMaterial(const char *skinLoc)
{
   //Con::errorf("TSShapeInstance::whichMaterial(%s)",skinLoc);
   char *period, *specificName;

   // Make our own copy of the materials list from the resource if necessary.
   setMaterialList(mShape->materialList);
   if (ownMaterialList() == false)
      cloneMaterialList();

   // This one is also a bit hacky. Please see if it's possible to improve

   //Con::errorf("whichMaterial(%s)",skinLoc);
   // 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];
      //Con::warnf("index=%d, name=%s",j,pName);
      // Bail if no name.
      if (pName == NULL)
      continue;

      // find the second part of the material name
      period = dStrchr((char *) pName, '.');
      if (period == NULL)
         continue;
      char *begin = dStrchr((char *) pName, '.') + 1;
      char *end = begin;
      char *t;
      while((t = dStrchr(end, '.')))
         end = t + 1;
      const char* extArray[4] = { "jpg", "png", "gif", "bmp" };
      for( U32 i = 0; i < 4; i++ )
      {
         if (dStricmp(end, extArray[i]) == 0)
         {
            char buf[128];
            S32 i;
            for(i = 0; begin < end - 1; ++i, ++begin)
            buf[i] = *begin;
            if (buf == NULL)
               continue;
            buf[i] = '\0';
            begin = buf;
            if (dStricmp(buf, skinLoc) == 0)
               return(j);
            break;
         }
      }
      if (dStricmp(end, skinLoc)== 0)
         return(j);
   }
   return(-1);
}

Art

base.skinname.jpg

You need to use "base.texname" naming on your ShapeBase objects to be able to use this resource.

Ask your artist(s) to change the texture names. E.g. if you have myplayer.jpg or someobject.png - rename textures to base.myplayer.jpg (base.someobject.png) - so the Torque will be aware, that the skinning can be used on this objects.

Then in a stock Torque you can use %obj.setSkinName("red"); and then it will search for red.someobject.png.

In case you don't have sources of objects you still can "hack" it - if the texture name (without extension) is more than 5 characters you can use any HEX editor to open the file in HEX mode, browse down to the bottom and you will see there something like:

................
..someobjectC...
................

The texture name can be broken into two lines. change the someobject to base.smobj leaving the trailing "C" char at the place (sometimes there can be G character - depends on exporter used). Then you rename your someobject.jpg file to base.smobj.jpg and all should work. More info/discussion can be found here.

The main point here, is to make base. to be in name of texture. If your texture name is less or equal 5 chars - it's not possible to use this "rename hack". Assuming you have boxer.png - you have space only to make it "base." and no space left. The player is fine - base.p is still good name for a texture.

Some exporters (IIRC - it's Maya's) are saving extension of texture into DTS file. So, if you see inside DTS (with HEX editor) boxer.png - you can rename it taking off the extension: From:

................
..boxer.pngG....
................

You can make:

................
..base.boxrG....
................

And rename your texture file from "boxer.png" to "base.boxr.png" - and you are ready.

mask

In this example I've changed player.dts to use base.p texture, so I can set a mask.p as mask
Enlarge
In this example I've changed player.dts to use base.p texture, so I can set a mask.p as mask

The mask bitmap should be either the RGB or RGBA. If you go with JPEG (or BMP) you can use up to three colors per image. If you use PNG, you have additional channel (alpha) to use.

RGBA Channels

The artists, when creating a bitmap need to make special images to use with this resource. The color level in "mask" image (from 0 upto 255) is used as opacity level for blending with specified color.

As you can see - red channel covers all "skin" while green adds some "marks" on it (in combination it looks like yellow).

Using colors

Let's make it a bit "death-looking" and add some "red stuff" in there.

%obj.addSkinModifier(0, "p", "base", "mask", "255 255 255 255", "255 0 0 255");

white skin + red marks

And now, skin - red, and some white paintings on it:

%obj.addSkinModifier(0, "p", "base", "mask", "0 0 255 255", "255 255 255 255");

red skin + light marks


The only "problem" now - the currently implemented "blending" procedure in GBitmap::fillByChannels() is only adding colors, so if you paint your skin with white, you can't add black "scars" on it.

If someone can make a good blending (that work for many solutions) - please share.

Or, it's possible to extend this resource, you can even specify what kind of blending to use (add/multiply/etc).