Game Development Community

T3D 1.1 Beta 3 - Reskin

by Daniel Eden · in Torque 3D Professional · 09/23/2010 (5:04 am) · 6 replies

It seems that with collada-based shapes no longer using the diffuse texture as an identifier, the reskin functionality in tsshapeinstance is broken. It works fine with older models that still use the texture ("base.Whatever.png"), but newer ones... Not so much.

Given my game uses a lot of this functionality, I put together a fix. It's a bit of a kludge and I haven't thoroughly tested it, but from the few run-throughs I've given it, it seems to work fine with my own code. It should also restore the broken functionality in shapebase and the like (precluding the need for that horrid changematerial console method).

Essentially this system of material switching works in the same manner as the old one. If you prepend "Base_" to your material name (eg. "Base_Face_Material" instead of just "Face_Material"), it'll be switched out and replaced with whatever you specify when you use reSkin (thereby allowing you to set other materials such as "Angry_Face_Material" using "Angry", "Happy_Face_Material" using "Happy", or whatever else strikes your fancy).

The changes are pretty minimal and should be easy enough to implement (though someone might want to look into ensuring they're a little less kludgey at some point).

In ts/tsShapeInstance.cpp at line 271, replace the entire reSkin method with what's below:

void TSShapeInstance::reSkin( String newBaseName, String oldBaseName )
{
	// If the new base name is empty, set the default value of 'base'...
	if (newBaseName.isEmpty())
	{
		newBaseName = "base";
	}

	// If the old base name is empty, set the default value of 'base'...
	if (oldBaseName.isEmpty())
	{
		oldBaseName = "base";
	}

	// If the instance doesn't already own a material list...
	if (ownMaterialList() == false)
	{
		// Clone a material list for it.
		cloneMaterialList();
	}

	// Determine the resource path.
	const String sPath = mShapeResource.getPath().getPath();

	// Get a pointer to the material list.
	TSMaterialList* pMatList = getMaterialList();

	// If there's no material list (such as when you call reskin on the server), exit out...
	if (!pMatList)
	{
		return;
	}

	// Get the material names list.
	const Vector<String>& sMaterialNames = pMatList->getMaterialNameList();

	// Determine the old base name length.
	const U32 iBaseLength = oldBaseName.length();

	// Set up some strings to check for.
	String sOldBaseOldMaterial = oldBaseName + String(".");
	String sOldBaseNewMaterial = oldBaseName + String("_");

	// Iterate through each of the material names...
	for (U32 i = 0; i < sMaterialNames.size(); i++)
	{
		// Get the name in question...
		const String& sName = sMaterialNames[i];

		// If the name is empty, skip the rest...
		if (sName.isEmpty())
		{
			continue;
		}

		// Set up a flag for determining whether something's replaced.
		bool bReplaced = false;

		// If the old base name is definitely in this string...
		if (sName.compare(sOldBaseOldMaterial, iBaseLength + 1, String::NoCase) == 0)
		{
			// Store the name temporarily.
			String sTemp = sName;

			// Determine the new path value.
			String sNew = (sPath.isNotEmpty() ? (sPath + "/") : "") + sTemp.replace(0, iBaseLength, newBaseName);

			// Attempt to set the new material.
			bReplaced = pMatList->setMaterial(i, (const Torque::Path)sNew);
		}
		else if (sName.compare(sOldBaseNewMaterial, iBaseLength + 1, String::NoCase) == 0)
		{
			// Store the modified name.
			String sTemp = sName;
			sTemp.replace(0, iBaseLength, newBaseName);

			// Attempt to set the new material.
			bReplaced = pMatList->setMaterial(i, sTemp);
		}

		// If nothing was replaced...
		if (!bReplaced)
		{
			// Restore the old material.
                        pMatList->setMaterial(i, (const Torque::Path)((sPath.isNotEmpty() ? (sPath + "/") : "") + sName));
		}
   }

   // Initialize the material instances.
   initMaterialList();
}

... then, in ts/tsMaterialList.h at line 74, just after the declaration of setMaterial, add:

// Set the material to a new-style non-diffuse texture-identified material.
   const bool setMaterial(const U32& _index, const String& _name);

... and, finally in ts/tsMaterialList.cpp at the end of the file, add:

// Set the material to a new-style non-diffuse texture-identified material.
const bool TSMaterialList::setMaterial(const U32& _index, const String& _name)
{
	// If the index is out of range, error out...
	if (_index > mMaterials.size())
	{
		return false;
	}

	// If there's no name, error out...
	if (_name == String())
	{
		return false;
	}
   
	// If the name is already set, early out...
	if (mMaterials[_index].isValid() && _name.equal(mMaterialNames[_index], String::NoCase))
	{
		return true;
	}

	// Null the material.
	mMaterials[_index] = NULL;

	// Set the new material name.
	mMaterialNames[_index] = _name;

	// If there's a previous material instance...
	if (mMatInstList[_index])
	{
		// Safely delete it so we can remap the material.
		SAFE_DELETE(mMatInstList[_index]);
	}

	// Map the material.
	mapMaterial(_index);

	// Everything went okay, so return true.
	return true;
}

Edit: Added a check for a valid material list since calling it on the server could cause a crash. Shouldn't be done by default though...

#1
10/08/2010 (1:01 am)
I was talking on IRC earlier and someone asked about getting the skinning working with TSStatics. I figured it wasn't that difficult, so... Here it is.

It works as you'd expect, change the "skin" field in the editors and the materials change (provided you've set your mesh up to use the appropriate Base_ material format as outlined above), call setSkin, the materials change, etc.

I haven't done a huge amount of testing. It doesn't crash and skinning works properly but if there's any code within the engine that relies on specific read/write orders from the pack/unpack things may go awry. A casual flip through the code didn't find anything obvious so it's probably all good. On the plus side, the re-factoring of the pack and unpack allowed me to reduce the networking overhead per object just a smidge -- even with the skin send.

Anyway, here we go:

In T3D/tsStatic on line 20 add:
#ifndef _NETSTRINGTABLE_H_
#include "sim/netStringTable.h"
#endif

In the same file around line 70 (now 73) change the mask bits to:
enum MaskBits 
   {
      AdvancedStaticOptionsMask = Parent::NextFreeMask,
      UpdateCollisionMask = Parent::NextFreeMask << 1,
	  SkinMask = Parent::NextFreeMask << 2,
      NextFreeMask = Parent::NextFreeMask << 3
   };

... and on line 79(now 83) change the mesh types to:
/// The different types of mesh data types
   enum MeshType
   {
      None = 0,            ///< No mesh
      Bounds = 1,          ///< Bounding box of the shape
      CollisionMesh = 2,   ///< Specifically designated collision meshes
      VisibleMesh = 3,     ///< Rendered mesh polygons
      MAX_MESHTYPE
   };

At line 186 (now 191 or so), add the following:
//-Render------------------------------------------------------------------
protected:
    NetStringHandle m_Skin;
    StringTableEntry m_Skin_Name;
    U32 m_Skin_Hash;

public:
    // Returns the name of the current skin.
    const char* getSkin();

    // Sets the skin by name.
    void setSkin(const char* _name, const bool& _force = false);

    // Sets the skin by handle.
    void setSkin(NetStringHandle _handle, const bool& _force = false);

... then just before the final #endif, add:

// Returns the name of the current skin.
inline const char* TSStatic::getSkin()
{
    return m_Skin.getString();
}
#2
10/08/2010 (1:02 am)
Okay, so done with the header, now onto the source file... T3D/tsStatic.cpp, at line 67, adjust the constructor to:
TSStatic::TSStatic() :
    m_Skin(),
    m_Skin_Hash(0),
    m_Skin_Name(0)

In ::initPersistFields at what's now line 138, add:
addField("skin",  TypeString, Offset(m_Skin_Name, TSStatic), "Specifies the skin identifier for the materials used on the tsstatic.");

In ::inspectPostApply at around line 169, change the if codeblock to:
if(isServerObject()) 
   {
      setMaskBits(AdvancedStaticOptionsMask);
      prepCollision();

      // Set the skin on the client.
      setSkin(m_Skin_Name);
   }

Then, in ::_createShape, line 268, add the following after the shape instance is created:
// If there's a shape instance on the client...
    if (mShapeInstance && isClientObject())
    {
        // Forcibly set the skin on the client.
        setSkin(m_Skin, true);
    }

Then, replace the ::packUpdate method with:
U32 TSStatic::packUpdate(NetConnection *con, U32 mask, BitStream *stream)
{
	// Call the parent method.
	U32 retMask = Parent::packUpdate(con, mask, stream);

	// Get the scale.
	const Point3F& aScale = getScale();

	// If the initial update mask, advanced static optionss mask, or scale mask are set and this isn't a unit scale...
	if (stream->writeFlag((mask & (AdvancedStaticOptionsMask | InitialUpdateMask | ScaleMask)) && (aScale.x != 1.f || aScale.y != 1.f || aScale.z != 1.f)))
	{
		// Write the scale to the stream.
		mathWrite(*stream, getScale());
	}

	// If the initial update mask or advanced static options mask are set...
	if (stream->writeFlag(mask & (AdvancedStaticOptionsMask | InitialUpdateMask)))
	{
		// Write the various data values.
		mathWrite(*stream, getTransform());
		stream->writeString(mShapeName);
		stream->write((U32)mDecalType);
		stream->writeFlag(mAllowPlayerStep);
		stream->writeFlag(mMeshCulling);   
		stream->writeFlag(mUseOriginSort);   
		stream->write(mRenderNormalScalar);
		stream->write(mForceDetail);
		stream->writeFlag(mPlayAmbient);
	}

	// If the collision update mask or initial update mask are set...
	if (stream->writeFlag(mask & (InitialUpdateMask | UpdateCollisionMask)))
	{
		// Write the collision type.
		stream->writeRangedU32((U32)mCollisionType, 0, MAX_MESHTYPE);
	}
   
	// If there's a light plugin...
	if (mLightPlugin)
	{
		// Call the pack for the plugin.
		retMask |= mLightPlugin->packUpdate(this, AdvancedStaticOptionsMask, con, mask, stream);
	}

	// If the skin mask or advanced options mask are set...
	if (stream->writeFlag(mask & (AdvancedStaticOptionsMask | InitialUpdateMask | SkinMask)))
	{
		// Pack the skin handle.
		con->packNetStringHandleU(stream, m_Skin);
	}

	// Return whatever mask is left.
	return retMask;
}
#3
10/08/2010 (1:05 am)
... and the ::unpackUpdate method with:
void TSStatic::unpackUpdate(NetConnection *con, BitStream *stream)
{
	// Call the parent method.
	Parent::unpackUpdate(con, stream);

	// AdvancedStaticOptionsMask | InitialUpdateMask | ScaleMask and non-unit scale.
	if (stream->readFlag())
	{
		// Read in and set the scale.
		Point3F vScale;
		mathRead(*stream, &vScale);
		setScale(vScale);
	}
	else
	{
		// Otherwise, set a unit scale.
		setScale(Point3F(1.f, 1.f, 1.f));
	}

	// AdvancedStaticOptionsMask | InitialUpdateMask
	if (stream->readFlag())
	{
		// Write the various data values.
		MatrixF xTransform;
		mathRead(*stream, &xTransform);
		setTransform(xTransform);

		mShapeName = stream->readSTString();
		stream->read((U32*)&mDecalType);
		mAllowPlayerStep = stream->readFlag();
		mMeshCulling = stream->readFlag();   
		mUseOriginSort = stream->readFlag();
		stream->read(&mRenderNormalScalar);
		stream->read(&mForceDetail);
		mPlayAmbient = stream->readFlag();
	}

	// InitialUpdateMask | UpdateCollisionMask
	if (stream->readFlag())
	{
		// Read in the collision type.
		MeshType iCollisionType = CollisionMesh;
		iCollisionType = (MeshType)stream->readRangedU32(0, MAX_MESHTYPE);

		// If the new collision type differs from the old one...
		if (iCollisionType != mCollisionType)
		{
			// Set the new collision type...
			mCollisionType = iCollisionType;

			// If the object has been added and has a shape...
			if (isProperlyAdded() && mShapeInstance)
			{
				// Prepare its collision.
				prepCollision();
			}
		}
	}

	// If there's a light plugin...
	if (mLightPlugin)
	{
		// Call the unpack method for the plugin.
		mLightPlugin->unpackUpdate(this, con, stream);
	}

	// AdvancedStaticOptionsMask | InitialUpdateMask | SkinMask
	if (stream->readFlag())
	{
		// Unpack the string handle and set the skin.
		setSkin(con->unpackNetStringHandleU(stream), true);
	}

	// If the object is now added properly...
	if (isProperlyAdded())
	{
		// Determine whether it should tick.
		_updateShouldTick();
	}
}

... and finally, at the end of the file, add the following:
// Sets the skin by name.
void TSStatic::setSkin(const char* _name, const bool& _force)
{
    // If this is the client, early out...
    if (isClientObject())
    {
        return;
    }

    // Set the skin,
    setSkin((_name && _name[0] && _name[0] != '?') ? (_name[0] == StringTagPrefixByte ? NetStringHandle(U32(dAtoi(_name + 1))) : NetStringHandle(_name)) : NetStringHandle(), _force);
}


// Sets the skin by handle.
void TSStatic::setSkin(NetStringHandle _handle, const bool& _force)
{
    // If this isn't forcing the skin and the skin is already set, early out...
    if (!_force && (m_Skin == _handle))
    {
        return;
    }

	// Store the old skin name.
	NetStringHandle sOldSkin = m_Skin;

    // Set the new skin.
    m_Skin = _handle;

    // Make sure the skin changes are sent across the network.
    setMaskBits(SkinMask);

    // If there's no shape instance, exit out...
    if (!mShapeInstance)
    {
        return;
    }

    // If this is the server, early out...
    if (isServerObject())
    {
        return;
    }

    // If the skin is valid...
    if (m_Skin.isValidString())
    {
		// Set the string name for the console.
		m_Skin_Name = StringTable->insert(m_Skin.getString());

		// Clone the material list on the client.
		mShapeInstance->cloneMaterialList();

        // Reskin the shape instance.
        mShapeInstance->reSkin(m_Skin_Name, sOldSkin.getString());

        // Set the skin hash.
        m_Skin_Hash = _StringTable::hashString(m_Skin.getString());
    }
    else
    {
        // Otherwise, zero the skin hash.
        m_Skin_Hash = 0;

		// Clear the skin name.
		m_Skin_Name = StringTable->insert("");
    }
}


DefineEngineMethod(TSStatic, getSkin, const char*, (),,
	"Returns the current skin of the tsstatic.n"
	)
{
    return object->getSkin();
}


DefineEngineMethod(TSStatic, setSkin, void, (const char* skin),,
	"Sets the tsstatic's skin.n"
	"@param skin the name of the skin.nn"
	)
{
    // If the object is client-side, early out...
    if (object->isClientObject())
    {
        return;
    }

    object->setSkin(skin);
}
#4
11/17/2010 (2:11 am)
Ok, I tried using this resource and I'm getting empty strings for the

String newBaseName, String oldBaseName

I am not sure what is going on, but it appears the server isn't finding the textures and is sending over an empty string to the client.

I have Model.dts
and I created
base_model.jpg
red_model.jpg
blue_model.jpg

I then tried to call mountobject(modelshapeimage,1,1,"red");

and I get a texture not found. Did I miss something?

Vince
#5
11/17/2010 (5:26 am)
"Base_" is used for actual materials names if you're exporting from collada. "base." is used for dts files. Your files should be called base.model.jpg, red.model.jpg, etc. If the dts is using those textures, it'll work.

As for mountobject, well... I don't use shapebase and its children anymore, but it was my understanding that for shapebaseimages you'd want to be using ::mountImage.

Oh and bear in mind that what you're sending isn't a regular string, it's a tagged string. You either want it in single quotes or you want to call addTaggedString on it.
#6
01/08/2011 (12:25 am)
Logged as THREED-1314.