Originally I was going to make this a copy-and-paste tutorial, but it got way too complex; I have a custom polygon-organization system, and making a new-but-simpler one specifically for a tutorial wouldn't be worth the effort. Besides, any mod that really needs anything like this probably has one already.
So, you want skies that you render and control yourself, eh? I'll start off be desribing the sky "effect" in Doom/Quake2 etc. style engines. Basically, you have 6 textures/images rendered in such a way that when they're mapped properly to a perfect cube, the appearance is a seemless sky. In other words, you have an up, down, left, right, front, and back sky image (up, dn, lf, rt, ft, bk). Imagine 6 quads forming a perfect cube around the player/view position, and map the appropriate images to the appropriate quads. Kinda simple huh?
So, how does one do this in Half-Life? There's quite a few steps, but it's not really difficult. First of all, think about what information is needed to do this. You need to know how far away each quad should be (they're all equidistant, so one number for the distance works fine); you also need to know the "base name" for the images to use (the "dusk" in "duskup", "duskdn", "dusklf", "duskrt", "duskft", and "duskbk"). Depending on what you plan on doing with this, you might also want to know how many frames there are (for animation), along with the color of the sky and the transparency value. For the sake of this "article," let's assume you're going to use one color and alpha/transparency value for all six quads, and you're not doing animations.
So, on the server-side dll project (hl.dll or mp.dll, whatever), you need to make a new entity (I called mine CSkyBox). Whether it's brush-based or point-based is up to you and how you want it to function, along with whether or not you want it to be toggle'able.
Here's what my .fgd looks like for this entity:
| | | @PointClass base (Target, Targetname) = env_skybox : "Sky Box" [ renderamt(integer) : "FX Amount (1 - 255)" rendercolor(color255) : "FX Color (R G B)" : "0 0 0" starton (choices) : "Start on?" : 1 = [ 0 : "No" 1 : "Yes" ] distance (string) : "Distance" : "500" skyset (string) : "Sky set name" : "" ]
|
When coding this class, I made use of a custom think method, and when the entity spawns, I set the think to fire in a few seconds (to make sure the client's there and all). That think method sends the on/off state, and if it's on, it sends the keyvalue data (WRITE_COORD for the distance, WRITE_STRING for the base name, WRITE_BYTE for the color and alpha).
On the client-side (cl_dll), I wrote a corresponding HUD class, CHudSkyBox. I use the HUD_ACTIVE bit and m_iFlags for the on/off state. After the message is done being received, I attempt to load all of the .tga images using gEngfuncs.LoadMapSprite. If and only if all images were successfully loaded, I set HUD_ACTIVE. Instead of the Draw method, I have a Render method, which takes a triangleapi_t pointer (for my typing ease, that's all). If HUD_ACTIVE is set in m_iFlags, I go ahead and render the quads. For the sake of this article, I wrote a sample Render method that would work without any polygon-organization system. However, there's one little problem. LoadMapSprite returns a model_s pointer containing the .tga split up into possibly several sprite frames. I think 128x128 (didn't it use to be 256x256?) is the base size, and any size larger gets split up into more than one frame. What does that mean? You need to draw multiple quads for actual image. (So, a 256x256 .tga would get split into 4, or 2x2, sprites. That means, for all six sky panels, you'd draw four sub-quads.)
| | | // (this works perfectly if the .tgas are 128x128) void CHudSkyBox :: Render (triangleapi_t *pRender) { if (!(m_iFlags & HUD_ACTIVE)) return;
float view[3], upv[3], rightv[3], vpnv[3]; // we only want view, but the others // are to avoid any errors IEngineStudio.GetViewInfo (view, upv, rightv, vpnv);
float x = view[0]; float y = view[1]; float z = view[2]; float d = m_flDistance;
float verts[8][3] = // 8x3--8 sets of 3 floats, so 8 vertexes { { -d + x, -d + y, -d + z }, // front-left-bottom { -d + x, -d + y, d + z }, // fromt-left-top { d + x, -d + y, d + z }, // front-right-top { d + x, -d + y, -d + z }, // front-right-bottom
{ -d + x, d + y, -d + z }, // back-left-bottom { -d + x, d + y, d + z }, // back-left-top { d + x, d + y, d + z }, // back-right-top { d + x, d + y, -d + z } // back-right-bottom };
float texCoords[4][2] = { { 0.0f, 1.0f }, // bottom-left { 0.0f, 0.0f }, // top-left { 1.0f, 0.0f }, // top-right { 1.0f, 1.0f } // bottom-right };
float *quads[NUM_QUADS][4] = { // front { verts[3], verts[2], verts[1], verts[0] }, // back { verts[4], verts[5], verts[6], verts[7] }, // left { verts[0], verts[1], verts[5], verts[4] }, // right { verts[7], verts[6], verts[2], verts[3] }, // top { verts[6], verts[5], verts[1], verts[2] }, // bottom { verts[4], verts[7], verts[3], verts[0] } };
for (int i = 0; i < NUM_QUADS; i++) { pRender->SpriteTexture (m_pTextures[i], 0); pRender->CullFace (TRI_NONE); if (m_iRenderAmount < 255) pRender->RenderMode (kRenderTransAlpha); else pRender->RenderMode (kRenderNormal);
pRender->Begin (TRI_QUADS);
pRender->Color4ub (m_vecRenderColor.x, m_vecRenderColor.y, m_vecRenderColor.z, m_iRenderAmount);
// Notice that we're not looping through each vertex. This is called // unrolling loops. It's a good performance booster. pRender->TexCoord2f (texCoords[0][0], texCoords[0][1]); pRender->Vertex3fv (quads[i][0]);
pRender->TexCoord2f (texCoords[1][0], texCoords[1][1]); pRender->Vertex3fv (quads[i][1]);
pRender->TexCoord2f (texCoords[2][0], texCoords[2][1]); pRender->Vertex3fv (quads[i][2]);
pRender->TexCoord2f (texCoords[3][0], texCoords[3][1]); pRender->Vertex3fv (quads[i][3]);
pRender->End (); } } |
Seeing as how this is my first "article"/tutorial, any and all feedback is greatly appreciated. Maybe I'll eventually turn it into a full-fledged tutorial like I originally planned. |