Overlaying a material onto model entities

Category:ProgrammingCategory:Tutorials
This article describes a method of overlaying a transparent material onto a model entity, by drawing it on top of the model's existing material(s). It can be applied to any model entity, and no editing of the model file or its materials is required. This makes this technique ideal for "shield belt" or "damage amplifier" effects, to borrow some examples from the Unreal Tournament series.
The first section describes a framework for controlling the effect; the reader is advised to significantly modify this section to suit their own requirements, only novice coders will require to follow it precisely.
Note: This technique currently causes minor problems with character faces on some graphics cards, due to the shaders used on them. It's not catastrophic, but it does look slightly odd.
== Control framework ==
=== Deciding which class to modify ===
The class you choose to implement this effect in is dependent on your mod and its requirements. A deathmatch mod may happily edit the HL2MP player classes, while a single-player mod may edit the Base AI classes. A mod that requires this effect to occur on both NPCs and players may wish to edit the Base Combat Character classes, as this is shared by both these groups. Regardless of the class selected, the controlling code should occur on the server class and the actual effect code on the client class. These will be linked by networking a variable (in this case, a boolean).
For the purposes of this section of this tutorial, I will assume an HL2DM-based deathmatch mod, that wishes to modify the CHL2MP_Player and C_HL2MP_Player classes. Applying it to a different class should be trivial.
=== Control functions ===
Open hl2mp_player.h, and add the following simple functions to a public section of the player class declaration. They can be renamed as you like, they won't be referenced again in this tutorial.
 void SetMaterialGlowEnabled(bool enabled) { m_bGlowEnabled = enabled; }
 bool IsMaterialGlowEnabled() { return m_bGlowEnabled; }
Into a private section of the same class, add
 CNetworkVar( bool, m_bGlowEnabled );
Now get the header for the client version of your class, in this case c_hl2mp_player.h, and into a private section in the player class there, add
 bool m_bGlowEnabled;
It will be up to you to decide when to enable & disable the glow effect. Disabling in the player's Event_Killed function is recommended, to prevent it from remaining on across a respawn.
=== Networking ===
These two booleans represent the same value, and we need to get them to synchronize, with the server's version being authoritative. For a proper discussion of how to network variables between entities, see Networking Entities.
In hl2mp_player.cpp, find the network send table, the easiest way of doing this is to search for SendProp.
Into this table, add the following (at the top, so its easiest to spot):
 SendPropBool( SENDINFO( m_bGlowEnabled ),
Now in c_hl2mp_player.cpp, find the network receive table, for this, the easiest way is to search for RecvProp.
Into this table (at the top again) add:
 RecvPropBool( RECVINFO( m_bGlowEnabled ),
The boolean value set on the server will now always be accessible on the client, allowing us to correctly decide when to draw the effect.
== Implementing the effect ==
=== Overriding DrawModel ===
In order to draw a new material precisely over the model, we will use ForcedMaterialOverride, which replaces every material drawn with the given parameter, until it is turned off. We will override the DrawModel function, call the base function to draw the model as normal, then turn on ForcedMaterialOverride, draw the model again using our transparent material, then turn ForcedMaterialOverride back off.
The class you are editing probably won't have a DrawModel function present (but do check!), so you'll have to create one. Note that this should be done entirely client-side. In the header, add a virtual function declaration, to override the DrawModel function of your base class. In this case, this would be in c_hl2mp_player.h, in a public section.
 virtual int DrawModel(int flags);
Now in the client-side cpp file, we'll create the actual function. In c_hl2mp_player.cpp:
 int C_HL2MP_Player::DrawModel(int flags)
 {
 int retVal = BaseClass::DrawModel(flags);
 
 if ( m_bGlowEnabled )
 {
 modelrender->ForcedMaterialOverride( materials->FindMaterial( "glowmaterial", TEXTURE_GROUP_CLIENT_EFFECTS ) );
 BaseClass::DrawModel(STUDIO_RENDER|STUDIO_TRANSPARENCY);
 modelrender->ForcedMaterialOverride(0);
 }
 
 return retVal;
 }
This will cause your override material to be drawn over the model's regular material. If you wanted to replace the regular material completely (even with something partly transparent!), then change this function to only call BaseClass::DrawModel once:
 int C_HL2MP_Player::DrawModel(int flags)
 {
 if ( m_bGlowEnabled )
 {
 modelrender->ForcedMaterialOverride( materials->FindMaterial( "glowmaterial", TEXTURE_GROUP_CLIENT_EFFECTS ) );
 int retVal = BaseClass::DrawModel(STUDIO_RENDER|STUDIO_TRANSPARENCY);
 modelrender->ForcedMaterialOverride(0);
 return retVal;
 }
 else
 return BaseClass::DrawModel(flags);
 }
That's all the code complete. If it doesn't compile at this point, something's gone wrong, so double-check all your changes. Note that "glowmaterial" should be replaced with the name of the material you create in the next section. For a simple (non-transparent) test, try "vgui/white"
=== Creating the glow material ===
For a proper material guide, see Creating a Material. You should give your material an alpha channel, and set the RGB channels to show pure white. The alpha should be where any detail in your material is created, but the channel should be quite dull in order to allow the model's existing texture to be partially visible underneath. Perhaps surprisingly, an alpha value of 64 should be significantly intense. A suggested vmt file format is shown here. Note that the material must be set to be additive in order for the transparency to function.
 "UnlitGeneric"
 {
 "$basetexture" "glow_strong"
 "$vertexcolor" "1"
 "$vertexalpha" "1"
 "$translucent" "1"
 "$model" "1"
 "$additive" "1"
 }
The whole purpose of using UnlitGeneric is so that the material is rendered "glowing" - i.e. independent of the shadowing currently on the model.
== Additional features ==
This basic material glow system can be expanded in many ways. Two examples are detailed here.
=== Multiple effect materials ===
If you wish to have multiple glow effect options, you could consider networking an integer instead of a boolean, and then in DrawModel, using a switch statement to select which material to draw, such as:
 int C_HL2MP_Player::DrawModel(int flags)
 {
 int retVal = BaseClass::DrawModel(flags);
 
 if ( m_iGlowEnabled > 0 )
 {
 char matName[32];
 switch ( m_iGlowEnabled )
 {
 case 1:
 matName = "glow_weak";
 break;
 case 2:
 matName = "glow_weak";
 break;
 case 3:
 matName = "glow_strong";
 break;
 }
 modelrender->ForcedMaterialOverride( materials->FindMaterial( matName, TEXTURE_GROUP_CLIENT_EFFECTS ) );
 BaseClass::DrawModel(STUDIO_RENDER|STUDIO_TRANSPARENCY);
 modelrender->ForcedMaterialOverride(0);
 }
 
 return retVal;
 }
=== Coloring the effect in code ===
By changing render colors, a white glow material can be made to appear in any color. Ideally, the render color of the player should be left untouched, while the render color of the glow would be changed. However, this proved somewhat difficult, as the values were immediately getting stomped back to the server value for the player. It was then discovered that the simplest way of achieving dynamic server-side control over the glow color was simply to change the player's render color directly, as the fact that the model underneath the glow effect was being tinted in the same color as the glow wouldn't be noticeable.
In the spawn function of your class (eg in Spawn of hl2mp_player.cpp), add
 SetRenderMode(kRenderTransColor);
 SetRenderColorA(255); // just to be safe
It would then seem prudent to remove the current SetMaterialGlowEnabled function with two others, EnableMaterialGlow & StopMaterialGlow. As they contain several lines of code, these should only be declared in the header, and defined in the cpp file.
So, in the same place as SetMaterialGlowEnabled was previously declared, hl2mp_player.h:
 void EnableMaterialGlow(int r=255, int g=255, int b=255);
 void StopMaterialGlow();
And in hl2mp_player.cpp, define the function
 void CHL2MPPlayer::EnableMaterialGlow(int r, int g, int b)
 {
 SetRenderColorR(r);
 SetRenderColorG(g);
 SetRenderColorB(b);
 m_bGlowEnabled = true;
 }
 
 void CHL2MPPlayer::StopMaterialGlow()
 {
 SetRenderColorR(255);
 SetRenderColorG(255);
 SetRenderColorB(255);
 m_bGlowEnabled = false;
 }
For a white glow material, calling EnableMaterialGlow(255,0,0) will cause a bright red glow, while EnableMaterialGlow(0,96,255) will cause a pale blue glow. Any other combination of RGB values should also be applicable.