/*** * * Copyright (c) 1996-2002, Valve LLC. All rights reserved. * * This product contains software technology licensed from Id * Software, Inc. ("Id Technology"). Id Technology (c) 1996 Id Software, Inc. * All Rights Reserved. * ****/ // qrad.c #include "qrad.h" /* NOTES ----- every surface must be divided into at least two patches each axis */ patch_t *g_face_patches[MAX_MAP_FACES]; entity_t *g_face_entity[MAX_MAP_FACES]; vec_t *g_skynormalsizes[SKYLEVELMAX+1]; int g_numskynormals[SKYLEVELMAX+1]; vec3_t *g_skynormals[SKYLEVELMAX+1]; patch_t *g_patches; uint g_num_patches; static vec3_t (*emitlight)[MAXLIGHTMAPS]; static vec3_t (*addlight)[MAXLIGHTMAPS]; static byte (*newstyles)[MAXLIGHTMAPS]; #ifdef HLRAD_DELUXEMAPPING static vec3_t (*emitlight_dir)[MAXLIGHTMAPS]; static vec3_t (*addlight_dir)[MAXLIGHTMAPS]; #endif vec3_t g_face_offset[MAX_MAP_FACES]; // for rotating bmodels dplane_t g_backplanes[MAX_MAP_PLANES]; // equal to MAX_MAP_FACES, there is no errors int g_overflowed_styles_onface[MAX_THREADS]; int g_overflowed_styles_onpatch[MAX_THREADS]; int g_direct_luxels[MAX_THREADS]; int g_lighted_luxels[MAX_THREADS]; vec3_t g_reflectivity[MAX_MAP_TEXTURES]; bool g_texture_init[MAX_MAP_TEXTURES]; static winding_t *g_windingArray[MAX_SUBDIVIDE]; static uint g_numwindings = 0; static size_t g_transfer_data_bytes; size_t g_transfer_data_size[MAX_THREADS]; uint g_numbounce = DEFAULT_BOUNCE; // originally this was 8 vec_t g_chop = DEFAULT_CHOP; vec_t g_texchop = DEFAULT_TEXCHOP; vec_t g_smoothvalue = DEFAULT_SMOOTHVALUE; float g_totalarea; bool g_fastmode = DEFAULT_FASTMODE; vec_t g_direct_scale = DEFAULT_DLIGHT_SCALE; vec_t g_indirect_scale = DEFAULT_LIGHT_SCALE; vec_t g_blur = DEFAULT_BLUR; vec_t g_gamma = DEFAULT_GAMMA; bool g_drawsample = false; vec3_t g_ambient = { 0, 0, 0 }; float g_maxlight = DEFAULT_LIGHTCLIP; // originally this was 196 float g_indirect_sun = DEFAULT_INDIRECT_SUN; bool g_dirtmapping = DEFAULT_DIRTMAPPING; bool g_lerp_enabled = DEFAULT_LERP_ENABLED; bool g_extra = DEFAULT_EXTRAMODE; bool g_nomodelshadow = false; bool g_lightbalance = false; bool g_onlylights = false; float g_smoothing_threshold; // cosine of smoothing angle(in radians) char source[MAX_PATH] = ""; uint g_gammamode = DEFAULT_GAMMAMODE; static char global_lights[MAX_PATH] = ""; static char level_lights[MAX_PATH] = ""; // pre-quantized table normals from Quake1 vec_t g_anorms[NUMVERTEXNORMALS][3] = { #include "anorms.h" }; /* =================================================================== MISC =================================================================== */ const dplane_t *GetPlaneFromFace( const dface_t *face ) { ASSERT( face != NULL ); if( face->side ) return &g_backplanes[face->planenum]; return &g_dplanes[face->planenum]; } const dplane_t *GetPlaneFromFace( const uint facenum ) { ASSERT( facenum < MAX_MAP_FACES ); dface_t *face = &g_dfaces[facenum]; if( face->side ) return &g_backplanes[face->planenum]; return &g_dplanes[face->planenum]; } dleaf_t *PointInLeaf( const vec3_t point ) { int nodenum = 0; while( nodenum >= 0 ) { dnode_t *node = &g_dnodes[nodenum]; if( PlaneDiff( point, &g_dplanes[node->planenum] ) > 0 ) nodenum = node->children[0]; else nodenum = node->children[1]; } return &g_dleafs[-nodenum - 1]; } dleaf_t *PointInLeaf_r( int nodenum, const vec3_t point ) { dplane_t *plane; dnode_t *node; vec_t dist; while( nodenum >= 0 ) { node = &g_dnodes[nodenum]; plane = &g_dplanes[node->planenum]; dist = DotProduct( point, plane->normal ) - plane->dist; if( dist > HUNT_WALL_EPSILON ) { nodenum = node->children[0]; } else if( dist < -HUNT_WALL_EPSILON ) { nodenum = node->children[1]; } else { dleaf_t *result[2]; result[0] = PointInLeaf_r( node->children[0], point ); result[1] = PointInLeaf_r( node->children[1], point ); if( result[0] == g_dleafs || result[0]->contents == CONTENTS_SOLID ) return result[0]; if( result[1] == g_dleafs || result[1]->contents == CONTENTS_SOLID ) return result[1]; if( result[0]->contents == CONTENTS_SKY ) return result[0]; if( result[1]->contents == CONTENTS_SKY ) return result[1]; return result[0]; } } return &g_dleafs[-nodenum - 1]; } dleaf_t *PointInLeaf2( const vec3_t point ) { return PointInLeaf_r( 0, point ); } /* ============== GetVisCache decompress visibility string ============== */ int GetVisCache( int lastoffset, int offset, byte *pvs ) { // get the PVS for the pos to limit the number of checks if( !g_visdatasize ) { if( offset != lastoffset ) { memset( pvs, 255, (g_numvisleafs + 7) / 8 ); lastoffset = -1; } } else if( offset != lastoffset ) { if( offset == -1 ) memset( pvs, 0, (g_numvisleafs + 7) / 8 ); else DecompressVis( &g_dvisdata[offset], pvs ); lastoffset = offset; } return lastoffset; } /* ============== SetDLightVis Init dlight visibility ============== */ void SetDLightVis( directlight_t *dl, int leafnum ) { if( !dl->pvs ) dl->pvs = (byte *)Mem_Alloc( (g_numvisleafs + 7) / 8 ); GetVisCache( -2, g_dleafs[leafnum].visofs, dl->pvs ); } /* ============== MergeDLightVis Merge dlight vis with another leaf ============== */ void MergeDLightVis( directlight_t *dl, int leafnum ) { if( !dl->pvs ) { SetDLightVis( dl, leafnum ); } else { byte pvs[(MAX_MAP_LEAFS + 7) / 8]; GetVisCache( -2, g_dleafs[leafnum].visofs, pvs ); // merge both vis graphs for( int i = 0; i < (g_numvisleafs + 7) / 8; i++ ) dl->pvs[i] |= pvs[i]; } } /* ============== PatchPlaneDist Fixes up patch planes for brush models with an origin brush ============== */ vec_t PatchPlaneDist( patch_t *patch ) { const dplane_t *plane = GetPlaneFromFace( patch->faceNumber ); return DotProduct( g_face_offset[patch->faceNumber], plane->normal ) + plane->dist; } bool PvsForOrigin( const vec3_t org, byte *pvs ) { dleaf_t *leaf; if( !g_visdatasize ) { memset( pvs, 255, (g_numvisleafs + 7) / 8 ); return true; } leaf = PointInLeaf( org ); if( leaf->visofs == -1 ) COM_FatalError( "leaf->visofs == -1\n" ); if( leaf->contents == CONTENTS_SOLID ) return false; DecompressVis( &g_dvisdata[leaf->visofs], pvs ); return true; } /* ============= HuntForWorld never return CONTENTS_SKY or CONTENTS_SOLID leafs ============= */ dleaf_t *HuntForWorld( vec3_t point, const vec3_t plane_offset, const dplane_t *plane, int hunt_size, vec_t hunt_scale, vec_t hunt_offset ) { vec3_t current_point; vec3_t original_point; dleaf_t *best_leaf = NULL; vec_t best_dist = 99999999.0; dplane_t new_plane = *plane; int a, x, y, z; vec3_t best_point; vec3_t scales; dleaf_t *leaf; scales[0] = 0.0; scales[1] = -hunt_scale; scales[2] = hunt_scale; VectorCopy( point, best_point ); VectorCopy( point, original_point ); new_plane.dist += DotProduct( plane->normal, plane_offset ); for( a = 0; a < hunt_size; a++ ) { for( x = 0; x < 3; x++ ) { current_point[0] = original_point[0] + (scales[x % 3] * a); for( y = 0; y < 3; y++ ) { current_point[1] = original_point[1] + (scales[y % 3] * a); for( z = 0; z < 3; z++ ) { if( a == 0 && ( x || y || z )) { continue; } vec3_t delta; vec_t dist; current_point[2] = original_point[2] + (scales[z % 3] * a); dist = DotProduct( current_point, new_plane.normal ) - new_plane.dist - hunt_offset; VectorMA( current_point, -dist, new_plane.normal, current_point ); VectorSubtract( current_point, original_point, delta ); dist = DotProduct( delta, delta ); if( dist < best_dist ) { if(( leaf = PointInLeaf2( current_point )) != g_dleafs ) { if(( leaf->contents != CONTENTS_SKY ) && ( leaf->contents != CONTENTS_SOLID )) { if( x || y || z ) { VectorCopy( current_point, best_point ); best_dist = dist; best_leaf = leaf; continue; } else { VectorCopy( current_point, point ); return leaf; } } } } } } } if( best_leaf ) break; } VectorCopy( best_point, point ); return best_leaf; } dworldlight_t *AllocWorldLight( word *lightnum ) { if( g_numworldlights == MAX_MAP_WORLDLIGHTS ) COM_FatalError( "MAX_MAP_WORLDLIGHTS limit exceeded\n" ); if( lightnum ) *lightnum = g_numworldlights; return &g_dworldlights[g_numworldlights++]; } void InitWorldLightFromDlight( directlight_t *dl, int leafnum ) { dworldlight_t *wl; if( dl->type != emit_skylight && leafnum == 0 ) return; // light that be removed wl = AllocWorldLight( &dl->lightnum ); wl->emittype = dl->type; wl->style = dl->style; VectorCopy( dl->origin, wl->origin ); VectorCopy( dl->intensity, wl->intensity ); VectorCopy( dl->normal, wl->normal ); wl->stopdot = dl->stopdot; wl->stopdot2 = dl->stopdot2; wl->fade = dl->fade; wl->falloff = dl->falloff; wl->leafnum = leafnum; // leaf where light is located wl->radius = dl->radius; wl->facenum = dl->facenum; wl->modelnumber = dl->modelnum; } void InitWorldLightFromPatch( dworldlight_t *wl, patch_t *p ) { dleaf_t *leaf; leaf = PointInLeaf( p->origin ); wl->emittype = emit_surface; wl->style = p->emitstyle; VectorCopy( p->origin, wl->origin ); VectorCopy( p->baselight, wl->intensity ); VectorScale( wl->intensity, p->area, wl->intensity ); VectorScale( wl->intensity, p->exposure, wl->intensity ); if( !VectorIsNull( p->reflectivity )) { VectorScale( wl->intensity, 0.5 / M_PI, wl->intensity ); VectorMultiply( wl->intensity, p->reflectivity, wl->intensity ); } else { VectorScale( wl->intensity, DIRECT_SCALE, wl->intensity ); } VectorCopy( GetPlaneFromFace( p->faceNumber )->normal, wl->normal ); wl->stopdot = wl->stopdot2 = 0.0f; // not used wl->fade = p->fade; // constant wl->falloff = falloff_valve; // inverse square falloff wl->leafnum = leaf - g_dleafs; // leaf where light is located wl->radius = VectorLength2( wl->intensity ) * 0.5f; wl->facenum = p->faceNumber; wl->modelnumber = p->modelnum; } // ===================================================================================== // CalcSightArea // ===================================================================================== vec_t CalcSightArea( const vec3_t receiver_origin, const vec3_t receiver_normal, const winding_t *emitter_winding, int skylevel ) { // maybe there are faster ways in calculating the weighted area, but at least this way is not bad. int numedges = emitter_winding->numpoints; vec3_t *edges = (vec3_t *)Mem_Alloc( numedges * sizeof( vec3_t )); vec3_t *pnormal, *pedge; vec_t *psize, dot; vec_t area = 0.0; int i, j; for( int x = 0; x < numedges; x++ ) { vec3_t v1, v2, normal; VectorSubtract( emitter_winding->p[x], receiver_origin, v1 ); VectorSubtract( emitter_winding->p[(x + 1) % numedges], receiver_origin, v2 ); CrossProduct( v1, v2, normal ); // pointing inward if( !VectorNormalize( normal )) break; VectorCopy( normal, edges[x] ); } if( x != numedges ) { Mem_Free( edges ); return area; } for( i = 0, pnormal = g_skynormals[skylevel], psize = g_skynormalsizes[skylevel]; i < g_numskynormals[skylevel]; i++, pnormal++, psize++ ) { dot = DotProduct( *pnormal, receiver_normal ); if( dot <= 0 ) continue; for( j = 0, pedge = edges; j < numedges; j++, pedge++ ) { if( DotProduct( *pnormal, *pedge ) <= 0 ) break; } if( j < numedges ) continue; area += dot * (*psize); } area = area * 4.0 * M_PI; // convert to absolute sphere area Mem_Free( edges ); return area; } // ===================================================================================== // GetAlternateOrigin // ===================================================================================== void GetAlternateOrigin( const vec3_t pos, const vec3_t normal, const patch_t *patch, vec3_t origin ) { const dplane_t *faceplane; const vec_t *faceplaneoffset; const vec_t *facenormal; dplane_t clipplane; winding_t *w; faceplane = GetPlaneFromFace( patch->faceNumber ); faceplaneoffset = g_face_offset[patch->faceNumber]; facenormal = faceplane->normal; VectorCopy( normal, clipplane.normal ); clipplane.dist = DotProduct( pos, clipplane.normal ); if( WindingOnPlaneSide( patch->winding, clipplane.normal, clipplane.dist, ON_EPSILON ) != SIDE_CROSS ) { VectorCopy( patch->origin, origin ); } else { w = CopyWinding( patch->winding ); ChopWindingInPlace( &w, clipplane.normal, clipplane.dist, ON_EPSILON, false ); if( w == NULL ) { VectorCopy( patch->origin, origin ); } else { vec3_t v, point, bestpoint; vec_t dist, bestdist = -1.0; bool found = false; vec3_t center; WindingCenter( w, center ); VectorMA( center, DEFAULT_HUNT_OFFSET, facenormal, point ); if( HuntForWorld( point, faceplaneoffset, faceplane, 2, 1.0, DEFAULT_HUNT_OFFSET )) { VectorSubtract( point, center, v ); dist = VectorLength( v ); if( !found || dist < bestdist ) { VectorCopy( point, bestpoint ); bestdist = dist; found = true; } } if( !found ) { for( int i = 0; i < (int)w->numpoints; i++ ) { const vec_t *p1 = w->p[i]; const vec_t *p2 = w->p[(i + 1) % w->numpoints]; VectorAdd( p1, p2, point ); VectorAdd( point, center, point ); VectorScale( point, 1.0 / 3.0, point ); VectorMA( point, DEFAULT_HUNT_OFFSET, facenormal, point ); if( HuntForWorld( point, faceplaneoffset, faceplane, 1, 0.0, DEFAULT_HUNT_OFFSET )) { VectorSubtract( point, center, v ); dist = VectorLength( v ); if( !found || dist < bestdist ) { VectorCopy( point, bestpoint ); bestdist = dist; found = true; } } } } if( found ) VectorCopy( bestpoint, origin ); else VectorCopy( patch->origin, origin ); FreeWinding( w ); } } } /* ============= MakeBackplanes ============= */ void MakeBackplanes( void ) { for( int i = 0; i < g_numplanes; i++ ) { VectorNegate( g_dplanes[i].normal, g_backplanes[i].normal ); g_backplanes[i].dist = -g_dplanes[i].dist; } } /* =================================================================== TEXTURE LIGHT VALUES =================================================================== */ static texlight_t g_texlights[MAX_TEXLIGHTS]; static int g_num_texlights; int ParseInfoTexlights( entity_t *mapent ) { int numtexlights = 0; float r, g, b, i; int j, values; epair_t *ep; if( Q_strcmp( ValueForKey( mapent, "classname" ), "info_texlights" )) return 0; MsgDev( D_REPORT, "Reading texlights from info_texlights map entity\n" ); for( ep = mapent->epairs; ep; ep = ep->next ) { if( !Q_strcmp( ep->key, "classname" ) || !Q_strcmp( ep->key, "origin" )) continue; // we dont care about these keyvalues values = sscanf( ep->value, "%f %f %f %f", &r, &g, &b, &i ); if( values == 1 ) { g = b = r; } else if( values == 4 ) // use brightness value. { r *= i / 255.0; g *= i / 255.0; b *= i / 255.0; } else if( values != 3 ) { MsgDev( D_WARN, "ignoring bad texlight '%s' in info_texlights entity", ep->key ); continue; } for( j = 0; j < g_num_texlights; j++ ) { texlight_t *tl = &g_texlights[j]; if( !Q_strcmp( tl->name, ep->key )) { if( !Q_strcmp( tl->filename, "info_texlights" )) { MsgDev( D_REPORT, "duplication of '%s' in info_texlights map entity!\n", tl->name ); } else if( tl->value[0] != r || tl->value[1] != g || tl->value[2] != b ) { MsgDev( D_REPORT, "overriding '%s' from '%s' with info_texlights map entity!\n", tl->name, tl->filename ); } else { MsgDev( D_WARN, "redundant '%s' def in '%s' AND info_texlights map entity!\n", tl->name, tl->filename ); } break; } } Q_strncpy( g_texlights[j].name, ep->key, sizeof( g_texlights[0].name )); g_texlights[j].filename = "info_texlights"; VectorSet( g_texlights[j].value, r, g, b ); g_texlights[j].fade = 1.0f; numtexlights++; g_num_texlights = Q_max( g_num_texlights, j + 1 ); if( g_num_texlights == MAX_TEXLIGHTS ) COM_FatalError( "MAX_TEXLIGHTS limit exceeded\n" ); } return numtexlights; } int ParseSurfaceLights( entity_t *mapent ) { char name[64]; char *pLight = NULL; char *pColor = NULL; vec3_t intensity; int j, argCnt; if( Q_strcmp( ValueForKey( mapent, "classname" ), "light" )) return 0; if( !CheckKey( mapent, "_surface" )) return 0; Q_strncpy( name, ValueForKey( mapent, "_surface" ), sizeof( name )); if( CheckKey( mapent, "_light" )) pLight = ValueForKey( mapent, "_light" ); else pLight = ValueForKey( mapent, "light" ); argCnt = ParseLightIntensity( pLight, intensity ); if( name[0] == '*' ) name[0] = '!'; // quake light if( argCnt <= 1 ) { double r, g, b, scaler; scaler = intensity[0]; r = g = b = 0; if( CheckKey( mapent, "_color" )) pColor = ValueForKey( mapent, "_color" ); else pColor = ValueForKey( mapent, "color" ); if( pColor[0] && sscanf( pColor, "%lf %lf %lf", &r, &g, &b ) == 3 ) { intensity[0] = r * (float)scaler; intensity[1] = g * (float)scaler; intensity[2] = b * (float)scaler; } } for( j = 0; j < g_num_texlights; j++ ) { texlight_t *tl = &g_texlights[j]; if( !Q_strcmp( tl->name, name )) { if( !Q_strcmp( tl->filename, "info_texlights" )) { MsgDev( D_REPORT, "duplication of '%s' in info_texlights map entity!\n", tl->name ); } else if( !VectorCompareEpsilon( tl->value, intensity, ON_EPSILON )) { MsgDev( D_REPORT, "overriding '%s' from '%s' with info_texlights map entity!\n", tl->name, tl->filename ); } else { MsgDev( D_WARN, "redundant '%s' def in '%s' AND info_texlights map entity!\n", tl->name, tl->filename ); } break; } } Q_strncpy( g_texlights[j].name, name, sizeof( g_texlights[0].name )); VectorCopy( intensity, g_texlights[j].value ); g_texlights[j].filename = "light_surface"; g_texlights[j].fade = FloatForKey( mapent, "wait" ); // to prevent division by zero if( g_texlights[j].fade <= 0.0 ) g_texlights[j].fade = 1.0; g_num_texlights = Q_max( g_num_texlights, j + 1 ); if( g_num_texlights == MAX_TEXLIGHTS ) COM_FatalError( "MAX_TEXLIGHTS limit exceeded\n" ); return 1; } // ===================================================================================== // ReadInfoTexlights // try and parse texlight info from the info_texlights entity // ===================================================================================== void ReadInfoTexlights( void ) { int numtexlights = 0; entity_t *mapent; for( int k = 0; k < g_numentities; k++ ) { mapent = &g_entities[k]; // all the vertex cache ID's should be removed now RemoveKey( mapent, "_vlight_id" ); numtexlights += ParseInfoTexlights( mapent ); numtexlights += ParseSurfaceLights( mapent ); } MsgDev( D_REPORT, "[%i texlights parsed from info_texlights map entity]\n\n", numtexlights ); } /* ============ ReadLightFile ============ */ void ReadLightFile( const char *filename, bool use_direct_path ) { int file_texlights = 0; int result = 0; char scan[128]; int j, argCnt; file_t *f; FS_AllowDirectPaths( use_direct_path ); f = FS_Open( filename, "r", false ); FS_AllowDirectPaths( false ); if( !f ) return; while( result != EOF ) { char szTexlight[256]; vec_t r, g, b, i = 1; char *comment = scan; result = FS_Gets( f, (byte *)&scan, sizeof( scan )); // skip the comments if( comment[0] == '/' && comment[1] == '/' ) continue; argCnt = sscanf( scan, "%s %f %f %f %f", szTexlight, &r, &g, &b, &i ); if( argCnt == 2 ) { // eith 1+1 args, the R,G,B values are all equal to the first value g = b = r; } else if( argCnt == 5 ) { // With 1 + 4 args, the R,G,B values are "scaled" by the fourth numeric value i; r *= i / 255.0; g *= i / 255.0; b *= i / 255.0; } else if( argCnt != 4 ) { if( Q_strlen( scan ) > 4 ) MsgDev( D_WARN, "ignoring bad texlight '%s' in %s", scan, filename ); continue; } for( j = 0; j < g_num_texlights; j++ ) { texlight_t *tl = &g_texlights[j]; if( !Q_strcmp( tl->name, szTexlight )) { if( !Q_strcmp( tl->filename, filename )) { MsgDev( D_REPORT, "duplication of '%s' in file '%s'!\n", tl->name, tl->filename ); } else if( tl->value[0] != r || tl->value[1] != g || tl->value[2] != b ) { MsgDev( D_REPORT, "overriding '%s' from '%s' with '%s'!\n", tl->name, tl->filename, filename ); } else { MsgDev( D_WARN, "redundant '%s' def in '%s' AND '%s'!\n", tl->name, tl->filename, filename ); } break; } } Q_strncpy( g_texlights[j].name, szTexlight, sizeof( g_texlights[0].name )); VectorSet( g_texlights[j].value, r, g, b ); g_texlights[j].filename = filename; g_texlights[j].fade = 1.0f; file_texlights++; g_num_texlights = Q_max( g_num_texlights, j + 1 ); if( g_num_texlights == MAX_TEXLIGHTS ) COM_FatalError( "MAX_TEXLIGHTS limit exceeded\n" ); } MsgDev( D_INFO, "[%i texlights parsed from '%s']\n\n", file_texlights, filename ); FS_Close( f ); } /* ============ LightForTexture ============ */ vec_t LightForTexture( const char *name, vec3_t result ) { VectorClear( result ); for( int i = 0; i < g_num_texlights; i++ ) { if( !Q_stricmp( name, g_texlights[i].name )) { VectorCopy( g_texlights[i].value, result ); MsgDev( D_REPORT, "Texture '%s': baselight is (%f,%f,%f).\n", name, result[0], result[1], result[2] ); return g_texlights[i].fade; } } return 1.0f; } /* ======================================================================= MAKE FACES ======================================================================= */ /* ============= WindingFromFace ============= */ winding_t *WindingFromFace( const dface_t *f ) { int i, se, v; dvertex_t *dv; winding_t *w; w = AllocWinding( f->numedges ); w->numpoints = f->numedges; for( i = 0; i < f->numedges; i++ ) { se = g_dsurfedges[f->firstedge + i]; if( se < 0 ) v = g_dedges[-se].v[1]; else v = g_dedges[se].v[0]; dv = &g_dvertexes[v]; VectorCopy( dv->point, w->p[i] ); } RemoveColinearPointsEpsilon( w, ON_EPSILON ); return w; } /* ============= BaseLightForFace ============= */ vec_t BaseLightForFace( dface_t *f, vec3_t light, vec3_t reflectivity ) { int miptex = g_texinfo[f->texinfo].miptex; vec_t fade; miptex_t *mt; // check for light emited by texture mt = GetTextureByMiptex( miptex ); if( !mt ) return 1.0f; fade = LightForTexture( mt->name, light ); VectorClear( reflectivity ); #ifdef HLRAD_REFLECTIVITY int samples = mt->width * mt->height; byte *pal = ((byte *)mt) + mt->offsets[0] + (((mt->width * mt->height) * 85) >> 6); byte *buf = ((byte *)mt) + mt->offsets[0]; vec3_t total; // check for cache if( g_texture_init[miptex] ) { VectorCopy( g_reflectivity[miptex], reflectivity ); return fade; } pal += sizeof( short ); // skip colorsize VectorClear( total ); for( int i = 0; i < samples; i++ ) { vec3_t reflectivity; if( mt->name[0] == '{' && buf[i] == 0xFF ) { VectorClear( reflectivity ); } else { int texel = buf[i]; reflectivity[0] = pow( pal[texel*3+0] * (1.0f / 255.0f), DEFAULT_TEXREFLECTGAMMA ); reflectivity[1] = pow( pal[texel*3+1] * (1.0f / 255.0f), DEFAULT_TEXREFLECTGAMMA ); reflectivity[2] = pow( pal[texel*3+2] * (1.0f / 255.0f), DEFAULT_TEXREFLECTGAMMA ); VectorScale( reflectivity, DEFAULT_TEXREFLECTSCALE, reflectivity ); } VectorAdd( total, reflectivity, total ); } VectorScale( total, 1.0 / (double)(mt->width * mt->height), g_reflectivity[miptex] ); VectorCopy( g_reflectivity[miptex], reflectivity ); MsgDev( D_REPORT, "Texture '%s': reflectivity is (%f,%f,%f).\n", mt->name, reflectivity[0], reflectivity[1], reflectivity[2] ); g_texture_init[miptex] = true; #endif return fade; } static void UpdateEmitterInfo( patch_t *patch ) { const vec_t *origin = patch->origin; const winding_t *winding = patch->winding; vec_t radius = ON_EPSILON; for( int x = 0; x < winding->numpoints; x++ ) { vec3_t delta; vec_t dist; VectorSubtract( winding->p[x], origin, delta ); dist = VectorLength( delta ); radius = Q_max( dist, radius ); } int skylevel = ACCURATEBOUNCE_DEFAULT_SKYLEVEL; vec_t area = WindingArea( winding ); vec_t size = 0.8f; if( area < size * radius * radius ) // the shape is too thin { skylevel++; size *= 0.25f; if( area < size * radius * radius ) { skylevel++; size *= 0.25f; if( area < size * radius * radius ) { // stop here radius = sqrt( area / size ); // just decrease the range to limit the use of the new method. // because when the area is small, the new method becomes randomized and unstable. } } } patch->emitter_range = ACCURATEBOUNCE_THRESHOLD * radius; patch->emitter_skylevel = skylevel; } // ===================================================================================== // PlacePatchInside // ===================================================================================== static bool PlacePatchInside( patch_t *patch ) { const vec_t *face_offset = g_face_offset[patch->faceNumber]; vec_t offset = DEFAULT_HUNT_OFFSET; vec3_t v, center, point, bestpoint; vec_t pointsfound, pointstested; vec_t dist, bestdist = -1.0; const dplane_t *plane; bool found; plane = GetPlaneFromFace( patch->faceNumber ); WindingCenter( patch->winding, center ); pointsfound = pointstested = 0; found = false; VectorMA( center, offset, plane->normal, point ); pointstested++; if( HuntForWorld( point, face_offset, plane, 4, 0.2, offset ) || HuntForWorld( point, face_offset, plane, 4, 0.8, offset )) { VectorSubtract( point, center, v ); dist = VectorLength( v ); pointsfound++; if( !found || dist < bestdist ) { VectorCopy( point, bestpoint ); bestdist = dist; found = true; } } for( int i = 0; i < patch->winding->numpoints; i++ ) { const vec_t *p1 = patch->winding->p[i]; const vec_t *p2 = patch->winding->p[(i+1) % patch->winding->numpoints]; VectorAdd( p1, p2, point ); VectorAdd( point, center, point ); VectorScale( point, 1.0 / 3.0, point ); VectorMA( point, offset, plane->normal, point ); pointstested++; if( HuntForWorld( point, face_offset, plane, 4, 0.2, offset ) || HuntForWorld( point, face_offset, plane, 4, 0.8, offset )) { VectorSubtract( point, center, v ); dist = VectorLength( v ); pointsfound++; if( !found || dist < bestdist ) { VectorCopy( point, bestpoint ); bestdist = dist; found = true; } } } patch->exposure = pointsfound / pointstested; if( found ) { VectorCopy( bestpoint, patch->origin ); return true; } else { VectorMA( center, offset, plane->normal, patch->origin ); SetBits( patch->flags, PATCH_OUTSIDE ); return false; } } // ===================================================================================== // // SUBDIVIDE PATCHES // // ===================================================================================== // ===================================================================================== // CutWindingWithGrid // Caller must free this returned value at some point // ===================================================================================== static void CutWindingWithGrid( patch_t *patch, const dplane_t *plA, const dplane_t *plB ) { // patch->winding->m_NumPoints must > 0 // plA->dist and plB->dist will not be used winding_t *winding = NULL; vec_t chop, epsilon; const int max_gridsize = 64; vec_t gridstartA, minA, maxA; vec_t gridstartB, minB, maxB; int gridsizeA; int gridsizeB; vec_t gridchopA; vec_t gridchopB; int i, numstrips; winding = CopyWinding( patch->winding ); // perform all the operations on the copy chop = Q_max( 1.0, patch->chop ); epsilon = 0.6; // optimize the grid minA = minB = BOGUS_RANGE; maxA = maxB = -BOGUS_RANGE; for( int x = 0; x < winding->numpoints; x++ ) { vec_t dotA, dotB; vec_t *point; point = winding->p[x]; dotA = DotProduct( point, plA->normal ); minA = Q_min( minA, dotA ); maxA = Q_max( maxA, dotA ); dotB = DotProduct( point, plB->normal ); minB = Q_min( minB, dotB ); maxB = Q_max( maxB, dotB ); } gridchopA = chop; gridsizeA = (int)ceil(( maxA - minA - 2 * epsilon ) / gridchopA ); gridsizeA = Q_max( 1, gridsizeA ); if( gridsizeA > max_gridsize ) { gridsizeA = max_gridsize; gridchopA = (maxA - minA) / (vec_t)gridsizeA; } gridstartA = (minA + maxA) / 2.0 - (gridsizeA / 2.0) * gridchopA; gridchopB = chop; gridsizeB = (int)ceil(( maxB - minB - 2 * epsilon ) / gridchopB); gridsizeB = Q_max( 1, gridsizeB ); if( gridsizeB > max_gridsize ) { gridsizeB = max_gridsize; gridchopB = (maxB - minB) / (vec_t)gridsizeB; } gridstartB = (minB + maxB) / 2.0 - (gridsizeB / 2.0) * gridchopB; // cut the winding by the direction of plane A and save into windingArray g_numwindings = 0; for( i = 1; i < gridsizeA; i++ ) { winding_t *front = NULL; winding_t *back = NULL; vec_t dist; dist = gridstartA + i * gridchopA; ClipWindingEpsilon( winding, plA->normal, dist, ON_EPSILON, &front, &back ); if( !front || WindingOnPlaneSide( front, plA->normal, dist, epsilon ) == SIDE_ON ) // ended { if( front ) { FreeWinding( front ); front = NULL; } if( back ) { FreeWinding( back ); back = NULL; } break; } if( !back || WindingOnPlaneSide( back, plA->normal, dist, epsilon ) == SIDE_ON ) // didn't begin { if( front ) { FreeWinding( front ); front = NULL; } if( back ) { FreeWinding( back ); back = NULL; } continue; } FreeWinding( winding ); winding = NULL; g_windingArray[g_numwindings] = back; g_numwindings++; back = NULL; winding = front; front = NULL; } g_windingArray[g_numwindings] = winding; g_numwindings++; winding = NULL; // cut by the direction of plane B numstrips = g_numwindings; for( i = 0; i < numstrips; i++ ) { winding_t *strip = g_windingArray[i]; g_windingArray[i] = NULL; for( int j = 1; j < gridsizeB; j++ ) { winding_t *front = NULL; winding_t *back = NULL; vec_t dist; dist = gridstartB + j * gridchopB; ClipWindingEpsilon( strip, plB->normal, dist, ON_EPSILON, &front, &back ); if( !front || WindingOnPlaneSide( front, plB->normal, dist, epsilon ) == SIDE_ON ) // ended { if( front ) { FreeWinding( front ); front = NULL; } if( back ) { FreeWinding( back ); back = NULL; } break; } if( !back || WindingOnPlaneSide( back, plB->normal, dist, epsilon ) == SIDE_ON ) // didn't begin { if( front ) { FreeWinding( front ); front = NULL; } if( back ) { FreeWinding( back ); back = NULL; } continue; } FreeWinding( strip ); strip = NULL; g_windingArray[g_numwindings] = back; g_numwindings++; back = NULL; strip = front; front = NULL; } g_windingArray[g_numwindings] = strip; g_numwindings++; strip = NULL; } FreeWinding( patch->winding ); patch->winding = NULL; } // ===================================================================================== // GetGridPlanes // From patch, determine perpindicular grid planes to subdivide with (returned in planeA and planeB) // assume S and T is perpindicular (they SHOULD be in worldcraft 3.3 but aren't always...) // ===================================================================================== static void GetGridPlanes( const patch_t *p, dplane_t *pl ) { const dface_t *f = g_dfaces + p->faceNumber; dtexinfo_t *tx = &g_texinfo[f->texinfo]; const dplane_t *faceplane = GetPlaneFromFace( p->faceNumber ); dplane_t *plane = pl; float lmvecs[2][4]; LightMatrixFromTexMatrix( tx, lmvecs ); for( int x = 0; x < 2; x++, plane++ ) { // cut the patch along texel grid planes vec_t val = DotProduct( faceplane->normal, lmvecs[!x] ); VectorMA( lmvecs[!x], -val, faceplane->normal, plane->normal ); VectorNormalize( plane->normal ); plane->dist = DotProduct( plane->normal, p->origin ); } } // ===================================================================================== // SubdividePatch // ===================================================================================== static void SubdividePatch( patch_t *patch ) { dplane_t planes[2]; dplane_t *plA = &planes[0]; dplane_t *plB = &planes[1]; patch_t *new_patch; winding_t **winding; int x; memset( g_windingArray, 0, sizeof( g_windingArray )); g_numwindings = 0; GetGridPlanes( patch, planes ); CutWindingWithGrid( patch, plA, plB ); x = 0; patch->next = NULL; winding = g_windingArray; while( *winding == NULL ) { winding++; x++; } patch->winding = *winding; winding++; x++; patch->area = WindingAreaAndBalancePoint( patch->winding, patch->origin ); SetBits( patch->flags, PATCH_SOURCE ); PlacePatchInside( patch ); UpdateEmitterInfo( patch ); new_patch = &g_patches[g_num_patches]; for( ; x < g_numwindings; x++, winding++ ) { if( *winding ) { memcpy( new_patch, patch, sizeof( patch_t )); ClearBits( new_patch->flags, PATCH_SOURCE ); new_patch->winding = *winding; new_patch->area = WindingAreaAndBalancePoint( new_patch->winding, new_patch->origin ); SetBits( new_patch->flags, PATCH_CHILD ); PlacePatchInside( new_patch ); UpdateEmitterInfo( new_patch ); g_num_patches++; new_patch++; if( g_num_patches == MAX_PATCHES ) COM_FatalError( "MAX_PATCHES limit exceeded\n" ); } } // ATTENTION: We let SortPatches relink all the ->next correctly! instead of doing it here too which is somewhat complicated } /* ============ getScale compute scale for patch ============ */ static vec_t getScale( const patch_t *patch ) { dface_t *f = &g_dfaces[patch->faceNumber]; dtexinfo_t *tx = &g_texinfo[f->texinfo]; const dplane_t *faceplane = GetPlaneFromFace( f ); vec3_t outvecs[2]; vec_t scale[2]; vec_t dot; // snap texture "vecs" to faceplane without affecting texture alignment for( int x = 0; x < 2; x++ ) { dot = DotProduct( faceplane->normal, tx->vecs[x] ); VectorMA( tx->vecs[x], -dot, faceplane->normal, outvecs[x] ); } scale[0] = 1.0 / Q_max( NORMAL_EPSILON, VectorLength( outvecs[0] )); scale[1] = 1.0 / Q_max( NORMAL_EPSILON, VectorLength( outvecs[1] )); // don't care about the angle between vecs[0] and vecs[1] // (given the length of "vecs", smaller angle = larger texel area), // because gridplanes will have the same angle (also smaller angle = larger patch area) return sqrt( scale[0] * scale[1] ); } /* ============ getEmitMode some pathches are emit_surfaces ============ */ static bool getEmitMode( const patch_t *patch ) { bool emitmode = false; vec_t value; if( !VectorIsNull( patch->reflectivity )) value = DotProduct( patch->baselight, patch->reflectivity ) / 3.0; else value = VectorAvg( patch->baselight ); // this patch is emitting surface if( value >= DLIGHT_THRESHOLD ) emitmode = true; return emitmode; } /* ============ getChop compute chop value for patch ============ */ static vec_t getChop( const patch_t *patch ) { vec_t rval; if( FBitSet( patch->flags, PATCH_EMITLIGHT )) rval = g_texchop * getScale( patch ); else rval = g_chop * getScale( patch ); return rval; } /* ============= IsSpecial ============= */ static bool IsSpecial( const dface_t *f ) { return FBitSet( g_texinfo[f->texinfo].flags, TEX_SPECIAL ); } /* ============= MakePatchForFace ============= */ bool MakePatchForFace( int modelnum, int fn, winding_t *w, int style ) { dface_t *f = g_dfaces + fn; vec3_t centroid = { 0, 0, 0 }; vec3_t mins, maxs, delta; patch_t *patch; vec_t fade; dworldlight_t *wl; int i; // No patches at all for the sky! if( IsSpecial( f )) return false; if( w->numpoints < 3 ) // g-cont. how this possible? return false; patch = &g_patches[g_num_patches]; if( g_num_patches == MAX_PATCHES ) COM_FatalError( "MAX_PATCHES limit exceeded\n" ); memset( patch, 0, sizeof( patch_t )); patch->area = WindingAreaAndBalancePoint( w, patch->origin ); patch->modelnum = modelnum; patch->faceNumber = fn; patch->winding = w; // update debug info g_totalarea += patch->area; fade = BaseLightForFace( f, patch->baselight, patch->reflectivity ); for( i = 0; i < MAXLIGHTMAPS; i++ ) patch->totalstyle[i] = 255; if( getEmitMode( patch )) { SetBits( patch->flags, PATCH_EMITLIGHT ); if( style ) { patch->totalstyle[0] = style; patch->emitstyle = style; } } SetBits( patch->flags, PATCH_SOURCE ); patch->scale = getScale( patch ); patch->chop = getChop( patch ); PlacePatchInside( patch ); UpdateEmitterInfo( patch ); patch->fade = fade; // NOTE: we alloc worldlights only once per face. // all child patches after subdivision has the same lightnum if( FBitSet( patch->flags, PATCH_EMITLIGHT )) { wl = AllocWorldLight( &patch->lightnum ); InitWorldLightFromPatch( wl, patch ); } g_face_patches[fn] = patch; g_num_patches++; // per-face data for( i = 0; i < f->numedges; i++ ) { int edge = g_dsurfedges[f->firstedge + i]; if( edge > 0 ) { VectorAdd( g_dvertexes[g_dedges[edge].v[0]].point, centroid, centroid ); VectorAdd( g_dvertexes[g_dedges[edge].v[1]].point, centroid, centroid ); } else { VectorAdd( g_dvertexes[g_dedges[-edge].v[1]].point, centroid, centroid ); VectorAdd( g_dvertexes[g_dedges[-edge].v[0]].point, centroid, centroid ); } } VectorScale( centroid, 1.0 / (f->numedges * 2), centroid ); VectorAdd( centroid, g_face_offset[fn], g_face_centroids[fn] ); // save them for generating the patch normals later. WindingBounds( patch->winding, mins, maxs ); VectorSubtract( maxs, mins, delta ); if( VectorLength( delta ) > patch->chop ) { if( patch->area < 1.0 ) MsgDev( D_WARN, "patch at face %d tiny area (%4.3f) not subdividing\n", patch->faceNumber, patch->area ); else SubdividePatch( patch ); } return true; } /* ============= MakePatches ============= */ void MakePatches( void ) { int fn, style = 0; vec3_t light_origin; vec3_t model_center; bool b_light_origin; bool b_model_center; int i, j, k; vec3_t origin; entity_t *ent; dmodel_t *mod; dface_t *f; winding_t *w; char *s; g_patches = (patch_t *)Mem_Alloc( sizeof( patch_t ) * MAX_PATCHES ); g_numworldlights = 0; for( i = 0; i < g_nummodels; i++ ) { mod = &g_dmodels[i]; ent = EntityForModel( i ); VectorClear( origin ); b_light_origin = false; b_model_center = false; // bmodels with origin brushes need to be offset // into their in-use position if( *( s = ValueForKey( ent, "origin" ))) { double v1, v2, v3; if( sscanf( s, "%lf %lf %lf", &v1, &v2, &v3 ) == 3 ) VectorSet( origin, v1, v2, v3 ); } // allow models to be lit in an alternate location (pt1) if( *( s = ValueForKey( ent, "light_origin" ))) { entity_t *e = FindTargetEntity( s ); if( e ) { if( *( s = ValueForKey( e, "origin" ))) { double v1, v2, v3; if( sscanf( s, "%lf %lf %lf", &v1, &v2, &v3 ) == 3 ) { VectorSet( light_origin, v1, v2, v3 ); b_light_origin = true; } } } } // allow models to be lit in an alternate location (pt2) if( *( s = ValueForKey( ent, "model_center" ))) { double v1, v2, v3; if( sscanf( s, "%lf %lf %lf", &v1, &v2, &v3 ) == 3 ) { VectorSet( model_center, v1, v2, v3 ); b_model_center = true; } } #ifndef HLRAD_PARANOIA_BUMP if( CheckKey( ent, "style" )) style = abs( IntForKey( ent, "style" )); else style = 0; #endif // allow models to be lit in an alternate location (pt3) if( b_light_origin && b_model_center ) { VectorSubtract( light_origin, model_center, origin ); } for( j = 0; j < mod->numfaces; j++ ) { fn = mod->firstface + j; g_face_entity[fn] = ent; VectorCopy( origin, g_face_offset[fn] ); f = &g_dfaces[fn]; w = WindingFromFace( f ); // adjust model origins for( k = 0; k < w->numpoints; k++ ) VectorAdd( w->p[k], origin, w->p[k] ); // some faces won't create patch on them if( !MakePatchForFace( i, fn, w, style )) FreeWinding( w ); } } MsgDev( D_REPORT, "%i square feet [%.2f square inches]\n", (int)(g_totalarea / 144), g_totalarea ); MsgDev( D_INFO, "%i base patches, required %s\n", g_num_patches, Q_memprint( sizeof( patch_t ) * g_num_patches )); MsgDev( D_NOTE, "patch_t = %s\n", Q_memprint( sizeof( patch_t ))); MsgDev( D_NOTE, "sample_t = %s\n", Q_memprint( sizeof( sample_t ))); } // ===================================================================================== // patch_sorter // ===================================================================================== static int CDECL patch_sorter( const void *p1, const void *p2 ) { patch_t *patch1 = (patch_t *)p1; patch_t *patch2 = (patch_t *)p2; if( patch1->faceNumber < patch2->faceNumber ) return -1; else if( patch1->faceNumber > patch2->faceNumber ) return 1; return 0; } /* ============= SortPatches ============= */ static void SortPatches( void ) { patch_t *old_patches = g_patches; g_patches = (patch_t *)Mem_Alloc( sizeof( patch_t ) * ( g_num_patches + 1 )); memcpy( g_patches, old_patches, g_num_patches * sizeof( patch_t )); Mem_Free( old_patches ); qsort((void *)g_patches, (size_t)g_num_patches, sizeof( patch_t ), patch_sorter ); // fixup g_face_patches & fFixup patch->next memset( g_face_patches, 0, sizeof( g_face_patches )); patch_t *patch = g_patches + 1; patch_t *prev = g_patches; int x; g_face_patches[prev->faceNumber] = prev; for( x = 1; x < g_num_patches; x++, patch++ ) { if( patch->faceNumber != prev->faceNumber ) { prev->next = NULL; g_face_patches[patch->faceNumber] = patch; } else { prev->next = patch; } prev = patch; } for( x = 0; x < g_num_patches; x++ ) { patch_t *patch = &g_patches[x]; patch->leafnum = PointInLeaf( patch->origin ) - g_dleafs; } // validate chains (debug thing) for( int facenum = 0; facenum < g_numfaces; facenum++ ) { for( patch = g_face_patches[facenum]; patch != NULL; patch = patch->next ) { ASSERT( patch->faceNumber == facenum ); } } // if we haven't patches we don't need bounces if( g_num_patches <= 0 ) g_numbounce = 0; if( g_numbounce <= 0 ) g_indirect_sun = 0.0f; } /* ============= FreePatches ============= */ static void FreePatches( void ) { patch_t *patch = g_patches; for( int i = 0; i < g_num_patches; i++, patch++ ) FreeWinding( patch->winding ); memset( g_patches, 0, sizeof( patch_t ) * g_num_patches ); Mem_Free( g_patches ); g_patches = NULL; } /* ============= FreeTransfers ============= */ static void FreeTransfers( void ) { patch_t *patch = g_patches; for( int i = 0; i < g_num_patches; i++, patch++ ) { if( patch->tData ) { Mem_Free( patch->tData ); patch->tData = NULL; } if( patch->tIndex ) { Mem_Free( patch->tIndex ); patch->tIndex = NULL; } } } //===================================================================== #ifdef HLRAD_COMPRESS_TRANSFERS static uint GetLengthOfRun( const uint *raw, const uint *end ) { uint run_size = 0; while( raw < end ) { if((( *raw ) + 1 ) == (*( raw + 1 ))) { run_size++; raw++; if( run_size >= MAX_COMPRESSED_TRANSFER_INDEX ) return run_size; } else { return run_size; } } return run_size; } static transfer_index_t *CompressTransferIndicies( const uint *tRaw, const uint rawSize, uint *iSize, int threadnum ) { uint *end = (uint *)tRaw + rawSize - 1; uint compressed_count_1 = 0; uint compressed_count = 0; uint *raw = (uint *)tRaw; uint x, size = rawSize; if( !size ) return NULL; for( x = 0; x < rawSize; x++ ) { x += GetLengthOfRun( tRaw + x, end ); compressed_count_1++; } if( !compressed_count_1 ) return NULL; transfer_index_t *CompressedArray = (transfer_index_t *)Mem_Alloc( sizeof( transfer_index_t ) * compressed_count_1 ); transfer_index_t *compressed = CompressedArray; for( x = 0; x < size; x++, raw++, compressed++ ) { compressed->index = (*raw); // zero based (count 0 still implies 1 item in the list, so 256 max entries result) compressed->size = GetLengthOfRun( raw, end ); raw += compressed->size; x += compressed->size; compressed_count++; // number of entries in compressed table } *iSize = compressed_count; if( compressed_count != compressed_count_1 ) COM_FatalError( "CompressTransferIndicies: internal error\n" ); g_transfer_data_size[threadnum] += sizeof( transfer_index_t ) * compressed_count; return CompressedArray; } #else static transfer_index_t *CompressTransferIndicies( const uint *tRaw, const uint rawSize, uint *iSize, int threadnum ) { uint *end = (uint *)tRaw + rawSize; uint compressed_count = 0; uint *raw = (uint *)tRaw; uint x, size = rawSize; if( !size ) return NULL; transfer_index_t *CompressedArray = (transfer_index_t *)Mem_Alloc( sizeof( transfer_index_t ) * size ); transfer_index_t *compressed = CompressedArray; for( x = 0; x < size; x++, raw++, compressed++ ) { compressed->index = (*raw); compressed->size = 0; compressed_count++; // number of entries in compressed table } *iSize = compressed_count; g_transfer_data_size[threadnum] += sizeof( transfer_index_t ) * compressed_count; return CompressedArray; } #endif /* =========== MakeTransfers This is run by multiple threads =========== */ static void MakeTransfers( int threadnum ) { transfer_data_t *tData_All = (transfer_data_t *)Mem_Alloc( sizeof( transfer_data_t ) * ( g_num_patches + 1 )); uint *tIndex_All = (uint *)Mem_Alloc( sizeof( transfer_index_t ) * ( g_num_patches + 1 )); patch_t **vispatches = (patch_t **)Mem_Alloc(( g_num_patches + 1 ) * sizeof( patch_t* )); byte pvs[(MAX_MAP_LEAFS+7)/8]; const vec_t *normal1, *normal2; float trans, total, dist; bool check_vis = true; patch_t *patch1, *patch2; int i, j, lastoffset; int count = 0; uint *tIndex; transfer_data_t *tData; vec3_t delta; while( 1 ) { if(( i = GetThreadWork( )) == -1 ) break; patch1 = g_patches + i; // calculate visibility for the patch if( !g_visdatasize ) { if( check_vis ) { memset( pvs, 255, (g_numvisleafs + 7) / 8 ); check_vis = false; } } else { dleaf_t *leaf = &g_dleafs[patch1->leafnum]; int thisoffset = leaf->visofs; if( check_vis || thisoffset != lastoffset ) { if( thisoffset == -1 ) { memset( pvs, 0, (g_numvisleafs + 7) / 8 ); } else { DecompressVis( &g_dvisdata[leaf->visofs], pvs ); } check_vis = false; } lastoffset = thisoffset; } const dplane_t *plane1 = GetPlaneFromFace( patch1->faceNumber ); count = 0; // find out which patch2's will collect light from patch for( j = 0, patch2 = g_patches; j < g_num_patches; j++, patch2++ ) { if( i == j ) continue; if( !patch2->leafnum || !CHECKVISBIT( pvs, patch2->leafnum - 1 )) continue; const dplane_t *plane2 = GetPlaneFromFace( patch2->faceNumber ); if( DotProduct( patch1->origin, plane2->normal ) <= PatchPlaneDist( patch2 ) + ON_EPSILON - patch1->emitter_range ) continue; // we need to do a real test vec3_t origin1, origin2, delta; vec_t dist; VectorSubtract( patch1->origin, patch2->origin, delta ); dist = VectorLength( delta ); if( dist < patch2->emitter_range - ON_EPSILON ) GetAlternateOrigin( patch1->origin, plane1->normal, patch2, origin2 ); else VectorCopy( patch2->origin, origin2 ); if( DotProduct( origin2, plane1->normal ) <= PatchPlaneDist( patch1 ) + MINIMUM_PATCH_DISTANCE ) continue; if( dist < patch1->emitter_range - ON_EPSILON ) GetAlternateOrigin( patch2->origin, plane2->normal, patch1, origin1 ); else VectorCopy( patch1->origin, origin1 ); if( DotProduct( origin1, plane2->normal ) <= PatchPlaneDist( patch2 ) + MINIMUM_PATCH_DISTANCE ) continue; // we ignore models here, brushes only if( TestLine( threadnum, origin1, origin2, true ) != CONTENTS_EMPTY ) continue; vispatches[count] = patch2; count++; } // compute transfers for this patch patch1->iIndex = patch1->iData = 0; normal1 = plane1->normal; tIndex = tIndex_All; tData = tData_All; for( j = 0; j < count; j++ ) { bool light_behind_surface = false; vec_t dot1, dot2; patch2 = vispatches[j]; normal2 = GetPlaneFromFace( patch2->faceNumber )->normal; // calculate transference VectorSubtract( patch2->origin, patch1->origin, delta ); // move emitter back to its plane VectorMA( delta, -DEFAULT_HUNT_OFFSET, normal2, delta ); dist = VectorNormalize( delta ); dot1 = DotProduct( delta, normal1 ); dot2 = -DotProduct( delta, normal2 ); if( dot1 <= NORMAL_EPSILON ) light_behind_surface = true; if( dot2 * dist <= MINIMUM_PATCH_DISTANCE ) continue; // inverse square falloff factoring angle between patch normals trans = (dot1 * dot2) / (dist * dist); // HLRAD_TRANSWEIRDFIX: // we should limit "trans( patch2receive ) * patch1area" // instead of "trans( patch2receive ) * patch2area". // also raise "0.4f" to "0.8f" ( 0.8 / M_PI = 1 / 4). if( trans * patch2->area > 0.8f ) trans = 0.8f / patch2->area; if( dist < patch2->emitter_range - ON_EPSILON ) { if( light_behind_surface ) trans = 0.0; vec_t sightarea = CalcSightArea( patch1->origin, normal1, patch2->winding, patch2->emitter_skylevel ); vec_t frac = dist / patch2->emitter_range; frac = (frac - 0.5f) * 2.0f; // make a smooth transition between the two methods frac = bound( 0.0, frac, 1.0 ); trans = frac * trans + (1 - frac) * (sightarea / patch2->area); // because later we will multiply this back } else if( light_behind_surface ) { continue; } trans *= patch2->exposure; // scale to 16 bit (black magic) trans = trans * patch2->area * INVERSE_TRANSFER_SCALE; if( trans > TRANSFER_SCALE_MAX ) trans = TRANSFER_SCALE_MAX; if( trans <= 0.0 ) continue; *tData = trans; patch1->iData++; *tIndex = patch2 - g_patches; tIndex++; tData++; } // copy the transfers out if( patch1->iData ) { uint data_size = patch1->iData * sizeof( transfer_data_t ); transfer_data_t *t1, *t2; patch1->tData = (transfer_data_t *)Mem_Alloc( data_size ); patch1->tIndex = CompressTransferIndicies( tIndex_All, patch1->iData, &patch1->iIndex, threadnum ); g_transfer_data_size[threadnum] += data_size; total = 0.5 / M_PI; t1 = patch1->tData; t2 = tData_All; for( uint x = 0; x < patch1->iData; x++, t1++, t2++ ) (*t1) = (*t2) * total; } } Mem_Free( vispatches ); Mem_Free( tIndex_All ); Mem_Free( tData_All ); } /* ============ CalcTransferSize ============ */ void CalcTransferSize( void ) { g_transfer_data_bytes = 0; for( int i = 0; i < MAX_THREADS; i++ ) g_transfer_data_bytes += g_transfer_data_size[i]; MsgDev( D_INFO, "transfer lists: %s\n", Q_memprint( g_transfer_data_bytes )); } /* ============== MakeTransfers ============== */ void MakeTransfers( void ) { RunThreadsOn( g_num_patches, true, MakeTransfers ); // display transfer size CalcTransferSize(); } /* ============= CollectLight ============= */ void CollectLight( void ) { patch_t *patch; int i, j; for( i = 0, patch = g_patches; i < g_num_patches; i++, patch++ ) { for( j = 0; j < MAXLIGHTMAPS && newstyles[i][j] != 255; j++ ) { VectorAdd( patch->totallight[j], addlight[i][j], patch->totallight[j] ); VectorScale( addlight[i][j], TRANSFER_SCALE, emitlight[i][j] ); VectorClear( addlight[i][j] ); #ifdef HLRAD_DELUXEMAPPING VectorAdd( patch->totallight_dir[j], addlight_dir[i][j], patch->totallight_dir[j] ); VectorCopy( addlight_dir[i][j], emitlight_dir[i][j] ); VectorClear( addlight_dir[i][j] ); #endif } // store new styles back into patch memcpy( g_patches[i].totalstyle, newstyles[i], sizeof( byte[MAXLIGHTMAPS] )); } } /* ============= BounceLight Get light from other patches Run multi-threaded ============= */ void BounceLight( int threadnum ) { int j, k, m; patch_t *patch; vec3_t v; while( 1 ) { if(( j = GetThreadWork()) == -1 ) break; patch = &g_patches[j]; transfer_data_t *tData = patch->tData; transfer_index_t *tIndex = patch->tIndex; uint iIndex = patch->iIndex; int overflowed_styles = 0; for( m = 0; m < MAXLIGHTMAPS && newstyles[j][m] != 255; m++ ) VectorClear( addlight[j][m] ); for( k = 0; k < iIndex; k++, tIndex++ ) { uint size = (tIndex->size + 1); uint patchnum = tIndex->index; uint l; for( l = 0; l < size; l++, tData++, patchnum++ ) { if( patchnum < 0 || patchnum >= g_num_patches ) { MsgDev( D_ERROR, "bad patchnum %i, max %i\n", patchnum, g_num_patches ); continue; } patch_t *emitpatch = &g_patches[patchnum]; #ifdef HLRAD_DELUXEMAPPING vec3_t direction; VectorSubtract( patch->origin, emitpatch->origin, direction ); VectorNormalize( direction ); #endif // for each style on the emitting patch for( int emitstyle = 0; emitstyle < MAXLIGHTMAPS && emitpatch->totalstyle[emitstyle] != 255; emitstyle++ ) { // find the matching style on this (destination) patch for( m = 0; m < MAXLIGHTMAPS && newstyles[j][m] != 255; m++ ) { if( newstyles[j][m] == emitpatch->totalstyle[emitstyle] ) break; } if( m < MAXLIGHTMAPS ) { VectorScale( emitlight[patchnum][emitstyle], (float)(*tData), v ); if( !VectorIsFinite( v )) continue; if( VectorMaximum( v ) < EQUAL_EPSILON ) continue; if( newstyles[j][m] == 255 ) newstyles[j][m] = emitpatch->totalstyle[emitstyle]; VectorAdd( addlight[j][m], v, addlight[j][m] ); #ifdef HLRAD_DELUXEMAPPING vec_t brightness = VectorAvg( v ); VectorMA( addlight_dir[j][m], brightness, direction, addlight_dir[j][m] ); #endif } else { overflowed_styles++; } } } } g_overflowed_styles_onpatch[threadnum] += overflowed_styles; } } /* ============= BounceLight ============= */ void BounceLight( void ) { int i, j; for( i = 0; i < g_num_patches; i++ ) { patch_t *patch = &g_patches[i]; for( j = 0; j < MAXLIGHTMAPS && g_patches[i].totalstyle[j] != 255; j++ ) { VectorScale( patch->totallight[j], TRANSFER_SCALE, emitlight[i][j] ); #ifdef HLRAD_DELUXEMAPPING VectorCopy( patch->totallight_dir[j], emitlight_dir[i][j] ); #endif } memcpy( newstyles[i], g_patches[i].totalstyle, sizeof( byte[MAXLIGHTMAPS] )); } for( i = 0; i < g_numbounce; i++ ) { RunThreadsOnIncremental( g_num_patches, true, BounceLight, i + 1 ); CollectLight(); } } //============================================================== /* ============= RadWorld ============= */ void RadWorld( void ) { MakeBackplanes(); // turn each face into a single patch MakePatches (); SortPatches (); PairEdges (); BuildFaceNeighbors(); BuildDiffuseNormals(); // create directlights out of patches and lights CreateDirectLights (); if( g_onlylights ) { DeleteDirectLights (); FreeDiffuseNormals (); FreeFaceNeighbors(); FreeSharedEdges(); FreeFaceLights(); FreePatches(); return; } InitWorldTrace(); // generate a position map for each face RunThreadsOnIndividual( g_numfaces, true, FindFacePositions ); CalcPositionsSize(); // build initial facelights RunThreadsOnIndividual( g_numfaces, true, BuildFaceLights ); CalcSampleSize (); #ifdef HLRAD_LIGHTMAPMODELS BuildModelLightmaps(); CalcModelSampleSize(); #endif #ifdef HLRAD_VERTEXLIGHTING BuildVertexLights(); #endif // free up the direct lights now that we have facelights DeleteDirectLights (); FreeFacePositions (); CalcLuxelsCount(); if( g_numbounce > 0 ) { // build transfer lists MakeTransfers(); emitlight = (vec3_t (*)[MAXLIGHTMAPS])Mem_Alloc(( g_num_patches + 1 ) * sizeof( vec3_t[MAXLIGHTMAPS] )); addlight = (vec3_t (*)[MAXLIGHTMAPS])Mem_Alloc(( g_num_patches + 1 ) * sizeof( vec3_t[MAXLIGHTMAPS] )); newstyles = (byte (*)[MAXLIGHTMAPS])Mem_Alloc(( g_num_patches + 1 ) * sizeof( byte[MAXLIGHTMAPS] )); #ifdef HLRAD_DELUXEMAPPING emitlight_dir = (vec3_t (*)[MAXLIGHTMAPS])Mem_Alloc(( g_num_patches + 1 ) * sizeof( vec3_t[MAXLIGHTMAPS] )); addlight_dir = (vec3_t (*)[MAXLIGHTMAPS])Mem_Alloc(( g_num_patches + 1 ) * sizeof( vec3_t[MAXLIGHTMAPS] )); #endif // spread light around BounceLight (); Mem_Free( emitlight ); Mem_Free( addlight ); Mem_Free( newstyles ); emitlight = NULL; addlight = NULL; newstyles = NULL; #ifdef HLRAD_DELUXEMAPPING Mem_Free( emitlight_dir ); Mem_Free( addlight_dir ); emitlight_dir = NULL; addlight_dir = NULL; #endif // transfers don't need anymore FreeTransfers(); } // remove direct light from patches for( int i = 0; i < g_num_patches; i++ ) { patch_t *p = &g_patches[i]; for( int j = 0; j < MAXLIGHTMAPS && p->totalstyle[j] != 255; j++ ) { VectorSubtract( p->totallight[j], p->directlight[j], p->totallight[j] ); #ifdef HLRAD_DELUXEMAPPING VectorSubtract( p->totallight_dir[j], p->directlight_dir[j], p->totallight_dir[j] ); #endif } } if( !g_lightbalance ) ScaleDirectLights(); // because fastmode uses patches instead of samples if( g_numbounce > 0 || g_fastmode ) RunThreadsOnIndividual( g_numfaces, false, CreateTriangulations ); // blend bounced light into direct light and save PrecompLightmapOffsets(); if( g_numbounce > 0 || g_fastmode ) { CreateFacelightDependencyList(); RunThreadsOnIndividual( g_numfaces, true, FacePatchLights ); #ifdef HLRAD_VERTEXLIGHTING VertexPatchLights(); #endif FreeFacelightDependencyList(); } FreeDiffuseNormals (); if( g_numbounce > 0 || g_fastmode ) FreeTriangulations(); if( g_lightbalance ) ScaleDirectLights(); RunThreadsOnIndividual( g_numfaces, true, FinalLightFace ); #ifdef HLRAD_LIGHTMAPMODELS FinalModelLightFace(); #endif // now can be freed FreeFaceLights(); #ifndef HLRAD_PARANOIA_BUMP ReduceLightmap(); #endif #ifdef HLRAD_AMBIENTCUBES ComputeLeafAmbientLighting(); #endif #ifdef HLRAD_LIGHTMAPMODELS WriteModelLighting(); #endif #ifdef HLRAD_VERTEXLIGHTING FinalLightVertex(); UnparseEntities(); #endif FreeFaceNeighbors(); FreeSharedEdges(); FreePatches(); } /* ============ PrintRadSettings show compiler settings like ZHLT ============ */ static void PrintRadSettings( void ) { char buf1[1024]; char buf2[1024]; Msg( "\nCurrent p2rad settings\n" ); Msg( "Name | Setting | Default\n" ); Msg( "---------------------|-----------|-------------------------\n" ); Msg( "developer [ %7d ] [ %7d ]\n", GetDeveloperLevel(), DEFAULT_DEVELOPER ); Msg( "fast rad [ %7s ] [ %7s ]\n", g_fastmode ? "on" : "off", DEFAULT_FASTMODE ? "on" : "off" ); Msg( "extra rad [ %7s ] [ %7s ]\n", g_extra ? "on" : "off", DEFAULT_EXTRAMODE ? "on" : "off" ); Msg( "bounces [ %7d ] [ %7d ]\n", g_numbounce, DEFAULT_BOUNCE ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_smoothvalue ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_SMOOTHVALUE ); Msg( "smoothing threshold [ %7s ] [ %7s ]\n", buf1, buf2 ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_blur ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_BLUR ); Msg( "blur size [ %7s ] [ %7s ]\n", buf1, buf2 ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_lightbalance ? 1.0f : g_direct_scale ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_DLIGHT_SCALE ); Msg( "direct light scale [ %7s ] [ %7s ]\n", buf1, buf2 ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_lightbalance ? g_direct_scale : 1.0f ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_LIGHT_SCALE ); Msg( "global light scale [ %7s ] [ %7s ]\n", buf1, buf2 ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_gamma ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_GAMMA ); Msg( "gamma factor [ %7s ] [ %7s ]\n", buf1, buf2 ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_chop ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_CHOP ); Msg( "chop value [ %7s ] [ %7s ]\n", buf1, buf2 ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_texchop ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_TEXCHOP ); Msg( "texchop value [ %7s ] [ %7s ]\n", buf1, buf2 ); Q_snprintf( buf1, sizeof( buf1 ), "%3.3f", g_indirect_sun ); Q_snprintf( buf2, sizeof( buf2 ), "%3.3f", DEFAULT_INDIRECT_SUN ); Msg( "global sky diffusion [ %7s ] [ %7s ]\n", buf1, buf2 ); Msg( "dirtmapping [ %7s ] [ %7s ]\n", g_dirtmapping ? "on" : "off", DEFAULT_DIRTMAPPING ? "on" : "off" ); #ifdef HLRAD_PARANOIA_BUMP Msg( "gamma mode [ %7d ] [ %7d ]\n", g_gammamode, DEFAULT_GAMMAMODE ); #endif Msg( "\n" ); } /* ============ PrintRadUsage show compiler usage like ZHLT ============ */ static void PrintRadUsage( void ) { Msg( "\n-= p2rad Options =-\n\n" ); Msg( " -dev # : compile with developer message (1 - 4). default is %d\n", DEFAULT_DEVELOPER ); Msg( " -threads # : manually specify the number of threads to run\n" ); Msg( " -extra : improve lighting quality with lightmap filtering\n" ); Msg( " -bounce # : set number of radiosity bounces\n" ); Msg( " -ambient r g b : set ambient world light (0.0 to 1.0, r g b)\n" ); Msg( " -smooth # : set smoothing threshold for blending (in degrees)\n" ); Msg( " -blur : filtering the lightmap by post-processing\n" ); Msg( " -dscale : direct light scaling factor\n" ); Msg( " -gamma : set global gamma value\n" ); Msg( " -chop # : set radiosity patch size for normal textures\n" ); Msg( " -texchop # : set radiosity patch size for texture light faces\n" ); Msg( " -sky # : set ambient sunlight contribution in the shade outside\n" ); Msg( " -nomodelshadow : ignore shadows from alias and studiomodels\n" ); Msg( " -balance : -dscale will be interpret as global scaling factor\n" ); Msg( " -dirty : enable dirtmapping (baked AO)\n" ); Msg( " -onlylights : update only worldlights lump\n" ); #ifdef HLRAD_PARANOIA_BUMP Msg( " -gammamode # : gamma correction mode (0, 1, 2)\n" ); #endif Msg( " bspfile : The bspfile to compile\n\n" ); exit( 1 ); } /* ======== main light modelfile ======== */ int main( int argc, char **argv ) { double start, end; char str[64]; int i; atexit( Sys_CloseLog ); source[0] = '\0'; g_smoothing_threshold = cos( DEG2RAD( g_smoothvalue )); // Originally zero. for( i = 1; i < argc; i++ ) { if( !Q_strcmp( argv[i], "-dev" )) { SetDeveloperLevel( atoi( argv[i+1] )); i++; } else if( !Q_strcmp( argv[i], "-threads" )) { g_numthreads = atoi( argv[i+1] ); i++; } else if( !Q_strcmp( argv[i], "-fast" )) { g_nomodelshadow = true; g_lerp_enabled = false; g_dirtmapping = false; g_fastmode = true; } else if( !Q_strcmp( argv[i], "-extra" )) { g_lerp_enabled = true; g_extra = true; g_blur = 1.5f; } else if( !Q_strcmp( argv[i], "-bounce" )) { g_numbounce = atoi( argv[i+1] ); g_numbounce = bound( 0, g_numbounce, 128 ); i++; } else if( !Q_strcmp( argv[i], "-ambient" )) { if( argc > ( i + 3 )) { g_ambient[0] = (float)atof( argv[i+1] ) * 0.5f; g_ambient[1] = (float)atof( argv[i+2] ) * 0.5f; g_ambient[2] = (float)atof( argv[i+3] ) * 0.5f; i += 3; } else { break; } } else if( !Q_strcmp( argv[i], "-smooth" )) { g_smoothvalue = atof( argv[i+1] ); g_smoothing_threshold = (float)cos( DEG2RAD( g_smoothvalue )); i++; } else if( !Q_strcmp( argv[i], "-blur" )) { g_blur = atof( argv[i+1] ); i++; } else if( !Q_strcmp( argv[i], "-dscale" )) { g_direct_scale = (float)atof( argv[i+1] ); i++; } else if( !Q_strcmp( argv[i], "-gamma" )) { g_gamma = (float)atof( argv[i+1] ); i++; } else if( !Q_strcmp( argv[i], "-chop" )) { g_chop = (float)atof( argv[i+1] ); g_chop = bound( 32, g_chop, 128 ); i++; } else if( !Q_strcmp( argv[i], "-texchop" )) { g_texchop = (float)atof( argv[i+1] ); g_texchop = bound( 32, g_texchop, 128 ); i++; } else if( !Q_strcmp( argv[i], "-sky" )) { g_indirect_sun = (float)atof( argv[i+1] ); g_indirect_sun = bound( 0.0f, g_indirect_sun, 2.0f ); i++; } else if( !Q_strcmp( argv[i], "-nomodelshadow" )) { g_nomodelshadow = true; } else if( !Q_strcmp( argv[i], "-balance" )) { g_lightbalance = true; } else if( !Q_strcmp( argv[i], "-onlylights" )) { g_onlylights = true; } else if( !Q_strcmp( argv[i], "-quake" )) { // special preset for quake g_lightbalance = true; g_indirect_scale = 1.0f; g_direct_scale = 0.5f; g_indirect_sun = 0.0f; g_gamma = 1.0f; } else if( !Q_strcmp( argv[i], "-dirty" )) { g_dirtmapping = !g_fastmode; } #ifdef HLRAD_PARANOIA_BUMP else if( !Q_strcmp( argv[i], "-gammamode" )) { g_gammamode = (float)atoi( argv[i+1] ); i++; } #endif else if( argv[i][0] == '-' ) { MsgDev( D_ERROR, "\nUnknown option \"%s\"\n", argv[i] ); break; } else if( !source[0] ) { Q_strncpy( source, COM_ExpandArg( argv[i] ), sizeof( source )); COM_StripExtension( source ); } else { MsgDev( D_ERROR, "\nUnknown option \"%s\"\n", argv[i] ); break; } } if( i != argc || !source[0] ) { if( !source[0] ) Msg( "no mapfile specified\n" ); PrintRadUsage(); } start = I_FloatTime (); Sys_InitLogAppend( va( "%s.log", source )); Msg( "\n%s %s (%s)\n", TOOLNAME, VERSIONSTRING, __DATE__ ); PrintRadSettings(); ThreadSetDefault (); // starting base filesystem FS_Init( source ); // Set the required global lights filename // try looking in the directory we were run from GetModuleFileName( NULL, global_lights, sizeof( global_lights )); COM_ExtractFilePath( global_lights, global_lights ); Q_strncat( global_lights, "\\lights.rad", sizeof( global_lights )); // Set the optional level specific lights filename COM_FileBase( source, str ); Q_snprintf( level_lights, sizeof( level_lights ), "maps/%s.rad", str ); if( !FS_FileExists( level_lights, false )) level_lights[0] = '\0'; ReadLightFile( global_lights, true ); // Required if( *level_lights ) ReadLightFile( level_lights, false ); // Optional & implied COM_DefaultExtension( source, ".bsp" ); LoadBSPFile( source ); if( g_nummodels <= 0 ) COM_FatalError( "map %s without any models\n", source ); ParseEntities(); TEX_LoadTextures(); ReadInfoTexlights(); SetupDirt(); if( !g_visdatasize && g_numbounce > 0 ) MsgDev( D_ERROR, "no vis information, compile time may be adversely affected.\n" ); // keep it in acceptable range g_blur = bound( 1.0, g_blur, 8.0 ); g_gamma = bound( 0.3, g_gamma, 1.0 ); RadWorld (); WriteBSPFile( source ); TEX_FreeTextures (); FreeWorldTrace (); FreeEntities (); FS_Shutdown(); SetDeveloperLevel( D_REPORT ); Mem_Check(); end = I_FloatTime (); Q_timestring((int)( end - start ), str ); Msg( "%s elapsed\n", str ); return 0; }