Enter Half-Life Re-imagined, our current competition!
Check out Skewing textures in Hammer, our newest tutorial!
Welcome, Bear, our newest member!


Site Stuff






Feeling Blue

What's your favourite shade of blue?














2 mins


9 mins


10 mins


40 mins


44 mins


46 mins


52 mins



A gaming and technology blog by TWHL admins Penguinboy and Ant. A music blog by TWHL users Ant and Hugh.

Real-Time "TRON 2.0" Glow in Half-Life

By Francis 'DeathWish' Woodhouse

I'm sure many of us have seen screenshots of TRON 2.0 and noticed the glowing effect that's overlaid onto everything bright. Well, it's possible to have the same effect in Half-Life, too, using vertex and pixel shaders with OpenGL. This article will walk you through the steps in getting it working, and will suggest some alternative effects that could be done using the system we set up here.

To whet your appetite, here's a comparison of scenes before and after the glow is applied. The images on the left are without the effect, and the images on the right are with the effect. Also, here's an example of a "TRON 2.0"-esque map with four bars of bright colour without the glow and with the glow.

Due to the way that this effect is being done, it will only work if the user is using OpenGL as their rendering API. In addition, their graphics card must have support for 4 texture units and fairly primitive vertex and pixel shaders. The effect was tested on an NVIDIA GeForce4 Ti 4200, and should work on the equivalent hardware of ATI.

Now, onward. To do the grunt work of the effect, we will be using vertex and pixel shaders written in Cg (C for graphics), a high-level shading language developed by NVIDIA. You'll need the Cg Toolkit to be able to compile and run the effects. Make sure you configure Visual C++ to look for include and library files in the appropriate directories of the Cg Toolkit installation.

First things first, we'll create the vertex and pixel shaders that we'll be using to do the effects. Create a folder within your mod directory called "cgshaders" - we'll be putting the shaders inside this folder. Download the 6 .cg files attached to this document and place them inside this folder. They do the following operations:

  • glow_darken_vp/fp.cg - Makes the dark areas of the supplied texture much darker whilst leaving bright areas relatively unaffected by multiplying the texture by itself twice. (Download: vertex, fragment)

  • glow_blur_vp/fp.cg - Blurs the supplied texture by sampling it at 4 points, depending on the X and Y offsets specified. (Download: vertex, fragment)

  • glow_combine_vp/fp.cg - Combines the blur texture with the original scene texture. (Download: vertex, fragment)

Ok, now that the shaders are in place, it's time to do the programming. Firstly, you'll need to add the "opengl32.lib", "cg.lib" and "cgGL.lib" libraries into the project's link settings (Project->Settings->Link->Object/library modules).

Now, open up the client DLL project and go into tri.cpp. You'll need to add the following include directives:

#include <windows.h>
#include <gl/gl.h>
#include <gl/glext.h>
#include <cg/cg.h>
#include <cg/cgGL.h>

Then, after the extern "C" { ... } block, add the following include and global variables:

#include "r_studioint.h"

extern engine_studio_api_t IEngineStudio;

bool g_bInitialised = false;


CGcontext g_cgContext;
CGprofile g_cgVertProfile;
CGprofile g_cgFragProfile;

CGprogram g_cgVP_GlowDarken;
CGprogram g_cgFP_GlowDarken;

CGprogram g_cgVP_GlowBlur;
CGprogram g_cgFP_GlowBlur;

CGprogram g_cgVP_GlowCombine;
CGprogram g_cgFP_GlowCombine;

CGparameter g_cgpVP0_ModelViewMatrix;
CGparameter g_cgpVP1_ModelViewMatrix;
CGparameter g_cgpVP1_XOffset;
CGparameter g_cgpVP1_YOffset;
CGparameter g_cgpVP2_ModelViewMatrix;

unsigned int g_uiSceneTex;
unsigned int g_uiBlurTex;

Don't worry if you don't have a clue what they're going to store - they'll all be explained in due course. Before we dive into the initialization code, add this utility function code after the globals (it'll make the initialization code less messy):

bool LoadProgram(CGprogram* pDest, CGprofile profile, const char* szFile)
     const char* szGameDir = gEngfuncs.pfnGetGameDirectory();
     char file[512];
     sprintf(file, "%s/%s", szGameDir, szFile);

     *pDest = cgCreateProgramFromFile(g_cgContext, CG_SOURCE, file, profile, "main", 0);
     if (!(*pDest)) {
          MessageBox(NULL, cgGetErrorString(cgGetError()), NULL, NULL);
          return false;

     return true;

All this does is apply a full path to the specified file (with szFile being relative to the modification's directory), then load that file as a Cg program using the supplied profile (which will be either a vertex or pixel shader profile). The program is then loaded into OpenGL if Cg successfully compiles it.

Now we're ready to create the initialization function. I'll give the code in sections and describe what they're doing as we go along.

void InitScreenGlow(void)
     if (IEngineStudio.IsHardware() != 1)

     gEngfuncs.pfnRegisterVariable("cg_blur_steps", "4", 0);

We check whether we're running in OGL mode, and bail out if we're not. Then, the first thing to do is register a CVAR that we'll use to control whether blurring is enabled and if so, how much the image will be blurred to create the glow. The value of this CVAR is how many times the Gaussian blur will be applied to the 'intensified' image of the scene.


     glActiveTextureARB = (PFNGLACTIVETEXTUREARBPROC)wglGetProcAddress("glActiveTextureARB");

Next, we get the function pointer for the function that controls multitexturing (using the GL_ARB_multitexure OpenGL extension). It enables us to set which texture unit we're changing the properties of, such as whether texturing is enabled on that unit and which texture is active.


     unsigned char* pBlankTex = new unsigned char[ScreenWidth*ScreenHeight*3];
     memset(pBlankTex, 0, ScreenWidth*ScreenHeight*3);

     glGenTextures(1, &g_uiSceneTex);
     glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiSceneTex);
     glTexImage2D(GL_TEXTURE_RECTANGLE_NV, 0, GL_RGB8, ScreenWidth, ScreenHeight, 0, GL_RGB8, GL_UNSIGNED_BYTE, pBlankTex);

     glGenTextures(1, &g_uiBlurTex);
     glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiBlurTex);
     glTexImage2D(GL_TEXTURE_RECTANGLE_NV, 0, GL_RGB8, ScreenWidth/2, ScreenHeight/2, 0, GL_RGB8, GL_UNSIGNED_BYTE, pBlankTex);

     delete[] pBlankTex;

     g_bInitialised = true;

We now need to set up two textures - one to store the unadulterated image of the rendered map, and the other to contain the blurred, 'glow' version that will be combined with the first texture. ScreenWidth and ScreenHeight are two macros defined elsewhere in the HLSDK that give the current width and height of the screen. Notice that we're using a different type of texture to the normal GL_TEXTURE_2D type; instead, we're making use of the GL_NV_texture_rectangle OpenGL extension to enable us to have non-power-of-2 dimensions to our textures. This extension is supported on nearly all graphics cards that people use these days.

Also notice that the blur texture is half the dimensions of the scene texture, in order to save on fillrate. The effect does not suffer, as we are blurring the image anyway - we don't need exact details.


     g_cgContext = cgCreateContext();
     if (!g_cgContext) {
          MessageBox(NULL, "Couldn't make Cg context", NULL, NULL);

A Cg context is needed to enable us to load Cg programs, so the next stage is to create one (and error out if we can't).


     g_cgVertProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
     if (g_cgVertProfile == CG_PROFILE_UNKNOWN) {
          MessageBox(NULL, "Couldn't fetch valid VP profile", NULL, NULL);


After creating the Cg context, we ask Cg to retrieve the most advanced vertex program profile available on the graphics card. These determine exactly what functionality can be achieved in the vertex shaders, and tells Cg how it should compile them when we load them. If we can't find a decent profile, then we error out.

     // VP LOADING

     if (!LoadProgram(&g_cgVP_GlowDarken, g_cgVertProfile, "cgprograms/glow_darken_vp.cg"))

     if (!LoadProgram(&g_cgVP_GlowBlur, g_cgVertProfile, "cgprograms/glow_blur_vp.cg"))

     if (!LoadProgram(&g_cgVP_GlowCombine, g_cgVertProfile, "cgprograms/glow_combine_vp.cg"))

Now that the vertex profile is set up, we can proceed to load and compile the vertex programs that we need. There are three vertex programs, as described earlier in this article. We use the LoadProgram utility function that we defined earlier to make the process easier.


     g_cgpVP0_ModelViewMatrix = cgGetNamedParameter(g_cgVP_GlowDarken, "ModelViewProj");

     g_cgpVP1_ModelViewMatrix = cgGetNamedParameter(g_cgVP_GlowBlur, "ModelViewProj");
     g_cgpVP1_XOffset = cgGetNamedParameter(g_cgVP_GlowBlur, "XOffset");
     g_cgpVP1_YOffset = cgGetNamedParameter(g_cgVP_GlowBlur, "YOffset");

     g_cgpVP2_ModelViewMatrix = cgGetNamedParameter(g_cgVP_GlowCombine, "ModelViewProj");

The final stage for loading the vertex programs is to grab handles to the uniform parameters that we need to specify in the vertex programs. Each program has a 4x4 matrix called "ModelViewProj" (which we set to the projection matrix multiplied by the modelview matrix when we come to use the program), and the second program also has two scalars, "XOffset" and "YOffset", which specify how much we should offset the source texture in each unit to do the blurring. (Look at the vertex program to see how it's used.)


     g_cgFragProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
     if (g_cgFragProfile == CG_PROFILE_UNKNOWN) {
          MessageBox(NULL, "Couldn't fetch valid FP profile", NULL, NULL);


Now that the vertex programs are loaded, it's time to do the same with the fragment programs (also known as pixel shaders in Direct3D terminology). We do the same for the fragment profile as we did for the vertex profile - try to find out what functionality is available, and if there's nothing useful, then error out.

     // FP LOADING

     if (!LoadProgram(&g_cgFP_GlowDarken, g_cgFragProfile, "cgprograms/glow_darken_fp.cg"))

     if (!LoadProgram(&g_cgFP_GlowBlur, g_cgFragProfile, "cgprograms/glow_blur_fp.cg"))

     if (!LoadProgram(&g_cgFP_GlowCombine, g_cgFragProfile, "cgprograms/glow_combine_fp.cg"))

The last part of the function loads the fragment programs themselves, again using the utility function LoadProgram that we created earlier. There are no uniform parameters that we need to specify to the fragment programs, so once we've loaded them, we're done with all the initialization.

Now that's done, we can move onto the rendering functionality that is called every frame to actually do the glow effect. Firstly, a couple more utility functions to make our life a bit easier in the rendering function itself. The next function we'll add is one that simply draws a quad with dimensions of 1x1 across the screen:

void DrawQuad(int width, int height)

     glVertex3f(0, 1, -1);
     glVertex3f(0, 0, -1);
     glVertex3f(1, 0, -1);
     glVertex3f(1, 1, -1);


For those who have used a rendering API such as OpenGL or Direct3D before, the texture coordinates may look a little different to the normal [0,1] range. This is due to the type of texture that will be used to store the image of the scene - instead of a regular 2D texture, we're using a rectangular texture via the GL_NV_texture_rectangle (or GL_EXT_texture_rectangle) OpenGL extensions, as mentioned earlier. They allow dimensions that're not powers of two, but texture coordinates have to be specified in the [0,width], [0,height] ranges rather than [0,1].

The other utility function we'll add is one that performs a blur pass:

void DoBlur(unsigned int uiSrcTex, unsigned int uiTargetTex, int srcTexWidth, int srcTexHeight, int destTexWidth, int destTexHeight, float xofs, float yofs)

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiSrcTex);

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiSrcTex);

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiSrcTex);

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiSrcTex);

     cgGLSetParameter1f(g_cgpVP1_XOffset, xofs);
     cgGLSetParameter1f(g_cgpVP1_YOffset, yofs);

     glViewport(0, 0, destTexWidth, destTexHeight);

     DrawQuad(srcTexWidth, srcTexHeight);
     glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiTargetTex);
     glCopyTexImage2D(GL_TEXTURE_RECTANGLE_NV, 0, GL_RGB, 0, 0, destTexWidth, destTexHeight, 0);

The first thing the function does is to activate the blurring vertex and fragment programs, so that any rendering from this point on goes through them. The next step is to bind the source texture to four texture units (so that the fragment program can sample the texture four times in four different places). The "XOffset" and "YOffset" uniform parameters of the blur vertex program are set, and the viewport is restricted to a rectangle the size of the destination texture's width and height. A quad is drawn covering the screen, with the texture coordinates such that all of the source texture is mapped onto the quad. Then, the target texture is bound, and the contents of the frame buffer is copied into the target texture.

How this function is used will become apparant when we go through the function that does the actual effect, which is what we need to specify next. As for the initialization function, I'll go through it step-by-step, explaining as we go.

void RenderScreenGlow(void)
     if (IEngineStudio.IsHardware() != 1)

     if (!g_bInitialised)

     if ((int)gEngfuncs.pfnGetCvarFloat("cg_blur_steps") == 0)

We see if we're in OGL mode, and if we're not, then bail out. Then, we check to see if we've done the initialization at all, and if not, execute the initialization function. Then, we check to see whether the "cg_blur_steps" CVAR (which is what we use to control how much blurring is done to the glow texture) is set to 0 - if it is, then we bail out, because no glow should be applied.

     // STEP 1: Grab the screen and put it into a texture


     glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiSceneTex);
     glCopyTexImage2D(GL_TEXTURE_RECTANGLE_NV, 0, GL_RGB, 0, 0, ScreenWidth, ScreenHeight, 0);

The first thing to do is to take the current contents of the framebuffer (which contains the map rendered as it would normally be) and store it in the scene texture, so that we can use it as a source to operate on, and so that we have it stored so that we can combine the glow with it in the final step of the rendering.

     // STEP 2: Set up an orthogonal projection

     glOrtho(0, 1, 1, 0, 0.1, 100);

We then set up an orthogonal projection. Simply put, an orthogonal projection is something that turns the screen into a 2D plane, giving depth no meaning. We set it so that the top-left is at (0,0) and the bottom-right is at (1,1) (so our DrawQuad utility function draws the quad across the entire screen). We also set the modelview matrix to the identity matrix (meaning that there won't be any rotation or translation of vertices), and set the current colour to white to avoid any weird blending from the colour being off-white.

     // STEP 3: Initialize Cg programs and parameters for darkening mid to dark areas of the scene



     cgGLSetStateMatrixParameter(g_cgpVP0_ModelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);

We enable the vertex and fragment profiles so that vertex and fragment programs will be run, rather than using OpenGL's fixed-function pipeline. The darkening programs are then loaded (see earlier for an explanation of what they do) and the "ModelViewMatrix" parameter of the vertex program is set to the required OpenGL matrix.

     // STEP 4: Render the current scene texture to a new, lower-res texture, darkening non-bright areas of the scene

     glViewport(0, 0, ScreenWidth/2, ScreenHeight/2);

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiSceneTex);

     DrawQuad(ScreenWidth, ScreenHeight);

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiBlurTex);
     glCopyTexImage2D(GL_TEXTURE_RECTANGLE_NV, 0, GL_RGB, 0, 0, ScreenWidth/2, ScreenHeight/2, 0);

The viewport is set to half the screen size (as the blur texture is half the screen size), and the scene texture is bound to the first texture unit. A quad is then drawn across the entire screen with the scene texture on it, and this is copied to the blur texture. The end result of this process is to have the original scene image in the blur texture at half the resolution and with dark to medium areas of colour made even darker.

     // STEP 5: Initialise Cg programs and parameters for blurring


     cgGLSetStateMatrixParameter(g_cgpVP1_ModelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);

The next step is to enable the blurring vertex and fragment programs and set the "ModelViewMatrix" parameter of the vertex program, as per the darkening vertex program earlier.

     // STEP 6: Apply blur

     int iNumBlurSteps = (int)gEngfuncs.pfnGetCvarFloat("cg_blur_steps");
     for (int i = 0; i < iNumBlurSteps; i++) {
          DoBlur(g_uiBlurTex, g_uiBlurTex, ScreenWidth/2, ScreenHeight/2, ScreenWidth/2, ScreenHeight/2, 1, 0);     
          DoBlur(g_uiBlurTex, g_uiBlurTex, ScreenWidth/2, ScreenHeight/2, ScreenWidth/2, ScreenHeight/2, 0, 1);

This stage is the one that does the grunt-work of blurring the darkened image of the scene to generate the glow that will be applied. We first blur horizontally, then blur the result of that vertically to give a Gaussian blur, and repeat the process however many times specified in the "cg_blur_steps" CVAR. The end result of this is a blurred version of the darkened image of the scene, with the amount of blurriness dependent on what "cg_blur_steps" is set to.

     // STEP 7: Set up Cg for combining blurred glow with original scene
     cgGLSetStateMatrixParameter(g_cgpVP2_ModelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiSceneTex);

     glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiBlurTex);

Now that we have our glow texture, all that's left is to combine it with the original scene. We enable the combining vertex and fragment programs and set the "ModelViewMatrix" parameter of the vertex program, like we did with the other two. The original image of the scene is bound to the first texture unit, and the darkened, blurred, lower-resolution version is bound to the second unit.

     // STEP 8: Do the combination, rendering to the screen without grabbing it to a texture

     glViewport(0, 0, ScreenWidth, ScreenHeight);

     DrawQuad(ScreenWidth/2, ScreenHeight/2);

We then render a quad across the entire screen (after setting the viewport to cover the whole frame buffer), which will be processed with the vertex and fragment programs to give the final image of the scene in the framebuffer. We don't need to store it in a texture, so we just leave it on-screen.

     // STEP 9: Restore the original projection and modelview matrices and disable rectangular textures on all units







The final step is to restore the original projection and modelview matrices, disable the use of vertex and fragment programs and disable rectangle textures on all four texture units.

All that's left now is to call the function from the right place. There are a few places that we can do it, but the only place where everything in the scene will get blurred is if we place it just before the HUD is rendered. Open up cdll_int.cpp and add the following just before the InitInput prototype:

extern void InitScreenGlow(void);
extern void RenderScreenGlow(void);

Then, find the implementation of HUD_Redraw and add this just before gHUD.Redraw is called:


If you compile and run, you should get the glow effect as desired. However, if you change the screen resolution, the textures don't resize as they should. So, find the implementation of HUD_VidInit and add this after VGui_Startup is called:


This will make it re-generate the textures when the screen resolution is changed. The glow effect is now (finally) complete.

The multi-pass system employed here is suitable for a variety of effects. Here are some suggestions for other effects that could be done:

  • Blurring - Rather than blurring to make a glow, simply blur the current scene and then render that to the screen. If you activate this when the player has received damage from a melee attack or falling (when the screen is skewed) it could create an interesting disorientation effect. Combine it with rendering two copies of the screen offset from one another for an interesting double-vision effect.

  • Emboss - For the more wacky mods, you could have an embossed effect. A convolution filter can be applied to the image (Google it) to do this with just one pass, such as the 3x3 filter given in this article on GameDev.net.

  • Inverse - Another simple-but-wacky effect would be to invert the image's colours, giving a feel as if it was a negative image. You could render an image of a film frame over the top to make it feel like a processed roll of film.

  • One of the above plus application map - You can blend one of the above effects with the original image in differing amounts by using another texture map that defines the opacity of the original effect. Simply multiply the effect at that pixel by the opacity at that pixel and add the original image multiplied by one minus the opacity.
  • attached files
  • glow_darken_fp.cg (0.32KB)
  • glow_darken_vp.cg (0.38KB)
  • tronmap_withglow.jpg (66.57KB)
  • tronmap_withoutglow.jpg (71.31KB)