Posted by: Patrick
Date posted: Aug 11 2004 User Rating: 4.5 out of 5.0 | Number of views: 8525 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. */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:
| | #ifndef _CAUSTICS_H_ #define _CAUSTICS_H_
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]; };
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); };
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); };
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:
| | #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:
| | #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) { 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, 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:
| | #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":

I believe that is that. Please leave comments/suggestions/hateful remarks/etc |
|
User Comments
Showing comments 1-14
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
|
|
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. |
|
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. |
|
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? |
|
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) |
|
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. |
|
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? |
|
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. |
|
Well, using a pointer doesn't make any difference. Maybe you should learn what you're talking about before YOU post.
I know OO. |
|
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 |
|
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 ;) |
|
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.
|
297 Approved Articless
8 Pending Articles
3940 Registered Members
0 People Online (4 guests)
|
|