#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;
PFNGLACTIVETEXTUREARBPROC glActiveTextureARB = NULL;
PFNGLMULTITEXCOORD2FARBPROC glMultiTexCoord2fARB = NULL;
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;
}
cgGLLoadProgram(*pDest);
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.void InitScreenGlow(void)
{
if (IEngineStudio.IsHardware() != 1)
return;
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.
// OPENGL EXTENSION LOADING
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.
// TEXTURE CREATION
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);
glTexParameteri(GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
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);
glTexParameteri(GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_RECTANGLE_NV, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
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.// CG INITIALISATION
g_cgContext = cgCreateContext();
if (!g_cgContext) {
MessageBox(NULL, "Couldn't make Cg context", NULL, NULL);
return;
}
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).
// VERTEX PROFILE
g_cgVertProfile = cgGLGetLatestProfile(CG_GL_VERTEX);
if (g_cgVertProfile == CG_PROFILE_UNKNOWN) {
MessageBox(NULL, "Couldn't fetch valid VP profile", NULL, NULL);
return;
}
cgGLSetOptimalOptions(g_cgVertProfile);
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"))
return;
if (!LoadProgram(&g;_cgVP_GlowBlur, g_cgVertProfile, "cgprograms/glow_blur_vp.cg"))
return;
if (!LoadProgram(&g;_cgVP_GlowCombine, g_cgVertProfile, "cgprograms/glow_combine_vp.cg"))
return;
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.
// VP PARAM GRABBING
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.)
// FRAGMENT PROFILE
g_cgFragProfile = cgGLGetLatestProfile(CG_GL_FRAGMENT);
if (g_cgFragProfile == CG_PROFILE_UNKNOWN) {
MessageBox(NULL, "Couldn't fetch valid FP profile", NULL, NULL);
return;
}
cgGLSetOptimalOptions(g_cgFragProfile);
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"))
return;
if (!LoadProgram(&g;_cgFP_GlowBlur, g_cgFragProfile, "cgprograms/glow_blur_fp.cg"))
return;
if (!LoadProgram(&g;_cgFP_GlowCombine, g_cgFragProfile, "cgprograms/glow_combine_fp.cg"))
return;
}
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.void DrawQuad(int width, int height)
{
glBegin(GL_QUADS);
glTexCoord2f(0,0);
glVertex3f(0, 1, -1);
glTexCoord2f(0,height);
glVertex3f(0, 0, -1);
glTexCoord2f(width,height);
glVertex3f(1, 0, -1);
glTexCoord2f(width,0);
glVertex3f(1, 1, -1);
glEnd();
}
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].void DoBlur(unsigned int uiSrcTex, unsigned int uiTargetTex, int srcTexWidth, int srcTexHeight, int destTexWidth, int destTexHeight, float xofs, float yofs)
{
cgGLBindProgram(g_cgVP_GlowBlur);
cgGLBindProgram(g_cgFP_GlowBlur);
glActiveTextureARB(GL_TEXTURE0_ARB);
glEnable(GL_TEXTURE_RECTANGLE_NV);
glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiSrcTex);
glActiveTextureARB(GL_TEXTURE1_ARB);
glEnable(GL_TEXTURE_RECTANGLE_NV);
glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiSrcTex);
glActiveTextureARB(GL_TEXTURE2_ARB);
glEnable(GL_TEXTURE_RECTANGLE_NV);
glBindTexture(GL_TEXTURE_RECTANGLE_NV, uiSrcTex);
glActiveTextureARB(GL_TEXTURE3_ARB);
glEnable(GL_TEXTURE_RECTANGLE_NV);
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.void RenderScreenGlow(void)
{
if (IEngineStudio.IsHardware() != 1)
return;
if (!g_bInitialised)
InitScreenGlow();
if ((int)gEngfuncs.pfnGetCvarFloat("cg_blur_steps") == 0)
return;
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
glActiveTextureARB(GL_TEXTURE0_ARB);
glEnable(GL_TEXTURE_RECTANGLE_NV);
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
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glOrtho(0, 1, 1, 0, 0.1, 100);
glColor3f(1,1,1);
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
cgGLEnableProfile(g_cgVertProfile);
cgGLEnableProfile(g_cgFragProfile);
cgGLBindProgram(g_cgVP_GlowDarken);
cgGLBindProgram(g_cgFP_GlowDarken);
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);
glActiveTextureARB(GL_TEXTURE0_ARB);
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
cgGLBindProgram(g_cgVP_GlowBlur);
cgGLBindProgram(g_cgFP_GlowBlur);
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
cgGLBindProgram(g_cgVP_GlowCombine);
cgGLBindProgram(g_cgFP_GlowCombine);
cgGLSetStateMatrixParameter(g_cgpVP2_ModelViewMatrix, CG_GL_MODELVIEW_PROJECTION_MATRIX, CG_GL_MATRIX_IDENTITY);
glActiveTextureARB(GL_TEXTURE0_ARB);
glEnable(GL_TEXTURE_RECTANGLE_NV);
glBindTexture(GL_TEXTURE_RECTANGLE_NV, g_uiSceneTex);
glActiveTextureARB(GL_TEXTURE1_ARB);
glEnable(GL_TEXTURE_RECTANGLE_NV);
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
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
cgGLDisableProfile(g_cgVertProfile);
cgGLDisableProfile(g_cgFragProfile);
glActiveTextureARB(GL_TEXTURE0_ARB);
glDisable(GL_TEXTURE_RECTANGLE_NV);
glActiveTextureARB(GL_TEXTURE1_ARB);
glDisable(GL_TEXTURE_RECTANGLE_NV);
glActiveTextureARB(GL_TEXTURE2_ARB);
glDisable(GL_TEXTURE_RECTANGLE_NV);
glActiveTextureARB(GL_TEXTURE3_ARB);
glDisable(GL_TEXTURE_RECTANGLE_NV);
glActiveTextureARB(GL_TEXTURE0_ARB);
}
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.cdll_int.cpp
and add the following just before the InitInput prototype:
// IMAGE-SPACE GLOW
extern void InitScreenGlow(void);
extern void RenderScreenGlow(void);
Then, find the implementation of HUD_Redraw
and add this just before gHUD.Redraw
is called:
// IMAGE-SPACE GLOW
RenderScreenGlow();
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:
// IMAGE-SPACE GLOW
InitScreenGlow();
This will make it re-generate the textures when the screen resolution is changed. The glow effect is now (finally) complete.You must log in to post a comment. You can login or register a new account.