Foreword
The Half-Life map format is almost identical to Quake. The primary difference is that it allows the use of 256 colors per individual texture and stores the palette (WAD3). Quake uses a global 256 color palette (WAD2).
We can't conveniently do this for software renderers (WinQuake) so this is going to be GL-only. The Quake software renderer uses only a single byte to store colors, so it is locked to 256 colors. FTEQW's software renderer uses 3 bytes per pixel giving it 24-bit color, but that is a somewhat heavy modification.
(The WinQuake rendering code does have some seemingly unused 16-bit color code to support display resolutions that support 65536 colors. Since software renderers are direct memory manipulation to the best of my knowledge, I would imagine that 16-bit and 24-bit color can be a lot slower than 8 bit.)
Planning
The basics of what we need to do here looks like this:
1. We need to make a define for Q1 BSP (29) and one for HL_BSP (30).
2. We will need to add in WAD3 support.
3. Half-Life supports external .wad textures.
4. Half-Life hull sizes are a little different.
5. Half-Life supports alpha textures. We won't be doing this right now, this will probably be included in a part 2 (because I like alpha textures but I also worry about them confusing "anti-wallhack").
6. Half-Life map format uses different texture prefixes for sky/water/etc.
The Code Changes
1. First, the Half-Life map format uses WAD3 instead of WAD2, so we need to add support for loading them.
2. And now add the function prototypes to wad.h ...wad.c - Add to end (borrowed from FuhQuake)
Code: Select all
/* ============================================================================= WAD3 Texture Loading for BSP 3.0 Support ============================================================================= */ #ifdef GLQUAKE #define TEXWAD_MAXIMAGES 16384 typedef struct { char name[MAX_QPATH]; FILE *file; int position; int size; } texwadlump_t; static texwadlump_t texwadlump[TEXWAD_MAXIMAGES]; void WAD3_LoadTextureWadFile (char *filename) { lumpinfo_t *lumps, *lump_p; wadinfo_t header; int i, j, infotableofs, numlumps, lowmark; FILE *file; if (COM_FOpenFile (va("textures/halflife/%s", filename), &file) != -1) goto loaded; if (COM_FOpenFile (va("textures/%s", filename), &file) != -1) goto loaded; if (COM_FOpenFile (filename, &file) != -1) goto loaded; Host_Error ("Couldn't load halflife wad \"%s\"\n", filename); loaded: if (fread(&header, 1, sizeof(wadinfo_t), file) != sizeof(wadinfo_t)) { Con_Printf ("WAD3_LoadTextureWadFile: unable to read wad header"); return; } if (memcmp(header.identification, "WAD3", 4)) { Con_Printf ("WAD3_LoadTextureWadFile: Wad file %s doesn't have WAD3 id\n",filename); return; } numlumps = LittleLong(header.numlumps); if (numlumps < 1 || numlumps > TEXWAD_MAXIMAGES) { Con_Printf ("WAD3_LoadTextureWadFile: invalid number of lumps (%i)\n", numlumps); return; } infotableofs = LittleLong(header.infotableofs); if (fseek(file, infotableofs, SEEK_SET)) { Con_Printf ("WAD3_LoadTextureWadFile: unable to seek to lump table"); return; } lowmark = Hunk_LowMark(); if (!(lumps = Hunk_Alloc(sizeof(lumpinfo_t) * numlumps))) { Con_Printf ("WAD3_LoadTextureWadFile: unable to allocate temporary memory for lump table"); return; } if (fread(lumps, 1, sizeof(lumpinfo_t) * numlumps, file) != sizeof(lumpinfo_t) * numlumps) { Con_Printf ("WAD3_LoadTextureWadFile: unable to read lump table"); Hunk_FreeToLowMark(lowmark); return; } for (i = 0, lump_p = lumps; i < numlumps; i++,lump_p++) { W_CleanupName (lump_p->name, lump_p->name); for (j = 0; j < TEXWAD_MAXIMAGES; j++) { if (!texwadlump[j].name[0] || !strcmp(lump_p->name, texwadlump[j].name)) break; } if (j == TEXWAD_MAXIMAGES) break; // we are full, don't load any more if (!texwadlump[j].name[0]) Q_strncpyz (texwadlump[j].name, lump_p->name, sizeof(texwadlump[j].name)); texwadlump[j].file = file; texwadlump[j].position = LittleLong(lump_p->filepos); texwadlump[j].size = LittleLong(lump_p->disksize); } Hunk_FreeToLowMark(lowmark); //leaves the file open } //converts paletted to rgba static byte *ConvertWad3ToRGBA(miptex_t *tex) { byte *in, *data, *pal; int i, p, image_size; if (!tex->offsets[0]) Sys_Error("ConvertWad3ToRGBA: tex->offsets[0] == 0"); image_size = tex->width * tex->height; in = (byte *) ((byte *) tex + tex->offsets[0]); data = malloc(image_size * 4); // Baker pal = in + ((image_size * 85) >> 6) + 2; for (i = 0; i < image_size; i++) { p = *in++; if (tex->name[0] == '{' && p == 255) { ((int *) data)[i] = 0; } else { p *= 3; data[i * 4 + 0] = pal[p]; data[i * 4 + 1] = pal[p + 1]; data[i * 4 + 2] = pal[p + 2]; data[i * 4 + 3] = 255; } } return data; } byte *WAD3_LoadTexture(miptex_t *mt) { char texname[MAX_QPATH]; int i, j, lowmark = 0; FILE *file; miptex_t *tex; byte *data; if (mt->offsets[0]) return ConvertWad3ToRGBA(mt); texname[sizeof(texname) - 1] = 0; W_CleanupName (mt->name, texname); for (i = 0; i < TEXWAD_MAXIMAGES; i++) { if (!texwadlump[i].name[0]) break; if (strcmp(texname, texwadlump[i].name)) continue; file = texwadlump[i].file; if (fseek(file, texwadlump[i].position, SEEK_SET)) { Con_Printf("WAD3_LoadTexture: corrupt WAD3 file"); return NULL; } lowmark = Hunk_LowMark(); tex = Hunk_Alloc(texwadlump[i].size); if (fread(tex, 1, texwadlump[i].size, file) < texwadlump[i].size) { Con_Printf("WAD3_LoadTexture: corrupt WAD3 file"); Hunk_FreeToLowMark(lowmark); return NULL; } tex->width = LittleLong(tex->width); tex->height = LittleLong(tex->height); if (tex->width != mt->width || tex->height != mt->height) { Hunk_FreeToLowMark(lowmark); return NULL; } for (j = 0;j < MIPLEVELS;j++) tex->offsets[j] = LittleLong(tex->offsets[j]); data = ConvertWad3ToRGBA(tex); Hunk_FreeToLowMark(lowmark); return data; } return NULL; } #endif
3. Next it will be important to determine the map version to know how to handle the textures ...wad.h - Add to bottom
Code: Select all
void WAD3_LoadTextureWadFile (char *filename); byte *WAD3_LoadTexture(miptex_t *mt);
4. And open bspfile.h and replace this:gl_model.h
char *entities;
int bspversion;
//
// additional model data
//
cache_user_t cache; // only access through Mod_Extradata
} model_t;
5. Now it is important to read the version number of the map at load time:with:Code: Select all
#define BSPVERSION 29
Code: Select all
#define Q1_BSPVERSION 29 #define HL_BSPVERSION 30
6. Normally, Quake reads the texture data as palette indexes (i.e. a byte is the color palette offset), but with bsp 30 each texture has it's own palette and the read process will convert it rgb. So we need a supporting texture upload function that won't try to impose a 256 color palette on the data.world.c in SV_HullForEntity - add the yellow
VectorSubtract (maxs, mins, size);
if (model->bspversion == HL_BSPVERSION) {
if (size[0] < 3) {
hull = &model->hulls[0]; // 0x0x0
} else if (size[0] <= 32) {
if (size[2] < 54) // pick the nearest of 36 or 72
hull = &model->hulls[3]; // 32x32x36
else
hull = &model->hulls[1]; // 32x32x72
} else {
hull = &model->hulls[2]; // 64x64x64
}
} else {
if (size[0] < 3)
hull = &model->hulls[0];
else if (size[0] <= 32)
hull = &model->hulls[1];
else
hull = &model->hulls[2];
}
Add this to the end of gl_draw.c:
Code: Select all
/* ================ GL_LoadTexture32 ================ */ int GL_LoadTexture32 (char *identifier, int width, int height, byte *data, qboolean mipmap, qboolean alpha) { qboolean noalpha; int i, p, s; gltexture_t *glt; int image_size = width * height; // see if the texture is already present if (identifier[0]) { for (i=0, glt=gltextures ; i<numgltextures ; i++, glt++) { if (!strcmp (identifier, glt->identifier)) { if (width != glt->width || height != glt->height) Sys_Error ("GL_LoadTexture: cache mismatch"); return gltextures[i].texnum; } } } else { glt = &gltextures[numgltextures]; numgltextures++; } strcpy (glt->identifier, identifier); glt->texnum = texture_extension_number; glt->width = width; glt->height = height; glt->mipmap = mipmap; GL_Bind(texture_extension_number ); #if 1 // Baker: this applies our -gamma parameter table if (1) { //extern byte vid_gamma_table[256]; for (i = 0; i < image_size; i++){ data[4 * i] = vid_gamma_table[data[4 * i]]; data[4 * i + 1] = vid_gamma_table[data[4 * i + 1]]; data[4 * i + 2] = vid_gamma_table[data[4 * i + 2]]; } } #endif GL_Upload32 ((unsigned *)data, width, height, mipmap, alpha); texture_extension_number++; return texture_extension_number-1; }
7. Problem #2: Stock GLQuake doesn't support hardware gamma and instead applies gamma using the -gamma command line parameter. We don't want fullbright looking textures, so there needs to be a gamma table.
8. On startup, we need to build the gamma table so ...gl_draw.c - Add this above the GL_LoadTexture32 we just added:
Code: Select all
byte vid_gamma_table[256]; void Build_Gamma_Table (void) { int i; float inf; float in_gamma; if ((i = COM_CheckParm("-gamma")) != 0 && i+1 < com_argc) { in_gamma = Q_atof(com_argv[i+1]); if (in_gamma < 0.3) in_gamma = 0.3; if (in_gamma > 1) in_gamma = 1.0; } else { in_gamma = 1; } if (in_gamma != 1) { for (i=0 ; i<256 ; i++) { inf = min(255 * pow((i + 0.5) / 255.5, in_gamma) + 0.5, 255); vid_gamma_table[i] = inf; } } else { for (i=0 ; i<256 ; i++) vid_gamma_table[i] = i; } }
open gl_vidnt.c and add the yellow:
static void Check_Gamma (unsigned char *pal)
{
float f, inf;
unsigned char palette[768];
int i;
if ((i = COM_CheckParm("-gamma")) == 0) {
if ((gl_renderer && strstr(gl_renderer, "Voodoo")) ||
(gl_vendor && strstr(gl_vendor, "3Dfx")))
vid_gamma = 1;
else
vid_gamma = 0.7; // default to 0.7 on non-3dfx hardware
} else
vid_gamma = Q_atof(com_argv[i+1]);
for (i=0 ; i<768 ; i++)
{
f = pow ( (pal+1)/256.0 , vid_gamma );
inf = f*255 + 0.5;
if (inf < 0)
inf = 0;
if (inf > 255)
inf = 255;
palette = inf;
}
memcpy (pal, palette, sizeof(palette));
Build_Gamma_Table ();
}
9. The code from FuhQuake uses the BOX_ON_PLANE_SIDE macro. For reasons unknown to me that I suspect might have something to do with assembly language, the code for this in mathlib.h is effectively IFDEF'd out on x86. So we need to remove this #IFDEF.
mathlib.h - delete the red
#ifndef id386
#define BOX_ON_PLANE_SIDE(emins, emaxs, p) \
(((p)->type < 3)? \
( \
((p)->dist <= (emins)[(p)->type])? \
1 \
: \
( \
((p)->dist >= (emaxs)[(p)->type])?\
2 \
: \
3 \
) \
) \
: \
BoxOnPlaneSide( (emins), (emaxs), (p)))
#endif
10. I kept the Q_strncpyz function that FuhQuake used. Add that to common.c and the prototype to common.h
common.h - add to end
Code: Select all
void Q_strncpyz (char *dest, char *src, size_t size);
11. While we are in common.c, increase MAX_FILES_IN_PACK to 4096 since the only way I had of immediately testing this at the moment was to test using the Half-Life pak0.pak which exceeds the file count limit of 2048 by a large margin:common.c - add to end
Code: Select all
void Q_strncpyz (char *dest, char *src, size_t size) { strncpy (dest, src, size - 1); dest[size-1] = 0; }
12a. Finally, we need to check for bsp 30 and if so, load the textures appropriately:common.c - find MAX_FILES_IN_PACK and increase from 2048 to 4096
Code: Select all
#define MAX_FILES_IN_PACK 4096 // Was 2048
12b. Next ...Open gl_model.c and find
Code: Select all
if (!Q_strncmp(mt->name,"sky",3)) R_InitSky (tx);
And replace with:
Code: Select all
if (!Q_strncmp(mt->name,"sky",3)) { R_InitSky (tx); continue; } if (loadmodel->bspversion == HL_BSPVERSION) { byte *data; if ((data = WAD3_LoadTexture(mt))) { //com_netpath[0] = 0; //alpha_flag = ISALPHATEX(tx->name) ? TEX_ALPHA : 0; texture_mode = GL_LINEAR_MIPMAP_NEAREST; //_LINEAR; tx->gl_texturenum = GL_LoadTexture32 (mt->name, tx->width, tx->height, (byte *)data, true, false); texture_mode = GL_LINEAR; free(data); continue; } }
Still gl_model.c in Mod_LoadBrushModel find:
Code: Select all
loadmodel->type = mod_brush; header = (dheader_t *)buffer; i = LittleLong (header->version); if (i != BSPVERSION) Sys_Error ("Mod_LoadBrushModel: %s has wrong version number (%i should be %i)", mod->name, i, BSPVERSION);
And replace with this:
Code: Select all
loadmodel->type = mod_brush; header = (dheader_t *)buffer; mod->bspversion = LittleLong (header->version); if (mod->bspversion != Q1_BSPVERSION && mod->bspversion != HL_BSPVERSION) Host_Error ("Mod_LoadBrushModel: %s has wrong version number (%i should be %i (Quake) or %i (HalfLife))", mod->name, mod->bspversion, Q1_BSPVERSION, HL_BSPVERSION);
Now ... this will work as-is. Unfortunately, GLQuake doesn't have colored light support and the lightmaps will look all wrong.
So we will cheat, since I have no intention of adding colored light support to this tutorial (there is a colored light tutorial here (QuakeSrc.org tutorial #95, unfortunately it doesn't work).
13. Find this and add the yellow. A stock GLQuake with colored light support, we would not need to do this, but what we are doing here is turning the colored light into gray. This is improper, but adding colored light support involves several changes and most engines already support colored light.
Still in gl_model.c
/*
=================
Mod_LoadLighting
=================
*/
void Mod_LoadLighting (lump_t *l)
{
if (!l->filelen)
{
loadmodel->lightdata = NULL;
return;
}
if (loadmodel->bspversion == HL_BSPVERSION) {
int i;
loadmodel->lightdata = Hunk_AllocName(l->filelen, loadname);
// dest, source, count
memcpy (loadmodel->lightdata, mod_base + l->fileofs, l->filelen);
// Cheat!
// Run thru the lightmap data and average the colors to make it a shade of gray, haha!
for (i=0; i<l->filelen; i+=3)
{
int grayscale;
byte out;
grayscale = (loadmodel->lightdata + loadmodel->lightdata[i+1] + loadmodel->lightdata[i+2])/3;
if (grayscale > 255) grayscale = 255;
if (grayscale < 0) grayscale = 0;
out = (byte)grayscale;
loadmodel->lightdata = loadmodel->lightdata[i+1] = loadmodel->lightdata[i+2] = out;
}
return;
}
loadmodel->lightdata = Hunk_AllocName ( l->filelen, loadname);
memcpy (loadmodel->lightdata, mod_base + l->fileofs, l->filelen);
}
Summary
To test this, make a folder called c:\quake\hl
Copy Half-Life pak0.pak into that folder.
Start it as such: glquake.exe -game hl +map c1a0 -gamma 0.7 -no8bit
[the -no8bit is a "just in case", I'm not certain it is actually required and my video card doesn't have that GL extension so I can't test whether or not the command line param is needed for card supporting the 8bit palette extension]
Now what this tutorial doesn't support:
1. Alpha textures, we didn't do that. Many modified engines already support it though.
2. Colored lighting, not in the scope of this
3. Half-Life skyboxes.
4. Half-life water, the prefix is different.
5. The glass entities in Half-Life use the "renderamt" field, which is not so much different than .alpha. For transparent glass, you'd need to add that into the engine and use a progs.dat to translate the renderamt to .alpha.
Interesting note: DarkPlaces treats renderamt as a standard QuakeC field so DarkPlaces happens to load the glass in the Half-Life maps properly without requiring a progs.dat