mirror of
https://bitbucket.org/CPMADevs/cnq3
synced 2024-11-10 06:31:48 +00:00
7c217a313d
- all: fixed depth test (yet another reverse Z pitfall...) - VL: fixed output color mismatch when a low-impact fragment is added - VL: fixed next closer fragment search ignoring the depth test
3310 lines
87 KiB
C++
3310 lines
87 KiB
C++
/*
|
|
===========================================================================
|
|
Copyright (C) 1999-2005 Id Software, Inc.
|
|
|
|
This file is part of Quake III Arena source code.
|
|
|
|
Quake III Arena source code 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.
|
|
|
|
Quake III Arena source code 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 Quake III Arena source code; if not, write to the Free Software
|
|
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
|
===========================================================================
|
|
*/
|
|
#include "tr_local.h"
|
|
|
|
// tr_shader.c -- this file deals with the parsing and definition of shaders
|
|
|
|
|
|
const uint8_t r_depthFadeScaleAndBias[DFT_COUNT] =
|
|
{
|
|
0x00, // DFT_NONE R G B A R G B A
|
|
0x07, // DFT_BLEND - scale = (1, 1, 1, 0) - bias = (0, 0, 0, 0)
|
|
0x08, // DFT_ADD - scale = (0, 0, 0, 1) - bias = (0, 0, 0, 0)
|
|
0x78, // DFT_MULT - scale = (0, 0, 0, 1) - bias = (1, 1, 1, 0)
|
|
0x00, // DFT_PMA - scale = (0, 0, 0, 0) - bias = (0, 0, 0, 0)
|
|
0x00 // DFT_TBD
|
|
};
|
|
|
|
static char* s_shaderText = 0;
|
|
static int s_numShaderFiles = 0;
|
|
static char* s_shaderFileNames = 0;
|
|
static int* s_shaderFileOffsets = 0;
|
|
static int* s_shaderFileNameOffsets = 0;
|
|
static int* s_shaderPakChecksums = 0;
|
|
|
|
// the shader is parsed into these global variables, then copied into
|
|
// dynamically allocated memory if it is valid.
|
|
static shader_t shader;
|
|
static shaderStage_t stages[MAX_SHADER_STAGES];
|
|
static texModInfo_t texMods[MAX_SHADER_STAGES][TR_MAX_TEXMODS];
|
|
|
|
#define FILE_HASH_SIZE 1024
|
|
static shader_t* hashTable[FILE_HASH_SIZE];
|
|
|
|
#define MAX_SHADERTEXT_HASH 2048
|
|
static char** shaderTextHashTable[MAX_SHADERTEXT_HASH];
|
|
|
|
static char parseMessage[1024];
|
|
|
|
|
|
static void ParserWarning( PRINTF_FORMAT_STRING const char* format, ... )
|
|
{
|
|
va_list ap;
|
|
va_start( ap, format );
|
|
Q_vsnprintf( parseMessage, sizeof( parseMessage ) - 1, format, ap );
|
|
va_end( ap );
|
|
|
|
ri.Printf( PRINT_WARNING, "WARNING: %s in shader '%s'\n", parseMessage, shader.name );
|
|
|
|
if ( tr.shaderParseSaveState ) {
|
|
if ( tr.shaderParseNumWarnings < ARRAY_LEN( tr.shaderParseWarnings ) ) {
|
|
Q_strncpyz( tr.shaderParseWarnings[tr.shaderParseNumWarnings++].message, parseMessage, sizeof( tr.shaderParseWarnings[0] ) );
|
|
}
|
|
}
|
|
}
|
|
|
|
static void ParserError( PRINTF_FORMAT_STRING const char* format, ... )
|
|
{
|
|
va_list ap;
|
|
va_start( ap, format );
|
|
Q_vsnprintf( parseMessage, sizeof( parseMessage ) - 1, format, ap );
|
|
va_end( ap );
|
|
|
|
ri.Printf( PRINT_WARNING, "ERROR: %s in shader '%s'\n", parseMessage, shader.name );
|
|
|
|
if ( tr.shaderParseSaveState ) {
|
|
Q_strncpyz( tr.shaderParseError.message, parseMessage, sizeof( tr.shaderParseError.message ) );
|
|
}
|
|
}
|
|
|
|
|
|
static qbool ParseVector( const char** text, int count, float *v )
|
|
{
|
|
int i;
|
|
|
|
// FIXME: spaces are currently required after parens, should change parseext...
|
|
const char* token = COM_ParseExt( text, qfalse );
|
|
if ( strcmp( token, "(" ) ) {
|
|
ParserWarning( "missing parenthesis" );
|
|
return qfalse;
|
|
}
|
|
|
|
for ( i = 0 ; i < count ; i++ ) {
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] ) {
|
|
ParserWarning( "missing vector element" );
|
|
return qfalse;
|
|
}
|
|
v[i] = atof( token );
|
|
}
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( strcmp( token, ")" ) ) {
|
|
ParserWarning( "missing parenthesis" );
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
NameToAFunc
|
|
===============
|
|
*/
|
|
static unsigned NameToAFunc( const char *funcname )
|
|
{
|
|
if ( !Q_stricmp( funcname, "GT0" ) )
|
|
{
|
|
return GLS_ATEST_GT_0;
|
|
}
|
|
else if ( !Q_stricmp( funcname, "LT128" ) )
|
|
{
|
|
return GLS_ATEST_LT_80;
|
|
}
|
|
else if ( !Q_stricmp( funcname, "GE128" ) )
|
|
{
|
|
return GLS_ATEST_GE_80;
|
|
}
|
|
|
|
ParserWarning( "invalid alphaFunc name '%s'", funcname );
|
|
return 0;
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
NameToSrcBlendMode
|
|
===============
|
|
*/
|
|
static int NameToSrcBlendMode( const char *name )
|
|
{
|
|
if ( !Q_stricmp( name, "GL_ONE" ) )
|
|
{
|
|
return GLS_SRCBLEND_ONE;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ZERO" ) )
|
|
{
|
|
return GLS_SRCBLEND_ZERO;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_DST_COLOR" ) )
|
|
{
|
|
return GLS_SRCBLEND_DST_COLOR;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ONE_MINUS_DST_COLOR" ) )
|
|
{
|
|
return GLS_SRCBLEND_ONE_MINUS_DST_COLOR;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_SRC_ALPHA" ) )
|
|
{
|
|
return GLS_SRCBLEND_SRC_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ONE_MINUS_SRC_ALPHA" ) )
|
|
{
|
|
return GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_DST_ALPHA" ) )
|
|
{
|
|
return GLS_SRCBLEND_DST_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ONE_MINUS_DST_ALPHA" ) )
|
|
{
|
|
return GLS_SRCBLEND_ONE_MINUS_DST_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_SRC_ALPHA_SATURATE" ) )
|
|
{
|
|
return GLS_SRCBLEND_ALPHA_SATURATE;
|
|
}
|
|
|
|
ParserWarning( "unknown blend mode '%s', using GL_ONE instead", name );
|
|
return GLS_SRCBLEND_ONE;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
NameToDstBlendMode
|
|
===============
|
|
*/
|
|
static int NameToDstBlendMode( const char *name )
|
|
{
|
|
if ( !Q_stricmp( name, "GL_ONE" ) )
|
|
{
|
|
return GLS_DSTBLEND_ONE;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ZERO" ) )
|
|
{
|
|
return GLS_DSTBLEND_ZERO;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_SRC_ALPHA" ) )
|
|
{
|
|
return GLS_DSTBLEND_SRC_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ONE_MINUS_SRC_ALPHA" ) )
|
|
{
|
|
return GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_DST_ALPHA" ) )
|
|
{
|
|
return GLS_DSTBLEND_DST_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ONE_MINUS_DST_ALPHA" ) )
|
|
{
|
|
return GLS_DSTBLEND_ONE_MINUS_DST_ALPHA;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_SRC_COLOR" ) )
|
|
{
|
|
return GLS_DSTBLEND_SRC_COLOR;
|
|
}
|
|
else if ( !Q_stricmp( name, "GL_ONE_MINUS_SRC_COLOR" ) )
|
|
{
|
|
return GLS_DSTBLEND_ONE_MINUS_SRC_COLOR;
|
|
}
|
|
|
|
ParserWarning( "unknown blend mode '%s', using GL_ONE instead", name );
|
|
return GLS_DSTBLEND_ONE;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
NameToGenFunc
|
|
===============
|
|
*/
|
|
static genFunc_t NameToGenFunc( const char *funcname )
|
|
{
|
|
if ( !Q_stricmp( funcname, "sin" ) )
|
|
{
|
|
return GF_SIN;
|
|
}
|
|
else if ( !Q_stricmp( funcname, "square" ) )
|
|
{
|
|
return GF_SQUARE;
|
|
}
|
|
else if ( !Q_stricmp( funcname, "triangle" ) )
|
|
{
|
|
return GF_TRIANGLE;
|
|
}
|
|
else if ( !Q_stricmp( funcname, "sawtooth" ) )
|
|
{
|
|
return GF_SAWTOOTH;
|
|
}
|
|
else if ( !Q_stricmp( funcname, "inversesawtooth" ) )
|
|
{
|
|
return GF_INVERSE_SAWTOOTH;
|
|
}
|
|
else if ( !Q_stricmp( funcname, "noise" ) )
|
|
{
|
|
return GF_NOISE;
|
|
}
|
|
|
|
ParserWarning( "invalid genfunc name '%s'", funcname );
|
|
return GF_SIN;
|
|
}
|
|
|
|
|
|
static void ParseWaveForm( const char** text, waveForm_t* wave )
|
|
{
|
|
const char* token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing waveform parm" );
|
|
return;
|
|
}
|
|
wave->func = NameToGenFunc( token );
|
|
|
|
// BASE, AMP, PHASE, FREQ
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing waveform parm" );
|
|
return;
|
|
}
|
|
wave->base = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing waveform parm" );
|
|
return;
|
|
}
|
|
wave->amplitude = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing waveform parm" );
|
|
return;
|
|
}
|
|
wave->phase = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing waveform parm" );
|
|
return;
|
|
}
|
|
wave->frequency = atof( token );
|
|
}
|
|
|
|
|
|
static void ParseTexMod( const char** text, shaderStage_t *stage )
|
|
{
|
|
const char *token;
|
|
texModInfo_t *tmi;
|
|
|
|
if ( stage->numTexMods == TR_MAX_TEXMODS ) {
|
|
ParserError( "too many tcMod stages" );
|
|
return;
|
|
}
|
|
|
|
tmi = &stage->texMods[stage->numTexMods];
|
|
stage->numTexMods++;
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
|
|
//
|
|
// turb
|
|
//
|
|
if ( !Q_stricmp( token, "turb" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing tcMod turb parms" );
|
|
return;
|
|
}
|
|
tmi->wave.base = atof( token );
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing tcMod turb parms" );
|
|
return;
|
|
}
|
|
tmi->wave.amplitude = atof( token );
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing tcMod turb parms" );
|
|
return;
|
|
}
|
|
tmi->wave.phase = atof( token );
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing tcMod turb parms" );
|
|
return;
|
|
}
|
|
tmi->wave.frequency = atof( token );
|
|
|
|
tmi->type = TMOD_TURBULENT;
|
|
}
|
|
//
|
|
// scale
|
|
//
|
|
else if ( !Q_stricmp( token, "scale" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing scale parms" );
|
|
return;
|
|
}
|
|
tmi->scale[0] = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing scale parms" );
|
|
return;
|
|
}
|
|
tmi->scale[1] = atof( token );
|
|
tmi->type = TMOD_SCALE;
|
|
}
|
|
//
|
|
// scroll
|
|
//
|
|
else if ( !Q_stricmp( token, "scroll" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing scale scroll parms" );
|
|
return;
|
|
}
|
|
tmi->scroll[0] = atof( token );
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing scale scroll parms" );
|
|
return;
|
|
}
|
|
tmi->scroll[1] = atof( token );
|
|
tmi->type = TMOD_SCROLL;
|
|
}
|
|
//
|
|
// stretch
|
|
//
|
|
else if ( !Q_stricmp( token, "stretch" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing stretch parms" );
|
|
return;
|
|
}
|
|
tmi->wave.func = NameToGenFunc( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing stretch parms" );
|
|
return;
|
|
}
|
|
tmi->wave.base = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing stretch parms" );
|
|
return;
|
|
}
|
|
tmi->wave.amplitude = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing stretch parms" );
|
|
return;
|
|
}
|
|
tmi->wave.phase = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing stretch parms" );
|
|
return;
|
|
}
|
|
tmi->wave.frequency = atof( token );
|
|
|
|
tmi->type = TMOD_STRETCH;
|
|
}
|
|
//
|
|
// transform
|
|
//
|
|
else if ( !Q_stricmp( token, "transform" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing transform parms" );
|
|
return;
|
|
}
|
|
tmi->matrix[0][0] = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing transform parms" );
|
|
return;
|
|
}
|
|
tmi->matrix[0][1] = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing transform parms" );
|
|
return;
|
|
}
|
|
tmi->matrix[1][0] = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing transform parms" );
|
|
return;
|
|
}
|
|
tmi->matrix[1][1] = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing transform parms" );
|
|
return;
|
|
}
|
|
tmi->translate[0] = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing transform parms" );
|
|
return;
|
|
}
|
|
tmi->translate[1] = atof( token );
|
|
|
|
tmi->type = TMOD_TRANSFORM;
|
|
}
|
|
//
|
|
// rotate
|
|
//
|
|
else if ( !Q_stricmp( token, "rotate" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing tcMod rotate parms" );
|
|
return;
|
|
}
|
|
tmi->rotateSpeed = atof( token );
|
|
tmi->type = TMOD_ROTATE;
|
|
}
|
|
//
|
|
// entityTranslate
|
|
//
|
|
else if ( !Q_stricmp( token, "entityTranslate" ) )
|
|
{
|
|
tmi->type = TMOD_ENTITY_TRANSLATE;
|
|
}
|
|
else
|
|
{
|
|
ParserWarning( "unknown tcMod '%s'", token );
|
|
}
|
|
}
|
|
|
|
|
|
static qbool ParseStage( const char** text, shaderStage_t* stage )
|
|
{
|
|
int depthMaskBits = GLS_DEPTHMASK_TRUE, blendSrcBits = 0, blendDstBits = 0, atestBits = 0, depthFuncBits = 0;
|
|
qbool depthMaskExplicit = qfalse;
|
|
|
|
stage->active = qtrue;
|
|
|
|
const char* token;
|
|
while ( 1 )
|
|
{
|
|
token = COM_ParseExt( text, qtrue );
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "no matching '}' found" );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( token[0] == '}' )
|
|
{
|
|
break;
|
|
}
|
|
//
|
|
// map <name>
|
|
//
|
|
else if ( !Q_stricmp( token, "map" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "missing parameter for 'map' keyword" );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "$whiteimage" ) )
|
|
{
|
|
stage->bundle.image[0] = tr.whiteImage;
|
|
continue;
|
|
}
|
|
else if ( !Q_stricmp( token, "$lightmap" ) )
|
|
{
|
|
if ( shader.lightmapIndex < 0 ) {
|
|
stage->bundle.image[0] = tr.whiteImage;
|
|
} else {
|
|
stage->bundle.image[0] = tr.lightmaps[shader.lightmapIndex];
|
|
}
|
|
stage->type = ST_LIGHTMAP;
|
|
/*
|
|
blendSrcBits = GLS_SRCBLEND_DST_COLOR;
|
|
blendDstBits = GLS_DSTBLEND_ZERO;
|
|
// this HAS to match the rgbgen of the previous stage (ie the diffuse)
|
|
// or they can't be collapsed - but the previous stage will have
|
|
// (incorrectly) been defaulted to CGEN_IDENTITY_LIGHTING
|
|
// when both of them SHOULD be CGEN_IDENTITY
|
|
stage->rgbGen = CGEN_IDENTITY_LIGHTING;
|
|
*/
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
stage->bundle.image[0] = R_FindImageFile( token, shader.imgflags, TW_REPEAT );
|
|
if ( !stage->bundle.image[0] )
|
|
{
|
|
ParserError( "R_FindImageFile could not find '%s'", token );
|
|
return qfalse;
|
|
}
|
|
}
|
|
}
|
|
//
|
|
// clampmap <name>
|
|
//
|
|
else if ( !Q_stricmp( token, "clampmap" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "missing parameter for 'clampmap' keyword" );
|
|
return qfalse;
|
|
}
|
|
|
|
stage->bundle.image[0] = R_FindImageFile( token, shader.imgflags, TW_CLAMP_TO_EDGE );
|
|
if ( !stage->bundle.image[0] )
|
|
{
|
|
ParserError( "R_FindImageFile could not find '%s'", token );
|
|
return qfalse;
|
|
}
|
|
}
|
|
//
|
|
// animMap <frequency> <image1> .... <imageN>
|
|
//
|
|
else if ( !Q_stricmp( token, "animMap" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "missing parameter for 'animMmap' keyword" );
|
|
return qfalse;
|
|
}
|
|
stage->bundle.imageAnimationSpeed = atof( token );
|
|
|
|
// parse up to MAX_IMAGE_ANIMATIONS animations
|
|
while ( 1 ) {
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] ) {
|
|
break;
|
|
}
|
|
int num = stage->bundle.numImageAnimations;
|
|
if ( num < MAX_IMAGE_ANIMATIONS ) {
|
|
stage->bundle.image[num] = R_FindImageFile( token, shader.imgflags, TW_REPEAT );
|
|
if ( !stage->bundle.image[num] )
|
|
{
|
|
ParserError( "R_FindImageFile could not find '%s'", token );
|
|
return qfalse;
|
|
}
|
|
stage->bundle.numImageAnimations++;
|
|
}
|
|
}
|
|
}
|
|
else if ( !Q_stricmp( token, "videoMap" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "missing parameter for 'videoMap' keyword" );
|
|
return qfalse;
|
|
}
|
|
stage->bundle.videoMapHandle = ri.CIN_PlayCinematic( token, 0, 0, 256, 256, (CIN_loop | CIN_silent | CIN_shader));
|
|
if (stage->bundle.videoMapHandle != -1) {
|
|
stage->bundle.isVideoMap = qtrue;
|
|
stage->bundle.image[0] = tr.scratchImage[stage->bundle.videoMapHandle];
|
|
}
|
|
}
|
|
//
|
|
// alphafunc <func>
|
|
//
|
|
else if ( !Q_stricmp( token, "alphaFunc" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "missing parameter for 'alphaFunc' keyword" );
|
|
return qfalse;
|
|
}
|
|
|
|
atestBits = NameToAFunc( token );
|
|
}
|
|
//
|
|
// depthFunc <func>
|
|
//
|
|
else if ( !Q_stricmp( token, "depthfunc" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "missing parameter for 'depthFunc' keyword" );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "lequal" ) )
|
|
{
|
|
depthFuncBits = 0;
|
|
}
|
|
else if ( !Q_stricmp( token, "equal" ) )
|
|
{
|
|
depthFuncBits = GLS_DEPTHFUNC_EQUAL;
|
|
}
|
|
else
|
|
{
|
|
ParserWarning( "unknown depthFunc '%s'", token );
|
|
continue;
|
|
}
|
|
}
|
|
//
|
|
// detail
|
|
//
|
|
else if ( !Q_stricmp( token, "detail" ) )
|
|
{
|
|
stage->isDetail = qtrue;
|
|
}
|
|
//
|
|
// blendfunc <srcFactor> <dstFactor>
|
|
// or blendfunc <add|filter|blend>
|
|
//
|
|
else if ( !Q_stricmp( token, "blendfunc" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing parm for blendFunc" );
|
|
continue;
|
|
}
|
|
// check for "simple" blends first
|
|
if ( !Q_stricmp( token, "add" ) ) {
|
|
blendSrcBits = GLS_SRCBLEND_ONE;
|
|
blendDstBits = GLS_DSTBLEND_ONE;
|
|
} else if ( !Q_stricmp( token, "filter" ) ) {
|
|
blendSrcBits = GLS_SRCBLEND_DST_COLOR;
|
|
blendDstBits = GLS_DSTBLEND_ZERO;
|
|
} else if ( !Q_stricmp( token, "blend" ) ) {
|
|
blendSrcBits = GLS_SRCBLEND_SRC_ALPHA;
|
|
blendDstBits = GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
|
|
} else {
|
|
// complex double blends
|
|
blendSrcBits = NameToSrcBlendMode( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing parm for blendFunc" );
|
|
continue;
|
|
}
|
|
blendDstBits = NameToDstBlendMode( token );
|
|
}
|
|
|
|
// clear depth mask for blended surfaces
|
|
if ( !depthMaskExplicit )
|
|
{
|
|
depthMaskBits = 0;
|
|
}
|
|
}
|
|
//
|
|
// rgbGen
|
|
//
|
|
else if ( !Q_stricmp( token, "rgbGen" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing parameters for rgbGen" );
|
|
continue;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "wave" ) )
|
|
{
|
|
ParseWaveForm( text, &stage->rgbWave );
|
|
stage->rgbGen = CGEN_WAVEFORM;
|
|
}
|
|
else if ( !Q_stricmp( token, "const" ) )
|
|
{
|
|
vec3_t color;
|
|
ParseVector( text, 3, color );
|
|
stage->constantColor[0] = 255 * color[0];
|
|
stage->constantColor[1] = 255 * color[1];
|
|
stage->constantColor[2] = 255 * color[2];
|
|
stage->rgbGen = CGEN_CONST;
|
|
}
|
|
else if ( !Q_stricmp( token, "identity" ) )
|
|
{
|
|
stage->rgbGen = CGEN_IDENTITY;
|
|
}
|
|
else if ( !Q_stricmp( token, "identityLighting" ) )
|
|
{
|
|
stage->rgbGen = CGEN_IDENTITY_LIGHTING;
|
|
}
|
|
else if ( !Q_stricmp( token, "entity" ) )
|
|
{
|
|
stage->rgbGen = CGEN_ENTITY;
|
|
}
|
|
else if ( !Q_stricmp( token, "oneMinusEntity" ) )
|
|
{
|
|
stage->rgbGen = CGEN_ONE_MINUS_ENTITY;
|
|
}
|
|
else if ( !Q_stricmp( token, "vertex" ) )
|
|
{
|
|
stage->rgbGen = CGEN_VERTEX;
|
|
if ( stage->alphaGen == 0 ) {
|
|
stage->alphaGen = AGEN_VERTEX;
|
|
}
|
|
}
|
|
else if ( !Q_stricmp( token, "exactVertex" ) )
|
|
{
|
|
stage->rgbGen = CGEN_EXACT_VERTEX;
|
|
}
|
|
else if ( !Q_stricmp( token, "lightingDiffuse" ) )
|
|
{
|
|
stage->rgbGen = CGEN_LIGHTING_DIFFUSE;
|
|
}
|
|
else if ( !Q_stricmp( token, "oneMinusVertex" ) )
|
|
{
|
|
stage->rgbGen = CGEN_ONE_MINUS_VERTEX;
|
|
}
|
|
else
|
|
{
|
|
ParserWarning( "unknown rgbGen parameter '%s'", token );
|
|
continue;
|
|
}
|
|
}
|
|
//
|
|
// alphaGen
|
|
//
|
|
else if ( !Q_stricmp( token, "alphaGen" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing parameters for alphaGen" );
|
|
continue;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "wave" ) )
|
|
{
|
|
ParseWaveForm( text, &stage->alphaWave );
|
|
stage->alphaGen = AGEN_WAVEFORM;
|
|
}
|
|
else if ( !Q_stricmp( token, "const" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
stage->constantColor[3] = 255 * atof( token );
|
|
stage->alphaGen = AGEN_CONST;
|
|
}
|
|
else if ( !Q_stricmp( token, "identity" ) )
|
|
{
|
|
stage->alphaGen = AGEN_IDENTITY;
|
|
}
|
|
else if ( !Q_stricmp( token, "entity" ) )
|
|
{
|
|
stage->alphaGen = AGEN_ENTITY;
|
|
}
|
|
else if ( !Q_stricmp( token, "oneMinusEntity" ) )
|
|
{
|
|
stage->alphaGen = AGEN_ONE_MINUS_ENTITY;
|
|
}
|
|
else if ( !Q_stricmp( token, "vertex" ) )
|
|
{
|
|
stage->alphaGen = AGEN_VERTEX;
|
|
}
|
|
else if ( !Q_stricmp( token, "lightingSpecular" ) )
|
|
{
|
|
stage->alphaGen = AGEN_LIGHTING_SPECULAR;
|
|
}
|
|
else if ( !Q_stricmp( token, "oneMinusVertex" ) )
|
|
{
|
|
stage->alphaGen = AGEN_ONE_MINUS_VERTEX;
|
|
}
|
|
else if ( !Q_stricmp( token, "portal" ) )
|
|
{
|
|
stage->alphaGen = AGEN_PORTAL;
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
shader.portalRange = 256;
|
|
ParserWarning( "missing range parameter for alphaGen portal, defaulting to 256" );
|
|
}
|
|
else
|
|
{
|
|
shader.portalRange = atof( token );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ParserWarning( "unknown alphaGen parameter '%s'", token );
|
|
continue;
|
|
}
|
|
}
|
|
//
|
|
// tcGen <function>
|
|
//
|
|
else if ( !Q_stricmp(token, "texgen") || !Q_stricmp( token, "tcGen" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing tcGen parm" );
|
|
continue;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "environment" ) )
|
|
{
|
|
stage->tcGen = TCGEN_ENVIRONMENT_MAPPED;
|
|
}
|
|
else if ( !Q_stricmp( token, "lightmap" ) )
|
|
{
|
|
stage->tcGen = TCGEN_LIGHTMAP;
|
|
}
|
|
else if ( !Q_stricmp( token, "texture" ) || !Q_stricmp( token, "base" ) )
|
|
{
|
|
stage->tcGen = TCGEN_TEXTURE;
|
|
}
|
|
else if ( !Q_stricmp( token, "vector" ) )
|
|
{
|
|
ParseVector( text, 3, stage->tcGenVectors[0] );
|
|
ParseVector( text, 3, stage->tcGenVectors[1] );
|
|
stage->tcGen = TCGEN_VECTOR;
|
|
}
|
|
else
|
|
{
|
|
ParserWarning( "unknown tcGen parm '%s'", token );
|
|
}
|
|
}
|
|
//
|
|
// tcMod <type> <...>
|
|
//
|
|
else if ( !Q_stricmp( token, "tcMod" ) )
|
|
{
|
|
ParseTexMod( text, stage );
|
|
continue;
|
|
}
|
|
//
|
|
// depthmask
|
|
//
|
|
else if ( !Q_stricmp( token, "depthwrite" ) )
|
|
{
|
|
depthMaskBits = GLS_DEPTHMASK_TRUE;
|
|
depthMaskExplicit = qtrue;
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
ParserError( "unknown parameter '%s'", token );
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
//
|
|
// if cgen isn't explicitly specified, use either identity or identitylighting
|
|
//
|
|
if ( stage->rgbGen == CGEN_BAD ) {
|
|
if ( blendSrcBits == 0 ||
|
|
blendSrcBits == GLS_SRCBLEND_ONE ||
|
|
blendSrcBits == GLS_SRCBLEND_SRC_ALPHA ) {
|
|
stage->rgbGen = CGEN_IDENTITY_LIGHTING;
|
|
} else {
|
|
stage->rgbGen = CGEN_IDENTITY;
|
|
}
|
|
}
|
|
|
|
|
|
//
|
|
// implicitly assume that a GL_ONE GL_ZERO blend mask disables blending
|
|
//
|
|
if ( ( blendSrcBits == GLS_SRCBLEND_ONE ) &&
|
|
( blendDstBits == GLS_DSTBLEND_ZERO ) )
|
|
{
|
|
blendDstBits = blendSrcBits = 0;
|
|
depthMaskBits = GLS_DEPTHMASK_TRUE;
|
|
}
|
|
|
|
// decide which agens we can skip
|
|
if ( stage->alphaGen == AGEN_IDENTITY ) {
|
|
if ( stage->rgbGen == CGEN_IDENTITY
|
|
|| stage->rgbGen == CGEN_LIGHTING_DIFFUSE ) {
|
|
stage->alphaGen = AGEN_SKIP;
|
|
}
|
|
}
|
|
|
|
//
|
|
// compute state bits
|
|
//
|
|
stage->stateBits = depthMaskBits | depthFuncBits |
|
|
blendSrcBits | blendDstBits |
|
|
atestBits;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
/*
|
|
===============
|
|
ParseDeform
|
|
|
|
deformVertexes wave <spread> <waveform> <base> <amplitude> <phase> <frequency>
|
|
deformVertexes normal <frequency> <amplitude>
|
|
deformVertexes move <vector> <waveform> <base> <amplitude> <phase> <frequency>
|
|
deformVertexes bulge <bulgeWidth> <bulgeHeight> <bulgeSpeed>
|
|
deformVertexes projectionShadow
|
|
deformVertexes autoSprite
|
|
deformVertexes autoSprite2
|
|
deformVertexes text[0-7]
|
|
===============
|
|
*/
|
|
static void ParseDeform( const char** text )
|
|
{
|
|
const char* token;
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing deform parm" );
|
|
return;
|
|
}
|
|
|
|
if ( shader.numDeforms == MAX_SHADER_DEFORMS ) {
|
|
ParserWarning( "too many deforms" );
|
|
return;
|
|
}
|
|
|
|
deformStage_t* ds = &shader.deforms[ shader.numDeforms ];
|
|
shader.numDeforms++;
|
|
|
|
if ( !Q_stricmp( token, "autosprite" ) ) {
|
|
ds->deformation = DEFORM_AUTOSPRITE;
|
|
return;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "autosprite2" ) ) {
|
|
ds->deformation = DEFORM_AUTOSPRITE2;
|
|
return;
|
|
}
|
|
|
|
if ( !Q_stricmpn( token, "text", 4 ) ) {
|
|
int n;
|
|
|
|
n = token[4] - '0';
|
|
if ( n < 0 || n > 7 ) {
|
|
n = 0;
|
|
}
|
|
ds->deformation = deform_t(DEFORM_TEXT0 + n);
|
|
return;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "bulge" ) ) {
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing deformVertexes bulge parm" );
|
|
return;
|
|
}
|
|
ds->bulgeWidth = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing deformVertexes bulge parm" );
|
|
return;
|
|
}
|
|
ds->bulgeHeight = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing deformVertexes bulge parm" );
|
|
return;
|
|
}
|
|
ds->bulgeSpeed = atof( token );
|
|
|
|
ds->deformation = DEFORM_BULGE;
|
|
return;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "wave" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing deformVertexes parm" );
|
|
return;
|
|
}
|
|
|
|
if ( atof( token ) != 0 )
|
|
{
|
|
ds->deformationSpread = 1.0f / atof( token );
|
|
}
|
|
else
|
|
{
|
|
ds->deformationSpread = 100.0f;
|
|
ParserWarning( "illegal div value of 0 in deformVertexes" );
|
|
}
|
|
|
|
ParseWaveForm( text, &ds->deformationWave );
|
|
ds->deformation = DEFORM_WAVE;
|
|
return;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "normal" ) )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing deformVertexes parm" );
|
|
return;
|
|
}
|
|
ds->deformationWave.amplitude = atof( token );
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing deformVertexes parm" );
|
|
return;
|
|
}
|
|
ds->deformationWave.frequency = atof( token );
|
|
|
|
ds->deformation = DEFORM_NORMALS;
|
|
return;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "move" ) ) {
|
|
int i;
|
|
|
|
for ( i = 0 ; i < 3 ; i++ ) {
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 ) {
|
|
ParserWarning( "missing deformVertexes parm" );
|
|
return;
|
|
}
|
|
ds->moveVector[i] = atof( token );
|
|
}
|
|
|
|
ParseWaveForm( text, &ds->deformationWave );
|
|
ds->deformation = DEFORM_MOVE;
|
|
return;
|
|
}
|
|
|
|
ParserWarning( "unknown deformVertexes subtype '%s'", token );
|
|
shader.numDeforms--;
|
|
}
|
|
|
|
|
|
// skyParms <outerbox> <cloudheight> <innerbox>
|
|
|
|
static void ParseSkyParms( const char** text )
|
|
{
|
|
static const char* suf[6] = { "rt", "lf", "bk", "ft", "up", "dn" };
|
|
const char* token;
|
|
char pathname[MAX_QPATH];
|
|
int i;
|
|
|
|
// outerbox
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 ) {
|
|
ParserWarning( "'skyParms' missing parameter" );
|
|
return;
|
|
}
|
|
if ( strcmp( token, "-" ) ) {
|
|
for (i = 0; i < 6; ++i) {
|
|
Com_sprintf( pathname, sizeof(pathname), "%s_%s.tga", token, suf[i] );
|
|
shader.sky.outerbox[i] = R_FindImageFile( pathname, IMG_NOPICMIP, TW_CLAMP_TO_EDGE );
|
|
if ( !shader.sky.outerbox[i] ) {
|
|
shader.sky.outerbox[i] = tr.defaultImage;
|
|
}
|
|
}
|
|
}
|
|
|
|
// cloudheight
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 ) {
|
|
ParserWarning( "'skyParms' missing parameter" );
|
|
return;
|
|
}
|
|
shader.sky.cloudHeight = atof( token );
|
|
if ( !shader.sky.cloudHeight ) {
|
|
shader.sky.cloudHeight = 512;
|
|
}
|
|
R_InitSkyTexCoords( shader.sky.cloudHeight );
|
|
|
|
// innerbox
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 ) {
|
|
ParserWarning( "'skyParms' missing parameter" );
|
|
return;
|
|
}
|
|
if ( strcmp( token, "-" ) ) {
|
|
for (i = 0; i < 6; ++i) {
|
|
Com_sprintf( pathname, sizeof(pathname), "%s_%s.tga", token, suf[i] );
|
|
shader.sky.innerbox[i] = R_FindImageFile( pathname, IMG_NOPICMIP, TW_CLAMP_TO_EDGE );
|
|
if ( !shader.sky.innerbox[i] ) {
|
|
shader.sky.innerbox[i] = tr.defaultImage;
|
|
}
|
|
}
|
|
}
|
|
|
|
shader.sort = SS_ENVIRONMENT;
|
|
shader.isSky = qtrue;
|
|
}
|
|
|
|
|
|
static void ParseSort( const char** text )
|
|
{
|
|
const char* token;
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 ) {
|
|
ParserWarning( "missing sort parameter" );
|
|
return;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "portal" ) ) {
|
|
shader.sort = SS_PORTAL;
|
|
} else if ( !Q_stricmp( token, "sky" ) ) {
|
|
shader.sort = SS_ENVIRONMENT;
|
|
} else if ( !Q_stricmp( token, "opaque" ) ) {
|
|
shader.sort = SS_OPAQUE;
|
|
}else if ( !Q_stricmp( token, "decal" ) ) {
|
|
shader.sort = SS_DECAL;
|
|
} else if ( !Q_stricmp( token, "seeThrough" ) ) {
|
|
shader.sort = SS_SEE_THROUGH;
|
|
} else if ( !Q_stricmp( token, "banner" ) ) {
|
|
shader.sort = SS_BANNER;
|
|
} else if ( !Q_stricmp( token, "additive" ) ) {
|
|
shader.sort = SS_BLEND1;
|
|
} else if ( !Q_stricmp( token, "nearest" ) ) {
|
|
shader.sort = SS_NEAREST;
|
|
} else if ( !Q_stricmp( token, "underwater" ) ) {
|
|
shader.sort = SS_UNDERWATER;
|
|
} else {
|
|
shader.sort = atof( token );
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// this table is also present in q3map
|
|
|
|
typedef struct {
|
|
const char* name;
|
|
int clearSolid, surfaceFlags, contents;
|
|
} infoParm_t;
|
|
|
|
static infoParm_t infoParms[] = {
|
|
// server relevant contents
|
|
{"water", 1, 0, CONTENTS_WATER },
|
|
{"slime", 1, 0, CONTENTS_SLIME }, // mildly damaging
|
|
{"lava", 1, 0, CONTENTS_LAVA }, // very damaging
|
|
{"playerclip", 1, 0, CONTENTS_PLAYERCLIP },
|
|
{"monsterclip", 1, 0, CONTENTS_MONSTERCLIP },
|
|
{"nodrop", 1, 0, int(CONTENTS_NODROP) }, // don't drop items or leave bodies (death fog, lava, etc)
|
|
{"nonsolid", 1, SURF_NONSOLID, 0}, // clears the solid flag
|
|
|
|
// utility relevant attributes
|
|
{"origin", 1, 0, CONTENTS_ORIGIN }, // center of rotating brushes
|
|
{"trans", 0, 0, CONTENTS_TRANSLUCENT }, // don't eat contained surfaces
|
|
{"detail", 0, 0, CONTENTS_DETAIL }, // don't include in structural bsp
|
|
{"structural", 0, 0, CONTENTS_STRUCTURAL }, // force into structural bsp even if trnas
|
|
{"areaportal", 1, 0, CONTENTS_AREAPORTAL }, // divides areas
|
|
{"clusterportal", 1,0, CONTENTS_CLUSTERPORTAL }, // for bots
|
|
{"donotenter", 1, 0, CONTENTS_DONOTENTER }, // for bots
|
|
|
|
{"fog", 1, 0, CONTENTS_FOG}, // carves surfaces entering
|
|
{"sky", 0, SURF_SKY, 0 }, // emit light from an environment map
|
|
{"lightfilter", 0, SURF_LIGHTFILTER, 0 }, // filter light going through it
|
|
{"alphashadow", 0, SURF_ALPHASHADOW, 0 }, // test light on a per-pixel basis
|
|
{"hint", 0, SURF_HINT, 0 }, // use as a primary splitter
|
|
|
|
// server attributes
|
|
{"slick", 0, SURF_SLICK, 0 },
|
|
{"noimpact", 0, SURF_NOIMPACT, 0 }, // don't make impact explosions or marks
|
|
{"nomarks", 0, SURF_NOMARKS, 0 }, // don't make impact marks, but still explode
|
|
{"ladder", 0, SURF_LADDER, 0 },
|
|
{"nodamage", 0, SURF_NODAMAGE, 0 },
|
|
{"metalsteps", 0, SURF_METALSTEPS,0 },
|
|
{"flesh", 0, SURF_FLESH, 0 },
|
|
{"nosteps", 0, SURF_NOSTEPS, 0 },
|
|
|
|
// drawsurf attributes
|
|
{"nodraw", 0, SURF_NODRAW, 0 }, // don't generate a drawsurface (or a lightmap)
|
|
{"pointlight", 0, SURF_POINTLIGHT, 0 }, // sample lighting at vertexes
|
|
{"nolightmap", 0, SURF_NOLIGHTMAP,0 }, // don't generate a lightmap
|
|
{"nodlight", 0, SURF_NODLIGHT, 0 }, // don't ever add dynamic lights
|
|
{"dust", 0, SURF_DUST, 0} // leave a dust trail when walking on this surface
|
|
};
|
|
|
|
|
|
// surfaceparm <name>
|
|
|
|
static void ParseSurfaceParm( const char** text )
|
|
{
|
|
const char* token;
|
|
int numInfoParms = sizeof(infoParms) / sizeof(infoParms[0]);
|
|
int i;
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
for ( i = 0 ; i < numInfoParms ; i++ ) {
|
|
if ( !Q_stricmp( token, infoParms[i].name ) ) {
|
|
shader.surfaceFlags |= infoParms[i].surfaceFlags;
|
|
shader.contentFlags |= infoParms[i].contents;
|
|
#if 0
|
|
if ( infoParms[i].clearSolid ) {
|
|
si->contents &= ~CONTENTS_SOLID;
|
|
}
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static qbool ParseFloat( float* out, const char** text, const char* commandName, int argIndex )
|
|
{
|
|
const char* const token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == '\0' || sscanf( token, "%f", out ) != 1 ) {
|
|
ParserWarning( "invalid/missing %s argument #%d", commandName, argIndex );
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
// q3map_sun R G B intensity azimuth elevation
|
|
// q3map_sunExt R G B intensity azimuth elevation shadow_degrees samples
|
|
|
|
static void ParseSun( const char** text )
|
|
{
|
|
float v[6];
|
|
for ( int i = 0; i < ARRAY_LEN( v ); i++ ) {
|
|
if ( !ParseFloat( &v[i], text, "q3map_sun", i + 1 ) ) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
shader.isSunDataValid = qtrue;
|
|
VectorSet( shader.sunColor, v[0], v[1], v[2] );
|
|
shader.sunAzimuth = v[4];
|
|
shader.sunInclination = 90.0f - v[5];
|
|
}
|
|
|
|
|
|
// q3map_cnq3_depthFade <scale> <bias>
|
|
|
|
static void ParseDepthFade( const char** text )
|
|
{
|
|
const char* token = COM_ParseExt( text, qfalse );
|
|
float scale;
|
|
if ( token[0] == '\0' || sscanf( token, "%f", &scale ) != 1 || scale <= 0.0f ) {
|
|
ParserWarning( "invalid/missing depth fade scale argument '%s'", token );
|
|
return;
|
|
}
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
float bias;
|
|
if ( token[0] == '\0' || sscanf( token, "%f", &bias ) != 1 ) {
|
|
ParserWarning( "invalid/missing depth fade bias argument '%s'", token );
|
|
return;
|
|
}
|
|
|
|
shader.dfType = DFT_TBD;
|
|
shader.dfInvDist = 1.0f / scale;
|
|
shader.dfBias = bias;
|
|
}
|
|
|
|
|
|
// the current text pointer is at the explicit text definition of the shader.
|
|
// parse it into the global shader variable. later functions will optimize it.
|
|
|
|
static qbool ParseShader( const char** text )
|
|
{
|
|
const char* token;
|
|
int s = 0;
|
|
|
|
token = COM_ParseExt( text, qtrue );
|
|
if ( token[0] != '{' )
|
|
{
|
|
ParserError( "expecting '{', found '%s' instead", token );
|
|
return qfalse;
|
|
}
|
|
|
|
while ( 1 )
|
|
{
|
|
token = COM_ParseExt( text, qtrue );
|
|
if ( !token[0] )
|
|
{
|
|
ParserError( "no concluding '}'" );
|
|
return qfalse;
|
|
}
|
|
|
|
// end of shader definition
|
|
if ( token[0] == '}' )
|
|
{
|
|
break;
|
|
}
|
|
// stage definition
|
|
else if ( token[0] == '{' )
|
|
{
|
|
if ( s >= MAX_SHADER_STAGES ) {
|
|
ParserError( "too many stages" );
|
|
return qfalse;
|
|
}
|
|
|
|
if ( !ParseStage( text, &stages[s] ) )
|
|
{
|
|
return qfalse;
|
|
}
|
|
stages[s].active = qtrue;
|
|
s++;
|
|
|
|
continue;
|
|
}
|
|
// skip stuff that only the QuakeEdRadient needs
|
|
else if ( !Q_stricmpn( token, "qer", 3 ) ) {
|
|
SkipRestOfLine( text );
|
|
continue;
|
|
}
|
|
else if ( !Q_stricmp( token, "deformVertexes" ) ) {
|
|
ParseDeform( text );
|
|
continue;
|
|
}
|
|
else if ( !Q_stricmp( token, "tesssize" ) ) {
|
|
SkipRestOfLine( text );
|
|
continue;
|
|
}
|
|
else if ( !Q_stricmp( token, "clampTime" ) ) {
|
|
token = COM_ParseExt( text, qfalse );
|
|
if (token[0]) {
|
|
shader.clampTime = atof(token);
|
|
}
|
|
}
|
|
else if ( !Q_stricmp( token, "q3map_cnq3_depthFade" ) ) {
|
|
ParseDepthFade( text );
|
|
continue;
|
|
}
|
|
else if ( !Q_stricmp( token, "q3map_sun" ) || !Q_stricmp( token, "q3map_sunExt" ) ) {
|
|
ParseSun( text );
|
|
SkipRestOfLine( text );
|
|
}
|
|
// skip stuff that only the q3map needs
|
|
else if ( !Q_stricmpn( token, "q3map", 5 ) ) {
|
|
SkipRestOfLine( text );
|
|
continue;
|
|
}
|
|
// skip stuff that only q3map or the server needs
|
|
else if ( !Q_stricmp( token, "surfaceParm" ) ) {
|
|
ParseSurfaceParm( text );
|
|
continue;
|
|
}
|
|
// no mip maps
|
|
else if ( !Q_stricmp( token, "nomipmaps" ) )
|
|
{
|
|
shader.imgflags |= IMG_NOMIPMAP | IMG_NOPICMIP;
|
|
continue;
|
|
}
|
|
// no picmip adjustment
|
|
else if ( !Q_stricmp( token, "nopicmip" ) )
|
|
{
|
|
shader.imgflags |= IMG_NOPICMIP;
|
|
continue;
|
|
}
|
|
// polygonOffset
|
|
else if ( !Q_stricmp( token, "polygonOffset" ) )
|
|
{
|
|
shader.polygonOffset = qtrue;
|
|
continue;
|
|
}
|
|
// entityMergable, allowing sprite surfaces from multiple entities
|
|
// to be merged into one batch. This is a savings for smoke
|
|
// puffs and blood, but can't be used for anything where the
|
|
// shader calcs (not the surface function) reference the entity color or scroll
|
|
else if ( !Q_stricmp( token, "entityMergable" ) )
|
|
{
|
|
shader.entityMergable = qtrue;
|
|
continue;
|
|
}
|
|
// fogParms
|
|
else if ( !Q_stricmp( token, "fogParms" ) )
|
|
{
|
|
if ( !ParseVector( text, 3, shader.fogParms.color ) ) {
|
|
ParserError( "invalid fogParms vector" );
|
|
return qfalse;
|
|
}
|
|
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( !token[0] )
|
|
{
|
|
ParserWarning( "missing parm for 'fogParms' keyword" );
|
|
continue;
|
|
}
|
|
shader.fogParms.depthForOpaque = atof( token );
|
|
|
|
// skip any old gradient directions
|
|
SkipRestOfLine( text );
|
|
continue;
|
|
}
|
|
// portal
|
|
else if ( !Q_stricmp(token, "portal") )
|
|
{
|
|
shader.sort = SS_PORTAL;
|
|
continue;
|
|
}
|
|
// skyparms <cloudheight> <outerbox> <innerbox>
|
|
else if ( !Q_stricmp( token, "skyparms" ) )
|
|
{
|
|
ParseSkyParms( text );
|
|
continue;
|
|
}
|
|
// light <value> determines flaring in q3map, not needed here
|
|
else if ( !Q_stricmp(token, "light") )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
continue;
|
|
}
|
|
// cull <face>
|
|
else if ( !Q_stricmp( token, "cull") )
|
|
{
|
|
token = COM_ParseExt( text, qfalse );
|
|
if ( token[0] == 0 )
|
|
{
|
|
ParserWarning( "missing cull parms" );
|
|
continue;
|
|
}
|
|
|
|
if ( !Q_stricmp( token, "none" ) || !Q_stricmp( token, "twosided" ) || !Q_stricmp( token, "disable" ) )
|
|
{
|
|
shader.cullType = CT_TWO_SIDED;
|
|
}
|
|
else if ( !Q_stricmp( token, "back" ) || !Q_stricmp( token, "backside" ) || !Q_stricmp( token, "backsided" ) )
|
|
{
|
|
shader.cullType = CT_BACK_SIDED;
|
|
}
|
|
else
|
|
{
|
|
ParserWarning( "invalid cull parm '%s'", token );
|
|
}
|
|
continue;
|
|
}
|
|
// sort
|
|
else if ( !Q_stricmp( token, "sort" ) )
|
|
{
|
|
ParseSort( text );
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
ParserError( "unknown general shader parameter '%s'", token );
|
|
return qfalse;
|
|
}
|
|
}
|
|
|
|
//
|
|
// ignore shaders that don't have any stages, unless it is a sky or fog
|
|
//
|
|
if ( s == 0 && !shader.isSky && !(shader.contentFlags & CONTENTS_FOG ) ) {
|
|
ParserError( "non-sky/fog shaders must have at least 1 stage" );
|
|
return qfalse;
|
|
}
|
|
|
|
shader.explicitlyDefined = qtrue;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
static int CompareShaders( const void* aPtr, const void* bPtr )
|
|
{
|
|
const shader_t* const a = *(const shader_t**)aPtr;
|
|
const shader_t* const b = *(const shader_t**)bPtr;
|
|
if ( a->sort < b->sort )
|
|
return -1;
|
|
if ( a->sort > b->sort )
|
|
return 1;
|
|
|
|
if ( a->polygonOffset ^ b->polygonOffset )
|
|
return a->polygonOffset - b->polygonOffset;
|
|
|
|
return a->cullType - b->cullType;
|
|
}
|
|
|
|
|
|
static void SortShaders()
|
|
{
|
|
qsort( tr.sortedShaders, tr.numShaders, sizeof(shader_t*), &CompareShaders );
|
|
for ( int i = 0; i < tr.numShaders; ++i ) {
|
|
tr.sortedShaders[i]->sortedIndex = i;
|
|
}
|
|
}
|
|
|
|
|
|
static qbool IsColorGenDynamic(colorGen_t cGen)
|
|
{
|
|
switch(cGen)
|
|
{
|
|
case CGEN_BAD:
|
|
case CGEN_IDENTITY:
|
|
case CGEN_IDENTITY_LIGHTING:
|
|
case CGEN_LIGHTING_DIFFUSE:
|
|
case CGEN_CONST:
|
|
case CGEN_VERTEX:
|
|
case CGEN_EXACT_VERTEX:
|
|
case CGEN_ONE_MINUS_VERTEX:
|
|
return qfalse;
|
|
|
|
case CGEN_WAVEFORM: // time-based
|
|
case CGEN_ENTITY: // mod can change it frame to frame
|
|
case CGEN_ONE_MINUS_ENTITY: // mod can change it frame to frame
|
|
return qtrue;
|
|
|
|
default:
|
|
Q_assert(!"Unsupported colorGen_t");
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
|
|
static qbool IsAlphaGenDynamic(alphaGen_t aGen)
|
|
{
|
|
switch(aGen)
|
|
{
|
|
case AGEN_SKIP:
|
|
case AGEN_IDENTITY:
|
|
case AGEN_CONST:
|
|
case AGEN_VERTEX:
|
|
case AGEN_ONE_MINUS_VERTEX:
|
|
return qfalse;
|
|
|
|
case AGEN_WAVEFORM: // time-based
|
|
case AGEN_LIGHTING_SPECULAR: // changes with camera position
|
|
case AGEN_ENTITY: // mod can change it frame to frame
|
|
case AGEN_ONE_MINUS_ENTITY: // mod can change it frame to frame
|
|
case AGEN_PORTAL: // changes with camera position
|
|
return qtrue;
|
|
|
|
default:
|
|
Q_assert(!"Unsupported alphaGen_t");
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
|
|
static qbool IsTexCoordGenDynamic(texCoordGen_t tcGen)
|
|
{
|
|
switch(tcGen)
|
|
{
|
|
case TCGEN_IDENTITY:
|
|
case TCGEN_BAD:
|
|
case TCGEN_TEXTURE:
|
|
case TCGEN_LIGHTMAP:
|
|
case TCGEN_VECTOR:
|
|
return qfalse;
|
|
|
|
case TCGEN_ENVIRONMENT_MAPPED: // changes with camera position
|
|
return qtrue;
|
|
|
|
default:
|
|
Q_assert(!"Unsupported texCoordGen_t");
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
|
|
static qbool IsTexModDynamic(texMod_t texMod)
|
|
{
|
|
switch(texMod)
|
|
{
|
|
case TMOD_NONE:
|
|
case TMOD_SCALE:
|
|
case TMOD_TRANSFORM:
|
|
return qfalse;
|
|
|
|
case TMOD_TURBULENT: // time
|
|
case TMOD_ENTITY_TRANSLATE: // time
|
|
case TMOD_SCROLL: // time
|
|
case TMOD_STRETCH: // time
|
|
case TMOD_ROTATE: // time
|
|
return qtrue;
|
|
|
|
default:
|
|
Q_assert(!"Unsupported texMod_t");
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
|
|
static qbool IsDeformDynamic(deform_t deform)
|
|
{
|
|
switch(deform)
|
|
{
|
|
case DEFORM_NONE:
|
|
case DEFORM_PROJECTION_SHADOW: // unsupported
|
|
case DEFORM_TEXT0:
|
|
case DEFORM_TEXT1:
|
|
case DEFORM_TEXT2:
|
|
case DEFORM_TEXT3:
|
|
case DEFORM_TEXT4:
|
|
case DEFORM_TEXT5:
|
|
case DEFORM_TEXT6:
|
|
case DEFORM_TEXT7:
|
|
return qfalse;
|
|
|
|
case DEFORM_WAVE: // time
|
|
case DEFORM_NORMALS: // time
|
|
case DEFORM_BULGE: // time
|
|
case DEFORM_MOVE: // time
|
|
case DEFORM_AUTOSPRITE: // changes with camera orientation
|
|
case DEFORM_AUTOSPRITE2: // changes with camera orientation
|
|
return qtrue;
|
|
|
|
default:
|
|
Q_assert(!"Unsupported deform_t");
|
|
return qtrue;
|
|
}
|
|
}
|
|
|
|
|
|
static void ClassifyShaderOpacity(shader_t* sh)
|
|
{
|
|
// @TODO: is this always correct?
|
|
const qbool isOpaque = sh->sort <= SS_OPAQUE;
|
|
qbool startsWithAlphaTest = qfalse;
|
|
if(sh->numStages > 0)
|
|
{
|
|
startsWithAlphaTest = (sh->stages[0]->stateBits & GLS_ATEST_BITS) != 0;
|
|
}
|
|
|
|
sh->isOpaque = isOpaque;
|
|
sh->isAlphaTestedOpaque = isOpaque && startsWithAlphaTest;
|
|
}
|
|
|
|
|
|
static void ClassifyShaderDynamism(shader_t* sh)
|
|
{
|
|
sh->isDynamic = qtrue;
|
|
|
|
for(int d = 0; d < sh->numDeforms; ++d)
|
|
{
|
|
if(IsDeformDynamic(sh->deforms[d].deformation))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
for(int s = 0; s < sh->numStages; ++s)
|
|
{
|
|
shaderStage_t* const stage = sh->stages[s];
|
|
for(int t = 0; t < stage->numTexMods; ++t)
|
|
{
|
|
if(IsTexModDynamic(stage->texMods[t].type))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
if(IsColorGenDynamic(stage->rgbGen))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(IsAlphaGenDynamic(stage->alphaGen))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if(IsTexCoordGenDynamic(stage->tcGen))
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
sh->isDynamic = qfalse;
|
|
}
|
|
|
|
|
|
static void ClassifyShader(shader_t* sh)
|
|
{
|
|
ClassifyShaderOpacity(sh);
|
|
ClassifyShaderDynamism(sh);
|
|
}
|
|
|
|
|
|
/*
|
|
Positions the most recently created shader in the tr.sortedShaders[] array
|
|
such that the shader->sort key is sorted relative to the other shaders.
|
|
|
|
Sets shader->sortedIndex
|
|
*/
|
|
static void SortNewShader()
|
|
{
|
|
shader_t* newShader = tr.shaders[ tr.numShaders - 1 ];
|
|
float sort = newShader->sort;
|
|
|
|
int i;
|
|
for ( i = tr.numShaders - 2 ; i >= 0 ; i-- ) {
|
|
if ( tr.sortedShaders[ i ]->sort <= sort ) {
|
|
break;
|
|
}
|
|
tr.sortedShaders[i+1] = tr.sortedShaders[i];
|
|
tr.sortedShaders[i+1]->sortedIndex++;
|
|
}
|
|
|
|
newShader->sortedIndex = i+1;
|
|
tr.sortedShaders[i+1] = newShader;
|
|
|
|
// sort it more aggressively for better performance
|
|
SortShaders();
|
|
|
|
//
|
|
// If we register a new shader when surfaces are already added,
|
|
// the decoded sorted shader index for the added surfaces will not always be correct anymore.
|
|
// This can lead to incorrect rendering (wrong shader used)
|
|
// and potentially crashes too (lit surfaces referencing shaders with no diffuse stage).
|
|
// We'll therefore update all surfaces that have already been added for this entire frame,
|
|
// not just the last view. Hence why we don't use firstDrawSurf / firstLitSurf.
|
|
// The extra CPU cost for the fix-up is nothing compared to loading new textures mid-frame.
|
|
//
|
|
|
|
int entityNum;
|
|
const shader_t* wrongShader;
|
|
|
|
const int numDrawSurfs = tr.refdef.numDrawSurfs;
|
|
drawSurf_t* drawSurfs = tr.refdef.drawSurfs;
|
|
for( i = 0; i < numDrawSurfs; ++i, ++drawSurfs ) {
|
|
R_DecomposeSort( drawSurfs->sort, &entityNum, &wrongShader );
|
|
drawSurfs->sort = R_ComposeSort( entityNum, tr.shaders[drawSurfs->shaderNum], drawSurfs->staticGeoChunk );
|
|
}
|
|
|
|
const int numLitSurfs = tr.refdef.numLitSurfs;
|
|
litSurf_t* litSurfs = tr.refdef.litSurfs;
|
|
for ( i = 0; i < numLitSurfs; ++i, ++litSurfs ) {
|
|
R_DecomposeLitSort( litSurfs->sort, &entityNum, &wrongShader );
|
|
litSurfs->sort = R_ComposeLitSort( entityNum, tr.shaders[litSurfs->shaderNum], drawSurfs->staticGeoChunk );
|
|
}
|
|
}
|
|
|
|
|
|
static shader_t* GeneratePermanentShader( shader_t* sh )
|
|
{
|
|
if ( tr.numShaders == MAX_SHADERS ) {
|
|
ri.Printf( PRINT_WARNING, "WARNING: GeneratePermanentShader - MAX_SHADERS hit\n" );
|
|
return tr.defaultShader;
|
|
}
|
|
|
|
shader_t* newShader;
|
|
if ( sh != NULL ) {
|
|
newShader = sh;
|
|
*newShader = shader;
|
|
} else {
|
|
newShader = RI_New<shader_t>();
|
|
*newShader = shader;
|
|
|
|
tr.shaders[tr.numShaders] = newShader;
|
|
newShader->index = tr.numShaders;
|
|
|
|
tr.sortedShaders[tr.numShaders] = newShader;
|
|
newShader->sortedIndex = tr.numShaders;
|
|
|
|
tr.numShaders++;
|
|
|
|
const int hash = Q_FileHash( newShader->name, FILE_HASH_SIZE );
|
|
newShader->next = hashTable[hash];
|
|
hashTable[hash] = newShader;
|
|
}
|
|
|
|
for ( int i = 0; i < newShader->numStages; ++i ) {
|
|
if ( !stages[i].active ) {
|
|
newShader->numStages = i;
|
|
break;
|
|
}
|
|
newShader->stages[i] = RI_New<shaderStage_t>();
|
|
*newShader->stages[i] = stages[i];
|
|
|
|
int n = newShader->stages[i]->numTexMods;
|
|
newShader->stages[i]->texMods = RI_New<texModInfo_t>( n );
|
|
Com_Memcpy( newShader->stages[i]->texMods, stages[i].texMods, n * sizeof( texModInfo_t ) );
|
|
}
|
|
|
|
ClassifyShader( newShader );
|
|
|
|
if ( sh != NULL ) {
|
|
SortShaders();
|
|
} else {
|
|
SortNewShader();
|
|
}
|
|
|
|
renderPipeline->ProcessShader( *newShader );
|
|
|
|
return newShader;
|
|
}
|
|
|
|
|
|
static void FindLightingStages()
|
|
{
|
|
int i;
|
|
|
|
for ( i = 0; i < ST_MAX; ++i )
|
|
shader.lightingStages[i] = -1;
|
|
|
|
for ( i = 0; i < shader.numStages; ++i ) {
|
|
stageType_t type = stages[i].type;
|
|
#if defined(_DEBUG)
|
|
//if ( (shader.lightingStages[type] != -1) && (type != ST_DIFFUSE) )
|
|
// ri.Printf( PRINT_WARNING, "Duplicate stagetype %d in shader %s\n", type, shader.name );
|
|
#endif
|
|
// the LAST at-least-partially-opaque layer is the one we want to use as the diffuse
|
|
// because of things like the T4 weapon spawn points etc
|
|
if (type == ST_DIFFUSE) {
|
|
if (stages[i].tcGen != TCGEN_TEXTURE)
|
|
continue;
|
|
if ((stages[i].stateBits & GLS_BLEND_BITS) == (GLS_SRCBLEND_ONE | GLS_DSTBLEND_ONE))
|
|
continue;
|
|
}
|
|
|
|
shader.lightingStages[ type ] = i;
|
|
}
|
|
}
|
|
|
|
|
|
static qbool IsAdditiveBlendDepthFade()
|
|
{
|
|
for (int i = 0; i < shader.numStages; ++i) {
|
|
if (!stages[i].active)
|
|
continue;
|
|
|
|
const unsigned int bits = stages[i].stateBits;
|
|
const unsigned int src = bits & GLS_SRCBLEND_BITS;
|
|
const unsigned int dst = bits & GLS_DSTBLEND_BITS;
|
|
if ((src != GLS_SRCBLEND_ONE && src != GLS_SRCBLEND_SRC_ALPHA && src != GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA) ||
|
|
dst != GLS_DSTBLEND_ONE ||
|
|
(bits & GLS_DEPTHMASK_TRUE) != 0)
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
static qbool IsNormalBlendDepthFade()
|
|
{
|
|
for (int i = 0; i < shader.numStages; ++i) {
|
|
if (!stages[i].active)
|
|
continue;
|
|
|
|
const unsigned int bits = stages[i].stateBits;
|
|
const unsigned int src = bits & GLS_SRCBLEND_BITS;
|
|
const unsigned int dst = bits & GLS_DSTBLEND_BITS;
|
|
if (src != GLS_SRCBLEND_SRC_ALPHA ||
|
|
dst != GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA ||
|
|
(bits & GLS_DEPTHMASK_TRUE) != 0)
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
static qbool IsMultiplicativeBlendDepthFade()
|
|
{
|
|
for (int i = 0; i < shader.numStages; ++i) {
|
|
if (!stages[i].active)
|
|
continue;
|
|
|
|
const unsigned int bits = stages[i].stateBits;
|
|
const unsigned int src = bits & GLS_SRCBLEND_BITS;
|
|
const unsigned int dst = bits & GLS_DSTBLEND_BITS;
|
|
const qbool multA = src == GLS_SRCBLEND_DST_COLOR && dst == GLS_DSTBLEND_ZERO;
|
|
const qbool multB = src == GLS_SRCBLEND_ZERO && dst == GLS_DSTBLEND_SRC_COLOR;
|
|
if ((!multA && !multB) ||
|
|
(bits & GLS_DEPTHMASK_TRUE) != 0)
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
static qbool IsPreMultAlphaBlendDepthFade()
|
|
{
|
|
for (int i = 0; i < shader.numStages; ++i) {
|
|
if (!stages[i].active)
|
|
continue;
|
|
|
|
const unsigned int bits = stages[i].stateBits;
|
|
const unsigned int src = bits & GLS_SRCBLEND_BITS;
|
|
const unsigned int dst = bits & GLS_DSTBLEND_BITS;
|
|
if (src != GLS_SRCBLEND_ONE ||
|
|
dst != GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA ||
|
|
(bits & GLS_DEPTHMASK_TRUE) != 0)
|
|
return qfalse;
|
|
}
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
static void ProcessDepthFade()
|
|
{
|
|
struct ssShader {
|
|
const char* name;
|
|
float distance;
|
|
float offset;
|
|
};
|
|
|
|
const ssShader ssShaders[] = {
|
|
{ "rocketExplosion", 24.0f },
|
|
{ "rocketExplosionNPM", 24.0f },
|
|
{ "grenadeExplosion", 24.0f },
|
|
{ "grenadeExplosionNPM", 24.0f },
|
|
{ "grenadeCPMA_NPM", 24.0f },
|
|
{ "grenadeCPMA", 24.0f },
|
|
{ "bloodTrail", 24.0f },
|
|
{ "sprites/particleSmoke", 24.0f },
|
|
{ "plasmaExplosion", 8.0f, 4.0f },
|
|
{ "plasmaExplosionNPM", 8.0f, 4.0f },
|
|
{ "plasmanewExplosion", 8.0f, 4.0f },
|
|
{ "plasmanewExplosionNPM", 8.0f, 4.0f },
|
|
{ "bulletExplosion", 8.0f, 4.0f },
|
|
{ "bulletExplosionNPM", 8.0f, 4.0f },
|
|
{ "railExplosion", 8.0f },
|
|
{ "railExplosionNPM", 8.0f },
|
|
{ "bfgExplosion", 8.0f },
|
|
{ "bfgExplosionNPM", 8.0f },
|
|
{ "bloodExplosion", 8.0f },
|
|
{ "bloodExplosionNPM", 8.0f },
|
|
{ "smokePuff", 8.0f },
|
|
{ "smokePuffNPM", 8.0f },
|
|
{ "shotgunSmokePuff", 8.0f },
|
|
{ "shotgunSmokePuffNPM", 8.0f }
|
|
};
|
|
|
|
const qbool shaderEnabled = shader.dfType == DFT_TBD;
|
|
shader.dfType = DFT_NONE;
|
|
|
|
if (!glInfo.depthFadeSupport)
|
|
return;
|
|
|
|
if (shader.sort <= SS_OPAQUE)
|
|
return;
|
|
|
|
int activeStages = 0;
|
|
for (int i = 0; i < shader.numStages; ++i) {
|
|
if (stages[i].active)
|
|
++activeStages;
|
|
}
|
|
|
|
if (activeStages <= 0)
|
|
return;
|
|
|
|
if (!shaderEnabled) {
|
|
qbool found = qfalse;
|
|
for (int i = 0; i < ARRAY_LEN(ssShaders); ++i) {
|
|
if (!Q_stricmp(shader.name, ssShaders[i].name)) {
|
|
shader.dfInvDist = 1.0f / ssShaders[i].distance;
|
|
shader.dfBias = ssShaders[i].offset;
|
|
found = qtrue;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
return;
|
|
}
|
|
|
|
if (IsAdditiveBlendDepthFade())
|
|
shader.dfType = DFT_ADD;
|
|
else if (IsNormalBlendDepthFade())
|
|
shader.dfType = DFT_BLEND;
|
|
else if (IsMultiplicativeBlendDepthFade())
|
|
shader.dfType = DFT_MULT;
|
|
else if (IsPreMultAlphaBlendDepthFade())
|
|
shader.dfType = DFT_PMA;
|
|
}
|
|
|
|
|
|
static void ProcessGreyscale()
|
|
{
|
|
// maps: q3wcp1/5/9/14/22 ct3ctf1/2 cpmctf1/2 q3w5
|
|
// I'll thank DEZ here for compiling most of this list
|
|
const char* names[] =
|
|
{
|
|
"textures/ctf_unified/banner01_red",
|
|
"textures/ctf_unified/bounce_red",
|
|
"textures/ssctf5/s_red_xian_dm3padwall",
|
|
"textures/base_light/light1red_2000",
|
|
"textures/ctf_unified/plaque_shiny_red",
|
|
"textures/ctf_unified/pointer_right_red",
|
|
"textures/ssctf5/s_redbase",
|
|
"textures/ctf_unified/pointer_left_red",
|
|
"textures/ssctf5/s_bluebase",
|
|
"textures/ctf_unified/bounce_blue",
|
|
"textures/base_light/light1blue_5000",
|
|
"textures/ctf_unified/weapfloor_blue",
|
|
"textures/ctf_unified/banner01_blue",
|
|
"textures/ctf_unified/pointer_left_blue",
|
|
"textures/ctf_unified/pointer_right_blue",
|
|
"textures/ctf_unified/weapfloor_red",
|
|
"textures/base_light/light1blue_2000",
|
|
"textures/ctf_unified/plaque_notshiny_blue",
|
|
"textures/ctf_unified/direction_blue",
|
|
"textures/ctf_unified/direction_red",
|
|
"textures/ctf_unified/monologo_noflash_red",
|
|
"textures/ctf_unified/wall_decal_red",
|
|
"textures/ssctf3/s_beam_red",
|
|
"textures/ctf_unified/banner02_red",
|
|
"textures/ctf_unified/floor_decal_red",
|
|
"textures/base_light/ceil1_22a_8k",
|
|
"textures/ssctf3/s_beam_blue",
|
|
"textures/ctf_unified/monologo_noflash_blue",
|
|
"textures/ctf_unified/wall_decal_blue",
|
|
"textures/ctf_unified/banner02_blue",
|
|
"textures/ctf_unified/floor_decal_blue",
|
|
"textures/base_light/ceil1_30_8k",
|
|
"textures/ct3ctf1/jumppad_01_blue",
|
|
"textures/base_light/ceil1_34",
|
|
"textures/base_light/ceil1_22a",
|
|
"textures/ct3ctf1/jumppad_01_red",
|
|
"textures/base_light/light1red_5000",
|
|
"textures/ninemil-ctf2/promode_logo_red",
|
|
"textures/ninemil-ctf2/promode_logo_blue",
|
|
"textures/ninemil-ctf2/bounce_red",
|
|
"textures/ninemil-ctf2/bounce_blue",
|
|
"textures/ninemil-ctf2/promode_logo_blue_large",
|
|
"textures/ninemil-ctf2/test_blue",
|
|
"textures/gothic_light/ironcrossltblue_5000",
|
|
"textures/ninemil-ctf2/ctf_blueflag",
|
|
"textures/ninemil-ctf2/test_red",
|
|
"textures/gothic_light/ironcrossltred_5000",
|
|
"textures/ninemil-ctf2/ctf_redflag",
|
|
"textures/ninemil-ctf2/promode_logo_red_large",
|
|
"textures/ninemil-ctf2/weapfloor_red",
|
|
"textures/ninemil-ctf2/weapfloor_blue",
|
|
"textures/japanc_q3w/Katana_r",
|
|
"textures/japanc_q3w/trim_shadow_r",
|
|
"textures/japanc_q3w/killblock_i2_small_r",
|
|
"textures/japanc_q3w/pic1_r",
|
|
"textures/japanc_q3w/drag_r",
|
|
"textures/ctf_unified/pointer_red",
|
|
"textures/ctf_unified/pointer_blue",
|
|
"textures/japanc_q3w/drag_b",
|
|
"textures/japanc_q3w/killblock_i2_small_b",
|
|
"textures/japanc_q3w/Katana_b",
|
|
"textures/japanc_q3w/trim_shadow_b",
|
|
"textures/japanc_q3w/samurai_b",
|
|
"textures/japanc_q3w/pic1_b",
|
|
"textures/japanc_q3w/samurai_r",
|
|
"textures/cpmctf1/banner2_b2",
|
|
"textures/cpmctf1/banner2_r2",
|
|
"textures/cpmctf1/metal_r",
|
|
"textures/cpmctf1/metal_b",
|
|
"textures/cpmctf1/trim_b",
|
|
"textures/cpmctf1/banner1_b2",
|
|
"textures/ctf/blocks18c_b",
|
|
"textures/cpmctf1/trim_r",
|
|
"textures/ctf/blocks18c_r",
|
|
"textures/cpmctf1/banner1_r2",
|
|
"textures/medieval/flr_marble1_c3trn_jp",
|
|
"textures/medieval/flr_marble5_c3trn_jp",
|
|
"textures/medieval/flr_marble1_c2trn",
|
|
"textures/medieval/flr_marble5_c2trn",
|
|
"textures/ssctf4/s_flameanim_blue",
|
|
"textures/ssctf4/s_flameanim_red",
|
|
"textures/ctf/ctf_tower_redfin_shiny",
|
|
"textures/ctf/ctf_tower_bluefin_shiny",
|
|
"textures/ctf/supportborder_blue",
|
|
"textures/gothic_trim/supportborder",
|
|
"textures/ssctf4/s_strucco_arch3",
|
|
"textures/ctf_unified/banner03_blue",
|
|
"textures/ssctf4/s_strucco_arch4",
|
|
"textures/ssctf4/ssctf4_fogblue",
|
|
"textures/ssctf4/ssctf4_fog",
|
|
"textures/gothic_trim/baseboard08_ered",
|
|
"textures/gothic_trim/baseboard08_dblue",
|
|
"textures/gothic_trim/baseboard09_blue",
|
|
"textures/gothic_trim/baseboard09_red",
|
|
"textures/ninemil-ctf2/killblock_i2_r",
|
|
"textures/ninemil-ctf2/killblock_i2_b",
|
|
"textures/ctf_unified/plaque_shiny_blue",
|
|
"textures/japanc_q3w/red_laq1",
|
|
"textures/japanc_q3w/blue_laq1",
|
|
"textures/sfx/xmetalfloor_wall_5b",
|
|
"textures/sfx/xmetalfloor_wall_9b",
|
|
"textures/sfx/xian_dm3padwall",
|
|
"textures/crew/redwall",
|
|
"textures/crew/bluewall",
|
|
"textures/ctf_unified/monologo_flash_red",
|
|
"textures/ctf_unified/monologo_flash_blue",
|
|
"textures/crew/border11cx_r",
|
|
"textures/crew/border11cx_b",
|
|
"textures/ct_ct3ctf2/red_arrows_2",
|
|
"textures/ct_ct3ctf2/blue_arrows_2",
|
|
"textures/ct_ct3ctf2/jp_01_red",
|
|
"textures/ct_ct3ctf2/jp_01",
|
|
"textures/ct_ct3ctf2/red_pipe_liquid",
|
|
"textures/ct_ct3ctf2/blue_pipe_liquid",
|
|
"textures/ct_ct3ctf2/light_trim_red",
|
|
"textures/ct_ct3ctf2/light_trim_blue",
|
|
"textures/ct_ct3ctf2/red_jumppad_wall",
|
|
"textures/ct_ct3ctf2/blue_jumppad_wall"
|
|
};
|
|
|
|
// This takes less than 80 us when loading cpm32_b1 on my old 2600K.
|
|
// So no, we don't need a hash map.
|
|
for (int i = 0; i < ARRAY_LEN(names); ++i) {
|
|
if (!Q_stricmp(shader.name, names[i])) {
|
|
shader.greyscaleCTF = qtrue;
|
|
return;
|
|
}
|
|
}
|
|
|
|
shader.greyscaleCTF = qfalse;
|
|
}
|
|
|
|
|
|
static alphaGen_t GetActualAlphaTest(const shaderStage_t* stage)
|
|
{
|
|
if (stage->alphaGen != AGEN_SKIP) {
|
|
return stage->alphaGen;
|
|
}
|
|
|
|
switch (stage->rgbGen) {
|
|
case CGEN_IDENTITY: return AGEN_IDENTITY;
|
|
case CGEN_IDENTITY_LIGHTING: return (alphaGen_t)__LINE__; // alpha = tr.identityLightByte
|
|
case CGEN_LIGHTING_DIFFUSE: return AGEN_IDENTITY;
|
|
case CGEN_CONST: return AGEN_CONST;
|
|
case CGEN_VERTEX: return AGEN_VERTEX;
|
|
case CGEN_EXACT_VERTEX: return AGEN_VERTEX;
|
|
case CGEN_ONE_MINUS_VERTEX: return (alphaGen_t)__LINE__; // doesn't write to alpha currently...
|
|
case CGEN_WAVEFORM: return AGEN_IDENTITY;
|
|
case CGEN_ENTITY: return AGEN_ENTITY;
|
|
case CGEN_ONE_MINUS_ENTITY: return AGEN_ONE_MINUS_ENTITY;
|
|
case CGEN_DEBUG_ALPHA: return AGEN_IDENTITY;
|
|
default: return (alphaGen_t)__LINE__;
|
|
}
|
|
}
|
|
|
|
|
|
static void FixRedundantAlphaTesting()
|
|
{
|
|
if (shader.numStages < 2 ||
|
|
!stages[0].active ||
|
|
!stages[1].active) {
|
|
return;
|
|
}
|
|
|
|
const unsigned int bits0 = stages[0].stateBits;
|
|
const unsigned int bits1 = stages[1].stateBits;
|
|
|
|
// both stages must be opaque
|
|
const unsigned int blendReplace = GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO;
|
|
const unsigned int blendNone = 0;
|
|
if ((bits0 & GLS_BLEND_BITS) != blendReplace && (bits0 & GLS_BLEND_BITS) != blendNone) {
|
|
return;
|
|
}
|
|
if ((bits1 & GLS_BLEND_BITS) != blendReplace && (bits1 & GLS_BLEND_BITS) != blendNone) {
|
|
return;
|
|
}
|
|
|
|
// both stages must have complementary alpha tests,
|
|
// meaning all fragment are written by the end of stage #2
|
|
// (provided texture, alphaGen and depth states match)
|
|
const unsigned int bitsOr = bits0 | bits1;
|
|
const unsigned int maskAT = GLS_ATEST_BITS;
|
|
const unsigned int expectAT = GLS_ATEST_GE_80 | GLS_ATEST_LT_80;
|
|
if ((bitsOr & maskAT) != expectAT) {
|
|
return;
|
|
}
|
|
|
|
// same texture
|
|
// @TODO: animMap support
|
|
if (stages[0].bundle.image[0] != stages[1].bundle.image[0]) {
|
|
return;
|
|
}
|
|
|
|
// same depth states and polygon fill
|
|
const unsigned int maskRest = GLS_DEPTHMASK_TRUE | GLS_POLYMODE_LINE | GLS_DEPTHTEST_DISABLE | GLS_DEPTHFUNC_EQUAL;
|
|
const unsigned int expectRest = GLS_DEPTHMASK_TRUE;
|
|
if ((bits0 & maskRest) != expectRest || (bits1 & maskRest) != expectRest) {
|
|
return;
|
|
}
|
|
|
|
// same alphaGen
|
|
const alphaGen_t ag0 = GetActualAlphaTest(&stages[0]);
|
|
const alphaGen_t ag1 = GetActualAlphaTest(&stages[1]);
|
|
if (ag0 != ag1) {
|
|
return;
|
|
}
|
|
|
|
stages[0].stateBits &= ~GLS_ATEST_BITS;
|
|
}
|
|
|
|
|
|
// we don't want to allow multiple strictly equivalent states that have different bit patterns
|
|
// this leads to more pixel shaders, more PSOs, more PSO switches...
|
|
static void FixUnusedBlendModes()
|
|
{
|
|
for (int s = 0; s < MAX_SHADER_STAGES; ++s) {
|
|
const unsigned int oldBlendBits = stages[s].stateBits & GLS_BLEND_BITS;
|
|
|
|
int newBlendBits = oldBlendBits;
|
|
if (oldBlendBits == (GLS_SRCBLEND_ZERO | GLS_DSTBLEND_SRC_COLOR)) {
|
|
newBlendBits = GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ZERO;
|
|
} else if (oldBlendBits == (GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO)) {
|
|
newBlendBits = 0;
|
|
}
|
|
|
|
stages[s].stateBits &= ~GLS_BLEND_BITS;
|
|
stages[s].stateBits |= newBlendBits;
|
|
}
|
|
}
|
|
|
|
|
|
static qbool UsesInternalLightmap( const shaderStage_t* stage )
|
|
{
|
|
return
|
|
stage->active &&
|
|
stage->type == ST_LIGHTMAP;
|
|
}
|
|
|
|
|
|
static qbool UsesExternalLightmap( const shaderStage_t* stage )
|
|
{
|
|
return
|
|
stage->active &&
|
|
!stage->bundle.isVideoMap &&
|
|
stage->bundle.numImageAnimations <= 1 &&
|
|
stage->bundle.image[0] != NULL &&
|
|
(stage->bundle.image[0]->flags & IMG_EXTLMATLAS) != 0;
|
|
}
|
|
|
|
|
|
static int GetLightmapStageIndex()
|
|
{
|
|
// look for "real" lightmaps first (straight from the .bsp file itself)
|
|
for ( int i = 0; i < shader.numStages; ++i ) {
|
|
if ( UsesInternalLightmap( &stages[i] ) )
|
|
return i;
|
|
}
|
|
|
|
// look for external lightmaps next
|
|
for ( int i = 0; i < shader.numStages; ++i ) {
|
|
if ( UsesExternalLightmap( &stages[i] ) )
|
|
return i;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
|
|
static qbool IsReplaceBlendMode( unsigned int stateBits )
|
|
{
|
|
if ( (stateBits & (GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS)) == 0 )
|
|
return qtrue;
|
|
|
|
if ( (stateBits & GLS_SRCBLEND_BITS) == GLS_SRCBLEND_ONE &&
|
|
(stateBits & GLS_DSTBLEND_BITS) == GLS_DSTBLEND_ZERO )
|
|
return qtrue;
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
|
|
static void ApplyVertexLighting()
|
|
{
|
|
if (shader.sort > SS_OPAQUE)
|
|
return;
|
|
|
|
for (int stage = 0; stage < MAX_SHADER_STAGES; stage++) {
|
|
shaderStage_t* const pStage = &stages[stage];
|
|
if (UsesInternalLightmap(pStage) || UsesExternalLightmap(pStage)) {
|
|
// keep the ST_LIGHTMAP type so that
|
|
// dynamic lighting uses the right texture
|
|
pStage->type = ST_LIGHTMAP;
|
|
pStage->bundle.image[0] = tr.whiteImage;
|
|
pStage->rgbGen = CGEN_EXACT_VERTEX;
|
|
pStage->alphaGen = AGEN_SKIP;
|
|
shader.vlApplied = qtrue;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void BuildPerImageShaderList( shader_t* newShader )
|
|
{
|
|
if ( newShader->isSky ) {
|
|
image_t** const boxes[2] = { (image_t**)newShader->sky.outerbox, (image_t**)newShader->sky.innerbox };
|
|
for ( int b = 0; b < 2; ++b ) {
|
|
image_t** const images = boxes[b];
|
|
for ( int i = 0; i < 6; ++i ) {
|
|
R_AddImageShader( images[i], newShader );
|
|
}
|
|
}
|
|
} else {
|
|
for ( int s = 0; s < newShader->numStages; ++s ) {
|
|
shaderStage_t* const newStage = newShader->stages[s];
|
|
const int numImages = max( newStage->bundle.numImageAnimations, 1 );
|
|
for ( int i = 0; i < numImages; ++i ) {
|
|
R_AddImageShader( newStage->bundle.image[i], newShader );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// returns a freshly allocated shader with info
|
|
// copied from the current global working shader
|
|
|
|
static shader_t* FinishShader( shader_t* sh = NULL )
|
|
{
|
|
//
|
|
// set polygon offset
|
|
//
|
|
if ( shader.polygonOffset && !shader.sort ) {
|
|
shader.sort = SS_DECAL;
|
|
}
|
|
|
|
// it's fine if there's polygonoffset, the effect on the depth buffer is acceptable
|
|
if ( r_pipeline->integer == 1 ) {
|
|
const int blendBits = stages[0].stateBits & GLS_BLEND_BITS;
|
|
if ( blendBits == 0 || blendBits == (GLS_SRCBLEND_ONE | GLS_DSTBLEND_ZERO) ) {
|
|
shader.sort = SS_OPAQUE;
|
|
}
|
|
}
|
|
|
|
//
|
|
// set appropriate stage information
|
|
//
|
|
int stage;
|
|
for ( stage = 0; stage < MAX_SHADER_STAGES; stage++ ) {
|
|
shaderStage_t *pStage = &stages[stage];
|
|
|
|
if ( !pStage->active ) {
|
|
break;
|
|
}
|
|
|
|
// check for a missing texture
|
|
if ( !pStage->bundle.image[0] ) {
|
|
ParserWarning( "found a stage with no image" );
|
|
pStage->active = qfalse;
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// ditch this stage if it's detail and detail textures are disabled
|
|
//
|
|
if ( pStage->isDetail && !r_detailTextures->integer ) {
|
|
const int toMove = MAX_SHADER_STAGES - stage - 1;
|
|
memmove( pStage, pStage + 1, sizeof( *pStage ) * toMove );
|
|
Com_Memset( &stages[MAX_SHADER_STAGES - 1], 0, sizeof( *pStage ) );
|
|
stage--;
|
|
continue;
|
|
}
|
|
|
|
//
|
|
// default texture coordinate generation
|
|
//
|
|
if ( pStage->type == ST_LIGHTMAP ) {
|
|
if ( pStage->tcGen == TCGEN_BAD ) {
|
|
pStage->tcGen = TCGEN_LIGHTMAP;
|
|
}
|
|
} else {
|
|
if ( pStage->tcGen == TCGEN_BAD ) {
|
|
pStage->tcGen = TCGEN_TEXTURE;
|
|
}
|
|
}
|
|
|
|
//
|
|
// determine sort order
|
|
//
|
|
if ( ( pStage->stateBits & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS ) ) &&
|
|
( stages[0].stateBits & ( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS ) ) ) {
|
|
// don't screw with sort order if this is a portal or environment
|
|
if ( !shader.sort ) {
|
|
// see through item, like a grill or grate
|
|
if ( pStage->stateBits & GLS_DEPTHMASK_TRUE ) {
|
|
shader.sort = SS_SEE_THROUGH;
|
|
} else {
|
|
shader.sort = SS_BLEND0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// there are times when you will need to manually apply a sort to
|
|
// opaque alpha tested shaders that have later blend passes
|
|
if ( !shader.sort ) {
|
|
shader.sort = SS_OPAQUE;
|
|
}
|
|
|
|
//
|
|
// if we are in r_vertexLight mode, never use a lightmap texture
|
|
//
|
|
if ( shader.vlWanted )
|
|
ApplyVertexLighting();
|
|
|
|
shader.numStages = stage;
|
|
|
|
FindLightingStages();
|
|
|
|
if ( r_fullbright->integer ) {
|
|
// we replace the lightmap texture with the white texture
|
|
for ( int i = 0; i < shader.numStages; ++i ) {
|
|
if ( UsesInternalLightmap( &stages[i] ) || UsesExternalLightmap( &stages[i] ) ) {
|
|
stages[i].type = ST_DIFFUSE;
|
|
stages[i].bundle.image[0] = tr.fullBrightImage;
|
|
}
|
|
}
|
|
} else if (
|
|
r_lightmap->integer &&
|
|
( shader.contentFlags & CONTENTS_TRANSLUCENT ) == 0 &&
|
|
shader.sort <= SS_OPAQUE ) {
|
|
// we reduce it down to a single lightmap stage with the same state bits as
|
|
// the current first stage (if the shader uses a lightmap at all)
|
|
|
|
// @NOTE: we ignore all surfaces that aren't fully opaque because:
|
|
// a) it's hard to know what level of uniform opacity would emulate the original look best
|
|
// b) alpha-tested surfaces that have been turned into alpha-blended ones can't just use any given sort key:
|
|
// you can always find or design a case that won't work correctly
|
|
// for decals entirely pressed against opaque surfaces, we could use a keyword ("polygonoffset" or something new)
|
|
// to know that we should not draw them at all, but we just shouldn't trust level designers
|
|
|
|
const int stageIndex = GetLightmapStageIndex();
|
|
const int stateBits = stages[0].stateBits;
|
|
if ( stageIndex > 0 )
|
|
memcpy( stages, stages + stageIndex, sizeof( stages[0] ) );
|
|
|
|
if ( stageIndex >= 0 ) {
|
|
for ( int i = 1; i < shader.numStages; ++i ) {
|
|
stages[i].active = qfalse;
|
|
}
|
|
stages[0].stateBits = stateBits;
|
|
shader.lightingStages[ST_DIFFUSE] = 0; // for working dynamic lights
|
|
shader.lightingStages[ST_LIGHTMAP] = 0;
|
|
}
|
|
} else if ( r_lightmap->integer ) {
|
|
// now we deal with r_lightmap on a non-opaque shader
|
|
// first stage must have: alphaFunc, depthWrite, blendFunc replace
|
|
// lightmap stage must have: depthFunc equal
|
|
// keep first stage as is, move lightmap stage as second stage, disable all others,
|
|
// change lightmap stage to enforce blendFunc replace
|
|
|
|
const int stageIndex = GetLightmapStageIndex();
|
|
|
|
if ( stageIndex > 0 ) {
|
|
const unsigned int firstBits = stages[0].stateBits;
|
|
const unsigned int lightBits = stages[stageIndex].stateBits;
|
|
|
|
if ( (firstBits & GLS_ATEST_BITS) != 0 &&
|
|
(firstBits & GLS_DEPTHMASK_TRUE) != 0 &&
|
|
IsReplaceBlendMode(firstBits) &&
|
|
(lightBits & GLS_DEPTHFUNC_EQUAL) != 0 &&
|
|
(lightBits & GLS_DEPTHTEST_DISABLE) == 0 ) {
|
|
if ( stageIndex > 1 )
|
|
memcpy( stages + 1, stages + stageIndex, sizeof(stages[0]) );
|
|
|
|
stages[1].stateBits &= ~( GLS_SRCBLEND_BITS | GLS_DSTBLEND_BITS );
|
|
|
|
for ( int i = 2; i < shader.numStages; ++i ) {
|
|
stages[i].active = qfalse;
|
|
}
|
|
|
|
shader.lightingStages[ST_DIFFUSE] = 0; // for working dynamic lights
|
|
shader.lightingStages[ST_LIGHTMAP] = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// non-sky fog-only shaders don't have any normal passes
|
|
if ( !shader.isSky && stage == 0 ) {
|
|
shader.sort = SS_FOG;
|
|
}
|
|
|
|
ProcessDepthFade();
|
|
|
|
ProcessGreyscale();
|
|
|
|
FixRedundantAlphaTesting();
|
|
|
|
FixUnusedBlendModes();
|
|
|
|
shader_t* const newShader = GeneratePermanentShader( sh );
|
|
|
|
BuildPerImageShaderList( newShader );
|
|
|
|
// make sure external lightmap stages are correctly marked as lightmap stages
|
|
for ( int s = 0; s < newShader->numStages; s++ ) {
|
|
shaderStage_t* const stagePtr = newShader->stages[s];
|
|
if ( stagePtr->type == ST_DIFFUSE && UsesExternalLightmap( stagePtr ) ) {
|
|
stagePtr->type = ST_LIGHTMAP;
|
|
}
|
|
}
|
|
|
|
newShader->hasLightmapStage = qfalse;
|
|
for ( int s = 0; s < newShader->numStages; s++ ) {
|
|
shaderStage_t* const stagePtr = newShader->stages[s];
|
|
if ( stagePtr->type == ST_LIGHTMAP ) {
|
|
newShader->hasLightmapStage = qtrue;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return newShader;
|
|
}
|
|
|
|
|
|
///////////////////////////////////////////////////////////////
|
|
|
|
|
|
// searches the combined text of ALL shader files for the given shader name
|
|
// return the body of the shader if found, else NULL
|
|
|
|
static const char* FindShaderInShaderText( const char* shadername )
|
|
{
|
|
const int hash = Q_FileHash( shadername, MAX_SHADERTEXT_HASH );
|
|
|
|
// since the hash table always contains all loaded shaders
|
|
// there's no need to actually scan through s_shaderText itself
|
|
for (int i = 0; shaderTextHashTable[hash][i]; i++) {
|
|
const char* p = shaderTextHashTable[hash][i];
|
|
const char* const token = COM_ParseExt( &p, qtrue );
|
|
|
|
if ( !Q_stricmp( token, shadername ) ) {
|
|
return p;
|
|
}
|
|
}
|
|
|
|
return NULL;
|
|
}
|
|
|
|
|
|
qbool R_EditShader( shader_t* sh, const shader_t* original, const char* shaderText )
|
|
{
|
|
Com_Memset( &shader, 0, sizeof( shader ) );
|
|
Q_strncpyz( shader.name, original->name, sizeof( shader.name ) ); // for console messages
|
|
Com_Memset( &stages, 0, sizeof( stages ) );
|
|
for ( int i = 0; i < MAX_SHADER_STAGES; i++ ) {
|
|
stages[i].texMods = texMods[i];
|
|
}
|
|
|
|
tr.shaderParseSaveState = qtrue;
|
|
tr.shaderParseNumWarnings = 0;
|
|
tr.shaderParseFailed = !ParseShader( &shaderText );
|
|
tr.shaderParseSaveState = qfalse;
|
|
if ( tr.shaderParseFailed ) {
|
|
*sh = *original;
|
|
SortShaders();
|
|
|
|
return qfalse;
|
|
}
|
|
|
|
FinishShader( sh );
|
|
|
|
Q_strncpyz( sh->name, original->name, sizeof( sh->name ) );
|
|
sh->index = original->index;
|
|
sh->lightmapIndex = original->lightmapIndex;
|
|
sh->text = original->text;
|
|
sh->next = original->next;
|
|
sh->isDynamic = true;
|
|
|
|
return qtrue;
|
|
}
|
|
|
|
|
|
void R_SetShaderData( shader_t* sh, const shader_t* original )
|
|
{
|
|
*sh = *original;
|
|
SortShaders();
|
|
}
|
|
|
|
|
|
/*
|
|
===============
|
|
R_FindShader
|
|
|
|
Will always return a valid shader, but it might be the
|
|
default shader if the real one can't be found.
|
|
|
|
In the interest of not requiring an explicit shader text entry to
|
|
be defined for every single image used in the game, three default
|
|
shader behaviors can be auto-created for any image:
|
|
|
|
If lightmapIndex == LIGHTMAP_NONE, then the image will have
|
|
dynamic diffuse lighting applied to it, as appropriate for most
|
|
entity skin surfaces.
|
|
|
|
If lightmapIndex == LIGHTMAP_2D, then the image will be used
|
|
for 2D rendering unless an explicit shader is found
|
|
|
|
Other lightmapIndex values will have a lightmap stage created
|
|
and src*dest blending applied with the texture, as appropriate for
|
|
most world construction surfaces.
|
|
|
|
===============
|
|
*/
|
|
shader_t* R_FindShader( const char *name, int lightmapIndex, int flags )
|
|
{
|
|
char strippedName[MAX_QPATH];
|
|
char fileName[MAX_QPATH];
|
|
int hash;
|
|
shader_t *sh;
|
|
|
|
if ( name[0] == 0 ) {
|
|
return tr.defaultShader;
|
|
}
|
|
|
|
// use (fullbright) vertex lighting if the bsp file doesn't have lightmaps
|
|
if ( lightmapIndex >= 0 && lightmapIndex >= tr.numLightmaps )
|
|
flags |= FINDSHADER_VERTEXLIGHT_BIT;
|
|
|
|
COM_StripExtension(name, strippedName, sizeof(strippedName));
|
|
|
|
hash = Q_FileHash(strippedName, FILE_HASH_SIZE);
|
|
|
|
//
|
|
// see if the shader is already loaded
|
|
//
|
|
for (sh = hashTable[hash]; sh; sh = sh->next) {
|
|
// NOTE: if there was no shader or image available with the name strippedName
|
|
// then a default shader is created with lightmapIndex == LIGHTMAP_NONE, so we
|
|
// have to check all default shaders otherwise for every call to R_FindShader
|
|
// with that same strippedName a new default shader is created.
|
|
if ( (sh->lightmapIndex == lightmapIndex || sh->defaultShader) &&
|
|
!Q_stricmp(sh->name, strippedName)) {
|
|
return sh;
|
|
}
|
|
}
|
|
|
|
// clear the global shader
|
|
Com_Memset( &shader, 0, sizeof( shader ) );
|
|
Com_Memset( &stages, 0, sizeof( stages ) );
|
|
Q_strncpyz(shader.name, strippedName, sizeof(shader.name));
|
|
shader.lightmapIndex = lightmapIndex;
|
|
for ( int i = 0 ; i < MAX_SHADER_STAGES ; i++ ) {
|
|
stages[i].texMods = texMods[i];
|
|
}
|
|
|
|
//
|
|
// attempt to define shader from an explicit parameter file
|
|
//
|
|
const char* shaderText = FindShaderInShaderText( strippedName );
|
|
if ( shaderText ) {
|
|
#if 0 // enable this when building a pak file to get a global list of all explicit shaders
|
|
ri.Printf( PRINT_ALL, "*SHADER* %s\n", name );
|
|
#endif
|
|
|
|
const int textOffset = (int)( shaderText - s_shaderText );
|
|
if ( !ParseShader( &shaderText ) ) {
|
|
// had errors, so use default shader
|
|
shader.defaultShader = qtrue;
|
|
}
|
|
if ( flags & FINDSHADER_VERTEXLIGHT_BIT ) {
|
|
shader.vlWanted = qtrue;
|
|
}
|
|
sh = FinishShader();
|
|
|
|
int fileIndex = -1;
|
|
for ( int i = 0; i < s_numShaderFiles; ++i ) {
|
|
if ( textOffset >= s_shaderFileOffsets[i] ) {
|
|
fileIndex = i;
|
|
}
|
|
}
|
|
sh->fileIndex = fileIndex;
|
|
sh->text = s_shaderText + textOffset;
|
|
|
|
return sh;
|
|
}
|
|
|
|
// if not defined in the in-memory shader descriptions,
|
|
// look for a raw texture (saves needing shaders for trivial opaque surfs)
|
|
//
|
|
Q_strncpyz( fileName, name, sizeof( fileName ) );
|
|
COM_DefaultExtension( fileName, sizeof( fileName ), ".tga" );
|
|
|
|
image_t* image;
|
|
if (flags & FINDSHADER_MIPRAWIMAGE_BIT)
|
|
image = R_FindImageFile( fileName, 0, TW_REPEAT );
|
|
else
|
|
image = R_FindImageFile( fileName, IMG_NOMIPMAP | IMG_NOPICMIP, TW_CLAMP_TO_EDGE );
|
|
|
|
if ( !image ) {
|
|
ri.Printf( PRINT_DEVELOPER, "Couldn't find image for shader %s\n", name );
|
|
shader.defaultShader = qtrue;
|
|
return FinishShader();
|
|
}
|
|
|
|
// create the default shading commands
|
|
|
|
stages[0].active = qtrue;
|
|
stages[0].type = ST_DIFFUSE;
|
|
stages[0].bundle.image[0] = image;
|
|
|
|
if ( shader.lightmapIndex == LIGHTMAP_BROKEN ) {
|
|
stages[0].rgbGen = CGEN_VERTEX;
|
|
stages[0].alphaGen = AGEN_VERTEX;
|
|
stages[0].stateBits = GLS_DEFAULT;
|
|
} else if ( shader.lightmapIndex == LIGHTMAP_NONE ) {
|
|
// dynamic colors at vertexes
|
|
stages[0].rgbGen = CGEN_LIGHTING_DIFFUSE;
|
|
stages[0].stateBits = GLS_DEFAULT;
|
|
} else if ( shader.lightmapIndex == LIGHTMAP_2D ) {
|
|
// GUI elements
|
|
stages[0].rgbGen = CGEN_VERTEX;
|
|
stages[0].alphaGen = AGEN_VERTEX;
|
|
stages[0].stateBits = GLS_DEPTHTEST_DISABLE |
|
|
GLS_SRCBLEND_SRC_ALPHA |
|
|
GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
|
|
} else if ( flags & FINDSHADER_VERTEXLIGHT_BIT ) {
|
|
// explicit colors at vertexes
|
|
stages[0].rgbGen = CGEN_EXACT_VERTEX;
|
|
stages[0].alphaGen = AGEN_SKIP;
|
|
stages[0].stateBits = GLS_DEFAULT;
|
|
shader.vlWanted = qtrue;
|
|
shader.vlApplied = qtrue;
|
|
} else {
|
|
// two pass lightmap
|
|
stages[0].rgbGen = CGEN_IDENTITY;
|
|
stages[0].stateBits = GLS_DEFAULT;
|
|
|
|
stages[1].active = qtrue;
|
|
stages[1].type = ST_LIGHTMAP;
|
|
stages[1].bundle.image[0] = tr.lightmaps[shader.lightmapIndex];
|
|
stages[1].rgbGen = CGEN_IDENTITY; // lightmaps are scaled on creation for identitylight
|
|
stages[1].stateBits = GLS_DEFAULT | GLS_SRCBLEND_DST_COLOR | GLS_DSTBLEND_ZERO;
|
|
}
|
|
|
|
return FinishShader();
|
|
}
|
|
|
|
|
|
// KHB !!! this code is stupid
|
|
// shaders registered from raw data should be "anonymous" and unsearchable
|
|
// because they don't have the supercession concept of "real" shaders
|
|
|
|
qhandle_t RE_RegisterShaderFromImage( const char* name, image_t* image )
|
|
{
|
|
const shader_t* sh;
|
|
|
|
// see if the shader is already loaded
|
|
int hash = Q_FileHash(name, FILE_HASH_SIZE);
|
|
for (sh = hashTable[hash]; sh; sh = sh->next) {
|
|
// NOTE: if there was no shader or image available with the name strippedName
|
|
// then a default shader is created with lightmapIndex == LIGHTMAP_NONE, so we
|
|
// have to check all default shaders otherwise for every call to R_FindShader
|
|
// with that same strippedName a new default shader is created.
|
|
if ((sh->lightmapIndex == LIGHTMAP_2D || sh->defaultShader) && !Q_stricmp(sh->name, name)) {
|
|
return sh->index;
|
|
}
|
|
}
|
|
|
|
// clear the global shader
|
|
Com_Memset( &shader, 0, sizeof( shader ) );
|
|
Com_Memset( &stages, 0, sizeof( stages ) );
|
|
Q_strncpyz(shader.name, name, sizeof(shader.name));
|
|
shader.lightmapIndex = LIGHTMAP_2D;
|
|
for (int i = 0; i < MAX_SHADER_STAGES; ++i) {
|
|
stages[i].texMods = texMods[i];
|
|
}
|
|
|
|
// create the default shading commands: this can only ever be a 2D/UI shader
|
|
stages[0].bundle.image[0] = image;
|
|
stages[0].active = qtrue;
|
|
stages[0].rgbGen = CGEN_VERTEX;
|
|
stages[0].alphaGen = AGEN_VERTEX;
|
|
stages[0].stateBits = GLS_DEPTHTEST_DISABLE | GLS_SRCBLEND_SRC_ALPHA | GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA;
|
|
|
|
sh = FinishShader();
|
|
return sh->index;
|
|
}
|
|
|
|
|
|
// we want to return 0 if the shader failed to load for some reason
|
|
// but R_FindShader should still keep a name allocated for it
|
|
// so we can fail quickly if something tries to register it again
|
|
|
|
static qhandle_t RE_RegisterShaderInternal( const char* name, int lightmapIndex, qbool mip )
|
|
{
|
|
if ( strlen( name ) >= MAX_QPATH ) {
|
|
ri.Printf( PRINT_WARNING, "WARNING: shader name is too long: '%s'\n", name );
|
|
return 0;
|
|
}
|
|
|
|
int flags = 0;
|
|
if ( mip )
|
|
flags |= FINDSHADER_MIPRAWIMAGE_BIT;
|
|
if ( r_vertexLight->integer )
|
|
flags |= FINDSHADER_VERTEXLIGHT_BIT;
|
|
const shader_t* sh = R_FindShader( name, lightmapIndex, flags );
|
|
return sh->defaultShader ? 0 : sh->index;
|
|
}
|
|
|
|
|
|
/*
|
|
these are the exported shader entry points for the rest of the system
|
|
they always return a valid index, ie the default shader if there's a problem
|
|
|
|
should really only be used for explicit shaders, because there is no
|
|
way to ask for different implicit lighting modes (vertex, lightmap, etc)
|
|
*/
|
|
|
|
qhandle_t RE_RegisterShader( const char* name )
|
|
{
|
|
return RE_RegisterShaderInternal( name, LIGHTMAP_2D, qtrue );
|
|
}
|
|
|
|
|
|
// for menu graphics that should never be picmiped
|
|
|
|
qhandle_t RE_RegisterShaderNoMip( const char* name )
|
|
{
|
|
return RE_RegisterShaderInternal( name, LIGHTMAP_2D, qfalse );
|
|
}
|
|
|
|
|
|
// when a handle is passed in by another module, this range checks
|
|
// it and returns a valid (possibly default) shader_t to be used internally
|
|
|
|
const shader_t* R_GetShaderByHandle( qhandle_t hShader )
|
|
{
|
|
if ((hShader < 0) || (hShader >= tr.numShaders)) {
|
|
ri.Printf( PRINT_WARNING, "R_GetShaderByHandle: shader handle out of range: %d\n", hShader );
|
|
return tr.defaultShader;
|
|
}
|
|
return tr.shaders[hShader];
|
|
}
|
|
|
|
|
|
// dump information on all valid shaders to the console
|
|
// a second parameter will cause it to print in sorted order
|
|
|
|
void R_ShaderList_f( void )
|
|
{
|
|
const char* const match = Cmd_Argc() > 1 ? Cmd_Argv( 1 ) : NULL;
|
|
|
|
ri.Printf( PRINT_ALL, "S : Stage count\n" );
|
|
ri.Printf( PRINT_ALL, "P : PSO count\n" );
|
|
ri.Printf( PRINT_ALL, "L : Lightmap index\n" );
|
|
ri.Printf( PRINT_ALL, "E : Explicitly defined (i.e. defined by code)\n" );
|
|
ri.Printf( PRINT_ALL, "func: 'sky' if it's a sky shader, empty otherwise\n" );
|
|
ri.Printf( PRINT_ALL, "\n" );
|
|
|
|
ri.Printf( PRINT_ALL, "S P L E func order name \n" );
|
|
|
|
int count = 0;
|
|
for ( int i = 0 ; i < tr.numShaders ; i++ ) {
|
|
const shader_t* sh = tr.sortedShaders[i];
|
|
|
|
if ( match && !Com_Filter( match, sh->name ) )
|
|
continue;
|
|
|
|
ri.Printf( PRINT_ALL, "%i %i ", sh->numStages, sh->numPipelines );
|
|
|
|
if (sh->lightmapIndex >= 0 ) {
|
|
ri.Printf( PRINT_ALL, "L " );
|
|
} else {
|
|
ri.Printf( PRINT_ALL, " " );
|
|
}
|
|
if ( sh->explicitlyDefined ) {
|
|
ri.Printf( PRINT_ALL, "E " );
|
|
} else {
|
|
ri.Printf( PRINT_ALL, " " );
|
|
}
|
|
|
|
if ( sh->isSky ) {
|
|
ri.Printf( PRINT_ALL, "sky " );
|
|
} else {
|
|
ri.Printf( PRINT_ALL, " " );
|
|
}
|
|
|
|
ri.Printf( PRINT_ALL, "%5.2f ", sh->sort );
|
|
|
|
if ( sh->defaultShader ) {
|
|
ri.Printf( PRINT_ALL, ": %s (DEFAULTED)\n", sh->name );
|
|
} else {
|
|
ri.Printf( PRINT_ALL, ": %s\n", sh->name );
|
|
}
|
|
count++;
|
|
}
|
|
|
|
ri.Printf( PRINT_ALL, "%i shaders found\n", count );
|
|
ri.Printf( PRINT_ALL, "--------------------\n" );
|
|
}
|
|
|
|
|
|
static void AutoCompleteShaderName( fieldCallback_t callback )
|
|
{
|
|
for ( int i = 0; i < tr.numShaders; i++ ) {
|
|
callback( tr.shaders[i]->name );
|
|
}
|
|
}
|
|
|
|
|
|
void R_CompleteShaderName_f( int startArg, int compArg )
|
|
{
|
|
if ( startArg + 1 == compArg )
|
|
Field_AutoCompleteCustom( startArg, compArg, &AutoCompleteShaderName );
|
|
}
|
|
|
|
|
|
void R_ShaderInfo_f()
|
|
{
|
|
if ( Cmd_Argc() <= 1 ) {
|
|
ri.Printf( PRINT_ALL, "usage: %s <shadername> [code]\n", Cmd_Argv(0) );
|
|
return;
|
|
}
|
|
|
|
const char* const name = Cmd_Argv(1);
|
|
const shader_t* sh = NULL;
|
|
for ( int i = 0; i < tr.numShaders; i++ ) {
|
|
if ( !Q_stricmp( tr.shaders[i]->name, name ) ) {
|
|
sh = tr.shaders[i];
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ( sh == NULL ) {
|
|
ri.Printf( PRINT_ALL, "shader not found\n" );
|
|
return;
|
|
}
|
|
|
|
if ( sh->text == NULL ) {
|
|
const char* type;
|
|
if ( sh->vlApplied ) {
|
|
type = "vertex-lit surface";
|
|
} else {
|
|
switch ( sh->lightmapIndex ) {
|
|
case LIGHTMAP_BROKEN: type = "broken lit surface"; break;
|
|
case LIGHTMAP_2D: type = "UI element"; break;
|
|
case LIGHTMAP_NONE: type = "opaque surface"; break;
|
|
default: type = "lit surface"; break;
|
|
}
|
|
}
|
|
ri.Printf( PRINT_ALL, "shader has no code (type: %s)\n", type );
|
|
return;
|
|
}
|
|
|
|
const char* const shaderPath = R_GetShaderPath( sh );
|
|
if ( shaderPath != NULL ) {
|
|
ri.Printf( PRINT_ALL, "%s\n", shaderPath );
|
|
}
|
|
|
|
if ( Q_stricmp( Cmd_Argv(2), "code" ) ) {
|
|
return;
|
|
}
|
|
|
|
const char* s = sh->text;
|
|
int tabs = 0;
|
|
for ( ;; ) {
|
|
const char c0 = s[0];
|
|
const char c1 = s[1];
|
|
if ( c0 == '{' ) {
|
|
tabs++;
|
|
ri.Printf( PRINT_ALL, "{" );
|
|
} else if ( c0 == '\n' ) {
|
|
ri.Printf( PRINT_ALL, "\n" );
|
|
if ( c1 == '}' ) {
|
|
tabs--;
|
|
if ( tabs == 0 ) {
|
|
ri.Printf( PRINT_ALL, "}\n" );
|
|
return;
|
|
}
|
|
}
|
|
for( int i = 0; i < tabs; i++ ) {
|
|
ri.Printf( PRINT_ALL, " " );
|
|
}
|
|
} else {
|
|
ri.Printf( PRINT_ALL, "%c", c0 );
|
|
}
|
|
s++;
|
|
}
|
|
}
|
|
|
|
|
|
void R_ShaderMixedUse_f()
|
|
{
|
|
for ( int i = 0; i < tr.numImages; ++i ) {
|
|
image_t* const image = tr.images[i];
|
|
if ( image->numShaders < 2 || !Q_stricmp(image->name, "*white") ) {
|
|
continue;
|
|
}
|
|
|
|
const int mixedFlags = image->flags0 & image->flags1;
|
|
if ( ( mixedFlags & ( IMG_NOMIPMAP | IMG_NOPICMIP ) ) == 0 ) {
|
|
continue;
|
|
}
|
|
|
|
ri.Printf( PRINT_ALL, "^5%s:\n", image->name );
|
|
for ( int is = 0; is < ARRAY_LEN( tr.imageShaders ); ++is ) {
|
|
const int imageIndex = tr.imageShaders[is] & 0xFFFF;
|
|
if ( imageIndex != i ) {
|
|
continue;
|
|
}
|
|
|
|
const int s = (tr.imageShaders[is] >> 16) & 0xFFFF;
|
|
const shader_t* const sh = tr.shaders[s];
|
|
const qbool nmmS = sh->imgflags & IMG_NOMIPMAP;
|
|
const qbool npmS = sh->imgflags & IMG_NOPICMIP;
|
|
ri.Printf( PRINT_ALL, "%s %s %s\n",
|
|
nmmS ? "NMM" : " ",
|
|
npmS ? "NPM" : " ",
|
|
sh->name);
|
|
|
|
const char* const shaderPath = R_GetShaderPath( sh );
|
|
if ( shaderPath != NULL ) {
|
|
ri.Printf( PRINT_ALL, " -> %s\n", shaderPath );
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// finds and loads all .shader files, combining them into
|
|
// a single large text block that can be scanned for shader names
|
|
// note that this does a lot of things very badly, e.g. still loads superceded shaders
|
|
|
|
static void ScanAndLoadShaderFiles()
|
|
{
|
|
static const int MAX_SHADER_FILES = 4096;
|
|
char* buffers[MAX_SHADER_FILES];
|
|
int len[MAX_SHADER_FILES];
|
|
|
|
int i;
|
|
char* p;
|
|
|
|
int numShaderFiles;
|
|
char** shaderFileNames = ri.FS_ListFiles( "scripts", ".shader", &numShaderFiles );
|
|
|
|
if ( !shaderFileNames || !numShaderFiles )
|
|
{
|
|
ri.Printf( PRINT_WARNING, "WARNING: no shader files found\n" );
|
|
return;
|
|
}
|
|
|
|
if ( numShaderFiles > MAX_SHADER_FILES )
|
|
ri.Error( ERR_DROP, "Shader file limit exceeded" );
|
|
|
|
s_shaderFileOffsets = RI_New<int>( numShaderFiles );
|
|
s_shaderFileNameOffsets = RI_New<int>( numShaderFiles );
|
|
s_shaderPakChecksums = RI_New<int>( numShaderFiles );
|
|
s_numShaderFiles = numShaderFiles;
|
|
|
|
long sum = 0;
|
|
long sumNames = 0;
|
|
// load and parse shader files
|
|
for ( i = 0; i < numShaderFiles; i++ )
|
|
{
|
|
char filename[MAX_QPATH];
|
|
Com_sprintf( filename, sizeof( filename ), "scripts/%s", shaderFileNames[i] );
|
|
ri.FS_ReadFilePak( filename, (void **)&buffers[i], &s_shaderPakChecksums[i] );
|
|
if ( !buffers[i] )
|
|
ri.Error( ERR_DROP, "Couldn't load %s", filename );
|
|
len[i] = COM_Compress( buffers[i] );
|
|
sum += len[i];
|
|
sumNames += strlen( shaderFileNames[i] ) + 1;
|
|
}
|
|
|
|
s_shaderText = RI_New<char>( sum + numShaderFiles + 1 );
|
|
s_shaderFileNames = RI_New<char>( sumNames );
|
|
|
|
char* s = s_shaderFileNames;
|
|
for ( i = 0; i < numShaderFiles; i++ ) {
|
|
s_shaderFileNameOffsets[i] = (int)( s - s_shaderFileNames );
|
|
const int l = strlen( shaderFileNames[i] );
|
|
Com_Memcpy( s, shaderFileNames[i], l );
|
|
s += l;
|
|
*s++ = '\0';
|
|
}
|
|
|
|
s = s_shaderText;
|
|
for ( i = 0; i < numShaderFiles; i++ ) {
|
|
s_shaderFileOffsets[i] = (int)( s - s_shaderText );
|
|
Com_Memcpy( s, buffers[i], len[i] );
|
|
s += len[i];
|
|
*s++ = '\n';
|
|
}
|
|
*s = '\0';
|
|
|
|
// the files have to be freed backwards because the hunk isn't a real MM
|
|
for (i = numShaderFiles - 1; i >= 0; --i)
|
|
ri.FS_FreeFile( buffers[i] );
|
|
|
|
ri.FS_FreeFileList( shaderFileNames );
|
|
|
|
int shaderTextHashTableSizes[MAX_SHADERTEXT_HASH];
|
|
Com_Memset(shaderTextHashTableSizes, 0, sizeof(shaderTextHashTableSizes));
|
|
|
|
const char* token;
|
|
int size = 0, hash;
|
|
|
|
p = s_shaderText;
|
|
while (p < s) {
|
|
token = COM_ParseExt( (const char**)&p, qtrue );
|
|
if ( token[0] == 0 )
|
|
break;
|
|
hash = Q_FileHash( token, MAX_SHADERTEXT_HASH );
|
|
shaderTextHashTableSizes[hash]++;
|
|
size++;
|
|
SkipBracedSection( (const char**)&p );
|
|
}
|
|
|
|
size += MAX_SHADERTEXT_HASH;
|
|
char** hashMem = RI_New<char*>( size );
|
|
|
|
for (i = 0; i < MAX_SHADERTEXT_HASH; i++) {
|
|
shaderTextHashTable[i] = hashMem;
|
|
hashMem += (shaderTextHashTableSizes[i] + 1);
|
|
}
|
|
|
|
Com_Memset( shaderTextHashTableSizes, 0, sizeof(shaderTextHashTableSizes) );
|
|
|
|
p = s_shaderText;
|
|
while (p < s) {
|
|
char* oldp = p;
|
|
token = COM_ParseExt( (const char**)&p, qtrue );
|
|
if ( token[0] == 0 )
|
|
break;
|
|
hash = Q_FileHash( token, MAX_SHADERTEXT_HASH );
|
|
shaderTextHashTable[hash][shaderTextHashTableSizes[hash]++] = oldp;
|
|
SkipBracedSection( (const char**)&p );
|
|
}
|
|
}
|
|
|
|
|
|
static void CreateInternalShaders()
|
|
{
|
|
tr.numShaders = 0;
|
|
|
|
// init the default shader
|
|
Com_Memset( &shader, 0, sizeof( shader ) );
|
|
Com_Memset( &stages, 0, sizeof( stages ) );
|
|
|
|
Q_strncpyz( shader.name, "<default>", sizeof( shader.name ) );
|
|
shader.lightmapIndex = LIGHTMAP_NONE;
|
|
stages[0].bundle.image[0] = tr.defaultImage;
|
|
stages[0].active = qtrue;
|
|
stages[0].stateBits = GLS_DEFAULT;
|
|
tr.defaultShader = FinishShader();
|
|
|
|
Q_strncpyz( shader.name, "<scratch>", sizeof( shader.name ) );
|
|
tr.scratchShader = FinishShader();
|
|
}
|
|
|
|
|
|
void R_InitShaders()
|
|
{
|
|
ri.Printf( PRINT_ALL, "Initializing Shaders\n" );
|
|
|
|
Com_Memset( hashTable, 0, sizeof(hashTable) );
|
|
|
|
CreateInternalShaders();
|
|
|
|
ScanAndLoadShaderFiles();
|
|
}
|
|
|
|
|
|
const char* R_GetShaderPath( const shader_t* sh )
|
|
{
|
|
const int fileIndex = sh->fileIndex;
|
|
if ( fileIndex < 0 || fileIndex >= s_numShaderFiles ) {
|
|
return NULL;
|
|
}
|
|
|
|
const int nameOffset = s_shaderFileNameOffsets[fileIndex];
|
|
const char* const fileName = s_shaderFileNames + nameOffset;
|
|
|
|
char pakName[256];
|
|
const int pakChecksum = s_shaderPakChecksums[fileIndex];
|
|
if( FS_GetPakPath( pakName, sizeof(pakName), pakChecksum ) ) {
|
|
return va( "%s/scripts/%s", pakName, fileName );
|
|
}
|
|
|
|
return va( "scripts/%s", fileName );
|
|
}
|
|
|
|
|
|
const char* R_GetSourceBlendName( unsigned int stateBits )
|
|
{
|
|
switch ( stateBits & GLS_SRCBLEND_BITS ) {
|
|
case GLS_SRCBLEND_ZERO: return "0";
|
|
case GLS_SRCBLEND_ONE: return "1";
|
|
case GLS_SRCBLEND_DST_COLOR: return "dst.rgb";
|
|
case GLS_SRCBLEND_ONE_MINUS_DST_COLOR: return "(1 - dst.rgb)";
|
|
case GLS_SRCBLEND_SRC_ALPHA: return "src.a";
|
|
case GLS_SRCBLEND_ONE_MINUS_SRC_ALPHA: return "(1 - src.a)";
|
|
case GLS_SRCBLEND_DST_ALPHA: return "dst.a";
|
|
case GLS_SRCBLEND_ONE_MINUS_DST_ALPHA: return "(1 - dst.a)";
|
|
case GLS_SRCBLEND_ALPHA_SATURATE: return "min(src.a, 1 - dst.a)";
|
|
case 0: return "1";
|
|
default: Q_assert( !"Invalid source blend bits" ); return "";
|
|
}
|
|
}
|
|
|
|
|
|
const char* R_GetDestBlendName( unsigned int stateBits )
|
|
{
|
|
switch ( stateBits & GLS_DSTBLEND_BITS ) {
|
|
case GLS_DSTBLEND_ZERO: return "0";
|
|
case GLS_DSTBLEND_ONE: return "1";
|
|
case GLS_DSTBLEND_SRC_COLOR: return "src.rgb";
|
|
case GLS_DSTBLEND_ONE_MINUS_SRC_COLOR: return "(1 - src.rgb)";
|
|
case GLS_DSTBLEND_SRC_ALPHA: return "src.a";
|
|
case GLS_DSTBLEND_ONE_MINUS_SRC_ALPHA: return "(1 - src.a)";
|
|
case GLS_DSTBLEND_DST_ALPHA: return "dst.a";
|
|
case GLS_DSTBLEND_ONE_MINUS_DST_ALPHA: return "(1 - dst.a)";
|
|
case 0: return "0";
|
|
default: Q_assert( !"Invalid dest blend bits" ); return "";
|
|
}
|
|
}
|