From a97a8b6981a516841aaacf8832d501db169ce1fb Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Wed, 9 Jul 2025 00:50:13 +0200 Subject: [PATCH 1/3] tr_bsp: mutualize ColorShiftLighting decision --- src/engine/renderer/tr_bsp.cpp | 43 +++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/engine/renderer/tr_bsp.cpp b/src/engine/renderer/tr_bsp.cpp index fa67ec60b6..71ae57d57b 100644 --- a/src/engine/renderer/tr_bsp.cpp +++ b/src/engine/renderer/tr_bsp.cpp @@ -41,6 +41,16 @@ static byte *fileBase; //=============================================================================== +static bool cannotColorShiftLighting() +{ + if ( tr.overbrightBits >= tr.mapOverBrightBits ) + { + return true; + } + + return false; +} + /* =============== R_ColorShiftLightingBytes @@ -48,7 +58,10 @@ R_ColorShiftLightingBytes */ static void R_ColorShiftLightingBytes( byte bytes[ 4 ] ) { - ASSERT_LT( tr.overbrightBits, tr.mapOverBrightBits ); + if ( cannotColorShiftLighting() ) + { + return; + } int shift = tr.mapOverBrightBits - tr.overbrightBits; @@ -76,7 +89,10 @@ static void R_ColorShiftLightingBytes( byte bytes[ 4 ] ) static void R_ColorShiftLightingBytesCompressed( byte bytes[ 8 ] ) { - ASSERT_LT( tr.overbrightBits, tr.mapOverBrightBits ); + if ( cannotColorShiftLighting() ) + { + return; + } // color shift the endpoint colors in the dxt block unsigned short rgb565 = bytes[1] << 8 | bytes[0]; @@ -117,7 +133,7 @@ R_ProcessLightmap */ void R_ProcessLightmap( byte *bytes, int width, int height, int bits ) { - if ( tr.overbrightBits >= tr.mapOverBrightBits ) + if ( cannotColorShiftLighting() ) { return; } @@ -621,10 +637,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.overbrightBits < tr.mapOverBrightBits ) - { - R_ColorShiftLightingBytes( &lightMapBuffer[( index * 4 ) + 0 ] ); - } + R_ColorShiftLightingBytes( &lightMapBuffer[( index * 4 ) + 0 ] ); } } @@ -972,9 +985,7 @@ static void ParseTriangleSurface( dsurface_t* ds, drawVert_t* verts, bspSurface_ cv->verts[ i ].lightColor = Color::Adapt( verts[ i ].color ); - if ( tr.overbrightBits < tr.mapOverBrightBits ) { - R_ColorShiftLightingBytes( cv->verts[ i ].lightColor.ToArray() ); - } + R_ColorShiftLightingBytes( cv->verts[ i ].lightColor.ToArray() ); } // Copy triangles @@ -1206,10 +1217,7 @@ static void ParseMesh( dsurface_t *ds, drawVert_t *verts, bspSurface_t *surf ) points[ i ].lightColor = Color::Adapt( verts[ i ].color ); - if ( tr.overbrightBits < tr.mapOverBrightBits ) - { - R_ColorShiftLightingBytes( points[ i ].lightColor.ToArray() ); - } + R_ColorShiftLightingBytes( points[ i ].lightColor.ToArray() ); } // center texture coords @@ -3494,11 +3502,8 @@ void R_LoadLightGrid( lump_t *l ) tmpDirected[ 2 ] = in->directed[ 2 ]; tmpDirected[ 3 ] = 255; - if ( tr.overbrightBits < tr.mapOverBrightBits ) - { - R_ColorShiftLightingBytes( tmpAmbient ); - R_ColorShiftLightingBytes( tmpDirected ); - } + R_ColorShiftLightingBytes( tmpAmbient ); + R_ColorShiftLightingBytes( tmpDirected ); for ( j = 0; j < 3; j++ ) { From 09e4e48de94d39191c10bd5f73fb86cad50b0d68 Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Mon, 25 Mar 2019 19:59:14 +0100 Subject: [PATCH 2/3] renderer: implement native sRGB support --- src/common/Color.h | 41 ++++ src/engine/renderer/gl_shader.cpp | 6 + src/engine/renderer/gl_shader.h | 13 ++ .../glsl_source/cameraEffects_fp.glsl | 21 +++ src/engine/renderer/tr_backend.cpp | 119 +++++++++++- src/engine/renderer/tr_bsp.cpp | 178 +++++++++++++++++- src/engine/renderer/tr_image.cpp | 36 ++-- src/engine/renderer/tr_init.cpp | 8 + src/engine/renderer/tr_local.h | 22 +++ src/engine/renderer/tr_scene.cpp | 6 + src/engine/renderer/tr_shade.cpp | 11 +- src/engine/renderer/tr_shade_calc.cpp | 17 +- src/engine/renderer/tr_shader.cpp | 55 +++++- src/engine/renderer/tr_surface.cpp | 16 +- 14 files changed, 511 insertions(+), 38 deletions(-) diff --git a/src/common/Color.h b/src/common/Color.h index 8c98ebc74c..fba2482fb4 100644 --- a/src/common/Color.h +++ b/src/common/Color.h @@ -38,6 +38,31 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "Compiler.h" #include "Math.h" +inline float convertFromSRGB( float f, bool accurate = true ) +{ + if ( accurate ) + { + return f <= 0.04045f ? f * (1.0f / 12.92f) : pow((f + 0.055f) * (1.0f / 1.055f), 2.4f); + } + + return pow( f, 2.2f ); +} + +inline void convertFromSRGB( float* v, bool accurate = true ) +{ + v[ 0 ] = convertFromSRGB( v[ 0 ], accurate ); + v[ 1 ] = convertFromSRGB( v[ 1 ], accurate ); + v[ 2 ] = convertFromSRGB( v[ 2 ], accurate ); +} + +inline void convertFromSRGB( byte* bytes, bool accurate = true ) +{ + vec3_t v; + VectorScale( bytes, 1.0f / 255.0f, v ); + convertFromSRGB( v, accurate ); + VectorScale( v, 255.0f, bytes ); +} + namespace Color { /* @@ -256,6 +281,22 @@ class BasicColor data_[ 3 ] = v; } + static component_type ConvertFromSRGB( component_type v, bool accurate = true ) NOEXCEPT + { + float f = float( v ) / float( component_max ); + f = convertFromSRGB( f, accurate ); + return component_type( f * float( component_max ) ); + } + + BasicColor ConvertFromSRGB( bool accurate = true ) const NOEXCEPT + { + return BasicColor( + ConvertFromSRGB( Red(), accurate ), + ConvertFromSRGB( Green(), accurate ), + ConvertFromSRGB( Blue(), accurate ), + Alpha() ); + } + CONSTEXPR_FUNCTION_RELAXED BasicColor& operator*=( float factor ) NOEXCEPT { *this = *this * factor; diff --git a/src/engine/renderer/gl_shader.cpp b/src/engine/renderer/gl_shader.cpp index bbc09fb189..a5b37202b5 100644 --- a/src/engine/renderer/gl_shader.cpp +++ b/src/engine/renderer/gl_shader.cpp @@ -759,6 +759,11 @@ static std::string GenEngineConstants() { AddConst( str, "r_RimExponent", r_rimExponent->value ); } + if ( r_accurateSRGB.Get() ) + { + AddDefine( str, "r_accurateSRGB", 1 ); + } + if ( r_showLightTiles->integer ) { AddDefine( str, "r_showLightTiles", 1 ); @@ -2801,6 +2806,7 @@ GLShader_cameraEffects::GLShader_cameraEffects() : u_CurrentMap( this ), u_GlobalLightFactor( this ), u_ColorModulate( this ), + u_SRGB( this ), u_Tonemap( this ), u_TonemapParms( this ), u_TonemapExposure( this ), diff --git a/src/engine/renderer/gl_shader.h b/src/engine/renderer/gl_shader.h index f5fea59e54..c21904f91a 100644 --- a/src/engine/renderer/gl_shader.h +++ b/src/engine/renderer/gl_shader.h @@ -3073,6 +3073,18 @@ class u_InverseGamma : } }; +class u_SRGB : + GLUniform1Bool { + public: + u_SRGB( GLShader* shader ) : + GLUniform1Bool( shader, "u_SRGB", true ) { + } + + void SetUniform_SRGB( bool tonemap ) { + this->SetValue( tonemap ); + } +}; + class u_Tonemap : GLUniform1Bool { public: @@ -3579,6 +3591,7 @@ class GLShader_cameraEffects : public u_CurrentMap, public u_GlobalLightFactor, public u_ColorModulate, + public u_SRGB, public u_Tonemap, public u_TonemapParms, public u_TonemapExposure, diff --git a/src/engine/renderer/glsl_source/cameraEffects_fp.glsl b/src/engine/renderer/glsl_source/cameraEffects_fp.glsl index bae518595b..c548eb065f 100644 --- a/src/engine/renderer/glsl_source/cameraEffects_fp.glsl +++ b/src/engine/renderer/glsl_source/cameraEffects_fp.glsl @@ -31,6 +31,22 @@ uniform sampler3D u_ColorMap3D; uniform vec4 u_ColorModulate; uniform float u_GlobalLightFactor; // 1 / tr.identityLight uniform float u_InverseGamma; +uniform bool u_SRGB; + +void convertToSRGB(inout vec3 color) { + #if defined(r_accurateSRGB) + float threshold = 0.0031308f; + + bvec3 cutoff = lessThan(color, vec3(threshold)); + vec3 low = vec3(12.92f) * color; + vec3 high = vec3(1.055f) * pow(color, vec3(1.0f / 2.4f)) - vec3(0.055f); + + color = mix(high, low, cutoff); + #else + float inverse = 0.4545454f; // 1 / 2.2 + color = pow(color, vec3(inverse)); + #endif +} // Tone mapping is not available when high-precision float framebuffer isn't enabled or supported. #if defined(r_highPrecisionRendering) && defined(HAVE_ARB_texture_float) @@ -59,6 +75,11 @@ void main() vec4 color = texture2D(u_CurrentMap, st); color *= u_GlobalLightFactor; + if ( u_SRGB ) + { + convertToSRGB( color.rgb ); + } + #if defined(r_highPrecisionRendering) && defined(HAVE_ARB_texture_float) 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 b9fdeea9ab..30bf342412 100644 --- a/src/engine/renderer/tr_backend.cpp +++ b/src/engine/renderer/tr_backend.cpp @@ -707,6 +707,99 @@ void GL_VertexAttribPointers( uint32_t attribBits ) } } +static GLint GL_ToSRGB( GLint internalFormat, bool isSRGB ) +{ + if ( !isSRGB ) + { + return internalFormat; + } + + auto convert = []( GLint format ) + { + switch ( format ) + { +#if 0 // Not used in the code base. + /* EXT_texture_sRGB_R8 extension. + See: https://github.com/KhronosGroup/OpenGL-Registry/blob/main/extensions/EXT/EXT_texture_sRGB_R8.txt */ + case GL_RED: + return GL_SR8_EXT; +#endif + case GL_RGB: + return GL_SRGB; + case GL_RGBA: + return GL_SRGB_ALPHA; + case GL_RGB8: + return GL_SRGB8; + case GL_RGBA8: + return GL_SRGB8_ALPHA8; +#if 0 // Internal formats, should not be used directly. + case GL_COMPRESSED_RGB: + return GL_COMPRESSED_SRGB; + case GL_COMPRESSED_RGBA: + return GL_COMPRESSED_SRGB_ALPHA; +#endif + case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: + return GL_COMPRESSED_SRGB_S3TC_DXT1_EXT; + case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: + return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT; + case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: + return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT; + case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: + return GL_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT; +#if 0 // Not used in the codebase, + /* Core 4.2, ARB_texture_compression_bptc extension. + See: https://github.com/KhronosGroup/OpenGL-Registry/blob/main/extensions/ARB/ARB_texture_compression_bptc.txt */ + case GL_COMPRESSED_RGBA_BPTC_UNORM: + return GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM; +#endif + default: + return format; + } + }; + + GLint finalFormat = convert( internalFormat ); + + if ( finalFormat == internalFormat ) + { + Log::Warn( "Missing sRGB conversion for GL format: %0#x", internalFormat ); + } + else + { + Log::Debug( "Using sRGB GL format: %0#x", finalFormat ); + } + + return finalFormat; +} + +void GL_TexImage2D( GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *data, bool isSRGB ) +{ + GLint finalFormat = GL_ToSRGB( internalFormat, isSRGB ); + + glTexImage2D( target, level, finalFormat, width, height, border, format, type, data ); + +} + +void GL_TexImage3D( GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const void *data, bool isSRGB ) +{ + GLint finalFormat = GL_ToSRGB( internalFormat, isSRGB ); + + glTexImage3D( target, level, finalFormat, width, height, depth, border, format, type, data ); +} + +void GL_CompressedTexImage2D( GLenum target, GLint level, GLenum internalFormat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void *data, bool isSRGB ) +{ + GLint finalFormat = GL_ToSRGB( internalFormat, isSRGB ); + + glCompressedTexImage2D( target, level, finalFormat, width, height, border, imageSize, data ); +} + +void GL_CompressedTexSubImage3D( GLenum target, GLint level, GLint xOffset, GLint yOffset, GLint zOffset, GLsizei width, GLsizei height, GLsizei depth, GLenum internalFormat, GLsizei size, const void *data, bool isSRGB ) +{ + GLint finalFormat = GL_ToSRGB( internalFormat, isSRGB ); + + glCompressedTexSubImage3D( target, level, xOffset, yOffset, zOffset, width, height, depth, finalFormat, size, data ); +} + /* ================ RB_Hyperspace @@ -1535,6 +1628,8 @@ void RB_CameraPostFX() { gl_cameraEffectsShader->SetUniform_InverseGamma( 1.0 / r_gamma->value ); + gl_cameraEffectsShader->SetUniform_SRGB( tr.worldLinearizeTexture ); + const bool tonemap = r_toneMapping.Get() && r_highPrecisionRendering.Get() && glConfig2.textureFloatAvailable; if ( tonemap ) { vec4_t tonemapParms { r_toneMappingContrast.Get(), r_toneMappingHighlightsCompressionSpeed.Get() }; @@ -2668,12 +2763,24 @@ void RE_UploadCinematic( int cols, int rows, const byte *data, int client, bool GL_Bind( tr.cinematicImage[ client ] ); // if the scratchImage isn't in the format we want, specify it as a new texture + /* HACK: This also detects we start playing a video to set the appropriate colorspace + because this assumes a video cannot have a 1×1 size (the RoQ format expects the size + to be a multiples of 4). */ if ( cols != tr.cinematicImage[ client ]->width || rows != tr.cinematicImage[ client ]->height ) { tr.cinematicImage[ client ]->width = tr.cinematicImage[ client ]->uploadWidth = cols; tr.cinematicImage[ client ]->height = tr.cinematicImage[ client ]->uploadHeight = rows; - glTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data ); + bool isSRGB = tr.worldLinearizeTexture; + + // Makes sure listImages lists the colorspace properly. + if ( isSRGB ) + { + tr.cinematicImage[ client ]->bits |= IF_SRGB; + } + // No need to delete the bit otherwise because R_InitImages() is called at every map load. + + GL_TexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data, isSRGB ); glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); @@ -2903,7 +3010,10 @@ const RenderCommand *Poly2dCommand::ExecuteSelf( ) const tess.verts[ tess.numVertexes ].texCoords[ 0 ] = verts[ i ].st[ 0 ]; tess.verts[ tess.numVertexes ].texCoords[ 1 ] = verts[ i ].st[ 1 ]; - tess.verts[ tess.numVertexes ].color = Color::Adapt( verts[ i ].modulate ); + Color::Color32Bit color = Color::Adapt( verts[ i ].modulate ); + color = tr.convertColorFromSRGB( color ); + tess.verts[ tess.numVertexes ].color = color; + tess.numVertexes++; } @@ -2960,7 +3070,10 @@ const RenderCommand *Poly2dIndexedCommand::ExecuteSelf( ) const tess.verts[ tess.numVertexes ].texCoords[ 0 ] = verts[ i ].st[ 0 ]; tess.verts[ tess.numVertexes ].texCoords[ 1 ] = verts[ i ].st[ 1 ]; - tess.verts[ tess.numVertexes ].color = Color::Adapt( verts[ i ].modulate ); + Color::Color32Bit color = Color::Adapt( verts[ i ].modulate ); + color = tr.convertColorFromSRGB( color ); + tess.verts[ tess.numVertexes ].color = color; + tess.numVertexes++; } diff --git a/src/engine/renderer/tr_bsp.cpp b/src/engine/renderer/tr_bsp.cpp index 71ae57d57b..13d7774e5f 100644 --- a/src/engine/renderer/tr_bsp.cpp +++ b/src/engine/renderer/tr_bsp.cpp @@ -41,6 +41,16 @@ static byte *fileBase; //=============================================================================== +static void R_LinearizeLightingColorBytes( byte* bytes ) +{ + if ( !tr.worldLinearizeLightMap ) + { + return; + } + + convertFromSRGB( bytes ); +} + static bool cannotColorShiftLighting() { if ( tr.overbrightBits >= tr.mapOverBrightBits ) @@ -48,6 +58,11 @@ static bool cannotColorShiftLighting() return true; } + if ( tr.worldLinearizeLightMap ) + { + return true; + } + return false; } @@ -467,6 +482,14 @@ static void R_LoadLightmaps( lump_t *l, const char *bspName ) return; } + int lightMapBits = IF_LIGHTMAP | IF_NOPICMIP; + int deluxeMapBits = IF_NORMALMAP | IF_NOPICMIP; + + if ( tr.worldLinearizeLightMap ) + { + lightMapBits |= IF_SRGB; + } + int len = l->filelen; if ( !len ) { @@ -509,7 +532,7 @@ static void R_LoadLightmaps( lump_t *l, const char *bspName ) LoadRGBEToBytes( va( "%s/%s", mapName, filename.c_str() ), &ldrImage, &width, &height ); imageParams_t imageParams = {}; - imageParams.bits = IF_NOPICMIP | IF_LIGHTMAP; + imageParams.bits = lightMapBits; imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; @@ -536,7 +559,7 @@ static void R_LoadLightmaps( lump_t *l, const char *bspName ) Log::Debug("...loading external lightmap '%s/%s'", mapName, filename); imageParams_t imageParams = {}; - imageParams.bits = IF_NOPICMIP | IF_NORMALMAP; + imageParams.bits = deluxeMapBits; imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; @@ -565,7 +588,7 @@ static void R_LoadLightmaps( lump_t *l, const char *bspName ) if (!tr.worldDeluxeMapping || i % 2 == 0) { imageParams_t imageParams = {}; - imageParams.bits = IF_NOPICMIP | IF_LIGHTMAP; + imageParams.bits = lightMapBits; imageParams.filterType = filterType_t::FT_LINEAR; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; @@ -575,7 +598,7 @@ static void R_LoadLightmaps( lump_t *l, const char *bspName ) else if (tr.worldDeluxeMapping) { imageParams_t imageParams = {}; - imageParams.bits = IF_NOPICMIP | IF_NORMALMAP; + imageParams.bits = deluxeMapBits; imageParams.filterType = filterType_t::FT_LINEAR; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; @@ -642,7 +665,7 @@ static void R_LoadLightmaps( lump_t *l, const char *bspName ) } imageParams_t imageParams = {}; - imageParams.bits = IF_NOPICMIP | IF_LIGHTMAP; + imageParams.bits = lightMapBits; imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_CLAMP; @@ -985,6 +1008,8 @@ static void ParseTriangleSurface( dsurface_t* ds, drawVert_t* verts, bspSurface_ cv->verts[ i ].lightColor = Color::Adapt( verts[ i ].color ); + R_LinearizeLightingColorBytes( cv->verts[ i ].lightColor.ToArray() ); + R_ColorShiftLightingBytes( cv->verts[ i ].lightColor.ToArray() ); } @@ -1217,6 +1242,8 @@ static void ParseMesh( dsurface_t *ds, drawVert_t *verts, bspSurface_t *surf ) points[ i ].lightColor = Color::Adapt( verts[ i ].color ); + R_LinearizeLightingColorBytes( points[ i ].lightColor.ToArray() ); + R_ColorShiftLightingBytes( points[ i ].lightColor.ToArray() ); } @@ -3502,6 +3529,9 @@ void R_LoadLightGrid( lump_t *l ) tmpDirected[ 2 ] = in->directed[ 2 ]; tmpDirected[ 3 ] = 255; + R_LinearizeLightingColorBytes( tmpAmbient ); + R_LinearizeLightingColorBytes( tmpDirected ); + R_ColorShiftLightingBytes( tmpAmbient ); R_ColorShiftLightingBytes( tmpDirected ); @@ -3617,6 +3647,47 @@ void R_LoadLightGrid( lump_t *l ) Log::Debug("%i light grid points created", w->numLightGridPoints ); } +// The NOP variants are in tr_init.cpp. +static float convertFloatFromSRGB_accurate( float f ) +{ + if ( backEnd.projection2D ) + { + return f; + } + + return convertFromSRGB( f ); +} + +static float convertFloatFromSRGB_cheap( float f ) +{ + if ( backEnd.projection2D ) + { + return f; + } + + return convertFromSRGB( f, false ); +} + +static Color::Color convertColorFromSRGB_accurate( Color::Color c ) +{ + if ( backEnd.projection2D ) + { + return c; + } + + return c.ConvertFromSRGB(); +} + +static Color::Color convertColorFromSRGB_cheap( Color::Color c ) +{ + if ( backEnd.projection2D ) + { + return c; + } + + return c.ConvertFromSRGB( false ); +} + /* ================ R_LoadEntities @@ -3773,6 +3844,75 @@ void R_LoadEntities( lump_t *l, std::string &externalEntities ) tr.worldDeluxeMapping = glConfig2.deluxeMapping; } + bool sRGBtex = false; + bool sRGBcolor = false; + bool sRGBlight = false; + + s = strstr( value, "-sRGB" ); + + if ( s && ( s[5] == ' ' || s[5] == '\0' ) ) + { + sRGBtex = true; + sRGBcolor = true; + sRGBlight = true; + } + + s = strstr( value, "-nosRGB" ); + + if ( s && ( s[5] == ' ' || s[5] == '\0' ) ) + { + sRGBtex = false; + sRGBcolor = false; + sRGBlight = false; + } + + if ( strstr( value, "-sRGBlight" ) ) + { + sRGBlight = true; + } + + if ( strstr( value, "-nosRGBlight" ) ) + { + sRGBlight = false; + } + + if ( strstr( value, "-sRGBcolor" ) ) + { + sRGBcolor = true; + } + + if ( strstr( value, "-nosRGBcolor" ) ) + { + sRGBcolor = false; + } + + if ( strstr( value, "-sRGBtex" ) ) + { + sRGBtex = true; + } + + if ( strstr( value, "-nosRGBtex" ) ) + { + sRGBtex = false; + } + + if ( sRGBlight ) + { + Log::Debug( "Map features lights in sRGB colorspace." ); + tr.worldLinearizeLightMap = true; + } + + if ( sRGBcolor && sRGBtex ) + { + Log::Debug( "Map features lights computed with linear colors and textures." ); + tr.worldLinearizeTexture = true; + } + else if ( sRGBcolor != sRGBtex ) + { + Log::Warn( "Map features lights computed with a mix of linear and non-linear colors or textures, acting like both colors and textures were linear when lights were computed." ); + tr.worldLinearizeTexture = true; + } + continue; } @@ -3790,6 +3930,20 @@ void R_LoadEntities( lump_t *l, std::string &externalEntities ) continue; } } + + if ( tr.worldLinearizeTexture ) + { + if ( r_accurateSRGB.Get() ) + { + tr.convertFloatFromSRGB = convertFloatFromSRGB_accurate; + tr.convertColorFromSRGB = convertColorFromSRGB_accurate; + } + else + { + tr.convertFloatFromSRGB = convertFloatFromSRGB_cheap; + tr.convertColorFromSRGB = convertColorFromSRGB_cheap; + } + } } /* @@ -4524,6 +4678,8 @@ void RE_LoadWorldMap( const char *name ) 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 + tr.worldLinearizeTexture = false; + tr.worldLinearizeLightMap = false; s_worldData = {}; Q_strncpyz( s_worldData.name, name, sizeof( s_worldData.name ) ); @@ -4573,7 +4729,17 @@ 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() ); + if ( tr.worldLinearizeLightMap ) + { + /* Never shift and clamp the lightmap when stored in sRGB space, that breaks it. + Also maps using lightmaps in sRGB space are maintained maps and there is + no need to provide such clamping meant for compatibility with legacy maps. */ + tr.overbrightBits = tr.mapOverBrightBits; + } + else + { + tr.overbrightBits = std::min( tr.mapOverBrightBits, r_overbrightBits.Get() ); + } R_LoadShaders( &header->lumps[ LUMP_SHADERS ] ); diff --git a/src/engine/renderer/tr_image.cpp b/src/engine/renderer/tr_image.cpp index 71deb9571f..26d39eb62c 100644 --- a/src/engine/renderer/tr_image.cpp +++ b/src/engine/renderer/tr_image.cpp @@ -249,6 +249,8 @@ class ListImagesCmd : public Cmd::StaticCmd { wrapTypeEnum_t::WT_ALPHA_ZERO_CLAMP, "a0clmp" }, }; + const char* colorspaces[] = { "linear", "sRGB" }; + const char *yesno[] = { "no", "yes" }; const char *filter = args.Argc() > 1 ? args.Argv( 1 ).c_str() : nullptr; @@ -260,6 +262,7 @@ class ListImagesCmd : public Cmd::StaticCmd std::string mm = "mm"; std::string type = "type"; std::string format = "format"; + std::string colorspace = "space"; std::string twrap = "wrap.t"; std::string swrap = "wrap.s"; std::string name = "name"; @@ -272,6 +275,7 @@ class ListImagesCmd : public Cmd::StaticCmd size_t mmLen = 3; size_t typeLen = 4; size_t formatLen = 4; + size_t colorspaceLen = std::string("linear").length(); size_t twrapLen = 4; size_t swrapLen = 4; @@ -283,6 +287,7 @@ class ListImagesCmd : public Cmd::StaticCmd mmLen = std::max( mmLen, mm.length() ); typeLen = std::max( typeLen, type.length() ); formatLen = std::max( formatLen, format.length() ); + colorspaceLen = std::max( colorspaceLen, colorspace.length() ); twrapLen = std::max( twrapLen, twrap.length() ); swrapLen = std::max( swrapLen, swrap.length() ); @@ -318,6 +323,7 @@ class ListImagesCmd : public Cmd::StaticCmd lineStream << std::setw(mmLen) << mm << separator; lineStream << std::setw(typeLen) << type << separator; lineStream << std::setw(formatLen) << format << separator; + lineStream << std::setw(colorspaceLen) << colorspace << separator; lineStream << std::setw(twrapLen) << twrap << separator; lineStream << std::setw(swrapLen) << swrap << separator; lineStream << name; @@ -425,6 +431,8 @@ class ListImagesCmd : public Cmd::StaticCmd } } + colorspace = colorspaces[ bool( image->bits & IF_SRGB ) ]; + if ( !wrapTypeName.count( image->wrapType.t ) ) { Log::Debug( "Undocumented wrapType.t %i for image %s", @@ -464,6 +472,7 @@ class ListImagesCmd : public Cmd::StaticCmd lineStream << std::setw(mmLen) << mm << separator; lineStream << std::setw(typeLen) << type << separator; lineStream << std::setw(formatLen) << format << separator; + lineStream << std::setw(colorspaceLen) << colorspace << separator; lineStream << std::setw(twrapLen) << twrap << separator; lineStream << std::setw(swrapLen) << swrap << separator; lineStream << name; @@ -842,6 +851,7 @@ void R_UploadImage( const char *name, const byte **dataArray, int numLayers, int GLenum target; GLenum format = GL_RGBA; GLenum internalFormat = GL_RGB; + bool isSRGB = image->bits & IF_SRGB; static const vec4_t oneClampBorder = { 1, 1, 1, 1 }; static const vec4_t zeroClampBorder = { 0, 0, 0, 1 }; @@ -1068,9 +1078,7 @@ void R_UploadImage( const char *name, const byte **dataArray, int numLayers, int mipLayers = numLayers; for( i = 0; i < numMips; i++ ) { - glTexImage3D( GL_TEXTURE_3D, i, internalFormat, - scaledWidth, scaledHeight, mipLayers, - 0, format, GL_UNSIGNED_BYTE, nullptr ); + GL_TexImage3D( GL_TEXTURE_3D, i, internalFormat, scaledWidth, scaledHeight, mipLayers, 0, format, GL_UNSIGNED_BYTE, nullptr, isSRGB ); if( mipWidth > 1 ) mipWidth >>= 1; if( mipHeight > 1 ) mipHeight >>= 1; @@ -1136,18 +1144,17 @@ void R_UploadImage( const char *name, const byte **dataArray, int numLayers, int } break; case GL_TEXTURE_CUBE_MAP: - glTexImage2D( target + i, 0, internalFormat, scaledWidth, scaledHeight, 0, format, GL_UNSIGNED_BYTE, - scaledBuffer ); + GL_TexImage2D( target + i, 0, internalFormat, scaledWidth, scaledHeight, 0, format, GL_UNSIGNED_BYTE, scaledBuffer, isSRGB ); break; default: if ( image->bits & IF_PACKED_DEPTH24_STENCIL8 ) { - glTexImage2D( target, 0, internalFormat, scaledWidth, scaledHeight, 0, format, GL_UNSIGNED_INT_24_8, nullptr ); + GL_TexImage2D( target, 0, internalFormat, scaledWidth, scaledHeight, 0, format, GL_UNSIGNED_INT_24_8, nullptr, isSRGB ); } else { - glTexImage2D( target, 0, internalFormat, scaledWidth, scaledHeight, 0, format, GL_UNSIGNED_BYTE, scaledBuffer ); + GL_TexImage2D( target, 0, internalFormat, scaledWidth, scaledHeight, 0, format, GL_UNSIGNED_BYTE, scaledBuffer, isSRGB ); } break; @@ -1186,16 +1193,14 @@ void R_UploadImage( const char *name, const byte **dataArray, int numLayers, int switch ( image->type ) { case GL_TEXTURE_3D: - glCompressedTexSubImage3D( GL_TEXTURE_3D, i, 0, 0, j, - scaledWidth, scaledHeight, 1, - internalFormat, mipSize, data ); + GL_CompressedTexSubImage3D( GL_TEXTURE_3D, i, 0, 0, j, scaledWidth, scaledHeight, 1, internalFormat, mipSize, data, isSRGB ); break; case GL_TEXTURE_CUBE_MAP: - glCompressedTexImage2D( target + j, i, internalFormat, mipWidth, mipHeight, 0, mipSize, data ); + GL_CompressedTexImage2D( target + j, i, internalFormat, mipWidth, mipHeight, 0, mipSize, data, isSRGB ); break; default: - glCompressedTexImage2D( target, i, internalFormat, mipWidth, mipHeight, 0, mipSize, data ); + GL_CompressedTexImage2D( target, i, internalFormat, mipWidth, mipHeight, 0, mipSize, data, isSRGB ); break; } @@ -1445,7 +1450,10 @@ R_CreateImage */ image_t *R_CreateImage( const char *name, const byte **pic, int width, int height, int numMips, const imageParams_t &imageParams ) { - Log::Debug( "Creating image %s (%d×%d, %d mips)", name, width, height, numMips ); + const char* colorspaces[] = { "linear", "sRGB" }; + const char* colorspace = colorspaces[ bool( imageParams.bits & IF_SRGB ) ]; + + Log::Debug( "Creating image %s (%d×%d, %s, %d mips)", name, width, height, colorspace, numMips ); image_t *image; @@ -1503,7 +1511,7 @@ image_t *R_CreateGlyph( const char *name, const byte *pic, int width, int height image->uploadHeight = height; image->internalFormat = GL_RGBA; - glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pic ); + GL_TexImage2D( GL_TEXTURE_2D, 0, image->internalFormat, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pic, false ); GL_CheckErrors(); diff --git a/src/engine/renderer/tr_init.cpp b/src/engine/renderer/tr_init.cpp index 0e73973ca0..987780380d 100644 --- a/src/engine/renderer/tr_init.cpp +++ b/src/engine/renderer/tr_init.cpp @@ -170,6 +170,7 @@ Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA cvar_t *r_rimExponent; Cvar::Cvar r_highPrecisionRendering("r_highPrecisionRendering", "use high precision frame buffers for rendering and blending", Cvar::NONE, true); + Cvar::Cvar r_accurateSRGB("r_accurateSRGB", "Use accurate sRGB computation when converting images", Cvar::NONE, true); cvar_t *r_gamma; @@ -1239,6 +1240,7 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p AssertCvarRange( r_rimExponent, 0.5, 8.0, false ); Cvar::Latch( r_highPrecisionRendering ); + Cvar::Latch( r_accurateSRGB ); r_drawBuffer = Cvar_Get( "r_drawBuffer", "GL_BACK", CVAR_CHEAT ); r_lockpvs = Cvar_Get( "r_lockpvs", "0", CVAR_CHEAT ); @@ -1271,6 +1273,9 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p Cvar::Latch( r_profilerRenderSubGroups ); } + static float convertFloatFromSRGB_NOP( float f ) { return f; } + static Color::Color convertColorFromSRGB_NOP( Color::Color c ) { return c; } + /* =============== R_Init @@ -1287,6 +1292,9 @@ ScreenshotCmd screenshotPNGRegistration("screenshotPNG", ssFormat_t::SSF_PNG, "p ResetStruct( backEnd ); ResetStruct( tess ); + tr.convertFloatFromSRGB = convertFloatFromSRGB_NOP; + tr.convertColorFromSRGB = convertColorFromSRGB_NOP; + if ( ( intptr_t ) tess.verts & 15 ) { Log::Warn( "tess.verts not 16 byte aligned" ); diff --git a/src/engine/renderer/tr_local.h b/src/engine/renderer/tr_local.h index 5ffa96e443..9fffda5f71 100644 --- a/src/engine/renderer/tr_local.h +++ b/src/engine/renderer/tr_local.h @@ -478,6 +478,7 @@ enum class ssaoMode { IF_RGBE = BIT( 15 ), IF_ALPHATEST = BIT( 16 ), // FIXME: this is unused IF_ALPHA = BIT( 17 ), + IF_SRGB = BIT( 18 ), IF_BC1 = BIT( 19 ), IF_BC2 = BIT( 20 ), IF_BC3 = BIT( 21 ), @@ -858,11 +859,19 @@ enum class ssaoMode { float value; }; + +enum +{ + EXP_NONE, + EXP_SRGB = BIT( 1 ), +}; + #define MAX_EXPRESSION_OPS 32 struct expression_t { expOperation_t ops[ MAX_EXPRESSION_OPS ]; size_t numOps; + int bits; bool operator==( const expression_t& other ) { if ( numOps != other.numOps ) { @@ -2483,6 +2492,9 @@ enum class ssaoMode { int h; }; + using floatProcessor_t = float(*)(float); + using colorProcessor_t = Color::Color(*)(Color::Color); + /* ** trGlobals_t ** @@ -2513,6 +2525,11 @@ enum class ssaoMode { bool worldLightMapping; bool worldDeluxeMapping; bool worldHDR_RGBE; + bool worldLinearizeTexture; + bool worldLinearizeLightMap; + + floatProcessor_t convertFloatFromSRGB; + colorProcessor_t convertColorFromSRGB; lightMode_t lightMode; lightMode_t worldLight; @@ -2798,6 +2815,7 @@ enum class ssaoMode { extern cvar_t *r_rimExponent; extern Cvar::Cvar r_highPrecisionRendering; + extern Cvar::Cvar r_accurateSRGB; extern Cvar::Range> r_shadows; @@ -3015,6 +3033,10 @@ inline bool checkGLErrors() void GL_VertexAttribsState( uint32_t stateBits ); void GL_VertexAttribPointers( uint32_t attribBits ); void GL_Cull( cullType_t cullType ); +void GL_TexImage2D( GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void *data, bool isSRGB ); +void GL_TexImage3D( GLenum target, GLint level, GLint internalFormat, GLsizei width, GLsizei height, GLsizei depth, GLint border, GLenum format, GLenum type, const void *data, bool isSRGB ); +void GL_CompressedTexImage2D( GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLsizei imageSize, const void *data, bool isSRGB ); +void GL_CompressedTexSubImage3D( GLenum target, GLint level, GLint xOffset, GLint yOffset, GLint zOffset, GLsizei width, GLsizei height, GLsizei depth, GLenum internalFormat, GLsizei size, const void *data, bool isSRGB ); void R_ShutdownBackend(); /* diff --git a/src/engine/renderer/tr_scene.cpp b/src/engine/renderer/tr_scene.cpp index 388d8a16ab..931c7d8e48 100644 --- a/src/engine/renderer/tr_scene.cpp +++ b/src/engine/renderer/tr_scene.cpp @@ -324,6 +324,12 @@ void RE_AddDynamicLightToSceneET( const vec3_t org, float radius, float intensit light->color[ 1 ] = g; light->color[ 2 ] = b; + // Linearize dynamic lights. + if ( tr.worldLinearizeTexture ) + { + convertFromSRGB( light->color ); + } + light->scale = intensity; } diff --git a/src/engine/renderer/tr_shade.cpp b/src/engine/renderer/tr_shade.cpp index b2b73ec3de..24577218eb 100644 --- a/src/engine/renderer/tr_shade.cpp +++ b/src/engine/renderer/tr_shade.cpp @@ -1674,6 +1674,9 @@ Tess_ComputeColor */ void Tess_ComputeColor( shaderStage_t *pStage ) { + /* Expression-based color computations are doing colorspace conversions + in RB_EvalExpression() directly. */ + float rgb; float red; float green; @@ -1709,6 +1712,8 @@ void Tess_ComputeColor( shaderStage_t *pStage ) case colorGen_t::CGEN_CONST: { tess.svars.color = pStage->constantColor; + tess.svars.color.Clamp(); + tess.svars.color = tr.convertColorFromSRGB( tess.svars.color ); break; } @@ -1718,6 +1723,7 @@ void Tess_ComputeColor( shaderStage_t *pStage ) { tess.svars.color = backEnd.currentEntity->e.shaderRGBA; tess.svars.color.Clamp(); + tess.svars.color = tr.convertColorFromSRGB( tess.svars.color ); } else { @@ -1733,6 +1739,7 @@ void Tess_ComputeColor( shaderStage_t *pStage ) { tess.svars.color = backEnd.currentEntity->e.shaderRGBA; tess.svars.color.Clamp(); + tess.svars.color = tr.convertColorFromSRGB( tess.svars.color ); } else { @@ -1763,9 +1770,9 @@ void Tess_ComputeColor( shaderStage_t *pStage ) glow = RB_EvalWaveForm( wf ); } - glow = Math::Clamp( glow, 0.0f, 1.0f ); - tess.svars.color = Color::White * glow; + tess.svars.color.Clamp(); + tess.svars.color = tr.convertColorFromSRGB( tess.svars.color ); break; } diff --git a/src/engine/renderer/tr_shade_calc.cpp b/src/engine/renderer/tr_shade_calc.cpp index 6329c71f0c..e9b8a1cc0c 100644 --- a/src/engine/renderer/tr_shade_calc.cpp +++ b/src/engine/renderer/tr_shade_calc.cpp @@ -202,7 +202,7 @@ static float GetOpValue( const expOperation_t *op ) const char* GetOpName(opcode_t type); -float RB_EvalExpression( const expression_t *exp, float defaultValue ) +static float EvalExpression( const expression_t *exp, float defaultValue ) { ASSERT( exp ); @@ -435,6 +435,21 @@ float RB_EvalExpression( const expression_t *exp, float defaultValue ) return GetOpValue( &ops[ 0 ] ); } +float RB_EvalExpression( const expression_t *exp, float defaultValue ) +{ + ASSERT( exp ); + + float value = EvalExpression( exp, defaultValue ); + + if ( exp->bits & EXP_SRGB ) + { + value = Math::Clamp( value, 0.0f, 1.0f ); + value = tr.convertFloatFromSRGB( value ); + } + + return value; +} + /* ==================================================================== diff --git a/src/engine/renderer/tr_shader.cpp b/src/engine/renderer/tr_shader.cpp index ebe23c3ca2..f2acbb333d 100644 --- a/src/engine/renderer/tr_shader.cpp +++ b/src/engine/renderer/tr_shader.cpp @@ -693,8 +693,10 @@ static char *ParseExpressionElement( const char **data_p ) ParseExpression =============== */ -static void ParseExpression( const char **text, expression_t *exp ) +static void ParseExpression( const char **text, expression_t *exp, int bits = 0 ) { + exp->bits = bits; + expOperation_t op, op2; expOperation_t inFixOps[ MAX_EXPRESSION_OPS ]; @@ -1479,6 +1481,28 @@ static bool LoadMap( shaderStage_t *stage, const char *buffer, stageType_t type, imageParams.minDimension = shader.imageMinDimension; imageParams.maxDimension = shader.imageMaxDimension; + if ( tr.worldLinearizeTexture ) + { + /* Some stage types may be meaningless in 2D but we can't prevent people + to use any shader in UI, so we better take care of more than ST_COLORMAP. */ + if ( ! ( shader.registerFlags & RSF_2D ) ) + { + switch ( type ) + { + case stageType_t::ST_COLORMAP: + case stageType_t::ST_DIFFUSEMAP: + case stageType_t::ST_GLOWMAP: + case stageType_t::ST_REFLECTIONMAP: + case stageType_t::ST_SKYBOXMAP: + case stageType_t::ST_SPECULARMAP: + imageParams.bits |= IF_SRGB; + break; + default: + break; + } + } + } + // determine image options if ( stage->overrideNoPicMip || shader.noPicMip || stage->highQuality || stage->forceHighQuality ) { @@ -2232,6 +2256,11 @@ static bool ParseStage( shaderStage_t *stage, const char **text ) imageParams.minDimension = shader.imageMinDimension; imageParams.maxDimension = shader.imageMaxDimension; + if ( tr.worldLinearizeTexture ) + { + imageParams.bits |= IF_SRGB; + } + stage->bundle[ 0 ].image[ num ] = R_FindImageFile( token, imageParams ); if ( !stage->bundle[ 0 ].image[ num ] ) @@ -2710,25 +2739,25 @@ static bool ParseStage( shaderStage_t *stage, const char **text ) else if ( !Q_stricmp( token, "rgb" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGB; - ParseExpression( text, &stage->rgbExp ); + ParseExpression( text, &stage->rgbExp, EXP_SRGB ); } // red else if ( !Q_stricmp( token, "red" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; - ParseExpression( text, &stage->redExp ); + ParseExpression( text, &stage->redExp, EXP_SRGB ); } // green else if ( !Q_stricmp( token, "green" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; - ParseExpression( text, &stage->greenExp ); + ParseExpression( text, &stage->greenExp, EXP_SRGB ); } // blue else if ( !Q_stricmp( token, "blue" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; - ParseExpression( text, &stage->blueExp ); + ParseExpression( text, &stage->blueExp, EXP_SRGB ); } // colored else if ( !Q_stricmp( token, "colored" ) ) @@ -2837,9 +2866,9 @@ static bool ParseStage( shaderStage_t *stage, const char **text ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; stage->alphaGen = alphaGen_t::AGEN_CUSTOM; - ParseExpression( text, &stage->redExp ); - ParseExpression( text, &stage->greenExp ); - ParseExpression( text, &stage->blueExp ); + ParseExpression( text, &stage->redExp, EXP_SRGB ); + ParseExpression( text, &stage->greenExp, EXP_SRGB ); + ParseExpression( text, &stage->blueExp, EXP_SRGB ); ParseExpression( text, &stage->alphaExp ); } // tcGen @@ -3492,6 +3521,11 @@ static void ParseSkyParms( const char **text ) imageParams.minDimension = shader.imageMinDimension; imageParams.maxDimension = shader.imageMaxDimension; + if ( tr.worldLinearizeTexture ) + { + imageParams.bits |= IF_SRGB; + } + shader.sky.outerbox = R_FindCubeImage( prefix, imageParams ); if ( !shader.sky.outerbox ) @@ -6168,6 +6202,11 @@ shader_t *R_FindShader( const char *name, int flags ) imageParams.filterType = filterType_t::FT_DEFAULT; imageParams.wrapType = wrapTypeEnum_t::WT_REPEAT; + if ( tr.worldLinearizeTexture ) + { + imageParams.bits |= IF_SRGB; + } + image = R_FindImageFile( fileName, imageParams ); } diff --git a/src/engine/renderer/tr_surface.cpp b/src/engine/renderer/tr_surface.cpp index e8271dca02..593e1495b8 100644 --- a/src/engine/renderer/tr_surface.cpp +++ b/src/engine/renderer/tr_surface.cpp @@ -630,8 +630,9 @@ static void Tess_SurfaceSprite() if ( backEnd.viewParms.isMirror ) VectorSubtract( vec3_origin, left, left ); - Tess_AddQuadStamp( backEnd.currentEntity->e.origin, left, up, - backEnd.currentEntity->e.shaderRGBA ); + Color::Color32Bit color = backEnd.currentEntity->e.shaderRGBA; + color = tr.convertColorFromSRGB( color ); + Tess_AddQuadStamp( backEnd.currentEntity->e.origin, left, up, color ); } /* @@ -660,7 +661,10 @@ static void Tess_SurfacePolychain( srfPoly_t *p ) { VectorCopy(p->verts[i].xyz, tess.verts[tess.numVertexes + i].xyz); - tess.verts[tess.numVertexes + i].color = Color::Adapt(p->verts[i].modulate); + Color::Color32Bit color = Color::Adapt( p->verts[ i ].modulate ); + color = tr.convertColorFromSRGB( color ); + tess.verts[tess.numVertexes + i].color = color; + tess.verts[tess.numVertexes + i].texCoords[0] = p->verts[i].st[0]; tess.verts[tess.numVertexes + i].texCoords[1] = p->verts[i].st[1]; } @@ -728,7 +732,11 @@ static void Tess_SurfacePolychain( srfPoly_t *p ) normals[i], qtangents); VectorCopy(p->verts[i].xyz, tess.verts[tess.numVertexes + i].xyz); - tess.verts[tess.numVertexes + i].color = Color::Adapt(p->verts[i].modulate); + + Color::Color32Bit color = Color::Adapt( p->verts[ i ].modulate ); + color = tr.convertColorFromSRGB( color ); + tess.verts[tess.numVertexes + i].color = color; + Vector4Copy(qtangents, tess.verts[tess.numVertexes + i].qtangents); tess.verts[tess.numVertexes + i].texCoords[0] = p->verts[i].st[0]; tess.verts[tess.numVertexes + i].texCoords[1] = p->verts[i].st[1]; From 925128be5025529c750c45f9ca048a592d5e4ed5 Mon Sep 17 00:00:00 2001 From: Thomas Debesse Date: Sat, 12 Jul 2025 06:26:14 +0200 Subject: [PATCH 3/3] renderer: add the EXP_CLAMP bit to normalize the expression value --- src/engine/renderer/tr_local.h | 1 + src/engine/renderer/tr_shade.cpp | 19 ++++++++----------- src/engine/renderer/tr_shade_calc.cpp | 6 +++++- src/engine/renderer/tr_shader.cpp | 18 +++++++++--------- 4 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/engine/renderer/tr_local.h b/src/engine/renderer/tr_local.h index 9fffda5f71..4b649e13de 100644 --- a/src/engine/renderer/tr_local.h +++ b/src/engine/renderer/tr_local.h @@ -863,6 +863,7 @@ enum class ssaoMode { enum { EXP_NONE, + EXP_CLAMP = BIT( 0 ), EXP_SRGB = BIT( 1 ), }; diff --git a/src/engine/renderer/tr_shade.cpp b/src/engine/renderer/tr_shade.cpp index 24577218eb..14c46c5580 100644 --- a/src/engine/renderer/tr_shade.cpp +++ b/src/engine/renderer/tr_shade.cpp @@ -1778,7 +1778,7 @@ void Tess_ComputeColor( shaderStage_t *pStage ) case colorGen_t::CGEN_CUSTOM_RGB: { - rgb = Math::Clamp( RB_EvalExpression( &pStage->rgbExp, 1.0 ), 0.0f, 1.0f ); + rgb = RB_EvalExpression( &pStage->rgbExp, 1.0 ); tess.svars.color = Color::White * rgb; break; @@ -1788,18 +1788,15 @@ void Tess_ComputeColor( shaderStage_t *pStage ) { if ( backEnd.currentEntity ) { - red = - Math::Clamp( RB_EvalExpression( &pStage->redExp, backEnd.currentEntity->e.shaderRGBA.Red() * ( 1.0 / 255.0 ) ), 0.0f, 1.0f ); - green = - Math::Clamp( RB_EvalExpression( &pStage->greenExp, backEnd.currentEntity->e.shaderRGBA.Green() * ( 1.0 / 255.0 ) ), 0.0f, 1.0f ); - blue = - Math::Clamp( RB_EvalExpression( &pStage->blueExp, backEnd.currentEntity->e.shaderRGBA.Blue() * ( 1.0 / 255.0 ) ), 0.0f, 1.0f ); + red = RB_EvalExpression( &pStage->redExp, backEnd.currentEntity->e.shaderRGBA.Red() * ( 1.0 / 255.0 ) ); + green = RB_EvalExpression( &pStage->greenExp, backEnd.currentEntity->e.shaderRGBA.Green() * ( 1.0 / 255.0 ) ); + blue = RB_EvalExpression( &pStage->blueExp, backEnd.currentEntity->e.shaderRGBA.Blue() * ( 1.0 / 255.0 ) ); } else { - red = Math::Clamp( RB_EvalExpression( &pStage->redExp, 1.0 ), 0.0f, 1.0f ); - green = Math::Clamp( RB_EvalExpression( &pStage->greenExp, 1.0 ), 0.0f, 1.0f ); - blue = Math::Clamp( RB_EvalExpression( &pStage->blueExp, 1.0 ), 0.0f, 1.0f ); + red = RB_EvalExpression( &pStage->redExp, 1.0 ); + green = RB_EvalExpression( &pStage->greenExp, 1.0 ); + blue = RB_EvalExpression( &pStage->blueExp, 1.0 ); } tess.svars.color.SetRed( red ); @@ -1878,7 +1875,7 @@ void Tess_ComputeColor( shaderStage_t *pStage ) case alphaGen_t::AGEN_CUSTOM: { - alpha = Math::Clamp( RB_EvalExpression( &pStage->alphaExp, 1.0 ), 0.0f, 1.0f ); + alpha = RB_EvalExpression( &pStage->alphaExp, 1.0 ); tess.svars.color.SetAlpha( alpha ); break; diff --git a/src/engine/renderer/tr_shade_calc.cpp b/src/engine/renderer/tr_shade_calc.cpp index e9b8a1cc0c..48a4cf0dc2 100644 --- a/src/engine/renderer/tr_shade_calc.cpp +++ b/src/engine/renderer/tr_shade_calc.cpp @@ -441,9 +441,13 @@ float RB_EvalExpression( const expression_t *exp, float defaultValue ) float value = EvalExpression( exp, defaultValue ); - if ( exp->bits & EXP_SRGB ) + if ( exp->bits & EXP_CLAMP ) { value = Math::Clamp( value, 0.0f, 1.0f ); + } + + if ( exp->bits & EXP_SRGB ) + { value = tr.convertFloatFromSRGB( value ); } diff --git a/src/engine/renderer/tr_shader.cpp b/src/engine/renderer/tr_shader.cpp index f2acbb333d..c59bb64b89 100644 --- a/src/engine/renderer/tr_shader.cpp +++ b/src/engine/renderer/tr_shader.cpp @@ -2739,25 +2739,25 @@ static bool ParseStage( shaderStage_t *stage, const char **text ) else if ( !Q_stricmp( token, "rgb" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGB; - ParseExpression( text, &stage->rgbExp, EXP_SRGB ); + ParseExpression( text, &stage->rgbExp, EXP_CLAMP | EXP_SRGB ); } // red else if ( !Q_stricmp( token, "red" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; - ParseExpression( text, &stage->redExp, EXP_SRGB ); + ParseExpression( text, &stage->redExp, EXP_CLAMP | EXP_SRGB ); } // green else if ( !Q_stricmp( token, "green" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; - ParseExpression( text, &stage->greenExp, EXP_SRGB ); + ParseExpression( text, &stage->greenExp, EXP_CLAMP | EXP_SRGB ); } // blue else if ( !Q_stricmp( token, "blue" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; - ParseExpression( text, &stage->blueExp, EXP_SRGB ); + ParseExpression( text, &stage->blueExp, EXP_CLAMP | EXP_SRGB ); } // colored else if ( !Q_stricmp( token, "colored" ) ) @@ -2859,17 +2859,17 @@ static bool ParseStage( shaderStage_t *stage, const char **text ) else if ( !Q_stricmp( token, "alpha" ) ) { stage->alphaGen = alphaGen_t::AGEN_CUSTOM; - ParseExpression( text, &stage->alphaExp ); + ParseExpression( text, &stage->alphaExp, EXP_CLAMP ); } // color , , , else if ( !Q_stricmp( token, "color" ) ) { stage->rgbGen = colorGen_t::CGEN_CUSTOM_RGBs; stage->alphaGen = alphaGen_t::AGEN_CUSTOM; - ParseExpression( text, &stage->redExp, EXP_SRGB ); - ParseExpression( text, &stage->greenExp, EXP_SRGB ); - ParseExpression( text, &stage->blueExp, EXP_SRGB ); - ParseExpression( text, &stage->alphaExp ); + ParseExpression( text, &stage->redExp, EXP_CLAMP | EXP_SRGB ); + ParseExpression( text, &stage->greenExp, EXP_CLAMP | EXP_SRGB ); + ParseExpression( text, &stage->blueExp, EXP_CLAMP | EXP_SRGB ); + ParseExpression( text, &stage->alphaExp, EXP_CLAMP ); } // tcGen else if ( !Q_stricmp( token, "texGen" ) || !Q_stricmp( token, "tcGen" ) )