Game Development Community

Adding Dynamic TorqueScript Variables to CustomMaterials

by Dave Calabrese · 08/07/2008 (11:17 am) · 2 comments

To add a new variable to the TGEA CustomMaterial path, we need to make a small engine change. The reason for this is that the engine manages all the shader data. A CustomMaterial is nothing more than a raw HLSL shader script with a few space based variables set to match the rules for TGEA. Whenever you define a CustomMaterial, you are able to pass information from the Scripting layer into the HLSL layer through variables, such as 'specularColor' and 'specularPower'. Well, adding new variables to this is actually very easy once you know what to put where!

There are 5 lines that we need to add to the code, each in a different place. Right now, it's a good idea to know the kind of effect you want to achieve with your new variables. This is because the variable type will need to be different depending what we want to do. For example, if we wanted to pass a single floating point value into the shader such as a timer value from a schedule so we could make a lamp flicker, we would need to make this an 'F32' type in the engine. In this example, we are going to add a color variable so we can dynamically change the color of the shader in real time. For that, we will use the ColorF type.

First, dig out your favorite C++ IDE and load up the TGEA solution file. Navigate to the file engine/materials/material.h. This file has one large class definition. In here, we need to define our new variable. You will want to add it as such:

//-----------------------------------------------------------------------
   // Data
   //-----------------------------------------------------------------------
  StringTableEntry  baseTexFilename[MAX_STAGES];
   StringTableEntry  detailFilename[MAX_STAGES];
   StringTableEntry  bumpFilename[MAX_STAGES];
   StringTableEntry  envFilename[MAX_STAGES];
   StageData         stages[MAX_STAGES];
   ColorF            diffuse[MAX_STAGES];
   ColorF            specular[MAX_STAGES];
[b]   ColorF            colorMod[MAX_STAGES]; //Our New Variable![/b]
   F32               brightVal[MAX_STAGES];
   F32               specularPower[MAX_STAGES];
   bool              pixelSpecular[MAX_STAGES];
   bool              vertexSpecular[MAX_STAGES];

So we have defined the new variable with the name 'colorMod' and made it the type 'ColorF'. This means that to the shader, it will have a value similar to "1.0 1.0 1.0 1.0", or in HLSL Script, this would be considered a 'Float4'.

Next, we need to tell the shader what its constants are. Yea, sure, technically this is not a constant since we can change it from script - however, we need to tell the HLSL script what register it will be getting the information from. Whenever we set this variable in our TorqueScript, it will store that value in a register - so here we are actually defining the constant register value, not its contents.

Load up engine/materials/processedShaderMaterial.cpp and add some code. Scroll down to:

void ProcessedShaderMaterial::setShaderConstants(const SceneGraphData &sgData, U32 pass)

And at the bottom, let's add our new variables in their own section to keep things clean:

//Script Controlled Variables
GFX->setPixelShaderConstF( PC_USERDEF1, (float*)&mMaterial->colorMod[stageNum], 1 );

Notice that we have told it to use the register constant of PC_USERDEF1. This, and all other register constants are located not within your engine code directory, but in your game directory! Go into your actual game folder, find the '/shaders/' folder (branched off from the same folder that your .EXE is in), and look inside there for a file called 'shdrConsts.h'. This actually holds all your shader constants. PC_USERDEF1 is simply just a register that has been set aside by the developers at GarageGames to be used for this exact purpose - custom script variables. However, you may add your own register constants - you will just need to be sure those registers are not being used by anything else, or you could run into problems!

Moving right along - open up "engine/materials/material.cpp". We need to give this variable a default value, or else things get screwy. Scroll down to a part of the code that looks like this:

//----------------------------------------------------------------------------
// Constructor
//----------------------------------------------------------------------------
Material::Material()
{
   for( U32 i=0; i<MAX_STAGES; i++ )
   {
      diffuse[i].set( 1.0, 1.0, 1.0, 1.0 );
      specular[i].set( 1.0, 1.0, 1.0, 1.0 );
      colorMultiply[i].set(0.0f, 0.0f, 0.0f, 0.0f);
      specularPower[i] = 8;
      brightVal[i] = 0;
      pixelSpecular[i] = false;
      vertexSpecular[i] = false;
      exposure[i] = 1;
      glow[i] = false;
      emissive[i] = false;
   }

This for loop is called for each stage, or pass, of the CustomMaterial. In here, we need to tell the engine to make sure that this variable is getting sent to the HLSL shader for each pass - yes, even if it's not getting used. Look at everything else in there - most of that is not used for all materials. The engine only uses what it needs, but we still have to define it.

As you may have already figured out, since we're adding a color modification variable, we need to add set up are variable data as a color, such as the 'diffuse' value. So pick yourself a comfortable place in that for loop and add this line:

colorMod[i].set( 1.0, 1.0, 1.0, 1.0 );

Almost there! Now we actually have to add this new variable into the shader. This is also done in material.cpp. Look for the following code:

//--------------------------------------------------------------------------
// Init fields
//--------------------------------------------------------------------------
void Material::initPersistFields()
{
   Parent::initPersistFields();

Just as before, find a nice place in this function and add in this line:

addField("colorMod",    TypeColorF, Offset(colorMod,   Material), MAX_STAGES);

That's all for the engine side! Give that engine a hearty compile - I need to grab some coffee and you might as well do the same - unless you from the other side of the pond, then head on down to your nearest Twinings store for something less coffee-like and more tea-like.

...
..
.
Back? Fantastic.
Done compiling? Excellent.

Now for the fun part - making it work! Hey hey! Don't just hammer on that .EXE file just yet. We are working with a shader script, here, so we need to have a shader script in use.

I'm going to presume that you know at least how to define a CustomShader, so I won't bore you with the details on that. (If you don't know how to set that up, I suggest reading the articles on tdn.garagegames.com about CustomMaterials as they'll get you on the right path). Load up the pixel/fragment shader for an HLSL script you want to add your new variable to. In the Fragout main, you will of course want to add the variable. So...

Fragout main( ConnectData IN,
              uniform float4    ambient         : register(C3),
              uniform sampler2D diffuseMap      : register(S0),
              uniform sampler2D bumpMap         : register(S1),
[b]              uniform float4    colorMod        : register(PC_USERDEF1), //Holy crap! A new variable![/b]
              uniform float4    specularColor   : register(C0),
              uniform float     specularPower   : register(C1),
              uniform float     visibility      : register(C6)
)

Since this example is showing off a color modification variable, let's change where we pull the diffuse color and make it read...

float4 diffuseColor = tex2D(diffuseMap, IN.texCoord) * colorMod;

Now this is getting exciting - almost time for the big finish! Head on over to your CustomMaterial definition that you wish to modify, and make it look something like...

new CustomMaterial( myAwesomeColorChangingMaterial )
{
   mapTo = "spiffyGraphics";
   texture[0] = "coolLookingImagery";
   
[b]   colorMod = "1.0 0.0 0.0 1.0"; //Our colorMod value![/b]
   
   shader = colorChangingShader;
   version = 2.0;
};

The most important part to remember here is that we created a new variable with the name 'colorMod'. It is here, in the CustomMaterial definition, that this variable name comes into play. So here, we need to have a new variable named 'colorMod' if we want to be able to use it! And as you can see, we have given colorMod the value of "1.0 0.0 0.0 1.0". Knowing that a ColorF stores data as RGBA, you'll know that this means we will have a solid red modification.

Now, at last - load the game and look at a model with your material on it - the color should have turned very red! But wait! Why don't we change that color - without closing the game? Drop the console and type in:

myAwesomeColorChangingMaterial.colorMod = "0.0 1.0 0.0 1.0";

Hark, what is this?? The material just changed from red to green without doing any reloading, refreshing of materials or anything!

Congratulations - you now have you first script controlled, HLSL shader variable. The uses for this technique are practically endless. Just remember to keep your registers in mind - since we are populating a register with a color value, then all materials using that register will now have that color value. So if you wanted to do this with more than one material at the same time, with different colors, then you would want to use different registers. But that's where proper pre-design and code architecture comes in to play to make sure you will have what you need before you start building!

Now you try - I'd love to see examples of what people do with this! Flickering lights? How about a character who turns redder the more damaged they are? Maybe an electric field that pulses faster the closer the player gets? Have some fun and post screenshots and video links as responses!

#1
08/12/2008 (10:14 am)
Wow! I love to keep cool flexibility things like this handy, thanks for writing it up!
#2
02/03/2010 (11:05 am)
Anyone able to make this modification to Torque 3d (1.1a)?