Tutorial: Coding Fog Last edited 2 months ago2024-03-26 09:39:33 UTC

Hello and welcome to the improved tutorial on implementing fog into a Half-Life mod. In this tutorial I will guide you through the process of implementing an entity and it's client-side code, which will allow you to have fog in your mod.

This version contains a series of improvements over the original tutorial:
- The fog color, end and start distance can now be blended between two or more fog entities with the Blend Time setting in the env_fog entity.
- Now entities outside the fog boundaries will be culled out of the entity packet sent to the client.
- Models rendered by the client will also be culled out by the fog from rendering.
- The max visibility distance for monsters will now depend on the fog end distance, so monsters won't be able to see you if you don't see them.

Notes:
- The fog can't blend between a no-fog state and a fog state. This means that the fog won't dissipate when you turn it off, it will just disappear instantly, same for appearing instantly when first turned on. For this reason, if you want to blend between an outdoor fog state and an indoor one, I recommend having the indoor fog be of black color and a longer start and end distance.
- This effect will work only in OpenGL mode. Software mode does not support fog.
- If you specify a blend time in the fog entity, that fog entity won't turn off fog states when triggered to turn off. This is done on purpose, so the previous fog state is not cleared, allowing for blending. This means that you can't arbitrarily turn off fogging with blended fog entities.
- For blending between two fog entities, you need for both of them to have a blend time set, otherwise the fog states will be cleared on the client by the entity that is turned off, and blending will not work.
- Multiple fog states will only work in single player mode. For multiplayer, you should only have a single fog entity that isn't switched on and off. This is because the fog states are not individually tracked for each client, they are global across all clients.

To test out the fog in action, I've added a test mod below:
Loading embedded content: Vault Item #6421
So if you've decided to want to implement fog, then let's begin. As a start, you'll need to download the files for the tutorial from the following Vault link:
Loading embedded content: Vault Item #6422
This includes a header and a source file, and the rmf of the test map used.

First of all, open up your FGD file and add the following entry for the fog entity:
@PointClass base(Targetname) size(-16 -16 -16, 16 16 16) = env_fog : "Client Fog"
[
    startdist(integer) : "Start Distance" : 1
    enddist(integer) : "End Distance" : 500
    rendercolor(color255) : "Fog Color (R G B)" : "0 0 0"
    blendtime(integer): "Blend time" : 0
    spawnflags(flags) =
    [
       1 : "Start On" : 0
    ]
]
Open up your solution for your mod in Visual Studio, and go to the server library. First of all we're going to add the entity definition, so open up effects.cpp, and at the bottom add the following piece of code:
//=========================================================
// Spawnflags
//=========================================================
#define SF_FOG_ACTIVE    1

//=========================================================
// Global variables for fog
//=========================================================
int CEnvFog::g_iCurrentEndDist = 0;
int CEnvFog::g_iIdealEndDist = 0;
float CEnvFog::g_flBlendDoneTime = 0;

//=========================================================
// Externals
//=========================================================
extern int gmsgFog;

//=========================================================
// CEnvFog
//=========================================================
LINK_ENTITY_TO_CLASS( env_fog, CEnvFog );
TYPEDESCRIPTION CEnvFog::m_SaveData[] =
{
    DEFINE_FIELD( CEnvFog, m_iStartDist, FIELD_INTEGER ),
    DEFINE_FIELD( CEnvFog, m_iEndDist, FIELD_INTEGER ),
    DEFINE_FIELD( CEnvFog, m_bActive, FIELD_BOOLEAN ),
    DEFINE_FIELD( CEnvFog, m_flBlendTime, FIELD_FLOAT ),
};
IMPLEMENT_SAVERESTORE( CEnvFog, CBaseEntity );

void CEnvFog::KeyValue( KeyValueData *pkvd )
{
    if (FStrEq(pkvd->szKeyName, "startdist"))
    {
        m_iStartDist = atoi(pkvd->szValue);
        pkvd->fHandled = TRUE;
    }
    else if (FStrEq(pkvd->szKeyName, "enddist"))
    {
        m_iEndDist = atoi(pkvd->szValue);
        pkvd->fHandled = TRUE;
    }
    else if (FStrEq(pkvd->szKeyName, "blendtime"))
    {
        m_flBlendTime = atof(pkvd->szValue);
        pkvd->fHandled = TRUE;
    }
    else
        CBaseEntity::KeyValue( pkvd );
}

void CEnvFog::Spawn( void )
{
    Precache();

    pev->solid = SOLID_NOT;
    pev->effects |= EF_NODRAW;
    pev->movetype = MOVETYPE_NONE;

    if(FBitSet(pev->spawnflags, SF_FOG_ACTIVE)
        || FStringNull(pev->targetname))
        m_bActive = TRUE;
    else
        m_bActive = FALSE;
}

void CEnvFog::Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value )
{
    BOOL prevState = m_bActive;
    switch(useType)
    {
    case USE_OFF:
        m_bActive = FALSE;
        break;
    case USE_ON:
        m_bActive = TRUE;
        break;
    default:
        m_bActive = !m_bActive;
        break;
    }

    // Only update if it was changed
    if(prevState != m_bActive)
    {
        // Update fog msg
        UpdateFog(m_bActive, TRUE, NULL);

        if(m_bActive || !m_flBlendTime)
        {
            // Set globalvars for target fog
            CEnvFog::SetCurrentEndDist(m_bActive ? m_iEndDist : 0, m_flBlendTime);
        }
    }
}

void CEnvFog::UpdateFog( BOOL isOn, BOOL doBlend, CBaseEntity* pPlayer )
{
    if(isOn)
    {
        if(pPlayer)
            MESSAGE_BEGIN(MSG_ONE, gmsgFog, NULL, pPlayer->pev);
        else
            MESSAGE_BEGIN(MSG_ALL, gmsgFog, NULL);

            WRITE_COORD(m_iStartDist);
            WRITE_COORD(m_iEndDist);
            WRITE_BYTE(pev->rendercolor.x);
            WRITE_BYTE(pev->rendercolor.y);
            WRITE_BYTE(pev->rendercolor.z);
            if(doBlend)
                WRITE_COORD(m_flBlendTime);
            else
                WRITE_COORD(0);
        MESSAGE_END();
    }
    else if(!m_flBlendTime)
    {
        if(pPlayer)
            MESSAGE_BEGIN(MSG_ONE, gmsgFog, NULL, pPlayer->pev);
        else
            MESSAGE_BEGIN(MSG_ALL, gmsgFog, NULL);

            WRITE_COORD(0);
            WRITE_COORD(0);
            WRITE_BYTE(0);
            WRITE_BYTE(0);
            WRITE_BYTE(0);
            WRITE_COORD(0);
        MESSAGE_END();
    }
}

void CEnvFog::SendInitMessages ( CBaseEntity* pPlayer )
{
    if(!m_bActive)
        return;

    UpdateFog(TRUE, FALSE, pPlayer);

    CEnvFog::SetCurrentEndDist(m_iEndDist, m_flBlendTime);
}

void CEnvFog::SetCurrentEndDist( int enddist, float blendtime )
{
    if((!blendtime || g_iCurrentEndDist < enddist) || !g_iCurrentEndDist)
    {
        g_iCurrentEndDist = enddist;
        g_iIdealEndDist = 0;
        g_flBlendDoneTime = 0;
    }
    else
    {
        g_iIdealEndDist = enddist;
        g_flBlendDoneTime = gpGlobals->time + blendtime;
    }
}

void CEnvFog::FogThink( void )
{
    if(!g_flBlendDoneTime || !g_iIdealEndDist)
        return;

    if(g_flBlendDoneTime <= gpGlobals->time)
    {
        g_iCurrentEndDist = g_iIdealEndDist;
        g_iIdealEndDist = 0;
        g_flBlendDoneTime = 0;
    }
}

BOOL CEnvFog::CheckBBox( edict_t* pplayer, edict_t* pedict )
{
    if(!g_iCurrentEndDist)
        return FALSE;

    // Don't let fog cull underwater
    if(pplayer->v.waterlevel == 3)
        return FALSE;

    // Calculate distance to edge
    Vector boxTotal = Vector(g_iCurrentEndDist, g_iCurrentEndDist, g_iCurrentEndDist);
    float edgeLength = boxTotal.Length();

    // Set the fog bbox mins maxs
    Vector viewOrigin = pplayer->v.origin + pplayer->v.view_ofs;
    Vector fogMins, fogMaxs;
    for(int i = 0; i < 3; i++)
    {
        fogMins[i] = viewOrigin[i] - edgeLength;
        fogMaxs[i] = viewOrigin[i] + edgeLength;
    }

    // Set the entity mins/maxs
    vec3_t entMins, entMaxs;
    entMins = pedict->v.origin+pedict->v.mins;
    entMaxs = pedict->v.origin+pedict->v.maxs;

    if (fogMins[0] > entMaxs[0])
        return TRUE;

    if (fogMins[1] > entMaxs[1])
        return TRUE;

    if (fogMins[2] > entMaxs[2])
        return TRUE;

    if (fogMaxs[0] < entMins[0])
        return TRUE;

    if (fogMaxs[1] < entMins[1])
        return TRUE;

    if (fogMaxs[2] < entMins[2])
        return TRUE;

    return FALSE;
}
Next up, open up the effects.h header and before the #endif at the end, we'll add the class definition:
//=========================================================
// CEnvFog
//=========================================================
class CEnvFog : public CBaseEntity
{
public:
    void    Spawn( void );
    void    SendInitMessages( CBaseEntity* pPlayer = NULL );
    void    KeyValue( KeyValueData *pkvd );
    void    UpdateFog( BOOL isOn, BOOL doBlend, CBaseEntity* pPlayer );
    void    Use( CBaseEntity *pActivator, CBaseEntity *pCaller, USE_TYPE useType, float value );

    virtual int        Save( CSave &save );
    virtual int        Restore( CRestore &restore );

public:
    static void SetCurrentEndDist( int enddist, float blendtime );
    static int GetCurrentEndDist( void ) { return g_iCurrentEndDist; }

    static void FogThink( void );
    static BOOL CheckBBox( edict_t* pplayer, edict_t* pedict );

private:
    static    TYPEDESCRIPTION m_SaveData[];

    int        m_iStartDist;
    int        m_iEndDist;
    float    m_flBlendTime;
    BOOL    m_bActive;

private:
    static int g_iCurrentEndDist;
    static int g_iIdealEndDist;
    static float g_flBlendDoneTime;
};
These define the env_fog entity, and some helper functions that are used on the server side. As you can see in the class definition, we're adding a function called "SendInitMessages". This will be called by each player upon spawning, so that the entities send their data to that client.

We need to add some extra routines for this particular functionality in the player code. Open up player.h, and look up the following piece of code:
void CBasePlayer::TabulateAmmo( void );
And add this:
virtual void InitializeEntities( void );
Next up, find this line:
float m_flNextChatTime;
Add this new line after:
BOOL m_bSendMessages;
Next up, open up player.cpp, and before this line of code:
void LinkUserMessages( void )
Add this:
int gmsgFog = 0;
Within the LinkUserMessages function, add this new line:
gmsgFog = REG_USER_MSG("Fog", 9);
Next up, look up the CBasePlayer :: Precache function, and at the bottom of the function definition, add this:
m_bSendMessages = TRUE;
This will inform the player code, so that whenever a player spawns, it will send through the init messages for all the entities. Following this, go to the CBasePlayer :: UpdateClientData function, and at the top, add the following piece of code:
if( m_bSendMessages )
{
    InitializeEntities();
    m_bSendMessages = FALSE;
}
Now before the CBasePlayer :: FBecomeProne function, add this new function:
//=========================================================
// InitializeEntities
//=========================================================
void CBasePlayer :: InitializeEntities ( void )
{
    edict_t* pEdict = g_engfuncs.pfnPEntityOfEntIndex( 1 );
    CBaseEntity* pEntity;

    for(int i = 0; i < gpGlobals->maxEntities; i++, pEdict++)
    {
        if(pEdict->free)
            continue;

        pEntity = CBaseEntity::Instance( pEdict );
        if(!pEntity)
            break;

        pEntity->SendInitMessages(this);
    }
}
We'll need to venture over to the client project for a momment. Open up hl_baseentity.cpp, and before the definition of the ClearMultiDamage blank function, add this line:
void CBasePlayer::InitializeEntities( void ) {};
Next, open up cbase.h, and find this line in the class definition of CBaseEntity:
virtual const char *TeamID( void ) { return ""; }
And add this new function definition:
virtual void    SendInitMessages( CBaseEntity* pPlayer = NULL ) {};
Next, open up client.cpp and add a new include at the top with the others:
#include "effects.h"
Go to the StartFrame function, and at the bottom, add this call:
CEnvFog::FogThink();
After this, we will add the piece of code that does fog culling on packet entities. This will allow you to save on visible and rendered entities using the fog effect. Look up this piece of code:
if ( !ENGINE_CHECK_VISIBILITY( (const struct edict_s *)ent, pSet ) )
{
    return 0;
}
And right after, add this:
if ( CEnvFog::CheckBBox( host, ent ) )
{
    return 0;
}
Find the ServerActivate function, and at the very bottom of the function add this call:
// Reset fog when reloading
CEnvFog::SetCurrentEndDist(0, 0);
Now that that is done, we will be adding the fog distance checks to the monster code. Open up monsters.cpp, and in the MonsterInit function, remove these two lines:
m_flDistTooFar = 1024.0;
m_flDistLook = 2048.0;
Next up, go to the MonsterThink function, and add this new bit of code before the call to RunAI:
int fogEndDist = CEnvFog::GetCurrentEndDist();
if(fogEndDist > 0)
{
    m_flDistTooFar = fogEndDist - 100;
    m_flDistLook = fogEndDist + 300;
}
else
{
    m_flDistTooFar = 1024.0;
    m_flDistLook = 2048.0;
}
With that, we're done with the server side implementation. Open up the client library, and first of all, copy the fog.cpp and fog.h files from the tutorial files zip, and add them to our cl_dll folder in your source code library, then add these files to your client project file.

Next, open up entity.cpp, and add the following include after the others at the top:
#include "fog.h"
Now go to the HUD_CreateEntities, and at the bottom of the function add this call:
gFog.HUD_CreateEntities();
Open up hud.cpp, and at the top, add the following include to the next to others:
#include "fog.h"
In the CHud :: Init function, add the following call at the end:
gFog.Init();
In the CHud :: VidInit function, add this at the end also:
gFog.VidInit();
Open up studio_util.cpp, and at the bottom add this function definition:
/*
====================
VectorRotate

====================
*/
void VectorRotate (const float *in1, float in2[3][4], float *out)
{
    out[0] = DotProduct(in1, in2[0]);
    out[1] = DotProduct(in1, in2[1]);
    out[2] = DotProduct(in1, in2[2]);
}
Open the header for this file, and add this line at the bottom before the #endif:
void    VectorRotate (const float *in1, float in2[3][4], float *out);
Next we'll add culling on rendered models. Open up StudioModelRenderer.cpp and add this include at the top next to the others:
#include "fog.h"
We'll be adding a new function to the CStudioModelRenderer class, which will calculate a bounding box for us based on the current sequence info. At the bottom of the file, add this new function:
/*
====================
StudioGetMinsMaxs

====================
*/
void CStudioModelRenderer::StudioGetMinsMaxs ( vec3_t& outMins, vec3_t& outMaxs )
{
    if (m_pCurrentEntity->curstate.sequence >=  m_pStudioHeader->numseq)
        m_pCurrentEntity->curstate.sequence = 0;

    // Build full bounding box
    mstudioseqdesc_t *pseqdesc = (mstudioseqdesc_t *)((byte *)m_pStudioHeader + m_pStudioHeader->seqindex) + m_pCurrentEntity->curstate.sequence;

    vec3_t vTemp;
    static vec3_t vBounds[8];

    for (int i = 0; i < 8; i++)
    {
        if ( i & 1 ) vTemp[0] = pseqdesc->bbmin[0];
        else vTemp[0] = pseqdesc->bbmax[0];
        if ( i & 2 ) vTemp[1] = pseqdesc->bbmin[1];
        else vTemp[1] = pseqdesc->bbmax[1];
        if ( i & 4 ) vTemp[2] = pseqdesc->bbmin[2];
        else vTemp[2] = pseqdesc->bbmax[2];
        VectorCopy( vTemp, vBounds[i] );
    }

    float rotationMatrix[3][4];
    m_pCurrentEntity->angles[PITCH] = -m_pCurrentEntity->angles[PITCH];
    AngleMatrix(m_pCurrentEntity->angles, rotationMatrix);
    m_pCurrentEntity->angles[PITCH] = -m_pCurrentEntity->angles[PITCH];

    for (int i = 0; i < 8; i++ )
    {
        VectorCopy(vBounds[i], vTemp);
        VectorRotate(vTemp, rotationMatrix, vBounds[i]);
    }

    // Set the bounding box
    outMins = Vector(9999, 9999, 9999);
    outMaxs = Vector(-9999, -9999, -9999);
    for(int i = 0; i < 8; i++)
    {
        // Mins
        if(vBounds[i][0] < outMins[0]) outMins[0] = vBounds[i][0];
        if(vBounds[i][1] < outMins[1]) outMins[1] = vBounds[i][1];
        if(vBounds[i][2] < outMins[2]) outMins[2] = vBounds[i][2];

        // Maxs
        if(vBounds[i][0] > outMaxs[0]) outMaxs[0] = vBounds[i][0];
        if(vBounds[i][1] > outMaxs[1]) outMaxs[1] = vBounds[i][1];
        if(vBounds[i][2] > outMaxs[2]) outMaxs[2] = vBounds[i][2];
    }

    VectorAdd(outMins, m_pCurrentEntity->origin, outMins);
    VectorAdd(outMaxs, m_pCurrentEntity->origin, outMaxs);
}
We also need to add the function declaration in the header file, so open up the header for this file, and in the class declaration of CStudioModelRenderer, after the following bit:
virtual void StudioProcessGait ( entity_state_t *pplayer );
Add this:
// Sets bounding box
virtual void StudioGetMinsMaxs ( vec3_t& outMins, vec3_t& outMaxs );
We'll add fog culling to the view culling bit of the renderer in the cpp file. Look up two instances of the following code:
// see if the bounding box lets us trivially reject, also sets
if (!IEngineStudio.StudioCheckBBox ())
    return 0;
And after each of these, add the following:
vec3_t mins, maxs;
StudioGetMinsMaxs(mins, maxs);
if (gFog.CullFogBBox(mins, maxs))
    return 0;
This is mainly useful to cull out temporary entities like bullet shells, gibs, pieces of metal fragments and wood chunks, etc, because the packet fog culling on the server side should already take care of this for the map entities.

Next, open up tri.cpp, and at the top add the following include:
#include "fog.h"
In the HUD_DrawNormalTriangles function, add the following call:
gFog.HUD_DrawNormalTriangles();
And in the HUD_DrawTransparentTriangles call, add this:
gFog.HUD_DrawTransparentTriangles();
Open up view.cpp, and add this include at the top:
#include "fog.h"
Look up the V_CalcRefdef function, and at the bottom of the function add this call:
gFog.V_CalcRefDef(pparams);
As a last thing, in your solution explorer, you need to right click on the cl_dll project and select Properties. There, go into Linker->Input, and in "Additional Dependencies", add "opengl32.lib". You need to do this for both your Release and Debug builds.

That should conclude the tutorial. Now compile your libraries and copy them to the destination folders. You can copy the fogtest.bsp file from the example mod files, or just compile it yourself from the source files package. When you load up the map, you should be able to activate the fog with the buttons.

I hope you enjoy your new fog entity. Due to Valve's 25th anniversary update breaking the fog, I have updated the tutorial source files to include custom shaders that fix this problem. You will need to copy the folder "gl_shaders" to your mod folder in order to make the fix work. You can find this in either of the vault items linked in this tutorial.

-Overfloater.

8 Comments

Commented 3 years ago2020-08-05 14:32:53 UTC Comment #102855
Following this though on a clean build of Solokiller's Half-Life Updated (on VS2019), it throws a single error on compile:

cdll_int.h(38,13): error C2040: 'HSPRITE': 'int' differs in levels of indirection from 'HSPRITE__ *'

Any ideas as to what is causing that?
Commented 2 years ago2021-09-12 13:51:37 UTC Comment #103716
This tutorial is not meant for Solokiller's SDK it seems. Looks like I have to use my brain for this one after all >:[
Probably replacing all instances of vec_3t with Vector and DotProduct with FDotProduct should do the trick
Commented 2 years ago2021-09-12 13:54:30 UTC Comment #103717
Also this tutorial doesn't specify where fog.h and fog.cpp should go
Hold on, it does. I'm blind
Commented 1 year ago2022-11-26 06:59:42 UTC Comment #104907
for Solokiller's Updated SDK users, replace all includes to "windows.h" to "PlatfromHeaders.h"
Commented 4 months ago2023-12-28 05:24:14 UTC Comment #105803
This article is outdated and should be updated for the 25th-anniversary update. Legacy GL functions are not functioning in the latest version of HL engine.
Commented 2 months ago2024-03-10 00:07:59 UTC Comment #106053
@FranticDreamer
That is not true, latest version of HL engine still uses Legacy GL functions but has shader compatibility. The reason why this tutorial doesn't work is that the shaders DON'T know if glEnable(GL_FOG) is run, it just is impossible. To make this tutorial work, theoretically you should replace glEnable(GL_FOG) with something like glUniform1i(glGetUniformLocation(SHADERID, "fogEnabled"), (int)true) but I have ZERO clue on how to find SHADERID atm.
There is also an alternative method, you will have to modify the shader code for your mod though; which should be in "Half-Life/platform/gl_shaders/fs_world.frag". Replace uniform bool fogEnabled with layout(location = 0) uniform bool fogEnabled. Now go back into your code and replace every glEnable(GL_FOG) with glUniform1i(0, (int)true) and obviously replace every glDisable(GL_FOG) with glUniform1i(0, (int)false).
(And yes, I don't know how to reply lol)
Commented 2 months ago2024-03-26 11:52:16 UTC Comment #106089
Update has been made available that fixes the fog tutorial for the HL25 update.
Commented 1 month ago2024-04-20 11:13:05 UTC Comment #106145
This tutorial sends messages to the player on every spawn, which is probably not something we want in multiplayer.

Suggested change, in CBasePlayer::Precache function, add m_bSendMessages here:
if ( gInitHUD )
{
    m_fInitHUD = TRUE;
    m_bSendMessages = TRUE;
}

You must log in to post a comment. You can login or register a new account.