Welcome, Guest! Login | Register

Effects Fun Part I [Print this Article]
Posted by: Patrick
Date posted: Aug 11 2004
User Rating: 4.5 out of 5.0
Number of views: 6584
Number of comments: 14
Description: The BSP map format that Half-Life uses makes it fairly easy to do certain types of special effects, including fake underwater caustics, so we'll play around with that a little...
I plan on doing a tutorials in this "Effects Fun" series. This one is on fake underwater caustics. Anyways...

Oh yeah, please note, this tutorial will only work in OpenGL.

Start out by opening up your cl_dlls workspace. Most likely under your "External Dependencies" folder is com_model.h, which you need to open. You see, in its current state, Half-Life has two different renderers. If you look in your Half-Life directory, you'll notice a "sw.dll" and a "hw.dll". SW is for software, as in the software renderer, and likewise for HW (hardware). It uses slightly different data structures, depending on the renderer. Refer to the attached updated com_model.h file for the new and updated data structures (thanks Sneaky_Bastard!).

*Update*
Originally I just copy and pasted the new data structures directly into this article, but there were some issues with that. What I honestly overlooked is that I didn't copy the surrounding comments from the original com_model.h that were placed in by the author, Sneaky_Bastard. If you've been using the old version that was originally pasted here, I encourage you to download the original copy written/modified by Sneaky_Bastard and give him proper credits. I can not say enough how sorry I am for this mistake.
Zip FileFilename: com_model.zip
File Size: 4.4 KB
*/Update*

Ok, now create a new folder in your workspace called "Caustics," or something along those lines. Create a file called "Caustics.h" and place it in there. Paste this in your new header:
 CODE (C++) 

#ifndef _CAUSTICS_H_
#define _CAUSTICS_H_

/*
CSurface:
a sort of "wrapper" class for the msurface_s objects, stored in model_s'
*/


class CSurface
{
protected:
    msurface_s *m_pSurface;

public:
    CSurface (msurface_s *pSurface);
    virtual ~CSurface (void);

    struct msurface_s *getSurface (void);
    void setSurface (msurface_s *pSurface);

    operator msurface_s * (void);

    msurface_s *operator = (msurface_s *pSurface);
    msurface_s *operator -> (void);
};

struct TexCoord_t
{
    float flTexCoords[2];
};

/*
CSurfaceAssociation:
How Quake/Half-Life stores the polygon information is kind of cryptic, so here's
a nice little class to ease the process of rendering them
*/


class CSurfaceAssociation
{
protected:
    bool m_bRendered;
    int m_iNumVertices;
    Vector *m_vecVertices;
    TexCoord_t *m_TexCoords;
    TexCoord_t *m_LightMapCoords;

    CSurface &m_hSurfaceObject;

public:
    CSurfaceAssociation (CSurface &hSurfaceObject);
    virtual ~CSurfaceAssociation (void);

    CSurface &getSurfaceObject (void);
    bool Rendered (void) const;

    void ToggleRendered (int bRendered = -1);

    void Render (triangleapi_t *t, Vector vecEntityBounds[2],
             model_s *pModelTexture, int iFrame);
};

/*
CSurfaceManager:
stores CSurfaceAssociation objects, and make sure that there is no duplicate
CSurfaceAssociation for an msurface_s
*/


class CSurfaceManager
{
protected:
    int m_iNumSurfaceEntries;
    CSurface **m_pSurfaceObjects;
    CSurfaceAssociation **m_pSurfaceAssociations;

private:
    void AddSurface (msurface_s *pSurface);
    void ReadLeaf (mleaf_s *pLeaf);


public:
    CSurfaceManager (model_s *pSourceModel);
    virtual ~CSurfaceManager (void);

    int getNumSurfaceEntries (void) const;
    CSurface *getSurfaceObject (int iIndex);
    CSurfaceAssociation *getSurfaceAssociation (int iIndex);
    CSurfaceAssociation *getSurfaceAssociation (msurface_s *pSurface);

    void RenderFilteredSurfaces (msurface_s **pMarkSurfaces, int iNumMarkSurfaces,
             cl_entity_s *pEntity, model_s *pModelTexture,
             int iFrame, bool bFilterWaterSurfaces = true);
};

// misc. helper functions
float ClampFloat (float flFloat, float flLower, float flHigher);
float CalculateAlpha (const Vector &pos, const Vector &vert, const Vector &mins,
                      const Vector &maxs);

#endif

Ok, I know that's a lot. CSurface is a sort of "wrapper class" for msurface_s. You might ask why not just use the msurface_s objects themselves, especially considering there's no REAL new member variables/functionality. My reasoning for it is extendability--let's say you decide to implement a texture manager class, along with texture objects, and have a nice robust animation system; if you do, you already have the class here, now you just have to add the new member variables, etc...

On to the next new class: CSurfaceAssociation
CSurfaceAssociation associates a CSurface/msurface_s object with our own form of polygon. Later on, in the class implementation, you'll see how reading the internal Half-Life vertex/polygon data is sort of cryptic. This class puts it in fairly plan and simple terms.

Finally, the last new class: CSurfaceManager
CSurfaceManager takes in a model_s pointer (ie, the world model, if you read my wireframe tutorial as well), which is where Half-Life stores its own world/polygon/vertex/whatever data. Half-Life stores all of its polygon data in leafs. All leafs have a contents type, like water, lava, empty, etc. We're basing what surfaces to draw caustics over on leafs with contents of CONTENTS_WATER. Each frame, the engine loops through all visible leafs and "marks" them. It then builds a list of "marksurfaces" (ie marked surfaces) from the surfaces of the marked leafs. After that it culls the polygons to the view frustum, so non-visible polygons are not rendered, but we're not too concerned with that right now.

Ok, so where does this all come into play? By time the engine gets to calling the HUD_DrawXTriangles functions, the marksurfaces list is already made. HOWEVER, most likely not all marked surfaces should have caustics drawn over them, and there's no default way of telling what the contents type of the leaf that contains any given surface is. That's the whole purpose of our new classes-to draw caustics polygons over the correct world polygons.

Finally, on with the tutorial. Create three new source files: "Surface.cpp", "SurfaceAssociation.cpp", and you guess it: "SurfaceManager.cpp". Paste this stuff in Surface.cpp:
 CODE (C++) 

#include "hud.h"
#include "cl_util.h"
#include "const.h"
#include "com_model.h"
#include "studio.h"
#include "entity_state.h"
#include "cl_entity.h"
#include "dlight.h"
#include "triangleapi.h"
#include "r_studioint.h"
#include "StudioModelRenderer.h"
#include "GameStudioModelRenderer.h"

#include "Caustics.h"

CSurface :: CSurface (msurface_s *pSurface)
{
    m_pSurface = pSurface;
}

CSurface :: ~CSurface (void)
{
}

msurface_s *CSurface :: getSurface (void)
{
    return m_pSurface;
}

void CSurface :: setSurface (msurface_s *pSurface)
{
    m_pSurface = pSurface;
}

CSurface :: operator struct msurface_s * (void)
{
    return m_pSurface;
}

msurface_s *CSurface :: operator = (msurface_s *pSurface)
{
    m_pSurface = pSurface;

    return m_pSurface;
}

msurface_s *CSurface :: operator -> (void)
{
    return m_pSurface;
}


SurfaceAssociation.cpp:
 CODE (C++) 
#include "hud.h"
#include "cl_util.h"
#include "const.h"
#include "com_model.h"
#include "studio.h"
#include "entity_state.h"
#include "cl_entity.h"
#include "dlight.h"
#include "triangleapi.h"
#include "r_studioint.h"
#include "StudioModelRenderer.h"
#include "GameStudioModelRenderer.h"

#include "Caustics.h"

CSurfaceAssociation :: CSurfaceAssociation (CSurface &hSurfaceObject)
: m_hSurfaceObject (hSurfaceObject)
{
    m_bRendered = false;

    if (m_hSurfaceObject->polys == NULL)
    {
        m_iNumVertices = 0;
        m_vecVertices = NULL;
        m_TexCoords = NULL;
        m_LightMapCoords = NULL;
    }
    else
    {
        glpoly_s *pPolys = m_hSurfaceObject->polys;
        float *v = pPolys->verts[0];

        m_iNumVertices = pPolys->numverts;
        m_vecVertices = new Vector [m_iNumVertices];
        m_TexCoords = new TexCoord_t [m_iNumVertices];
        m_LightMapCoords = new TexCoord_t [m_iNumVertices];

        for (int i = 0; i < m_iNumVertices; i++)
        {
            m_vecVertices[i].x = *v++;
            m_vecVertices[i].y = *v++;
            m_vecVertices[i].z = *v++;

            m_TexCoords[i].flTexCoords[0] = *v++;
            m_TexCoords[i].flTexCoords[1] = *v++;

            m_LightMapCoords[i].flTexCoords[0] = *v++;
            m_LightMapCoords[i].flTexCoords[1] = *v++;
        }
    }
}

CSurfaceAssociation :: ~CSurfaceAssociation (void)
{
    if (m_iNumVertices > 0)
    {
        delete [] m_vecVertices;
        delete [] m_TexCoords;
        delete [] m_LightMapCoords;
    }
}

CSurface &CSurfaceAssociation :: getSurfaceObject (void)
{
    return m_hSurfaceObject;
}

bool CSurfaceAssociation :: Rendered (void) const
{
    return m_bRendered;
}

void CSurfaceAssociation :: ToggleRendered (int bRendered)
{
    if (bRendered < 0)
        m_bRendered = !m_bRendered;
    else
        m_bRendered = (bRendered ? true : false);
}

void CSurfaceAssociation :: Render (triangleapi_t *t, Vector vecEntityBounds[2],
            model_s *pModelTexture, int iFrame)
{
    // calculate the center of the top and bottom entity bounds polygons, and store
    // them
    Vector vecTop (((vecEntityBounds[0].x + vecEntityBounds[1].x) / 2),
        ((vecEntityBounds[0].y + vecEntityBounds[1].y) / 2),
        vecEntityBounds[1].z);

    Vector vecBottom (vecTop.x, vecTop.y, vecEntityBounds[0].z);

    t->CullFace (TRI_NONE);
    t->RenderMode (kRenderTransAdd);

    if (pModelTexture != NULL)
    {
        t->SpriteTexture (pModelTexture, iFrame);
    }

    t->Begin (TRI_POLYGON);

    for (int i = 0; i < m_iNumVertices; i++)
    {
        float flAlpha = CalculateAlpha (m_vecVertices[i], vecTop, vecBottom,
                vecTop);

        // t->Color4f (1.0f, 1.0f, 1.0f, flAlpha);
        t->Color4f (1.0f, 1.0f, 1.0f, 1.0);
        t->TexCoord2f (m_TexCoords[i].flTexCoords[0],
               m_TexCoords[i].flTexCoords[1]);
        t->Vertex3fv (m_vecVertices[i]);
    }

    t->End ();
}

float ClampFloat (float flFloat, float flLower, float flHigher)
{
    if (flFloat < flLower) return flLower;
    if (flFloat > flHigher) return flHigher;

    return flFloat;
}

float CalculateAlpha (const Vector &pos, const Vector &vert,
        const Vector &mins, const Vector &maxs)
{
    float z = (pos - vert).Length ();
    float f = (z / (maxs - mins).Length ());
   
    return ClampFloat (f, 0.0f, 1.0f);
}

As you should be able to tell, each internal Half-Life polygon is stored in one giant float array, totalling 7 floats per vertex. Essentially, we're "rolling them out."

SurfaceManager.cpp:
 CODE (C++) 

#include "hud.h"
#include "cl_util.h"
#include "const.h"
#include "com_model.h"
#include "studio.h"
#include "entity_state.h"
#include "cl_entity.h"
#include "dlight.h"
#include "triangleapi.h"
#include "r_studioint.h"
#include "StudioModelRenderer.h"
#include "GameStudioModelRenderer.h"

#include "Caustics.h"

CSurfaceManager :: CSurfaceManager (model_s *pModelSource)
{
    m_iNumSurfaceEntries = 0;
    m_pSurfaceObjects = NULL;
    m_pSurfaceAssociations = NULL;

    for (int i = 0; i < pModelSource->numleafs; i++)
    {
        mleaf_s *pLeaf = &pModelSource->leafs[i];

        if (pLeaf->contents == CONTENTS_WATER)
            ReadLeaf (pLeaf);
    }
}

CSurfaceManager :: ~CSurfaceManager (void)
{
    for (int i = 0; i < m_iNumSurfaceEntries; i++)
    {
        delete m_pSurfaceObjects[i];
        delete m_pSurfaceAssociations[i];
    }

    if (m_iNumSurfaceEntries > 0)
    {
        delete [] m_pSurfaceObjects[i];
        delete [] m_pSurfaceAssociations[i];
    }
}

void CSurfaceManager :: AddSurface (msurface_s *pSurface)
{
    if (getSurfaceAssociation (pSurface) != NULL)
        return;

    int i = 0;
    const int iOldSize = m_iNumSurfaceEntries;
    CSurface **pOldSurfaceObjects = m_pSurfaceObjects;
    CSurfaceAssociation **pOldSurfaceAssociations = m_pSurfaceAssociations;

    m_iNumSurfaceEntries++;

    m_pSurfaceObjects = new CSurface *[m_iNumSurfaceEntries];
    m_pSurfaceAssociations = new CSurfaceAssociation *[m_iNumSurfaceEntries];

    for (i; i < iOldSize; i++)
    {
        m_pSurfaceObjects[i] = pOldSurfaceObjects[i];
        m_pSurfaceAssociations[i] = pOldSurfaceAssociations[i];
    }

    m_pSurfaceObjects[i] = new CSurface (pSurface);
    m_pSurfaceAssociations[i] = new CSurfaceAssociation (*m_pSurfaceObjects[i]);

    delete [] pOldSurfaceObjects;
    delete [] pOldSurfaceAssociations;
}

void CSurfaceManager :: ReadLeaf (mleaf_s *pLeaf)
{
    msurface_s **pMarkSurfaces = pLeaf->firstmarksurface;
    int iNumSurfaces = pLeaf->nummarksurfaces;

    if ((pMarkSurfaces != NULL) && (iNumSurfaces > 0))
    {
        do
        {
            msurface_s *pSurface = *pMarkSurfaces++;

            if ((pSurface->texinfo != NULL) &&
                (pSurface->texinfo->texture != NULL) &&
                (pSurface->texinfo->texture->name[0] == '!'))
            {
                continue;
            }

            if ((pSurface == NULL) || (pSurface->texinfo == NULL) ||
                (pSurface->texinfo->texture == NULL))
            {
                continue;
            }

            AddSurface (pSurface);
        }
        while (--iNumSurfaces);
    }
}

int CSurfaceManager :: getNumSurfaceEntries (void) const
{
    return m_iNumSurfaceEntries;
}

CSurface *CSurfaceManager :: getSurfaceObject (int iIndex)
{
    if ((iIndex < 0) || (iIndex >= m_iNumSurfaceEntries))
        return NULL;

    return m_pSurfaceObjects[iIndex];
}

CSurfaceAssociation *CSurfaceManager :: getSurfaceAssociation (int iIndex)
{
    if ((iIndex < 0) || (iIndex >= m_iNumSurfaceEntries))
        return NULL;

    return m_pSurfaceAssociations[iIndex];
}

CSurfaceAssociation *CSurfaceManager :: getSurfaceAssociation (msurface_s *pSurface)
{
    for (int i = 0; i < m_iNumSurfaceEntries; i++)
    {
        if (pSurface == m_pSurfaceObjects[i]->getSurface ())
            return m_pSurfaceAssociations[i];
    }

    return NULL;
}

void CSurfaceManager :: RenderFilteredSurfaces (msurface_s **pMarkSurfaces,
    int iNumMarkSurfaces,
    cl_entity_s *pEntity,
    model_s *pModelTexture,
    int iFrame,
    bool bFilterWaterSurfaces)
{
    Vector vecBounds[2] =
    {
        pEntity->curstate.mins + pEntity->origin,
        pEntity->curstate.maxs + pEntity->origin
    };

    do
    {
        msurface_s *pSurface = *pMarkSurfaces++;

        if (bFilterWaterSurfaces && ((pSurface->texinfo != NULL) &&
            (pSurface->texinfo->texture != NULL) &&
            (pSurface->texinfo->texture->name[0] == '!')))
        {
            continue;
        }

        CSurfaceAssociation *pSurfaceAssociation = getSurfaceAssociation (pSurface);

        if ((pSurfaceAssociation != NULL) && !pSurfaceAssociation->Rendered ())
        {
            pSurfaceAssociation->Render (gEngfuncs.pTriAPI, vecBounds,
                 pModelTexture, iFrame);
            pSurfaceAssociation->ToggleRendered (true);
        }
    }
    while (--iNumMarkSurfaces);

    for (int i = 0; i < m_iNumSurfaceEntries; i++)
        getSurfaceAssociation (i)->ToggleRendered (false);
}

Notice the use of the "ToggleRendered" for the CSurfaceAssociation objects. The reason why I put this in there is because, when developing this tutorial, I notice some polygons were much brighter than the others. I figured it was because the same polygon was being rendered more than once, and I was right. The CSurfaceManager is NOT allowing the same msurface_s object in more than once, so what could be the possible explanation? Surprisingly, some of the surfaces have been marked more than once, as a result of how the engine handles its rendering process.
----
Ok, what's left? You need to get the model_s pointer to the world model, and pass it in to the CSurfaceManager object when creating it. I'm not going to spoon feed EVERYTHING, so I'll let you figure that much out on your own-it's not that difficult. Also, the textures... well, the fake caustics effect is basically all in the texture; there's obviously no special calculations going on here. Here's a nice website to check out for that: Cool Caustics Program Website.

Oh, and here's a picture of this effect in "action":
user posted image

I believe that is that. Please leave comments/suggestions/hateful remarks/etc

Rate This Article
This article is currently rated: 4.5 out of 5.0 (2 Votes)

You have to register to rate this article.
Related Files
Zip FileFilename: causticsA.jpg
File Size: 52.2 KB
Zip FileFilename: com_model.zip
File Size: 4.4 KB

User Comments Showing comments 1-14

Posted By: Patrick on Sep 09 2003 at 17:43:09
One thing I forgot to change is my explanation of the marksurfaces. The engine marks surfaces as it recurses through the world nodes, not build a pre-render list. It will add the same surface to the list if it encounters is multiple times, but it WON'T render it more than once.
Also, a lot of people have been coming to me asking why this won't work for them. I assumed that readers would know about how maps are compiled a little bit. Water ENTITIES do not produce BSP leafs/nodes; this effect is only applied to leafs with CONTENTS_WATER, and thus only WORLD water brushes.Edited by Patrick on Jan 08 2004, 00:05:54

Posted By: combat on Sep 23 2003 at 00:44:53
Nice!!!

Posted By: Mazor on Oct 04 2003 at 11:25:13
So.... where does one actually put this 'RenderFilteredSurfaces' call? HUD_DrawTransparentTriangles?

Annnnd, what is the breakdown of the paramater list for that function, I don't follow it. What should cl_entity_t *pEntity be, the player or the map, or the entity you want the effect to apply to. What sort of model_s do we need? Some sprite I assume? I dunno, there's just a LOT of unanswered questions in my head.

Posted By: Patrick on Oct 04 2003 at 17:16:17
Yeah, call RenderFilteredSurfaces from HUD_DrawTransparentTriangles.
The cl_entity_t pointer you pass won't be the entity the effect is applied to yet. Rather, it'll be the entity whose origin is used in the calculation of the alpha value for each polygon; this isn't really 100% yet (I'm planning on using it in a follow-up tutorial). If you look at the name of the model_s pointer being passed, it's a sprite texture.
Please post whatever other questions you have.

Posted By: Mazor on Oct 05 2003 at 01:44:43
I put this in HUD_DrawTransparentTriangles:
if( g_pSurfaceManager )
{
cl_entity_t *world = gEngfuncs.GetEntityByIndex(0);
model_t *caustic = gEngfuncs.CL_LoadModel("sprites/ballsmoke.spr", NULL);
g_pSurfaceManager->RenderFilteredSurfaces(world->model->marksurfaces, world->model->nummarksurfaces, gEngfuncs.GetLocalPlayer(), caustic, 0, false );
}

Is that right?

Posted By: kapalkaudi on Oct 26 2003 at 21:35:13
sweeeeeeet :þ

Posted By: CheapAlert on Oct 27 2003 at 16:05:31
Hoepfully, i'll try to hack this around to make Detail Textures (i've done detail textures for GLQuake before, got a tut on Quakesrc.org on it)

Posted By: Patrick on Oct 27 2003 at 23:34:55
That would definitely be interesting to see. I need to write the next tutorial. If I have some time, I may do it kind of soon. It'd probably be helpul to you as well.

Posted By: Trauts on Dec 15 2003 at 01:02:14
I can't get it to work...

void DLLEXPORT HUD_DrawTransparentTriangles( void )
{
static CSurfaceManager g_pSurfaceManager;
cl_entity_t *world = gEngfuncs.GetEntityByIndex(0);
model_s *caustic = gEngfuncs.CL_LoadModel("sprites/causics.spr", NULL);
g_pSurfaceManager.RenderFilteredSurfaces(world->model->marksurfaces, world->model->nummarksurfaces, gEngfuncs.GetLocalPlayer(), caustic, 0, false );

that doesn't do ANYTHING. What am I doing wrong?

Posted By: DarkAngelZLT on Dec 17 2003 at 07:46:48
1) post in the forums, thats what theyre for
2) learn OO C++ and read the tut over again

Youre creating the surface manager with "static CSurfaceManager g_pSurfaceManager;", but you actually need something like "static CSurfaceManager *g_pSurfaceManager = new CSurfaceManager( caustic );"

And BTW it shouldnt be static, it should be global so you can delete it and re-create it when a new map is loaded.

Posted By: Trauts on Dec 19 2003 at 23:32:41
Well, using a pointer doesn't make any difference. Maybe you should learn what you're talking about before YOU post.

I know OO.

Posted By: cct on Jan 19 2004 at 20:05:26
hm
can someone post his working of the drawtransparenttriangles();

i don´t get it working
i did it nearly same way as mazor but it doesn´t work

Posted By: InternetNightmare on May 17 2004 at 17:44:20
Nice! Well I got this working and it works in DirectX mode as well... But I have strange effect... When I'm in the water I see player model's texture :/ Well if there are any effects (from guns...) I see effect textures... The code needs more explenations though. But still nice ;)

Posted By: jim_the_coder on May 26 2004 at 09:20:48
Heh...I can tell Trauts why his code isn't working...

gEngfuncs.CL_LoadModel("sprites/causics.spr", NULL);

Typo :DEdited by jim_the_coder on May 26 2004, 09:21:52


You must register to post a comment. If you have already registered, you must login.

Latest Articles
3rd person View in Multiplayer
Half-Life 2 | Coding | Client Side Tutorials
How to enable it in HL2DM

By: cct | Nov 13 2006

Making a Camera
Half-Life 2 | Level Design
This camera is good for when you join a map, it gives you a view of the map before you join a team

By: slackiller | Mar 05 2006

Making a camera , Part 2
Half-Life 2 | Level Design
these cameras are working monitors that turn on when a button is pushed.

By: slackiller | Mar 04 2006

Storing weapons on ladder
Half-Life 2 | Coding | Snippets
like Raven Sheild or BF2

By: British_Bomber | Dec 24 2005

Implementation of a string lookup table
Half-Life 2 | Coding | Snippets
A string lookup table is a set of functions that is used to convert strings to pre-defined values

By: deathz0rz | Nov 13 2005


Latest Comments
knock knock
General | News
By: omega | Dec 22 2016
 
knock knock
General | News
By: MIFUNE | Oct 10 2015
 
New HL HUD Message System
Half-Life | Coding | Shared Tutorials
By: chbrules | Dec 31 2011
 
knock knock
General | News
By: Whistler | Nov 05 2011
 
Particle Engine tutorial part 4
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 18 2010
 
Particle Engine tutorial part 2
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Particle Engine tutorial part 3
Half-Life | Coding | Client Side Tutorials
By: darkPhoenix | Feb 11 2010
 
Game Movement Series #2: Analog Jumping and Floating
Half-Life 2 | Coding | Shared Tutorials
By: mars3554 | Oct 26 2009
 
Particle Engine tutorial part 5
Half-Life | Coding | Client Side Tutorials
By: Deadpool | Aug 02 2009
 
Particle Engine tutorial part 5
Half-Life | Coding | Client Side Tutorials
By: Persuter | Aug 02 2009
 

Site Info
297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (4 guests)
About - Credits - Contact Us

Wavelength version: 3.0.0.9
Valid XHTML 1.0! Valid CSS!