/* ------------------------------------------------------------------------------- Copyright (C) 1999-2007 id Software, Inc. and contributors. For a list of contributors, see the accompanying CONTRIBUTORS file. This file is part of GtkRadiant. GtkRadiant is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. GtkRadiant is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GtkRadiant; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA ---------------------------------------------------------------------------------- This code has been altered significantly from its original form, to support several games based on the Quake III Arena engine, in the form of "Q3Map2." ------------------------------------------------------------------------------- */ /* dependencies */ #include "q3map2.h" #include "shaders.h" /* ColorMod() routines for dealing with vertex color/alpha modification */ void ColorMod( colorMod_t *cm, int numVerts, bspDrawVert_t *drawVerts ){ /* dummy check */ if ( cm == NULL || numVerts < 1 || drawVerts == NULL ) { return; } /* walk vertex list */ for ( bspDrawVert_t& dv : Span( drawVerts, numVerts ) ) { /* walk colorMod list */ for ( ; cm != NULL; cm = cm->next ) { float c; /* default */ Color4f mult( 1, 1, 1, 1 ); Color4f add( 0, 0, 0, 0 ); const Vector3 cm_vec3 = vector3_from_array( cm->data ); /* switch on type */ switch ( cm->type ) { case EColorMod::ColorSet: mult.rgb().set( 0 ); add.rgb() = cm_vec3 * 255.0f; break; case EColorMod::AlphaSet: mult.alpha() = 0.0f; add.alpha() = cm->data[ 0 ] * 255.0f; break; case EColorMod::ColorScale: mult.rgb() = cm_vec3; break; case EColorMod::AlphaScale: mult.alpha() = cm->data[ 0 ]; break; case EColorMod::ColorDotProduct: c = vector3_dot( dv.normal, cm_vec3 ); mult.rgb().set( c ); break; case EColorMod::ColorDotProductScale: c = vector3_dot( dv.normal, cm_vec3 ); c = ( c - cm->data[3] ) / ( cm->data[4] - cm->data[3] ); mult.rgb().set( c ); break; case EColorMod::AlphaDotProduct: mult.alpha() = vector3_dot( dv.normal, cm_vec3 ); break; case EColorMod::AlphaDotProductScale: c = vector3_dot( dv.normal, cm_vec3 ); c = ( c - cm->data[3] ) / ( cm->data[4] - cm->data[3] ); mult.alpha() = c; break; case EColorMod::ColorDotProduct2: c = vector3_dot( dv.normal, cm_vec3 ); c *= c; mult.rgb().set( c ); break; case EColorMod::ColorDotProduct2Scale: c = vector3_dot( dv.normal, cm_vec3 ); c *= c; c = ( c - cm->data[3] ) / ( cm->data[4] - cm->data[3] ); mult.rgb().set( c ); break; case EColorMod::AlphaDotProduct2: mult.alpha() = vector3_dot( dv.normal, cm_vec3 ); mult.alpha() *= mult.alpha(); break; case EColorMod::AlphaDotProduct2Scale: c = vector3_dot( dv.normal, cm_vec3 ); c *= c; c = ( c - cm->data[3] ) / ( cm->data[4] - cm->data[3] ); mult.alpha() = c; break; default: break; } /* apply mod */ for ( auto& color : dv.color ) { color = color_to_byte( mult * static_cast>( color ) + add ); } } } } /* TCMod*() routines for dealing with a 3x3 texture mod matrix */ void TCMod( const tcMod_t& mod, Vector2& st ){ const Vector2 old = st; st[ 0 ] = ( mod[ 0 ][ 0 ] * old[ 0 ] ) + ( mod[ 0 ][ 1 ] * old[ 1 ] ) + mod[ 0 ][ 2 ]; st[ 1 ] = ( mod[ 1 ][ 0 ] * old[ 0 ] ) + ( mod[ 1 ][ 1 ] * old[ 1 ] ) + mod[ 1 ][ 2 ]; } void TCModIdentity( tcMod_t& mod ){ mod[ 0 ][ 0 ] = 1.0f; mod[ 0 ][ 1 ] = 0.0f; mod[ 0 ][ 2 ] = 0.0f; mod[ 1 ][ 0 ] = 0.0f; mod[ 1 ][ 1 ] = 1.0f; mod[ 1 ][ 2 ] = 0.0f; mod[ 2 ][ 0 ] = 0.0f; mod[ 2 ][ 1 ] = 0.0f; mod[ 2 ][ 2 ] = 1.0f; /* this row is only used for multiples, not transformation */ } void TCModMultiply( const tcMod_t& a, const tcMod_t& b, tcMod_t& out ){ for ( int i = 0; i < 3; i++ ) { out[ i ][ 0 ] = ( a[ i ][ 0 ] * b[ 0 ][ 0 ] ) + ( a[ i ][ 1 ] * b[ 1 ][ 0 ] ) + ( a[ i ][ 2 ] * b[ 2 ][ 0 ] ); out[ i ][ 1 ] = ( a[ i ][ 0 ] * b[ 0 ][ 1 ] ) + ( a[ i ][ 1 ] * b[ 1 ][ 1 ] ) + ( a[ i ][ 2 ] * b[ 2 ][ 1 ] ); out[ i ][ 2 ] = ( a[ i ][ 0 ] * b[ 0 ][ 2 ] ) + ( a[ i ][ 1 ] * b[ 1 ][ 2 ] ) + ( a[ i ][ 2 ] * b[ 2 ][ 2 ] ); } } void TCModTranslate( tcMod_t& mod, float s, float t ){ mod[ 0 ][ 2 ] += s; mod[ 1 ][ 2 ] += t; } void TCModScale( tcMod_t& mod, float s, float t ){ mod[ 0 ][ 0 ] *= s; mod[ 1 ][ 1 ] *= t; } void TCModRotate( tcMod_t& mod, float euler ){ tcMod_t old, temp; float radians, sinv, cosv; memcpy( old, mod, sizeof( tcMod_t ) ); TCModIdentity( temp ); radians = degrees_to_radians( euler ); sinv = sin( radians ); cosv = cos( radians ); temp[ 0 ][ 0 ] = cosv; temp[ 0 ][ 1 ] = -sinv; temp[ 1 ][ 0 ] = sinv; temp[ 1 ][ 1 ] = cosv; TCModMultiply( old, temp, mod ); } const surfaceParm_t *GetSurfaceParm( const char *name ){ /* walk the current game's surfaceparms */ for( const surfaceParm_t& sp : g_game->surfaceParms ) if ( striEqual( name, sp.name ) ) return &sp; /* check custom info parms */ for ( const surfaceParm_t& sp : Span( custSurfaceParms, numCustSurfaceParms ) ) if ( striEqual( name, sp.name ) ) return &sp; return nullptr; } /* ApplySurfaceParm() - ydnar applies a named surfaceparm to the supplied flags */ bool ApplySurfaceParm( const char *name, int *contentFlags, int *surfaceFlags, int *compileFlags ){ if( const surfaceParm_t *sp = GetSurfaceParm( name ) ){ /* clear and set flags */ if( contentFlags != nullptr ){ *contentFlags &= ~( sp->contentFlagsClear ); *contentFlags |= sp->contentFlags; } if( surfaceFlags != nullptr ){ *surfaceFlags &= ~( sp->surfaceFlagsClear ); *surfaceFlags |= sp->surfaceFlags; } if( compileFlags != nullptr ){ *compileFlags &= ~( sp->compileFlagsClear ); *compileFlags |= sp->compileFlags; } /* return ok */ return true; } /* no matching surfaceparm found */ return false; } /* BeginMapShaderFile() - ydnar erases and starts a new map shader script */ void BeginMapShaderFile( const char *mapFile ){ /* dummy check */ mapName.clear(); mapShaderFile = ""; if ( strEmptyOrNull( mapFile ) ) { return; } /* extract map name */ mapName( PathFilename( mapFile ) ); /* append ../scripts/q3map2_.shader */ mapShaderFile = StringOutputStream( 256 )( PathFilenameless( mapFile ), "../", g_game->shaderPath, "/q3map2_", mapName.c_str(), ".shader" ); Sys_FPrintf( SYS_VRB, "Map has shader script %s\n", mapShaderFile.c_str() ); /* remove it */ remove( mapShaderFile.c_str() ); /* stop making warnings about missing images */ warnImage = false; } /* WriteMapShaderFile() - ydnar writes a shader to the map shader script */ void WriteMapShaderFile( void ){ /* dummy check */ if ( mapShaderFile.empty() ) { return; } /* are there any custom shaders? */ if( std::none_of( shaderInfo, shaderInfo + numShaderInfo, []( const shaderInfo_t& si ){ return si.custom; } ) ) return; /* note it */ Sys_FPrintf( SYS_VRB, "--- WriteMapShaderFile ---\n" ); Sys_FPrintf( SYS_VRB, "Writing %s", mapShaderFile.c_str() ); /* open shader file */ FILE *file = fopen( mapShaderFile.c_str(), "wt" ); if ( file == NULL ) { Sys_Warning( "Unable to open map shader file %s for writing\n", mapShaderFile.c_str() ); return; } /* print header */ fprintf( file, "// Custom shader file for %s.bsp\n" "// Generated by Q3Map2 (ydnar)\n" "// Do not edit! This file is overwritten on recompiles.\n\n", mapName.c_str() ); /* walk the shader list */ int num = 0; for ( const shaderInfo_t& si : Span( shaderInfo, numShaderInfo ) ) { if ( si.custom && !strEmptyOrNull( si.shaderText ) ) { num++; /* print it to the file */ fprintf( file, "%s%s\n", si.shader.c_str(), si.shaderText ); //Sys_Printf( "%s%s\n", si.shader.c_str(), si.shaderText ); /* FIXME: remove debugging code */ Sys_FPrintf( SYS_VRB, "." ); } } /* close the shader */ fflush( file ); fclose( file ); Sys_FPrintf( SYS_VRB, "\n" ); /* print some stats */ Sys_Printf( "%9d custom shaders emitted\n", num ); } /* CustomShader() - ydnar sets up a custom map shader */ const shaderInfo_t *CustomShader( const shaderInfo_t *si, const char *find, char *replace ){ shaderInfo_t *csi; char shader[ MAX_QPATH ]; char *s; int loc; byte digest[ 16 ]; char *srcShaderText, temp[ 8192 ], shaderText[ 8192 ]; /* ydnar: fixme (make this bigger?) */ /* dummy check */ if ( si == NULL ) { return ShaderInfoForShader( "default" ); } /* default shader text source */ srcShaderText = si->shaderText; /* et: implicitMap */ if ( si->implicitMap == EImplicitMap::Opaque ) { srcShaderText = temp; sprintf( temp, "\n" "{ // Q3Map2 defaulted (implicitMap)\n" "\t{\n" "\t\tmap $lightmap\n" "\t\trgbGen identity\n" "\t}\n" "\tq3map_styleMarker\n" "\t{\n" "\t\tmap %s\n" "\t\tblendFunc GL_DST_COLOR GL_ZERO\n" "\t\trgbGen identity\n" "\t}\n" "}\n", si->implicitImagePath.c_str() ); } /* et: implicitMask */ else if ( si->implicitMap == EImplicitMap::Masked ) { srcShaderText = temp; sprintf( temp, "\n" "{ // Q3Map2 defaulted (implicitMask)\n" "\tcull none\n" "\t{\n" "\t\tmap %s\n" "\t\talphaFunc GE128\n" "\t\tdepthWrite\n" "\t}\n" "\t{\n" "\t\tmap $lightmap\n" "\t\trgbGen identity\n" "\t\tdepthFunc equal\n" "\t}\n" "\tq3map_styleMarker\n" "\t{\n" "\t\tmap %s\n" "\t\tblendFunc GL_DST_COLOR GL_ZERO\n" "\t\tdepthFunc equal\n" "\t\trgbGen identity\n" "\t}\n" "}\n", si->implicitImagePath.c_str(), si->implicitImagePath.c_str() ); } /* et: implicitBlend */ else if ( si->implicitMap == EImplicitMap::Blend ) { srcShaderText = temp; sprintf( temp, "\n" "{ // Q3Map2 defaulted (implicitBlend)\n" "\tcull none\n" "\t{\n" "\t\tmap %s\n" "\t\tblendFunc GL_SRC_ALPHA GL_ONE_MINUS_SRC_ALPHA\n" "\t}\n" "\t{\n" "\t\tmap $lightmap\n" "\t\trgbGen identity\n" "\t\tblendFunc GL_DST_COLOR GL_ZERO\n" "\t}\n" "\tq3map_styleMarker\n" "}\n", si->implicitImagePath.c_str() ); } /* default shader text */ else if ( srcShaderText == NULL ) { srcShaderText = temp; sprintf( temp, "\n" "{ // Q3Map2 defaulted\n" "\t{\n" "\t\tmap $lightmap\n" "\t\trgbGen identity\n" "\t}\n" "\tq3map_styleMarker\n" "\t{\n" "\t\tmap %s.tga\n" "\t\tblendFunc GL_DST_COLOR GL_ZERO\n" "\t\trgbGen identity\n" "\t}\n" "}\n", si->shader.c_str() ); } /* error check */ if ( ( strlen( mapName ) + 1 + 32 ) >= MAX_QPATH ) { Error( "Custom shader name length (%d) exceeded. Shorten your map name.\n", MAX_QPATH - 1 ); } /* do some bad find-replace */ s = strIstr( srcShaderText, find ); if ( s == NULL ) { //% strcpy( shaderText, srcShaderText ); return si; /* testing just using the existing shader if this fails */ } else { /* substitute 'find' with 'replace' */ loc = s - srcShaderText; strcpy( shaderText, srcShaderText ); shaderText[ loc ] = '\0'; strcat( shaderText, replace ); strcat( shaderText, &srcShaderText[ loc + strlen( find ) ] ); } /* make md4 hash of the shader text */ Com_BlockFullChecksum( shaderText, strlen( shaderText ), digest ); /* mangle hash into a shader name */ sprintf( shader, "%s/%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", mapName.c_str(), digest[ 0 ], digest[ 1 ], digest[ 2 ], digest[ 3 ], digest[ 4 ], digest[ 5 ], digest[ 6 ], digest[ 7 ], digest[ 8 ], digest[ 9 ], digest[ 10 ], digest[ 11 ], digest[ 12 ], digest[ 13 ], digest[ 14 ], digest[ 15 ] ); /* get shader */ csi = ShaderInfoForShader( shader ); /* might be a preexisting shader */ if ( csi->custom ) { return csi; } /* clone the existing shader and rename */ *csi = *si; csi->shader = shader; csi->custom = true; /* store new shader text */ csi->shaderText = copystring( shaderText ); /* LEAK! */ /* return it */ return csi; } /* EmitVertexRemapShader() adds a vertexremapshader key/value pair to worldspawn */ void EmitVertexRemapShader( char *from, char *to ){ byte digest[ 16 ]; char key[ 64 ], value[ 256 ]; /* dummy check */ if ( strEmptyOrNull( from ) || strEmptyOrNull( to ) ) { return; } /* build value */ sprintf( value, "%s;%s", from, to ); /* make md4 hash */ Com_BlockFullChecksum( value, strlen( value ), digest ); /* make key (this is annoying, as vertexremapshader is precisely 17 characters, which is one too long, so we leave off the last byte of the md5 digest) */ sprintf( key, "vertexremapshader%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X", digest[ 0 ], digest[ 1 ], digest[ 2 ], digest[ 3 ], digest[ 4 ], digest[ 5 ], digest[ 6 ], digest[ 7 ], digest[ 8 ], digest[ 9 ], digest[ 10 ], digest[ 11 ], digest[ 12 ], digest[ 13 ], digest[ 14 ] ); /* no: digest[ 15 ] */ /* add key/value pair to worldspawn */ entities[ 0 ].setKeyValue( key, value ); } /* AllocShaderInfo() allocates and initializes a new shader */ static shaderInfo_t *AllocShaderInfo( void ){ shaderInfo_t *si; /* allocate? */ if ( shaderInfo == NULL ) { shaderInfo = safe_malloc( sizeof( shaderInfo_t ) * MAX_SHADER_INFO ); numShaderInfo = 0; } /* bounds check */ if ( numShaderInfo == MAX_SHADER_INFO ) { Error( "MAX_SHADER_INFO exceeded. Remove some PK3 files or shader scripts from shaderlist.txt and try again." ); } si = &shaderInfo[ numShaderInfo ]; numShaderInfo++; /* ydnar: clear to 0 first */ // memset( si, 0, sizeof( shaderInfo_t ) ); new (si) shaderInfo_t{}; // placement new /* set defaults */ ApplySurfaceParm( "default", &si->contentFlags, &si->surfaceFlags, &si->compileFlags ); si->backsplashFraction = DEF_BACKSPLASH_FRACTION * g_backsplashFractionScale; si->backsplashDistance = g_backsplashDistance < -900.0f ? DEF_BACKSPLASH_DISTANCE : g_backsplashDistance; si->bounceScale = DEF_RADIOSITY_BOUNCE; si->lightStyle = LS_NORMAL; si->polygonOffset = false; si->shadeAngleDegrees = 0.0f; si->lightmapSampleSize = 0; si->lightmapSampleOffset = DEFAULT_LIGHTMAP_SAMPLE_OFFSET; si->patchShadows = false; si->vertexShadows = true; /* ydnar: changed default behavior */ si->forceSunlight = false; si->lmBrightness = lightmapBrightness; si->vertexScale = vertexglobalscale; si->notjunc = false; /* ydnar: set texture coordinate transform matrix to identity */ TCModIdentity( si->mod ); /* ydnar: lightmaps can now be > 128x128 in certain games or an externally generated tga */ si->lmCustomWidth = lmCustomSizeW; si->lmCustomHeight = lmCustomSizeH; /* return to sender */ return si; } /* FinishShader() - ydnar sets a shader's width and height among other things */ void FinishShader( shaderInfo_t *si ){ int x, y; Vector2 st; /* don't double-dip */ if ( si->finished ) { return; } /* if they're explicitly set, copy from image size */ if ( si->shaderWidth == 0 && si->shaderHeight == 0 ) { si->shaderWidth = si->shaderImage->width; si->shaderHeight = si->shaderImage->height; } /* legacy terrain has explicit image-sized texture projection */ if ( si->legacyTerrain && !si->tcGen ) { /* set xy texture projection */ si->tcGen = true; si->vecs[ 0 ] = { ( 1.0f / ( si->shaderWidth * 0.5f ) ), 0, 0 }; si->vecs[ 1 ] = { 0, ( 1.0f / ( si->shaderHeight * 0.5f ) ), 0 }; } /* find pixel coordinates best matching the average color of the image */ float bestDist = 99999999; const Vector2 o( 1.0f / si->shaderImage->width, 1.0f / si->shaderImage->height ); for ( y = 0, st[ 1 ] = 0.0f; y < si->shaderImage->height; y++, st[ 1 ] += o[ 1 ] ) { for ( x = 0, st[ 0 ] = 0.0f; x < si->shaderImage->width; x++, st[ 0 ] += o[ 0 ] ) { /* sample the shader image */ Color4f color; RadSampleImage( si->shaderImage->pixels, si->shaderImage->width, si->shaderImage->height, st, color ); /* determine error squared */ const Color4f delta = color - si->averageColor; const float dist = vector4_dot( delta, delta ); if ( dist < bestDist ) { si->stFlat = st; } } } if( noob && !( si->compileFlags & C_OB ) ){ ApplySurfaceParm( "noob", nullptr, &si->surfaceFlags, nullptr ); } /* set to finished */ si->finished = true; } /* LoadShaderImages() loads a shader's images ydnar: image.c made this a bit simpler */ static void LoadShaderImages( shaderInfo_t *si ){ /* nodraw shaders don't need images */ if ( si->compileFlags & C_NODRAW ) { si->shaderImage = ImageLoad( DEFAULT_IMAGE ); } else { /* try to load editor image first */ si->shaderImage = ImageLoad( si->editorImagePath ); /* then try shadername */ if ( si->shaderImage == NULL ) { si->shaderImage = ImageLoad( si->shader ); } /* then try implicit image path (note: new behavior!) */ if ( si->shaderImage == NULL ) { si->shaderImage = ImageLoad( si->implicitImagePath ); } /* then try lightimage (note: new behavior!) */ if ( si->shaderImage == NULL ) { si->shaderImage = ImageLoad( si->lightImagePath ); } /* otherwise, use default image */ if ( si->shaderImage == NULL ) { si->shaderImage = ImageLoad( DEFAULT_IMAGE ); if ( warnImage && !strEqual( si->shader, "noshader" ) ) { Sys_Warning( "Couldn't find image for shader %s\n", si->shader.c_str() ); } } /* load light image */ si->lightImage = ImageLoad( si->lightImagePath ); /* load normalmap image (ok if this is NULL) */ si->normalImage = ImageLoad( si->normalImagePath ); if ( si->normalImage != NULL ) { Sys_FPrintf( SYS_VRB, "Shader %s has\n" " NM %s\n", si->shader.c_str(), si->normalImagePath.c_str() ); } } /* if no light image, reuse shader image */ if ( si->lightImage == NULL ) { si->lightImage = si->shaderImage; } /* create default and average colors */ const int count = si->lightImage->width * si->lightImage->height; Color4f color( 0, 0, 0, 0 ); for ( int i = 0; i < count; i++ ) { color[ 0 ] += si->lightImage->pixels[ i * 4 + 0 ]; color[ 1 ] += si->lightImage->pixels[ i * 4 + 1 ]; color[ 2 ] += si->lightImage->pixels[ i * 4 + 2 ]; color[ 3 ] += si->lightImage->pixels[ i * 4 + 3 ]; } if ( vector3_length( si->color ) == 0.0f ) { si->color = color.rgb(); ColorNormalize( si->color ); si->averageColor = color / count; } else { si->averageColor.rgb() = si->color; si->averageColor.alpha() = 1.0f; } } /* ShaderInfoForShader() finds a shaderinfo for a named shader */ #define MAX_SHADER_DEPRECATION_DEPTH 16 shaderInfo_t *ShaderInfoForShaderNull( const char *shaderName ){ if ( strEqual( shaderName, "noshader" ) ) { return NULL; } return ShaderInfoForShader( shaderName ); } shaderInfo_t *ShaderInfoForShader( const char *shaderName ){ int i; int deprecationDepth; shaderInfo_t *si; /* dummy check */ if ( strEmptyOrNull( shaderName ) ) { Sys_Warning( "Null or empty shader name\n" ); shaderName = "missing"; } /* strip off extension */ auto shader = String64()( PathExtensionless( shaderName ) ); /* search for it */ deprecationDepth = 0; for ( i = 0; i < numShaderInfo; i++ ) { si = &shaderInfo[ i ]; if ( striEqual( shader, si->shader ) ) { /* check if shader is deprecated */ if ( deprecationDepth < MAX_SHADER_DEPRECATION_DEPTH && !strEmptyOrNull( si->deprecateShader ) ) { /* override name */ shader( PathExtensionless( si->deprecateShader ) ); /* increase deprecation depth */ deprecationDepth++; if ( deprecationDepth == MAX_SHADER_DEPRECATION_DEPTH ) { Sys_Warning( "Max deprecation depth of %i is reached on shader '%s'\n", MAX_SHADER_DEPRECATION_DEPTH, shader.c_str() ); } /* search again from beginning */ i = -1; continue; } /* load image if necessary */ if ( !si->finished ) { LoadShaderImages( si ); FinishShader( si ); } /* return it */ return si; } } /* allocate a default shader */ si = AllocShaderInfo(); si->shader = shader; LoadShaderImages( si ); FinishShader( si ); /* return it */ return si; } static void Parse1DMatrixAppend( ShaderTextCollector& text, int x, float *m ){ if ( !text.GetToken( true ) || !strEqual( token, "(" ) ) { Error( "Parse1DMatrixAppend(): line %d: ( not found!\nFile location be: %s\n", scriptline, g_strLoadedFileLocation ); } for ( int i = 0; i < x; i++ ) { if ( !text.GetToken( false ) ) { Error( "Parse1DMatrixAppend(): line %d: Number not found!\nFile location be: %s\n", scriptline, g_strLoadedFileLocation ); } m[ i ] = atof( token ); } if ( !text.GetToken( true ) || !strEqual( token, ")" ) ) { Error( "Parse1DMatrixAppend(): line %d: ) not found!\nFile location be: %s\n", scriptline, g_strLoadedFileLocation ); } } /* ParseShaderFile() parses a shader file into discrete shaderInfo_t */ static void ParseShaderFile( const char *filename ){ ShaderTextCollector text; /* load the shader */ LoadScriptFile( filename ); /* tokenize it */ while ( GetToken( true ) ) /* test for end of file */ { /* shader name is initial token */ shaderInfo_t *si = AllocShaderInfo(); /* ignore ":q3map" suffix */ if( striEqualSuffix( token, ":q3map" ) ) si->shader << StringRange( token, strlen( token ) - strlen( ":q3map" ) ); else si->shader << token; /* handle { } section */ if ( !( text.GetToken( true ) && strEqual( token, "{" ) ) ) { Error( "ParseShaderFile(): %s, line %d: { not found!\nFound instead: %s\nLast known shader: %s\nFile location be: %s\n", filename, scriptline, token, si->shader.c_str(), g_strLoadedFileLocation ); } while ( text.GetToken( true ) && !strEqual( token, "}" ) ) { /* ----------------------------------------------------------------- shader stages (passes) ----------------------------------------------------------------- */ /* parse stage directives */ if ( strEqual( token, "{" ) ) { si->hasPasses = true; while ( text.GetToken( true ) && !strEqual( token, "}" ) ) { /* only care about images if we don't have a editor/light image */ if ( si->editorImagePath.empty() && si->lightImagePath.empty() && si->implicitImagePath.empty() ) { /* digest any images */ if ( striEqual( token, "map" ) || striEqual( token, "clampMap" ) || striEqual( token, "animMap" ) || striEqual( token, "clampAnimMap" ) || striEqual( token, "mapComp" ) || striEqual( token, "mapNoComp" ) ) { /* skip one token for animated stages */ if ( striEqual( token, "animMap" ) || striEqual( token, "clampAnimMap" ) ) { text.GetToken( false ); } /* get an image */ text.GetToken( false ); if ( token[ 0 ] != '*' && token[ 0 ] != '$' ) { si->lightImagePath( PathExtensionless( token ) ); /* debug code */ //% Sys_FPrintf( SYS_VRB, "Deduced shader image: %s\n", si->lightImagePath ); } } } } } /* ----------------------------------------------------------------- surfaceparm * directives ----------------------------------------------------------------- */ /* match surfaceparm */ else if ( striEqual( token, "surfaceparm" ) ) { text.GetToken( false ); if ( !ApplySurfaceParm( token, &si->contentFlags, &si->surfaceFlags, &si->compileFlags ) ) { Sys_Warning( "Unknown surfaceparm: \"%s\"\n", token ); } } /* ----------------------------------------------------------------- game-related shader directives ----------------------------------------------------------------- */ /* ydnar: fogparms (for determining fog volumes) */ else if ( striEqual( token, "fogparms" ) ) { si->fogParms = true; } /* ydnar: polygonoffset (for no culling) */ else if ( striEqual( token, "polygonoffset" ) ) { si->polygonOffset = true; } /* tesssize is used to force liquid surfaces to subdivide */ else if ( striEqual( token, "tessSize" ) || striEqual( token, "q3map_tessSize" ) /* sof2 */ ) { text.GetToken( false ); si->subdivisions = atof( token ); } /* cull none will set twoSided (ydnar: added disable too) */ else if ( striEqual( token, "cull" ) ) { text.GetToken( false ); if ( striEqual( token, "none" ) || striEqual( token, "disable" ) || striEqual( token, "twosided" ) ) { si->twoSided = true; } } /* deformVertexes autosprite[ 2 ] we catch this so autosprited surfaces become point lights instead of area lights */ else if ( striEqual( token, "deformVertexes" ) ) { text.GetToken( false ); /* deformVertexes autosprite(2) */ if ( striEqualPrefix( token, "autosprite" ) ) { /* set it as autosprite and detail */ si->autosprite = true; ApplySurfaceParm( "detail", &si->contentFlags, &si->surfaceFlags, &si->compileFlags ); /* ydnar: gs mods: added these useful things */ si->noClip = true; si->notjunc = true; } /* deformVertexes move (ydnar: for particle studio support) */ if ( striEqual( token, "move" ) ) { Vector3 amt; float base, amp; /* get move amount */ text.GetToken( false ); amt[ 0 ] = atof( token ); text.GetToken( false ); amt[ 1 ] = atof( token ); text.GetToken( false ); amt[ 2 ] = atof( token ); /* skip func */ text.GetToken( false ); /* get base and amplitude */ text.GetToken( false ); base = atof( token ); text.GetToken( false ); amp = atof( token ); /* calculate */ si->minmax.mins = amt * base; si->minmax.maxs = amt * amp + si->minmax.mins; } } /* light (old-style flare specification) */ else if ( striEqual( token, "light" ) ) { text.GetToken( false ); si->flareShader = g_game->flareShader; } /* ydnar: damageShader (sof2 mods) */ else if ( striEqual( token, "damageShader" ) ) { text.GetToken( false ); if ( !strEmpty( token ) ) { si->damageShader = copystring( token ); } text.GetToken( false ); /* don't do anything with health */ } /* ydnar: enemy territory implicit shaders */ else if ( striEqual( token, "implicitMap" ) ) { si->implicitMap = EImplicitMap::Opaque; text.GetToken( false ); if ( strEqual( token, "-" ) ) { si->implicitImagePath = si->shader; } else{ si->implicitImagePath = token; } } else if ( striEqual( token, "implicitMask" ) ) { si->implicitMap = EImplicitMap::Masked; text.GetToken( false ); if ( strEqual( token, "-" ) ) { si->implicitImagePath = si->shader; } else{ si->implicitImagePath = token; } } else if ( striEqual( token, "implicitBlend" ) ) { si->implicitMap = EImplicitMap::Blend; text.GetToken( false ); if ( strEqual( token, "-" ) ) { si->implicitImagePath = si->shader; } else{ si->implicitImagePath = token; } } /* ----------------------------------------------------------------- image directives ----------------------------------------------------------------- */ /* qer_editorimage */ else if ( striEqual( token, "qer_editorImage" ) ) { text.GetToken( false ); si->editorImagePath( PathExtensionless( token ) ); } /* ydnar: q3map_normalimage (bumpmapping normal map) */ else if ( striEqual( token, "q3map_normalImage" ) ) { text.GetToken( false ); si->normalImagePath( PathExtensionless( token ) ); } /* q3map_lightimage */ else if ( striEqual( token, "q3map_lightImage" ) ) { text.GetToken( false ); si->lightImagePath( PathExtensionless( token ) ); } /* ydnar: skyparms */ else if ( striEqual( token, "skyParms" ) ) { /* get image base */ text.GetToken( false ); /* ignore bogus paths */ if ( !strEqual( token, "-" ) && !striEqual( token, "full" ) ) { si->skyParmsImageBase = token; /* use top image as sky light image */ if ( si->lightImagePath.empty() ) { si->lightImagePath( si->skyParmsImageBase, "_up" ); } } /* skip rest of line */ text.GetToken( false ); text.GetToken( false ); } /* ----------------------------------------------------------------- q3map_* directives ----------------------------------------------------------------- */ /* q3map_sun color will be normalized, so it doesn't matter what range you use intensity falls off with angle but not distance 100 is a fairly bright sun degree of 0 = from the east, 90 = north, etc. altitude of 0 = sunrise/set, 90 = noon ydnar: sof2map has bareword 'sun' token, so we support that as well */ else if ( striEqual( token, "sun" ) /* sof2 */ || striEqual( token, "q3map_sun" ) || striEqual( token, "q3map_sunExt" ) ) { sun_t *sun; /* ydnar: extended sun directive? */ const bool ext = striEqual( token, "q3map_sunext" ); /* allocate sun */ sun = safe_calloc( sizeof( *sun ) ); /* set style */ sun->style = si->lightStyle; /* get color */ text.GetToken( false ); sun->color[ 0 ] = atof( token ); text.GetToken( false ); sun->color[ 1 ] = atof( token ); text.GetToken( false ); sun->color[ 2 ] = atof( token ); if ( colorsRGB ) { sun->color[0] = Image_LinearFloatFromsRGBFloat( sun->color[0] ); sun->color[1] = Image_LinearFloatFromsRGBFloat( sun->color[1] ); sun->color[2] = Image_LinearFloatFromsRGBFloat( sun->color[2] ); } /* normalize it */ ColorNormalize( sun->color ); /* scale color by brightness */ text.GetToken( false ); sun->photons = atof( token ); /* get sun angle/elevation */ text.GetToken( false ); const double a = degrees_to_radians( atof( token ) ); text.GetToken( false ); const double b = degrees_to_radians( atof( token ) ); sun->direction = vector3_for_spherical( a, b ); /* get filter radius from shader */ sun->filterRadius = si->lightFilterRadius; /* ydnar: get sun angular deviance/samples */ if ( ext && TokenAvailable() ) { text.GetToken( false ); sun->deviance = degrees_to_radians( atof( token ) ); text.GetToken( false ); sun->numSamples = atoi( token ); } /* store sun */ sun->next = si->sun; si->sun = sun; /* apply sky surfaceparm */ ApplySurfaceParm( "sky", &si->contentFlags, &si->surfaceFlags, &si->compileFlags ); /* don't process any more tokens on this line */ continue; } /* match q3map_ */ else if ( striEqualPrefix( token, "q3map_" ) ) { /* ydnar: q3map_baseShader (inherit this shader's parameters) */ if ( striEqual( token, "q3map_baseShader" ) ) { shaderInfo_t *si2; bool oldWarnImage; /* get shader */ text.GetToken( false ); //% Sys_FPrintf( SYS_VRB, "Shader %s has base shader %s\n", si->shader, token ); oldWarnImage = warnImage; warnImage = false; si2 = ShaderInfoForShader( token ); warnImage = oldWarnImage; /* subclass it */ if ( si2 != NULL ) { /* preserve name */ const String64 temp = si->shader; /* copy shader */ *si = *si2; /* restore name and set to unfinished */ si->shader = temp; si->shaderWidth = 0; si->shaderHeight = 0; si->finished = false; } } /* ydnar: q3map_surfacemodel */ else if ( striEqual( token, "q3map_surfacemodel" ) ) { /* allocate new model and attach it */ surfaceModel_t& model = si->surfaceModels.emplace_back(); /* get parameters */ text.GetToken( false ); model.model = token; text.GetToken( false ); model.density = atof( token ); text.GetToken( false ); model.odds = atof( token ); text.GetToken( false ); model.minScale = atof( token ); text.GetToken( false ); model.maxScale = atof( token ); text.GetToken( false ); model.minAngle = atof( token ); text.GetToken( false ); model.maxAngle = atof( token ); text.GetToken( false ); model.oriented = ( token[ 0 ] == '1' ); } /* ydnar/sd: q3map_foliage */ else if ( striEqual( token, "q3map_foliage" ) ) { /* allocate new foliage struct and attach it */ foliage_t& foliage = si->foliage.emplace_back(); /* get parameters */ text.GetToken( false ); foliage.model = token; text.GetToken( false ); foliage.scale = atof( token ); text.GetToken( false ); foliage.density = atof( token ); text.GetToken( false ); foliage.odds = atof( token ); text.GetToken( false ); foliage.inverseAlpha = atoi( token ); } /* ydnar: q3map_bounce (fraction of light to re-emit during radiosity passes) */ else if ( striEqual( token, "q3map_bounce" ) || striEqual( token, "q3map_bounceScale" ) ) { text.GetToken( false ); si->bounceScale = atof( token ); } /* ydnar/splashdamage: q3map_skyLight */ else if ( striEqual( token, "q3map_skyLight" ) ) { text.GetToken( false ); si->skyLightValue = atof( token ); text.GetToken( false ); si->skyLightIterations = atoi( token ); /* clamp */ value_maximize( si->skyLightValue, 0.0f ); value_maximize( si->skyLightIterations, 2 ); } /* q3map_surfacelight */ else if ( striEqual( token, "q3map_surfacelight" ) ) { text.GetToken( false ); si->value = atof( token ); } /* q3map_lightStyle (sof2/jk2 lightstyle) */ else if ( striEqual( token, "q3map_lightStyle" ) ) { text.GetToken( false ); si->lightStyle = std::clamp( atoi( token ), 0, LS_NONE ); } /* wolf: q3map_lightRGB */ else if ( striEqual( token, "q3map_lightRGB" ) ) { si->color.set( 0 ); text.GetToken( false ); si->color[ 0 ] = atof( token ); text.GetToken( false ); si->color[ 1 ] = atof( token ); text.GetToken( false ); si->color[ 2 ] = atof( token ); if ( colorsRGB ) { si->color[0] = Image_LinearFloatFromsRGBFloat( si->color[0] ); si->color[1] = Image_LinearFloatFromsRGBFloat( si->color[1] ); si->color[2] = Image_LinearFloatFromsRGBFloat( si->color[2] ); } ColorNormalize( si->color ); } /* q3map_lightSubdivide */ else if ( striEqual( token, "q3map_lightSubdivide" ) ) { text.GetToken( false ); si->lightSubdivide = atoi( token ); } /* q3map_backsplash */ else if ( striEqual( token, "q3map_backsplash" ) ) { text.GetToken( false ); si->backsplashFraction = atof( token ) * 0.01f; text.GetToken( false ); si->backsplashDistance = atof( token ); } /* q3map_floodLight */ else if ( striEqual( token, "q3map_floodLight" ) ) { /* get color */ text.GetToken( false ); si->floodlightRGB[ 0 ] = atof( token ); text.GetToken( false ); si->floodlightRGB[ 1 ] = atof( token ); text.GetToken( false ); si->floodlightRGB[ 2 ] = atof( token ); text.GetToken( false ); si->floodlightDistance = atof( token ); text.GetToken( false ); si->floodlightIntensity = atof( token ); text.GetToken( false ); si->floodlightDirectionScale = atof( token ); if ( colorsRGB ) { si->floodlightRGB[0] = Image_LinearFloatFromsRGBFloat( si->floodlightRGB[0] ); si->floodlightRGB[1] = Image_LinearFloatFromsRGBFloat( si->floodlightRGB[1] ); si->floodlightRGB[2] = Image_LinearFloatFromsRGBFloat( si->floodlightRGB[2] ); } ColorNormalize( si->floodlightRGB ); } /* jal: q3map_nodirty : skip dirty */ else if ( striEqual( token, "q3map_nodirty" ) ) { si->noDirty = true; } /* q3map_lightmapSampleSize */ else if ( striEqual( token, "q3map_lightmapSampleSize" ) ) { text.GetToken( false ); si->lightmapSampleSize = atoi( token ); } /* q3map_lightmapSampleOffset */ else if ( striEqual( token, "q3map_lightmapSampleOffset" ) ) { text.GetToken( false ); si->lightmapSampleOffset = atof( token ); } /* ydnar: q3map_lightmapFilterRadius */ else if ( striEqual( token, "q3map_lightmapFilterRadius" ) ) { text.GetToken( false ); si->lmFilterRadius = atof( token ); text.GetToken( false ); si->lightFilterRadius = atof( token ); } /* ydnar: q3map_lightmapAxis [xyz] */ else if ( striEqual( token, "q3map_lightmapAxis" ) ) { text.GetToken( false ); if ( striEqual( token, "x" ) ) { si->lightmapAxis = g_vector3_axis_x; } else if ( striEqual( token, "y" ) ) { si->lightmapAxis = g_vector3_axis_y; } else if ( striEqual( token, "z" ) ) { si->lightmapAxis = g_vector3_axis_z; } else { Sys_Warning( "Unknown value for lightmap axis: %s\n", token ); si->lightmapAxis.set( 0 ); } } /* ydnar: q3map_lightmapSize (for autogenerated shaders + external tga lightmaps) */ else if ( striEqual( token, "q3map_lightmapSize" ) ) { text.GetToken( false ); si->lmCustomWidth = atoi( token ); text.GetToken( false ); si->lmCustomHeight = atoi( token ); /* must be a power of 2 */ if ( ( ( si->lmCustomWidth - 1 ) & si->lmCustomWidth ) || ( ( si->lmCustomHeight - 1 ) & si->lmCustomHeight ) ) { Sys_Warning( "Non power-of-two lightmap size specified (%d, %d)\n", si->lmCustomWidth, si->lmCustomHeight ); si->lmCustomWidth = lmCustomSizeW; si->lmCustomHeight = lmCustomSizeH; } } /* ydnar: q3map_lightmapBrightness N (for autogenerated shaders + external tga lightmaps) */ else if ( striEqual( token, "q3map_lightmapBrightness" ) || striEqual( token, "q3map_lightmapGamma" ) ) { text.GetToken( false ); si->lmBrightness *= atof( token ); if ( si->lmBrightness < 0 ) { si->lmBrightness = 1.0; } } /* q3map_vertexScale (scale vertex lighting by this fraction) */ else if ( striEqual( token, "q3map_vertexScale" ) ) { text.GetToken( false ); si->vertexScale *= atof( token ); } /* q3map_noVertexLight */ else if ( striEqual( token, "q3map_noVertexLight" ) ) { si->noVertexLight = true; } /* q3map_flare[Shader] */ else if ( striEqual( token, "q3map_flare" ) || striEqual( token, "q3map_flareShader" ) ) { text.GetToken( false ); if ( !strEmpty( token ) ) { si->flareShader = copystring( token ); } } /* q3map_backShader */ else if ( striEqual( token, "q3map_backShader" ) ) { text.GetToken( false ); if ( !strEmpty( token ) ) { si->backShader = copystring( token ); } } /* ydnar: q3map_cloneShader */ else if ( striEqual( token, "q3map_cloneShader" ) ) { text.GetToken( false ); if ( !strEmpty( token ) ) { si->cloneShader = copystring( token ); } } /* q3map_remapShader */ else if ( striEqual( token, "q3map_remapShader" ) ) { text.GetToken( false ); if ( !strEmpty( token ) ) { si->remapShader = copystring( token ); } } /* q3map_deprecateShader */ else if ( striEqual( token, "q3map_deprecateShader" ) ) { text.GetToken( false ); if ( !strEmpty( token ) ) { si->deprecateShader = copystring( token ); } } /* ydnar: q3map_offset */ else if ( striEqual( token, "q3map_offset" ) ) { text.GetToken( false ); si->offset = atof( token ); } /* ydnar: q3map_fur */ else if ( striEqual( token, "q3map_fur" ) ) { text.GetToken( false ); si->furNumLayers = atoi( token ); text.GetToken( false ); si->furOffset = atof( token ); text.GetToken( false ); si->furFade = atof( token ); } /* ydnar: gs mods: legacy support for terrain/terrain2 shaders */ else if ( striEqual( token, "q3map_terrain" ) ) { /* team arena terrain is assumed to be nonplanar, with full normal averaging, passed through the metatriangle surface pipeline, with a lightmap axis on z */ si->legacyTerrain = true; si->noClip = true; si->notjunc = true; si->indexed = true; si->nonplanar = true; si->forceMeta = true; si->shadeAngleDegrees = 179.0f; //% si->lightmapAxis = g_vector3_axis_z; /* ydnar 2002-09-21: turning this off for better lightmapping of cliff faces */ } /* ydnar: picomodel: q3map_forceMeta (forces brush faces and/or triangle models to go through the metasurface pipeline) */ else if ( striEqual( token, "q3map_forceMeta" ) ) { si->forceMeta = true; } /* ydnar: gs mods: q3map_shadeAngle */ else if ( striEqual( token, "q3map_shadeAngle" ) ) { text.GetToken( false ); si->shadeAngleDegrees = atof( token ); } /* ydnar: q3map_textureSize (substitute for q3map_lightimage derivation for terrain) */ else if ( striEqual( token, "q3map_textureSize" ) ) { text.GetToken( false ); si->shaderWidth = atoi( token ); text.GetToken( false ); si->shaderHeight = atoi( token ); } /* ydnar: gs mods: q3map_tcGen