Skip to content

Implement partial overbright clamping like Q3 #1557

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 4 additions & 9 deletions src/engine/renderer/Material.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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:
{
Expand Down
1 change: 1 addition & 0 deletions src/engine/renderer/gl_shader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 ),
Expand Down
16 changes: 16 additions & 0 deletions src/engine/renderer/gl_shader.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/engine/renderer/glsl_source/cameraEffects_fp.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 );
Expand Down
1 change: 1 addition & 0 deletions src/engine/renderer/tr_backend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
85 changes: 34 additions & 51 deletions src/engine/renderer/tr_bsp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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;
Expand All @@ -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];
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 ] );
}
Expand Down Expand Up @@ -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() );
}
Expand Down Expand Up @@ -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() );
}
Expand Down Expand Up @@ -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() );
}
Expand Down Expand Up @@ -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 );

Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ) );
Expand Down Expand Up @@ -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 );
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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;
Expand Down
78 changes: 41 additions & 37 deletions src/engine/renderer/tr_image.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
Expand Down Expand Up @@ -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
Expand All @@ -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();
Expand Down
9 changes: 7 additions & 2 deletions src/engine/renderer/tr_init.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> r_overbrightDefaultExponent("r_overbrightDefaultExponent", "default map light color shift (multiply by 2^x)", Cvar::NONE, 2);
Cvar::Cvar<bool> r_overbrightDefaultClamp("r_overbrightDefaultClamp", "clamp lightmap colors to 1 (in absence of map worldspawn value)", Cvar::NONE, false);
Cvar::Range<Cvar::Cvar<int>> 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<bool> r_overbrightQ3("r_overbrightQ3", "brighten entire color buffer like Quake 3 (incompatible with newer assets)", Cvar::NONE, false);

Cvar::Cvar<bool> r_overbrightIgnoreMapSettings("r_overbrightIgnoreMapSettings", "force usage of r_overbrightDefaultClamp / r_overbrightDefaultExponent, ignoring worldspawn", Cvar::NONE, false);
Cvar::Range<Cvar::Cvar<int>> 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<bool> r_colorGrading( "r_colorGrading", "Use color grading", Cvar::NONE, true );
Expand Down Expand Up @@ -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 );
Expand Down
Loading