TorqueX/CustomMaterials

From TDN


Contents

Introduction


By default, everything created with TXB for a 2D game is rendered using SimpleMaterial. This is fine for most things, but occasionally some more custom functionality is required. In this tutorial, we will go through the steps to create a custom material, complete with a .fx file written in HLSL that carries out our desired behavior.

In the interest of reaching as broad an audience as possible, this tutorial will not assume that you have access to the source code (TorqueX Pro), but if you do have it, you can compare what I've done with the various parallel files in the source code tree for a more full understanding of where this comes from.

Inheriting From SimpleMaterial

First off, you need to create a new class in its own .cs file. I'm going to be creating a material that saturates/desaturates the texture of the object being drawn with it. Thus, I created SaturationMaterial.cs. For now, it's a pretty simple file:

//// SaturationMaterial.cs ////

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

using GarageGames.Torque.Materials;
using GarageGames.Torque.SceneGraph;
using GarageGames.Torque.RenderManager;

namespace MyGame
{
    // A specialized material that saturates or desaturates the object's texture.
    public class SaturationMaterial : SimpleMaterial
    {
        //======================================================
        #region Constructors
        // Initializes a new instance of the SaturationMaterial class.
        public SaturationMaterial()
        {
            // We'll add a bit of code here later to change what's automatically loaded for us.
        }
        #endregion

        //======================================================
        #region Private, protected, internal methods
        // Sets up this effect and selects a technique to render with.
        protected override string _SetupEffect(SceneRenderState srs, MaterialInstanceData materialData)
        {
            // For now, we'll accept whatever SimpleMaterial would use.
            return base._SetupEffect(srs, materialData);
        }

        // Performs per-object parameter setup.
        protected override void _SetupObjectParameters(RenderInstance renderInstance, SceneRenderState srs)
        {
            base._SetupObjectParameters(renderInstance, srs);

            // We'll access some instance data here soon.
        }

        // Loads the parameters from our effect into this material instance.
        protected override void _LoadParameters()
        {
            base._LoadParameters();

            // Here is where we'll find the parameters in our .fx file so we can set them from C# code.
        }

        // Clears references to parameters from this material.
        protected override void _ClearParameters()
        {
            // Any parameters you found in _LoadParameters are no longer valid when this is called, so you should
            // set them to null.

            base._ClearParameters();
        }
        #endregion

        //======================================================
        #region Private, protected, internal fields
        // We'll add our parameters here shortly.
        #endregion
    }
}

Using SaturationMaterial on a T2DStaticSprite


Now we've got a custom material, but it's not being used anywhere in our game. When exactly this gets set up will depend very much on your project, but I'll explain the two most common cases I can think of:

Case 1: TXB generated scene


This case kinda sucks, as you'll have to redo it every time your .txscene file is saved out from TXB, as TXB isn't modifiable.  :-(

Anyways, open up your .txscene file and find the material you want to change. It'll look something like this:

<SimpleMaterial name="CharacterMaterial" type="GarageGames.Torque.Materials.SimpleMaterial">
    <TextureFilename>data/images/Character.png</TextureFilename>
    <IsTranslucent>false</IsTranslucent>
    <IsAdditive>false</IsAdditive>
</SimpleMaterial>

What you want to do is change the references to SimpleMaterial and GarageGames.Torque.Materials.SimpleMaterial to SaturationMaterial and MyGame.SaturationMaterial (where MyGame was the namespace of the SaturationMaterial class).

This is what you get:

<SaturationMaterial name="CharacterMaterial" type="MyGame.SaturationMaterial">
    <TextureFilename>data/images/Character.png</TextureFilename>
    <IsTranslucent>false</IsTranslucent>
    <IsAdditive>false</IsAdditive>
</SaturationMaterial>

The result is that whenever CharacterMaterial is used, it will actually be using SaturationMaterial to render it. Kinda hard to tell at this point, but if you set a breakpoint in any of the functions in SaturationMaterial.cs, you'll see that it is hit.

Case 2: Object created in code


This case is cleaner than the first case, as you don't have an unmodifiable game editor trouncing all over your changes. Unfortunately, it's a bit more nitty-gritty than the first one. Let's say somewhere, you create a T2DStaticSprite named Character. You might have some code that looks like this:

T2DStaticSprite Character = new T2DStaticSprite();
Character.Material = TorqueObjectDatabase.Instance.FindObject<SimpleMaterial>("CharacterMaterial");
// Set the position, add components, etc.
TorqueObjectDatabase.Instance.Register(Character);

The FindObject call is a bit sketchy, but the point is, you create a new Sprite, you set its material to something, and you register it with the game.

Now, instead of finding some existing SimpleMaterial, we wanna use a SaturationMaterial:

T2DStaticSprite Character = new T2DStaticSprite();
Character.Material = new SaturationMaterial();
// Set the position, add components, etc.
TorqueObjectDatabase.Instance.Register(Character);

Just like in Case 1, we're now using SaturationMaterial to render our character, although again, it's hard to tell at this point.

Writing a Custom .fx File


Now that we've got something being draw with our SaturationMaterial, it's time to make it draw with the saturation/desaturation we promised. To do that, we need to create a .fx file. We need to put the file in our Content project, so I create it at data/effects/SaturationEffect.fx

This file has a bit more interesting stuff going on with it, and it's written in HLSL. If you're not familiar with HLSL there's a ton of resources if you search it, with wildly different levels of accessibility and thoroughness. Good luck :-)

//// SaturationEffect.fx ////

// As a SimpleMaterial, we get worldViewProjection, opacity, and baseTexture passed up to us with
// valid values.
float4x4 worldViewProjection;
float opacity = 1.0;
texture baseTexture;

// We add the saturation parameter.
float saturation = 0.0;

// Set up our sampler to read from baseTexture.  NOTE: To do a "retro" pixelated look, change the
// "Linear"s to "Point"s.
sampler2D baseTextureSampler = sampler_state
{
	Texture = <baseTexture>;
	MipFilter = Linear;
	MinFilter = Linear;
	MagFilter = Linear;
};

struct VSInput
{
	float4 position : POSITION;
	float2 texCoord : TEXCOORD0;
};

struct VSOutput
{
	float4 position : POSITION;
	float2 texCoord : TEXCOORD0;
};

// Our vertex shader doesn't have to do anything surprising.
VSOutput SaturationVS(VSInput input)
{
	VSOutput output;
	output.position = mul(input.position, worldViewProjection);
	output.texCoord = input.texCoord;
	return output;
}

// Our pixel shader is where it's at.
float4 SaturationPS(VSOutput input) : COLOR
{
	// First get our color out of the texture.
    float4 color = tex2D(baseTextureSampler, input.texCoord);
	
	// Then get a gray that's the same luminance (in some sense.  Don't get me started about L1 vs. L2 norms etc.)
	float gray = (color.r + color.g + color.b) / 3;

	// And finally use our saturation value to increase/decrease our saturation.
	color.rgb = lerp(gray, color, saturation);

	// This lets the VisibilityLevel of our object (which is passed to opacity) show up as fading in/out.
	color.a *= opacity;
		
    return color;
}

technique SaturationTechnique
{
    pass P0
    {
        VertexShader = compile vs_2_0 SaturationVS();
        PixelShader  = compile ps_2_0 SaturationPS();
    }
}

That's all well and good, but we're not using this effect yet. To do that, we need to make some changes in SaturationMaterial.cs.

First, in the Constructor, load up the .fx file:

public SaturationMaterial()
{
    EffectFilename = "data/effects/SaturationEffect";
}

Second, choose our SaturationTechnique (one .fx file can have several different techniques if you wish). In _SetupEffect, replace the contents of the function with:

// Sets up this effect and selects a technique to render with.
protected override string _SetupEffect(SceneRenderState srs, MaterialInstanceData materialData)
{
    // We're gonna ignore the technique SimpleMaterial would choose, but still let it setup.
    base._SetupEffect(srs, materialData);

    return "SaturationTechnique";
}

If all went well, you should have your character rendering in grayscale (or greyscale, you silly Brits/Canadians).

Passing Parameters to the .fx File


Our SaturationMaterial is going to be much more useful if we can pass parameters up to it, so that our saturation value isn't always 0. Unfortunately, doing this efficiently on a per-object basis requires you to modify the source code of T2DStaticSprite or T2DAnimatedSprite to use MaterialInstanceData correctly. If you have TorqueX Pro, or you're a GarageGames employee, I can hook you up with my changes (once you prove to me you own the source), but in the meantime, all I can show you is how to change the parameters on a per-material basis.

All of these changes happen in the SaturationMaterial.cs file. First, we add a couple of member variables, one of which will be publicly accessible:

// The parameter in our effect to which saturation can be uploaded.
private EffectParameter _saturationParameter;

// The current saturation value to upload.
private float _saturation = 0;

// Gets or sets our current saturation value.
public float Saturation
{
    get { return _saturation; }
    set { _saturation = value; }
}

Next, we find our parameters by name from the .fx file, in the _LoadParameters function:

// Here is where we'll find the parameters in our .fx file so we can set them from C# code.
_saturationParameter = EffectManager.GetParameter(Effect, "saturation");

Now, to be proper, in _ClearParameters, we need to null out our invalid _saturationParameter:

// Any parameters you found in _LoadParameters are no longer valid when this is called, so you should
// set them to null.
_saturationParameter = null;

Finally, in _SetupObjectParameters, we pass our saturation value up to our technique:

// Upload our current saturation value.
EffectManager.SetParameter(_saturationParameter, _saturation);

Now, if we do something like...

((SaturationMaterial)Character.Material).Saturation = -1;

...we'll see the material change based upon this parameter, in realtime.

Conclusion

Hopefully you've got enough information now to write all kinds of crazy custom materials for your games. Let me know if I can help ya with anything.

Author:William Clark