diff --git a/src/engine/renderer/Material.cpp b/src/engine/renderer/Material.cpp index f9beb9a05f..a858389947 100644 --- a/src/engine/renderer/Material.cpp +++ b/src/engine/renderer/Material.cpp @@ -59,19 +59,14 @@ static void ComputeDynamics( shaderStage_t* pStage ) { // TODO: Move color and texMatrices stuff to a compute shader pStage->colorDynamic = false; switch ( pStage->rgbGen ) { + case colorGen_t::CGEN_IDENTITY_LIGHTING: case colorGen_t::CGEN_IDENTITY: case colorGen_t::CGEN_ONE_MINUS_VERTEX: - default: - case colorGen_t::CGEN_IDENTITY_LIGHTING: - /* Historically CGEN_IDENTITY_LIGHTING was done this way: - - tess.svars.color = Color::White * tr.identityLight; - - But tr.identityLight is always 1.0f in Dæmon engine - as the as the overbright bit implementation is fully - software. */ case colorGen_t::CGEN_VERTEX: case colorGen_t::CGEN_CONST: + default: + break; + case colorGen_t::CGEN_ENTITY: case colorGen_t::CGEN_ONE_MINUS_ENTITY: { diff --git a/src/engine/renderer/gl_shader.cpp b/src/engine/renderer/gl_shader.cpp index 6ef7a0a48b..1508b6bb6d 100644 --- a/src/engine/renderer/gl_shader.cpp +++ b/src/engine/renderer/gl_shader.cpp @@ -2897,6 +2897,7 @@ GLShader_cameraEffects::GLShader_cameraEffects( GLShaderManager *manager ) : GLShader( "cameraEffects", ATTR_POSITION | ATTR_TEXCOORD, manager ), u_ColorMap3D( this ), u_CurrentMap( this ), + u_GlobalLightFactor( this ), u_ColorModulate( this ), u_TextureMatrix( this ), u_ModelViewProjectionMatrix( this ), diff --git a/src/engine/renderer/gl_shader.h b/src/engine/renderer/gl_shader.h index e53b8119a2..80c5054a67 100644 --- a/src/engine/renderer/gl_shader.h +++ b/src/engine/renderer/gl_shader.h @@ -3536,6 +3536,21 @@ class u_Time : } }; +class u_GlobalLightFactor : + GLUniform1f +{ +public: + u_GlobalLightFactor( GLShader *shader ) : + GLUniform1f( shader, "u_GlobalLightFactor" ) + { + } + + void SetUniform_GlobalLightFactor( float value ) + { + this->SetValue( value ); + } +}; + class GLDeformStage : public u_Time { @@ -4459,6 +4474,7 @@ class GLShader_cameraEffects : public GLShader, public u_ColorMap3D, public u_CurrentMap, + public u_GlobalLightFactor, public u_ColorModulate, public u_TextureMatrix, public u_ModelViewProjectionMatrix, diff --git a/src/engine/renderer/glsl_source/cameraEffects_fp.glsl b/src/engine/renderer/glsl_source/cameraEffects_fp.glsl index f4b4fbcfd6..56874c6644 100644 --- a/src/engine/renderer/glsl_source/cameraEffects_fp.glsl +++ b/src/engine/renderer/glsl_source/cameraEffects_fp.glsl @@ -29,6 +29,7 @@ uniform sampler3D u_ColorMap3D; #endif uniform vec4 u_ColorModulate; +uniform float u_GlobalLightFactor; // 1 / tr.identityLight uniform float u_InverseGamma; IN(smooth) vec2 var_TexCoords; @@ -55,6 +56,7 @@ void main() vec2 st = gl_FragCoord.st / r_FBufSize; vec4 color = texture2D(u_CurrentMap, st); + color *= u_GlobalLightFactor; if( u_Tonemap ) { color.rgb = TonemapLottes( color.rgb * u_TonemapExposure ); diff --git a/src/engine/renderer/tr_backend.cpp b/src/engine/renderer/tr_backend.cpp index 133f2dfba6..0e3c749abe 100644 --- a/src/engine/renderer/tr_backend.cpp +++ b/src/engine/renderer/tr_backend.cpp @@ -3350,6 +3350,7 @@ void RB_CameraPostFX() // enable shader, set arrays gl_cameraEffectsShader->BindProgram( 0 ); + gl_cameraEffectsShader->SetUniform_GlobalLightFactor( 1.0f / tr.identityLight ); gl_cameraEffectsShader->SetUniform_ColorModulate( backEnd.viewParms.gradingWeights ); gl_cameraEffectsShader->SetUniform_InverseGamma( 1.0 / r_gamma->value ); diff --git a/src/engine/renderer/tr_bsp.cpp b/src/engine/renderer/tr_bsp.cpp index ad2cf25bfa..14e9d3dd75 100644 --- a/src/engine/renderer/tr_bsp.cpp +++ b/src/engine/renderer/tr_bsp.cpp @@ -74,10 +74,7 @@ static void R_ColorShiftLightingBytes( byte bytes[ 4 ] ) backward compatible with this bug for diagnostic purpose and fair comparison with other buggy engines. */ - if ( tr.mapOverBrightBits == 0 ) - { - return; - } + ASSERT_LT( tr.overbrightBits, tr.mapOverBrightBits ); /* Shift the color data based on overbright range. @@ -94,7 +91,7 @@ static void R_ColorShiftLightingBytes( byte bytes[ 4 ] ) what hardware overbright bit feature was not doing, but this implementation is entirely software. */ - int shift = tr.mapOverBrightBits; + int shift = tr.mapOverBrightBits - tr.overbrightBits; // shift the data based on overbright range int r = bytes[ 0 ] << shift; @@ -120,10 +117,7 @@ static void R_ColorShiftLightingBytes( byte bytes[ 4 ] ) static void R_ColorShiftLightingBytesCompressed( byte bytes[ 8 ] ) { - if ( tr.mapOverBrightBits == 0 ) - { - return; - } + ASSERT_LT( tr.overbrightBits, tr.mapOverBrightBits ); // color shift the endpoint colors in the dxt block unsigned short rgb565 = bytes[1] << 8 | bytes[0]; @@ -164,7 +158,7 @@ R_ProcessLightmap */ void R_ProcessLightmap( byte *bytes, int width, int height, int bits ) { - if ( tr.mapOverBrightBits == 0 ) + if ( tr.overbrightBits >= tr.mapOverBrightBits ) { return; } @@ -668,7 +662,7 @@ static void R_LoadLightmaps( lump_t *l, const char *bspName ) lightMapBuffer[( index * 4 ) + 2 ] = buf_p[( ( x + ( y * internalLightMapSize ) ) * 3 ) + 2 ]; lightMapBuffer[( index * 4 ) + 3 ] = 255; - if ( tr.legacyOverBrightClamping ) + if ( tr.overbrightBits < tr.mapOverBrightBits ) { R_ColorShiftLightingBytes( &lightMapBuffer[( index * 4 ) + 0 ] ); } @@ -1029,7 +1023,7 @@ static void ParseFace( dsurface_t *ds, drawVert_t *verts, bspSurface_t *surf, in cv->verts[ i ].lightColor = Color::Adapt( verts[ i ].color ); - if ( tr.legacyOverBrightClamping ) + if ( tr.overbrightBits < tr.mapOverBrightBits ) { R_ColorShiftLightingBytes( cv->verts[ i ].lightColor.ToArray() ); } @@ -1239,7 +1233,7 @@ static void ParseMesh( dsurface_t *ds, drawVert_t *verts, bspSurface_t *surf ) points[ i ].lightColor = Color::Adapt( verts[ i ].color ); - if ( tr.legacyOverBrightClamping ) + if ( tr.overbrightBits < tr.mapOverBrightBits ) { R_ColorShiftLightingBytes( points[ i ].lightColor.ToArray() ); } @@ -1366,7 +1360,7 @@ static void ParseTriSurf( dsurface_t *ds, drawVert_t *verts, bspSurface_t *surf, cv->verts[ i ].lightColor = Color::Adapt( verts[ i ].color ); - if ( tr.legacyOverBrightClamping ) + if ( tr.overbrightBits < tr.mapOverBrightBits ) { R_ColorShiftLightingBytes( cv->verts[ i ].lightColor.ToArray() ); } @@ -3889,14 +3883,7 @@ static void R_LoadFogs( lump_t *l, lump_t *brushesLump, lump_t *sidesLump ) out->fogParms = shader->fogParms; out->color = Color::Adapt( shader->fogParms.color ); - - /* Historically it was done: - - out->color *= tr.identityLight; - - But tr.identityLight is always 1.0f in Dæmon engine - as the as the overbright bit implementation is fully - software. */ + out->color *= tr.identityLight; out->color.SetAlpha( 1 ); @@ -4112,7 +4099,7 @@ void R_LoadLightGrid( lump_t *l ) tmpDirected[ 2 ] = in->directed[ 2 ]; tmpDirected[ 3 ] = 255; - if ( tr.legacyOverBrightClamping ) + if ( tr.overbrightBits < tr.mapOverBrightBits ) { R_ColorShiftLightingBytes( tmpAmbient ); R_ColorShiftLightingBytes( tmpDirected ); @@ -4372,24 +4359,6 @@ void R_LoadEntities( lump_t *l, std::string &externalEntities ) tr.mapOverBrightBits = Math::Clamp( atof( value ), 0.0, 3.0 ); continue; } - - if ( !Q_stricmp( keyname, "overbrightClamping" ) ) - { - if ( !Q_stricmp( value, "0" ) ) - { - tr.legacyOverBrightClamping = false; - } - else if ( !Q_stricmp( value, "1" ) ) - { - tr.legacyOverBrightClamping = true; - } - else - { - Log::Warn( "invalid value for worldspawn key overbrightClamping" ); - } - - continue; - } } // check for deluxe mapping provided by NetRadiant's q3map2 @@ -5070,11 +5039,15 @@ void RE_LoadWorldMap( const char *name ) // try will not look at the partially loaded version tr.world = nullptr; - // tr.worldDeluxeMapping will be set by R_LoadLightmaps() - tr.worldLightMapping = false; - // tr.worldDeluxeMapping will be set by R_LoadEntities() - tr.worldDeluxeMapping = false; - tr.worldHDR_RGBE = false; + // It's probably a mistake if any of these lighting parameters are actually + // used before a map is loaded. + tr.worldLightMapping = false; // set by R_LoadLightmaps + tr.worldDeluxeMapping = false; // set by R_LoadEntities + tr.worldHDR_RGBE = false; // set by R_LoadEntities + tr.mapOverBrightBits = r_overbrightDefaultExponent.Get(); // maybe set by R_LoadEntities + tr.overbrightBits = std::min( tr.mapOverBrightBits, r_overbrightBits.Get() ); // set by RE_LoadWorldMap + tr.mapLightFactor = 1.0f; // set by RE_LoadWorldMap + tr.identityLight = 1.0f; // set by RE_LoadWorldMap s_worldData = {}; Q_strncpyz( s_worldData.name, name, sizeof( s_worldData.name ) ); @@ -5122,6 +5095,9 @@ void RE_LoadWorldMap( const char *name ) } R_LoadEntities( &header->lumps[ LUMP_ENTITIES ], externalEntities ); + // Now we can set this after checking a possible worldspawn value for mapOverbrightBits + tr.overbrightBits = std::min( tr.mapOverBrightBits, r_overbrightBits.Get() ); + R_LoadShaders( &header->lumps[ LUMP_SHADERS ] ); R_LoadLightmaps( &header->lumps[ LUMP_LIGHTMAPS ], name ); @@ -5159,7 +5135,6 @@ void RE_LoadWorldMap( const char *name ) tr.worldLight = tr.lightMode; tr.modelLight = lightMode_t::FULLBRIGHT; tr.modelDeluxe = deluxeMode_t::NONE; - tr.mapLightFactor = 1.0f; // Use fullbright lighting for everything if the world is fullbright. if ( tr.worldLight != lightMode_t::FULLBRIGHT ) @@ -5231,11 +5206,19 @@ void RE_LoadWorldMap( const char *name ) } } - /* Set GLSL overbright parameters if the legacy clamped overbright isn't used - and the lighting mode is not fullbright. */ - if ( !tr.legacyOverBrightClamping && tr.lightMode != lightMode_t::FULLBRIGHT ) + /* Set GLSL overbright parameters if the lighting mode is not fullbright. */ + if ( tr.lightMode != lightMode_t::FULLBRIGHT ) { - tr.mapLightFactor = pow( 2, tr.mapOverBrightBits ); + if ( r_overbrightQ3.Get() ) + { + // light factor is applied to entire color buffer; identityLight can be used to cancel it + tr.identityLight = 1.0f / float( 1 << tr.overbrightBits ); + } + else + { + // light factor is applied wherever a precomputed light is sampled + tr.mapLightFactor = float( 1 << tr.overbrightBits ); + } } tr.worldLoaded = true; diff --git a/src/engine/renderer/tr_image.cpp b/src/engine/renderer/tr_image.cpp index 381265293a..4b608e7487 100644 --- a/src/engine/renderer/tr_image.cpp +++ b/src/engine/renderer/tr_image.cpp @@ -1853,7 +1853,7 @@ image_t *R_FindImageFile( const char *imageName, imageParams_t &imageParams ) return nullptr; } - if ( imageParams.bits & IF_LIGHTMAP && tr.legacyOverBrightClamping ) + if ( imageParams.bits & IF_LIGHTMAP ) { R_ProcessLightmap( *pic, width, height, imageParams.bits ); } @@ -3054,16 +3054,11 @@ void R_InitImages() tr.lightmaps.reserve( 128 ); tr.deluxemaps.reserve( 128 ); - /* These are the values expected by the rest of the renderer - (esp. tr_bsp), used for "gamma correction of the map". - Both were set to 0 if we had neither COMPAT_ET nor COMPAT_Q3, - it may be interesting to remember. - - Quake 3 and Tremulous values: - - tr.overbrightBits = 0; // Software implementation. - tr.mapOverBrightBits = 2; // Quake 3 default. - tr.identityLight = 1.0f / ( 1 << tr.overbrightBits ); + /* + **** Map overbright bits **** + Lightmaps and vertex light colors are notionally scaled up by a factor of + pow(2, tr.mapOverBrightBits). This is a good idea because we would like a bright light + to make a texture brighter than its original diffuse image. Games like Quake 3 and Tremulous require tr.mapOverBrightBits to be set to 2. Because this engine is primarily maintained for @@ -3089,32 +3084,41 @@ void R_InitImages() require to set a different default than what Unvanquished requires. - Using a non-zero value for tr.mapOverBrightBits turns light - non-linear and makes deluxe mapping buggy though. - - Mappers may port and fix maps by multiplying the lights by 2.5 - and set the mapOverBrightBits key to 0 in map entities lump. - - It will be possible to assume tr.mapOverBrightBits is 0 when - loading maps compiled with sRGB lightmaps as there is no - legacy map using sRGB lightmap yet, and then we will be - able to avoid the need to explicitly set mapOverBrightBits - to 0 in map entities. It will be required to assume that - tr.mapOverBrightBits is 0 when loading maps compiled with - sRGB lightmaps because otherwise the color shift computation - will break the light computation, not only the deluxe one. - - In legacy engines, tr.overbrightBits was non-zero when - hardware overbright bits were enabled, zero when disabled. - This engine do not implement hardware overbright bit, so - this is always zero, and we can remove it and simplify all - the computations making use of it. - - Because tr.overbrightBits is always 0, tr.identityLight is - always 1.0f. We can entirely remove it. */ - - tr.mapOverBrightBits = r_overbrightDefaultExponent.Get(); - tr.legacyOverBrightClamping = r_overbrightDefaultClamp.Get(); + **** r_overbrightBits **** + Although lightmaps are scaled up by pow(2, tr.overbrightBits), the actual ceiling for lightmap + values is pow(2, tr.overbrightBits). tr.overbrightBits may + be less than tr.mapOverbrightBits. This is a STUPID configuration because then you are + just throwing away 1 or bits of precision from the lightmap. But it was used for many games. + + The excess (tr.mapOverbrightBits - tr.overbrightBits) bits of scaling are done to the lightmap + before uploading it. If some component exceeds 1, the color is proportionally downscaled until + the max component is 1. + + Quake 3 and vanilla Tremulous used these default cvar values: + r_overbrightBits - 1 + r_mapOverBrightBits - 2 + + So the same as Daemon. But if the game was not running in fullscreen mode or the system was + not detected as supporting hardware gamma control, tr.overbrightBit would be set to 0. + Tremfusion shipped with r_overbrightBits 0 and r_ignorehwgamma 1, either of which forces + tr.overbrightBits to 0, making it the same as the vanilla client's windowed mode. + + **** How Quake 3 originally implemented overbright **** + When hardware overbright was on (tr.overbrightBits = 1), the color buffer notionally ranged + from 0 to 2, rather than 0 to 1. So a buffer with 8-bit colors only had 7 bits of + output precision (all values 128+ produced the same output), but the extra bit was useful + for intermediate values during blending. The rescaling was effected by using the hardware + gamma ramp, which affected the whole monitor (or whole system). + Shaders for materials that were not illuminated by any precomputed lighting could use + CGEN_IDENTITY_LIGHTING to multiply by tr.identityLight, which would cancel out the + rescaling so that the material looked the same regardless of tr.overbrightBits. + + In Daemon tr.identityLight is usually 1, so any distincion between + CGEN_IDENTITY/CGEN_IDENTITY_LIGHTING is ignored. But if you set the cvar r_overbrightQ3, + which emulates Quake 3's technique of brightening the whole color buffer, it will be used. + + For even more information, see https://github.com/DaemonEngine/Daemon/issues/1542. + */ // create default texture and white texture R_CreateBuiltinImages(); diff --git a/src/engine/renderer/tr_init.cpp b/src/engine/renderer/tr_init.cpp index 7156dcf52d..d734da5641 100644 --- a/src/engine/renderer/tr_init.cpp +++ b/src/engine/renderer/tr_init.cpp @@ -92,7 +92,11 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA cvar_t *r_realtimeLightingCastShadows; cvar_t *r_precomputedLighting; Cvar::Cvar r_overbrightDefaultExponent("r_overbrightDefaultExponent", "default map light color shift (multiply by 2^x)", Cvar::NONE, 2); - Cvar::Cvar r_overbrightDefaultClamp("r_overbrightDefaultClamp", "clamp lightmap colors to 1 (in absence of map worldspawn value)", Cvar::NONE, false); + Cvar::Range> r_overbrightBits("r_overbrightBits", "clamp lightmap colors to 2^x", Cvar::NONE, 1, 0, 3); + + // also set r_highPrecisionRendering 0 for an even more authentic q3 experience + Cvar::Cvar r_overbrightQ3("r_overbrightQ3", "brighten entire color buffer like Quake 3 (incompatible with newer assets)", Cvar::NONE, false); + Cvar::Cvar r_overbrightIgnoreMapSettings("r_overbrightIgnoreMapSettings", "force usage of r_overbrightDefaultClamp / r_overbrightDefaultExponent, ignoring worldspawn", Cvar::NONE, false); Cvar::Range> r_lightMode("r_lightMode", "lighting mode: 0: fullbright (cheat), 1: vertex light, 2: grid light (cheat), 3: light map", Cvar::NONE, Util::ordinal(lightMode_t::MAP), Util::ordinal(lightMode_t::FULLBRIGHT), Util::ordinal(lightMode_t::MAP)); Cvar::Cvar r_colorGrading( "r_colorGrading", "Use color grading", Cvar::NONE, true ); @@ -1185,7 +1189,8 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p r_realtimeLightingCastShadows = Cvar_Get( "r_realtimeLightingCastShadows", "1", 0 ); r_precomputedLighting = Cvar_Get( "r_precomputedLighting", "1", CVAR_CHEAT | CVAR_LATCH ); Cvar::Latch( r_overbrightDefaultExponent ); - Cvar::Latch( r_overbrightDefaultClamp ); + Cvar::Latch( r_overbrightBits ); + Cvar::Latch( r_overbrightQ3 ); Cvar::Latch( r_overbrightIgnoreMapSettings ); Cvar::Latch( r_lightMode ); Cvar::Latch( r_colorGrading ); diff --git a/src/engine/renderer/tr_local.h b/src/engine/renderer/tr_local.h index ed59f14404..4bee0e9c9b 100644 --- a/src/engine/renderer/tr_local.h +++ b/src/engine/renderer/tr_local.h @@ -825,7 +825,7 @@ enum class shaderProfilerRenderSubGroupsMode { enum class colorGen_t { CGEN_BAD, - CGEN_IDENTITY_LIGHTING, // Always (1,1,1,1) in Dæmon engine as the overbright bit implementation is fully software. + CGEN_IDENTITY_LIGHTING, // Always (1,1,1,1) in Dæmon engine, unless you set r_overbrightQ3. CGEN_IDENTITY, // always (1,1,1,1) CGEN_ENTITY, // grabbed from entity's modulate field CGEN_ONE_MINUS_ENTITY, // grabbed from 1 - entity.modulate @@ -2809,12 +2809,16 @@ enum class shaderProfilerRenderSubGroupsMode { viewParms_t viewParms; - // r_overbrightDefaultExponent, but can be overridden by mapper using the worldspawn + // Brightness scaling: roughly speaking, a lightmap value of x will be interpreted as + // min(x * pow(2, mapOverBrightBits), pow(2, overbrightBits)) + // (but when a component hits the max allowed value, others are scaled down to keep the "same color") int mapOverBrightBits; - // pow(2, mapOverbrightBits) + // min(r_overbrightBits.Get(), mapOverBrightBits) + int overbrightBits; + // pow(2, overbrightBits), unless r_overbrightQ3 is on float mapLightFactor; - // May have to be true on some legacy maps: clamp and normalize multiplied colors. - bool legacyOverBrightClamping; + // 1/pow(2, overbrightBits) if r_overbrightQ3 is on + float identityLight; orientationr_t orientation; // for current entity @@ -2938,7 +2942,8 @@ enum class shaderProfilerRenderSubGroupsMode { extern cvar_t *r_realtimeLightingCastShadows; extern cvar_t *r_precomputedLighting; extern Cvar::Cvar r_overbrightDefaultExponent; - extern Cvar::Cvar r_overbrightDefaultClamp; + extern Cvar::Range> r_overbrightBits; + extern Cvar::Cvar r_overbrightQ3; extern Cvar::Cvar r_overbrightIgnoreMapSettings; extern Cvar::Range> r_lightMode; extern Cvar::Cvar r_colorGrading; diff --git a/src/engine/renderer/tr_shade.cpp b/src/engine/renderer/tr_shade.cpp index 19dd45a2fc..faff8eaff6 100644 --- a/src/engine/renderer/tr_shade.cpp +++ b/src/engine/renderer/tr_shade.cpp @@ -846,12 +846,10 @@ void Render_generic3D( shaderStage_t *pStage ) colorGen_t rgbGen = SetRgbGen( pStage ); alphaGen_t alphaGen = SetAlphaGen( pStage ); - // Here, it's safe to multiply the overbright factor for vertex lighting into the color gen` - // since the `generic` fragment shader only takes a single input color. `lightMapping` on the - // hand needs to know the real diffuse color, hence the separate u_LightFactor. bool mayUseVertexOverbright = pStage->type == stageType_t::ST_COLORMAP && tess.bspSurface; const bool styleLightMap = pStage->type == stageType_t::ST_STYLELIGHTMAP || pStage->type == stageType_t::ST_STYLECOLORMAP; - gl_genericShader->SetUniform_ColorModulateColorGen( rgbGen, alphaGen, mayUseVertexOverbright, styleLightMap ); + gl_genericShader->SetUniform_ColorModulateColorGen( + rgbGen, alphaGen, mayUseVertexOverbright, /*useMapLightFactor=*/ styleLightMap); // u_Color gl_genericShader->SetUniform_Color( tess.svars.color ); @@ -2321,21 +2319,15 @@ void Tess_ComputeColor( shaderStage_t *pStage ) // rgbGen switch ( pStage->rgbGen ) { + case colorGen_t::CGEN_IDENTITY_LIGHTING: + tess.svars.color = Color::Color(tr.identityLight, tr.identityLight, tr.identityLight); + break; + case colorGen_t::CGEN_IDENTITY: case colorGen_t::CGEN_ONE_MINUS_VERTEX: default: - case colorGen_t::CGEN_IDENTITY_LIGHTING: - /* Historically CGEN_IDENTITY_LIGHTING was done this way: - - tess.svars.color = Color::White * tr.identityLight; - - But tr.identityLight is always 1.0f in Dæmon engine - as the as the overbright bit implementation is fully - software. */ - { tess.svars.color = Color::White; break; - } case colorGen_t::CGEN_VERTEX: { diff --git a/src/engine/renderer/tr_shader.cpp b/src/engine/renderer/tr_shader.cpp index eeac8658df..d24cb13b61 100644 --- a/src/engine/renderer/tr_shader.cpp +++ b/src/engine/renderer/tr_shader.cpp @@ -4836,7 +4836,7 @@ static void CollapseStages() bool rgbGen_identity = stages[ i ].rgbGen == colorGen_t::CGEN_IDENTITY - || stages[ i ].rgbGen == colorGen_t::CGEN_IDENTITY_LIGHTING; + || ( stages[ i ].rgbGen == colorGen_t::CGEN_IDENTITY_LIGHTING && !r_overbrightQ3.Get() ); bool alphaGen_identity = stages[ i ].alphaGen == alphaGen_t::AGEN_IDENTITY;