Game Development Community

A reskinable object class for T3D Beta 4 (TSReSkinable)

by Michael Reino · 07/29/2009 (11:37 pm) · 8 comments

TSReSkinable – An object class that is based on TSStatic and is reskinable in the world editor for T3D Beta 4

We developed this class because we have a series of bunker objects that are different shapes but can be skinned with the same basic set of textures. We also have complete sets of textures for these objects in different colors and logos. By combining different textures and models we can create the appearance of 500 different objects from our 10 base models. Our level builder enjoyed placing and customizing the bunkers so much that we now also have reskinable cones and fences with more objects on the way.

Modeling – In order for the TSReSkinable class to find and reassign the materials for your textures, a set naming convention must be followed. In our reskinable models all textures assigned in the modeler must be named textureN where N = {0,…,5}. You can easily change the number of textures to suit your needs. You will also need to create a material definition for each texture that you want to apply. In the world editor you will be selecting materials to apply and not texture file names.

www.modernpaintball.net/downloads/airbunkers.jpg

Code Changes
To add the TSReskinnable class to your build you will need to add two files ( tsReskinable.h and tsReSkinable.cpp) to the source folder in your project directory. You will also need to edit seven engine source files and one script file. All of the changes are described below.

Add the following code to your project/source directory and save as tsReSkinable.h. The number of textures that can be reassigned per model can be changed by editing line 19.
//-----------------------------------------------------------------------------
// TSReSkinable - Reskinable object class
// Copyright (C) Michael A. Reino 2009
//-----------------------------------------------------------------------------

#ifndef _TSRESKINABLE_H_
#define _TSRESKINABLE_H_

#ifndef _SCENEOBJECT_H_
#include "sceneGraph/sceneObject.h"
#endif
#ifndef _TSSTATIC_H_
#include "T3D/tsStatic.h"
#endif
#ifndef _TSSHAPEINSTANCE_H_
#include "ts/tsShapeInstance.h"
#endif

#define TEXTURE_COUNT 6		// The max number of reskinable textures per model

//------------------------------------------------------------------------------
// Class: TSReSkinable
//------------------------------------------------------------------------------
class TSReSkinable : public TSStatic
{
private:
	typedef TSStatic         Parent;

public:
	TSReSkinable();
	~TSReSkinable();

	static void initPersistFields();
	bool onAdd();
	void onRemove();
	U32 packUpdate( NetConnection *conn, U32 mask, BitStream *stream );
	void unpackUpdate( NetConnection *conn, BitStream *stream );

	void inspectPostApply();

protected:
	void _assignMaterials();

	enum 
	{ 
		ReSkinableObjectMask = Parent::NextFreeMask,
		NextFreeMask  = Parent::NextFreeMask << 6,
	};   

	String mMaterialName[TEXTURE_COUNT];
	SimObjectPtr<Material> mSurfaceMat[TEXTURE_COUNT];

public:
	DECLARE_CONOBJECT(TSReSkinable);
};

#endif // _TSRESKINABLE_H_

Add the following code to your project/source directory and save as tsReSkinable.cpp. Edit line 22 to provide a default material name. This is the material that will be assigned when a new TSReSkinable object is added in the world editor. You can use the warning material if you choose. Edit line 29 to specify your default model. You can set this to any of your reskinable models. This is the model that is initially loaded when you add a new TSReSkinable object in the editor. You can then change the model and select the materials in the inspector.
//-----------------------------------------------------------------------------
// TSReSkinable - Reskinable object class
// Copyright (C) Michael A. Reino 2009
//-----------------------------------------------------------------------------

#include "console/consoleTypes.h"
#include "core/stream/bitStream.h"
#include "console/simBase.h"
#include "TSReSkinable.h"

//------------------------------------------------------------------------------

IMPLEMENT_CO_NETOBJECT_V1(TSReSkinable);

//------------------------------------------------------------------------------
// Class: TSReSkinable
//------------------------------------------------------------------------------

TSReSkinable::TSReSkinable()
{	// Set all textures to the default material
	for ( int i = 0; i < TEXTURE_COUNT; i++ )
		mMaterialName[i] = "default_mat";

	for ( int i = 0; i < TEXTURE_COUNT; ++i )
		mSurfaceMat[i] = NULL;

	// Set a default shape file because the editor will not assign one before
	// createing a new object
	mShapeName = "art/shapes/reSkin/default_model.dts";

	// Sets the most common options for our models
	mCollisionType = VisibleMesh;
	mAllowPlayerStep = false;
}

//------------------------------------------------------------------------------

TSReSkinable::~TSReSkinable()
{
}

//------------------------------------------------------------------------------

void TSReSkinable::initPersistFields()
{
	// Add the TSReSkinable specific persistent fields.
	addGroup( "Materials" );
	char fieldStr[12], tipStr[32];
	for ( int i = 0; i < TEXTURE_COUNT; i++ )
	{
		dSprintf( fieldStr, sizeof(fieldStr), "Material%d", i);
		dSprintf( tipStr, sizeof(tipStr), "Material to apply to texture%d", i);
		addField( fieldStr,	TypeMaterialName, Offset( mMaterialName[i],	TSReSkinable ), tipStr );
	}
	endGroup( "Materials" );

   // Initialise parents' persistent fields.
   Parent::initPersistFields();
}

//------------------------------------------------------------------------------

bool TSReSkinable::onAdd()
{
	if(!Parent::onAdd())
		return(false);

	// The shape has been created, registered and added to the scene in the parent

	// If this is a client and the model is loaded, assign the selected materials
	if ( isProperlyAdded() && isClientObject() )
	   _assignMaterials(); // Assign the materials to the shape

   return true;
}

//------------------------------------------------------------------------------

void TSReSkinable::onRemove()
{
   Parent::onRemove();
}

//------------------------------------------------------------------------------

void TSReSkinable::inspectPostApply()
{
   // Set Parent.
   Parent::inspectPostApply();

   // Set ReSkin Mask so client gets updated.
   setMaskBits(ReSkinableObjectMask);
}

//------------------------------------------------------------------------------

U32 TSReSkinable::packUpdate(NetConnection * con, U32 mask, BitStream * stream)
{
	// Pack Parent.
	U32 retMask = Parent::packUpdate(con, mask, stream);

	// Write the ReSkin flag.
	if (stream->writeFlag(mask & ReSkinableObjectMask))
	{
		// Write the material name for each texture
		for ( int i = 0; i < TEXTURE_COUNT; ++i )
			stream->write( mMaterialName[i] );
	}

	// Were done ...
	return(retMask);
}

//------------------------------------------------------------------------------

void TSReSkinable::unpackUpdate(NetConnection * con, BitStream * stream)
{
	// Unpack Parent.
	Parent::unpackUpdate(con, stream);

	if(stream->readFlag())
	{
		// Read in the material names
		Material *pMat = NULL;
		for ( int i = 0; i < TEXTURE_COUNT; ++i )
		{
			stream->read( &mMaterialName[i] );
			if ( !Sim::findObject( mMaterialName[i], pMat ) )
			{
				mSurfaceMat[i] = pMat = NULL;
				if ( !mMaterialName[i].isEmpty() )
					Con::printf( "TSReSkinable::unpackUpdate, failed to find Material (%s)!", mMaterialName[i].c_str() );
			}
			else         
				mSurfaceMat[i] = pMat;         
		}

		if ( isProperlyAdded() )
			_assignMaterials(); // Assign the materials to the shape 
	}
}

void TSReSkinable::_assignMaterials()
{	// Assign the materials to this shape instance
	char textureName[12];
	for ( int i = 0; i < TEXTURE_COUNT; i++ )
	{
		dSprintf( textureName, sizeof(textureName), "texture%d", i);
		mShapeInstance->setMeshMaterial( textureName, mSurfaceMat[i] );
	}
}

Changes to T3D/tsStatic.h.
Change the MaskBits enumeration (~ line 76) to be a protected member so TSReSkinable can access it.
Add a protected member variable to record the loaded shape name so we can detect if it has been changed in the editor and load the new shape. Add the following line (~ line 116):
StringTableEntry  mLoadedShapeName;

Changes to T3D/tsStatic.cpp.
Initialize the new mLoadedShapeName member variable. Add the following line (~ line 44):
mLoadedShapeName	 = "";
After:
mShapeName	 = "";

Check to see if the shape name has changed in the inspectPostApply method (~ line 110). Add the following:
if ( mLoadedShapeName != mShapeName )
		_createShape();
After:
// Apply any transformations set in the editor
   Parent::inspectPostApply();

Update mLoadedShapeName after the new shape has been loaded. In the _createShape method (~ line 204) add:
mLoadedShapeName = mShapeName;
After:
mShapeInstance = new TSShapeInstance( mShape, isClientObject() );

Perform the same check on the client in the unpackUpdate() method (~ line 495) add:
if ( isProperlyAdded() && (mLoadedShapeName != mShapeName) )
		_createShape();		// If the shape name has changed, reload
After:
mShapeName = stream->readSTString();

Changes to ts/tsShape.h.
Include the definition file for custom materials (~ line 27) add:
#ifndef _CUSTOMMATERIALDEFINITION_H_
#include "materials/customMaterialDefinition.h"
#endif
After:
#ifndef _MATERIALLIST_H_
#include "materials/materialList.h"
#endif

Add the declaration for a new setMaterial() method (~ line 639) add:
bool setMaterial(U32 index, Material* newMat);
After:
bool setMaterial(U32 index, const Torque::Path &texturePath); // use to support reskinning

Changes to ts/tsMaterialList.cpp.
Add the definition for the new method to the end of the file:
bool TSMaterialList::setMaterial(U32 index, Material* newMat)
{
	if (index < 0 || index > mMaterials.size())
		return false;

	if( newMat )
	{
		if( mMatInstList[index] && (mMatInstList[index]->getMaterial() == newMat) )
			return true;	// Already assigned

		// dump the old mat instance
		if (mMatInstList[index])
			delete mMatInstList[index];		

		mMatInstList[index] = NULL;

		// get diffuse map name
		CustomMaterial* cust = dynamic_cast<CustomMaterial*>(newMat);
		String texPath, newMatName;
		if(cust)
			newMatName = cust->mTexFilename[0];
		else
		{
			newMatName = newMat->mBaseTexFilename[0];
			if ( newMatName.isEmpty() )
				newMatName = newMat->mDiffuseMapFilename[0];
		}

		U32 slashPos = newMatName.find('/',0,String::Right);
		if (slashPos == String::NPos)
			// no '/' character, must be no path, just the filename
			texPath = newMat->getPath() + newMatName;
		else
			texPath = newMatName;

		GFXTexHandle tex;
		if(!tex.set(texPath, &GFXDefaultStaticDiffuseProfile, avar("%s() - textureHandle (line %d)", __FUNCTION__, __LINE__)))
			return false;

		// change texture
		mMaterials[index] = tex;

		MatInstance *matInst = new MatInstance( *newMat );
		mMatInstList[index] = matInst;
		matInst->init(MATMGR->getDefaultFeatures(), getGFXVertexFormat<GFXVertexPNTTB>());
	}
	return true;
}

Changes to ts/tsShapeInstance.h.
Add the declaration for a new method to reskin a mesh without changing the lookup name in material name list (~ line 328) add:
bool setMeshMaterial(const char* skinTag, Material* newMat);
After:
bool ownMaterialList() const { return mOwnMaterialList; }

Changes to ts/tsShapeInstance.cpp.
Add the definition for the new method to the end of the file. Add:
bool TSShapeInstance::setMeshMaterial(const char* skinTag, Material* newMat)
{	// Replace the material for the named texture.
	if ( !newMat || !skinTag )
		return false;

	if (ownMaterialList() == false)
		cloneMaterialList();

	TSMaterialList* pMatList = getMaterialList();	// Get the list of material names
	const Vector<String> &materialNames = pMatList->getMaterialNameList();

	S32 foundMatInstIndex = -1;
	for (S32 j = 0; j < materialNames.size(); j++) 
	{	// See if this skin tag exists in our material name list
		const String &pName = materialNames[j];

		if ( pName.isEmpty() )
			continue;

		if(0 == dStricmp(pName,skinTag))
		{	// Skin tag found, save the index and exit loop
			foundMatInstIndex = j;
			break;
		}
	}

	if(-1 == foundMatInstIndex)
		return false;	// No matching skin tag in this model

	// Get the currently assigned material and change if different
	BaseMatInstance* matInst = pMatList->getMaterialInst(foundMatInstIndex);

	Material* currentMat = NULL;
	if(matInst)
		currentMat = dynamic_cast<Material*>(matInst->getMaterial());

	if(newMat != currentMat)
		pMatList->setMaterial(foundMatInstIndex, newMat);

	return true;
}

Changes to materials/matInstance.h.
Make the MatInstance( Material &mat ) constructor a public declaration. Move the following two lines (~ line 67):
/// Create a material instance by reference to a Material.
   MatInstance( Material &mat );
Up to line 63 so they are in the public declaration section.

Changes to gametoolsworldEditorscriptseditorscreator.ed.cs.
To make the TSReSkinable object appear in the editor menus add the following line (~ line 51):
%this.registerMissionObject( "TSReSkinable" );
After:
%this.registerMissionObject( "CameraSpawnPoint" );

Recommendations for testing.
The fastest way to get a TSReskinable object into your level to see it work is to create a cube in your modeling program. Assign a different texture to each side of the cube. The textures must be named texture0…texture5. These textures will never actually be rendered, they are there to guaranty the expected lookup names in the material name list. Create a reSkin directory inside of the art/shapes directory and export the cube there as default_model.dts. Inside any of your material.cs files create a material named default_mat to be used as the initially assigned material for all textures. Start your game, load a mission and enter the editor. In the Object Editor Scene Tree select Library->Level->Level and you will see the TSReSkinable object listed. Double-click on it and then click Create New. Your first TSReskinable object is now created, you can reassign the material used for any of the six textures and change the shape.

Optional changes
We have made a couple of changes to the inspector to simplify working with our models and textures. Because all of our reskinable models and their textures are in subdirectories of the reSkin directory we have restricted the material selection boxes to only show materials that are created under this directory. All of our materials for these objects start with a two-letter abbreviation for the shapes that it can be used for so it makes the list much easier to navigate. We have also added a file type filter to the open file dialog so you can have only .dts files show up when searching for your shape.

Changes to gui/editor/guiInspectorTypes.cpp.
Change the GuiInspectorTypeMaterialName::_populateMenu method (~ line 185) from:
void GuiInspectorTypeMaterialName::_populateMenu( GuiPopUpMenuCtrl *menu )
{
   SimSetIterator iter( MATMGR->getMaterialSet() );
   for ( ; *iter; ++iter )
   {
      menu->addEntry( (*iter)->getName(), 0 );
   }

   menu->sort();
}
To:
void GuiInspectorTypeMaterialName::_populateMenu( GuiPopUpMenuCtrl *menu )
{
   SimSetIterator iter( MATMGR->getMaterialSet() );
   bool useReskinFilter = ( 0 == dStrcmp(this->mTarget->getClassName(), "TSReSkinable") );
   for ( ; *iter; ++iter )
   {
	   if ( useReskinFilter )
	   {
		   StringTableEntry fileName = (*iter)->getFilename();
		   if ( fileName && (NULL != dStrstr( fileName, "reSkin" )) )
			  menu->addEntry( (*iter)->getName(), 0 );
	   }
	   else
		   menu->addEntry( (*iter)->getName(), 0 );
   }

   menu->sort();
}

Change the GuiInspectorTypeFileName::constructEditControl() method to filter the file open dialog (~ line 355) replace the three lines:
RectI browseRect( Point2I( ( getLeft() + getWidth()) - 26, getTop() + 2), Point2I(20, getHeight() - 4) );
      char szBuffer[512];
      dSprintf( szBuffer, 512, "getLoadFilename("*.*|*.*", "%d.apply", %d.getData());", getId(), getId() );
With:
RectI browseRect( Point2I( ( getLeft() + getWidth()) - 26, getTop() + 2), Point2I(20, getHeight() - 4) );
      char szBuffer[512];
	  if ( 0 == dStrcmp(this->mTarget->getClassName(), "TSReSkinable") )
		 dSprintf( szBuffer, 512, "getLoadFilename("dts files (*.dts)|*.dts|All files (*.*)|*.*", "%d.apply", %d.getData());", getId(), getId() );
	  else
		 dSprintf( szBuffer, 512, "getLoadFilename("*.*|*.*", "%d.apply", %d.getData());", getId(), getId() );

#1
07/30/2009 (12:17 am)
Awesome resource ... and the term Bunker and seeing the objects ... do I smell a Paintball game coming ... more importantly a Airball one. :)

C'mon ... tell me I am not wrong.

I am a 5 year veteran of Airball and played "A" division in South Africa before I left. :)
#2
07/30/2009 (7:13 am)
Cool resource. I'll definitely be using this in my game once I can afford T3D. Thanks a bunch!
#3
07/31/2009 (9:13 pm)
Great resource. Once I get T3D I'll definitely use this. Thanks!
#4
08/31/2009 (11:01 pm)
Sweet Resource. Anyone tried it in B5 yet?

Would be great to get into the next build!
#5
01/14/2010 (3:05 am)
Compiled fine into T3d 1.1 with the addition of this into
source/materials/materialDefinition.h
at or around line 208.

bool mPlanarReflection;
	String mBaseTexFilename; // added line
   bool mAutoGenerated;
#6
02/16/2010 (11:03 pm)
Hi,
i want to compiled this resource to T3D 1.0.1,
but i really don't understand about this
"
Change the MaskBits enumeration (~ line 76) to be a protected member so TSReSkinable can access it.
"

how to code it ?
#7
07/27/2010 (6:28 am)
This resource is very helpful.
I have added it to T3D 1.0.1
Thank you Michael Reino :-)
#8
11/16/2010 (12:31 pm)
I'm confused, can this be used for DTS's mounted to players? If so, could someone kindly give a basic example?

Also, can this be used for skins on player objects? I.e. have 1 dog dts and 5 different skins to create 5 different dog types?

Any guidance would be real cool, kinda new to this side of t3d