Tutorial: Coding Fog Last edited 5 months ago2020-05-21 09:42:44 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: Notes: 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.

-Overfloater.

1 Comment

Commented 2 months 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?

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