diff --git a/Core/GameEngine/CMakeLists.txt b/Core/GameEngine/CMakeLists.txt
index 4de35b21a25..cc84e9a899b 100644
--- a/Core/GameEngine/CMakeLists.txt
+++ b/Core/GameEngine/CMakeLists.txt
@@ -148,6 +148,7 @@ set(GAMEENGINE_SRC
Include/GameClient/ClientInstance.h
Include/GameClient/ClientRandomValue.h
Include/GameClient/Color.h
+ Include/GameClient/GlobalLightingModifier.h
# Include/GameClient/CommandXlat.h
# Include/GameClient/ControlBar.h
# Include/GameClient/ControlBarResizer.h
@@ -700,6 +701,7 @@ set(GAMEENGINE_SRC
Source/Common/WorkerProcess.cpp
Source/GameClient/ClientInstance.cpp
Source/GameClient/Color.cpp
+ Source/GameClient/GlobalLightingModifier.cpp
Source/GameClient/Credits.cpp
# Source/GameClient/Display.cpp
Source/GameClient/DisplayString.cpp
diff --git a/Core/GameEngine/Include/GameClient/GlobalLightingModifier.h b/Core/GameEngine/Include/GameClient/GlobalLightingModifier.h
new file mode 100644
index 00000000000..bf541d41fee
--- /dev/null
+++ b/Core/GameEngine/Include/GameClient/GlobalLightingModifier.h
@@ -0,0 +1,63 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program 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 3 of the License, or
+** (at your option) any later version.
+**
+** This program 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 this program. If not, see .
+*/
+
+// FILE: GlobalLightingModifier.h ////////////////////////////////////////////////////////////////
+// Client-side aggregator for "global lighting modifier" contributors (e.g. objects that darken /
+// colorize / brighten the whole map's lighting while active). Contributors register themselves;
+// the renderer asks for the combined multiply+add each frame and applies it to the scene lighting.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "Lib/BaseType.h"
+#include "Common/STLTypedefs.h"
+
+//-------------------------------------------------------------------------------------------------
+/** Anything that contributes to the global lighting modifier implements this. The contribution is
+ returned pre-scaled by the contributor's own fade weight / intensity: a multiply color (default
+ white = no change) and an additive color (default black = no change). */
+//-------------------------------------------------------------------------------------------------
+class LightingModifierContributor
+{
+public:
+ virtual ~LightingModifierContributor() {}
+ virtual void getLightingContribution( RGBColor& outMul, RGBColor& outAdd ) const = 0;
+};
+
+//-------------------------------------------------------------------------------------------------
+/** Global registry + combiner. Presentation-only; not part of the deterministic sim. */
+//-------------------------------------------------------------------------------------------------
+class GlobalLightingModifierManager
+{
+public:
+ static GlobalLightingModifierManager& get( void );
+
+ void registerContributor( const LightingModifierContributor* c );
+ void unregisterContributor( const LightingModifierContributor* c );
+
+ /** Combined multiply (component-wise product) and additive (sum) across all contributors.
+ Returns identity (mul = 1,1,1 add = 0,0,0) when there are none. */
+ void computeCombined( RGBColor& outMul, RGBColor& outAdd ) const;
+
+ Bool hasActiveContributors( void ) const { return !m_contributors.empty(); }
+
+private:
+ GlobalLightingModifierManager() {}
+
+ std::vector m_contributors;
+};
diff --git a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
index dda88f88f06..2bd5458d31f 100644
--- a/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
+++ b/Core/GameEngine/Source/Common/System/GameMemoryInitPools_GeneralsMD.inl
@@ -151,6 +151,7 @@ static PoolSizeRec PoolSizes[] =
{ "FireWeaponAdvancedUpdate", 32, 32 },
{ "FlammableUpdate", 512, 256 },
{ "FloatUpdate", 512, 128 },
+ { "GlobalLightingModifierUpdate", 16, 16 },
{ "TensileFormationUpdate", 256, 32 },
{ "GarrisonContain", 256, 32 },
{ "HealCrateCollide", 32, 32 },
diff --git a/Core/GameEngine/Source/GameClient/GlobalLightingModifier.cpp b/Core/GameEngine/Source/GameClient/GlobalLightingModifier.cpp
new file mode 100644
index 00000000000..396f4e2449e
--- /dev/null
+++ b/Core/GameEngine/Source/GameClient/GlobalLightingModifier.cpp
@@ -0,0 +1,83 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program 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 3 of the License, or
+** (at your option) any later version.
+**
+** This program 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 this program. If not, see .
+*/
+
+// FILE: GlobalLightingModifier.cpp //////////////////////////////////////////////////////////////
+
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+
+#include "GameClient/GlobalLightingModifier.h"
+
+//-------------------------------------------------------------------------------------------------
+/*static*/ GlobalLightingModifierManager& GlobalLightingModifierManager::get( void )
+{
+ static GlobalLightingModifierManager theInstance;
+ return theInstance;
+}
+
+//-------------------------------------------------------------------------------------------------
+void GlobalLightingModifierManager::registerContributor( const LightingModifierContributor* c )
+{
+ if( c == nullptr )
+ return;
+ // avoid duplicate registration
+ for( std::vector::const_iterator it = m_contributors.begin();
+ it != m_contributors.end(); ++it )
+ {
+ if( *it == c )
+ return;
+ }
+ m_contributors.push_back( c );
+}
+
+//-------------------------------------------------------------------------------------------------
+void GlobalLightingModifierManager::unregisterContributor( const LightingModifierContributor* c )
+{
+ for( std::vector::iterator it = m_contributors.begin();
+ it != m_contributors.end(); ++it )
+ {
+ if( *it == c )
+ {
+ m_contributors.erase( it );
+ return;
+ }
+ }
+}
+
+//-------------------------------------------------------------------------------------------------
+void GlobalLightingModifierManager::computeCombined( RGBColor& outMul, RGBColor& outAdd ) const
+{
+ outMul.red = outMul.green = outMul.blue = 1.0f; // identity multiply
+ outAdd.red = outAdd.green = outAdd.blue = 0.0f; // identity add
+
+ for( std::vector::const_iterator it = m_contributors.begin();
+ it != m_contributors.end(); ++it )
+ {
+ RGBColor mul, add;
+ mul.red = mul.green = mul.blue = 1.0f;
+ add.red = add.green = add.blue = 0.0f;
+ (*it)->getLightingContribution( mul, add );
+
+ // multiplies compound (darker stacks darker), additives sum (brighten/colorize stack)
+ outMul.red *= mul.red;
+ outMul.green *= mul.green;
+ outMul.blue *= mul.blue;
+ outAdd.red += add.red;
+ outAdd.green += add.green;
+ outAdd.blue += add.blue;
+ }
+}
diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h
index 6352058dcf1..a7758d19615 100644
--- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h
+++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/HeightMap.h
@@ -113,6 +113,7 @@ class HeightMapRenderObjClass : public BaseHeightMapRenderObjClass
///update vertex buffers associated with the given rectangle
void initDestAlphaLUT(void); ///flush();
+ // Phase 2: draw-time global lighting tint over the terrain footprint (terrain + roads + scorches +
+ // bridges). Skipped on the reflection pass. Drawn before the shroud so fog-of-war stays black.
+ if (!ShaderClass::Is_Backface_Culling_Inverted() && GlobalLightingModifierManager::get().hasActiveContributors())
+ renderLightingModifierOverlay();
+
if (m_shroud && rinfo.Additional_Pass_Count())
{
rinfo.Peek_Additional_Pass(0)->Install_Materials();
@@ -2131,6 +2137,96 @@ void HeightMapRenderObjClass::Render(RenderInfoClass & rinfo)
+//-------------------------------------------------------------------------------------------------
+static UnsignedInt packLightingColor01( Real r, Real g, Real b )
+{
+ Int ir = REAL_TO_INT( r * 255.0f + 0.5f ); if (ir < 0) ir = 0; if (ir > 255) ir = 255;
+ Int ig = REAL_TO_INT( g * 255.0f + 0.5f ); if (ig < 0) ig = 0; if (ig > 255) ig = 255;
+ Int ib = REAL_TO_INT( b * 255.0f + 0.5f ); if (ib < 0) ib = 0; if (ib > 255) ib = 255;
+ return 0xff000000 | (((UnsignedInt)ir) << 16) | (((UnsignedInt)ig) << 8) | ((UnsignedInt)ib);
+}
+
+//-------------------------------------------------------------------------------------------------
+/** Draw-time global lighting tint: re-draws the terrain tiles as flat-color overlay passes that
+ multiply and/or add the combined lighting-modifier color over whatever is already in the frame
+ buffer within the terrain footprint (terrain + roads + scorches + bridges). Cheap (no vertex
+ re-bake); only runs when a modifier is active. Depth test is ALWAYS so on-terrain features at a
+ slight z-offset are tinted too; z-write is off and objects render afterwards, so objects are
+ unaffected (they get their own tint from the object light-environment path). */
+//-------------------------------------------------------------------------------------------------
+void HeightMapRenderObjClass::renderLightingModifierOverlay(void)
+{
+ RGBColor mul, add;
+ GlobalLightingModifierManager::get().computeCombined( mul, add );
+
+ const Bool doMul = (mul.red < 0.999f || mul.green < 0.999f || mul.blue < 0.999f);
+ const Bool doAdd = (add.red > 0.001f || add.green > 0.001f || add.blue > 0.001f);
+ if (!doMul && !doAdd)
+ return;
+
+ // build the multiply / additive overlay shaders once (untextured solid, depth-test always, no z-write)
+ static Bool s_init = false;
+ static ShaderClass s_mulShader;
+ static ShaderClass s_addShader;
+ if (!s_init)
+ {
+ s_mulShader = ShaderClass::_PresetOpaqueSolidShader;
+ s_mulShader.Set_Depth_Mask( ShaderClass::DEPTH_WRITE_DISABLE );
+ s_mulShader.Set_Depth_Compare( ShaderClass::PASS_ALWAYS );
+ s_mulShader.Set_Src_Blend_Func( ShaderClass::SRCBLEND_ZERO );
+ s_mulShader.Set_Dst_Blend_Func( ShaderClass::DSTBLEND_SRC_COLOR );
+
+ s_addShader = s_mulShader;
+ s_addShader.Set_Src_Blend_Func( ShaderClass::SRCBLEND_ONE );
+ s_addShader.Set_Dst_Blend_Func( ShaderClass::DSTBLEND_ONE );
+
+ s_init = true;
+ }
+
+ Matrix3D tm(Transform);
+ DX8Wrapper::Set_Texture(0, nullptr);
+ DX8Wrapper::Set_Texture(1, nullptr);
+ m_stageTwoTexture->restore();
+ ShaderClass::Invalidate();
+ DX8Wrapper::Set_Material(m_vertexMaterialClass);
+ DX8Wrapper::Set_Transform(D3DTS_WORLD, tm);
+ DX8Wrapper::Set_Index_Buffer(m_indexBuffer, 0);
+
+ for (Int passIndex = 0; passIndex < 2; passIndex++)
+ {
+ if (passIndex == 0 && !doMul) continue;
+ if (passIndex == 1 && !doAdd) continue;
+
+ DX8Wrapper::Set_Shader( passIndex == 0 ? s_mulShader : s_addShader );
+ DX8Wrapper::Set_Texture(0, nullptr);
+ DX8Wrapper::Apply_Render_State_Changes();
+
+ // force stage 0 to emit a flat constant color from TFACTOR (ignore vertex diffuse / textures)
+ DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_COLOROP, D3DTOP_SELECTARG1);
+ DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_COLORARG1, D3DTA_TFACTOR);
+ DX8Wrapper::Set_DX8_Texture_Stage_State(0, D3DTSS_ALPHAOP, D3DTOP_DISABLE);
+ DX8Wrapper::Set_DX8_Texture_Stage_State(1, D3DTSS_COLOROP, D3DTOP_DISABLE);
+
+ UnsignedInt col = (passIndex == 0) ? packLightingColor01(mul.red, mul.green, mul.blue)
+ : packLightingColor01(add.red, add.green, add.blue);
+ DX8Wrapper::Set_DX8_Render_State(D3DRS_TEXTUREFACTOR, col);
+
+ for (Int j = 0; j < m_numVBTilesY; j++)
+ for (Int i = 0; i < m_numVBTilesX; i++)
+ {
+ DX8Wrapper::Set_Vertex_Buffer(getVertexBufferTile(i, j));
+ if (Is_Hidden() == 0)
+ DX8Wrapper::Draw_Triangles(0, HEIGHTMAP_POLYGON_NUM, 0, HEIGHTMAP_VERTEX_NUM);
+ }
+ }
+
+ // leave the pipeline in a sane state for subsequent draws
+ DX8Wrapper::Set_DX8_Render_State(D3DRS_TEXTUREFACTOR, 0xffffffff);
+ DX8Wrapper::Set_Texture(0, nullptr);
+ DX8Wrapper::Set_Texture(1, nullptr);
+ ShaderClass::Invalidate();
+}
+
///Performs additional terrain rendering pass, blending in the black shroud texture.
void HeightMapRenderObjClass::renderTerrainPass(CameraClass *pCamera)
{
diff --git a/GeneralsMD/Code/GameEngine/CMakeLists.txt b/GeneralsMD/Code/GameEngine/CMakeLists.txt
index a4693497647..c847f8c8ea1 100644
--- a/GeneralsMD/Code/GameEngine/CMakeLists.txt
+++ b/GeneralsMD/Code/GameEngine/CMakeLists.txt
@@ -343,6 +343,7 @@ set(GAMEENGINE_SRC
Include/GameLogic/Module/FlightDeckBehavior.h
Include/GameLogic/Module/DroneCarrierAIUpdate.h
Include/GameLogic/Module/FloatUpdate.h
+ Include/GameLogic/Module/GlobalLightingModifierUpdate.h
Include/GameLogic/Module/FXListDie.h
Include/GameLogic/Module/GarrisonContain.h
Include/GameLogic/Module/GenerateMinefieldBehavior.h
@@ -1085,6 +1086,7 @@ set(GAMEENGINE_SRC
Source/GameLogic/Object/Update/FireWeaponAdvancedUpdate.cpp
Source/GameLogic/Object/Update/FlammableUpdate.cpp
Source/GameLogic/Object/Update/FloatUpdate.cpp
+ Source/GameLogic/Object/Update/GlobalLightingModifierUpdate.cpp
Source/GameLogic/Object/Update/HeightDieUpdate.cpp
Source/GameLogic/Object/Update/HelicopterSlowDeathUpdate.cpp
Source/GameLogic/Object/Update/ShipSlowDeathBehavior.cpp
diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/GlobalLightingModifierUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/GlobalLightingModifierUpdate.h
new file mode 100644
index 00000000000..499a8b632f4
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/GlobalLightingModifierUpdate.h
@@ -0,0 +1,77 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program 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 3 of the License, or
+** (at your option) any later version.
+**
+** This program 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 this program. If not, see .
+*/
+
+// FILE: GlobalLightingModifierUpdate.h //////////////////////////////////////////////////////////
+// Desc: While active, this module contributes a darken/colorize/brighten shift to the GLOBAL map
+// lighting (Phase 1: object lighting). Multiple instances aggregate. Fades in/out on
+// activation. Presentation-only: it registers with the client-side lighting manager and
+// never feeds back into the deterministic sim.
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+#pragma once
+
+#include "GameLogic/Module/UpdateModule.h"
+#include "GameClient/GlobalLightingModifier.h"
+
+//-------------------------------------------------------------------------------------------------
+class GlobalLightingModifierUpdateModuleData : public UpdateModuleData
+{
+public:
+ enum LightingBlendMode CPP_11(: Int)
+ {
+ LIGHTINGMOD_MULTIPLY = 0, ///< multiply lighting toward TargetColor (darken / tint)
+ LIGHTINGMOD_ADDITIVE ///< add TargetColor to lighting (brighten / additive tint)
+ };
+
+ RGBColor m_targetColor; ///< the color to multiply/add toward
+ Int m_blendMode; ///< LightingBlendMode
+ Real m_intensity; ///< 0..1 (or higher for ADDITIVE) strength at full fade
+ UnsignedInt m_initialDelayFrames; ///< after the gate is met, wait this long before activating (0 = none)
+ UnsignedInt m_durationFrames; ///< once activated, stay active this long then fade out (0 = infinite)
+ UnsignedInt m_fadeInFrames; ///< frames to ramp in when activated
+ UnsignedInt m_fadeOutFrames; ///< frames to ramp out when deactivated
+ AsciiString m_requiredUpgrade; ///< if set, only active while this upgrade is owned (else always active while alive)
+
+ GlobalLightingModifierUpdateModuleData();
+ static void buildFieldParse(MultiIniFieldParse& p);
+};
+
+//-------------------------------------------------------------------------------------------------
+class GlobalLightingModifierUpdate : public UpdateModule, public LightingModifierContributor
+{
+ MEMORY_POOL_GLUE_WITH_USERLOOKUP_CREATE( GlobalLightingModifierUpdate, "GlobalLightingModifierUpdate" )
+ MAKE_STANDARD_MODULE_MACRO_WITH_MODULE_DATA( GlobalLightingModifierUpdate, GlobalLightingModifierUpdateModuleData )
+
+public:
+
+ GlobalLightingModifierUpdate( Thing *thing, const ModuleData* moduleData );
+ // virtual destructor prototype provided by memory pool declaration
+
+ virtual UpdateSleepTime update();
+
+ // LightingModifierContributor
+ virtual void getLightingContribution( RGBColor& outMul, RGBColor& outAdd ) const;
+
+protected:
+
+ Bool computeConditionMet( void ) const; ///< is the activation gate satisfied right now (alive + upgrade)?
+
+ Real m_weight; ///< current fade weight 0..1
+ Bool m_triggered; ///< has the gate been met and the timed cycle started?
+ UnsignedInt m_triggerFrame; ///< frame the gate was first met (start of InitialDelay)
+};
diff --git a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
index 5942b811a89..d520be35b95 100644
--- a/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
+++ b/GeneralsMD/Code/GameEngine/Source/Common/Thing/ModuleFactory.cpp
@@ -134,6 +134,7 @@
#include "GameLogic/Module/FireWeaponAdvancedUpdate.h"
#include "GameLogic/Module/FlammableUpdate.h"
#include "GameLogic/Module/FloatUpdate.h"
+#include "GameLogic/Module/GlobalLightingModifierUpdate.h"
#include "GameLogic/Module/TensileFormationUpdate.h"
#include "GameLogic/Module/HackInternetAIUpdate.h"
#include "GameLogic/Module/DeployStyleAIUpdate.h"
@@ -466,6 +467,7 @@ void ModuleFactory::init( void )
addModule( FireWeaponAdvancedUpdate );
addModule( FlammableUpdate );
addModule( FloatUpdate );
+ addModule( GlobalLightingModifierUpdate );
addModule( TensileFormationUpdate );
addModule( HeightDieUpdate );
addModule( ScatterShotUpdate );
diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/GlobalLightingModifierUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/GlobalLightingModifierUpdate.cpp
new file mode 100644
index 00000000000..2e76e865166
--- /dev/null
+++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/GlobalLightingModifierUpdate.cpp
@@ -0,0 +1,231 @@
+/*
+** Command & Conquer Generals Zero Hour(tm)
+** Copyright 2025 Electronic Arts Inc.
+**
+** This program 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 3 of the License, or
+** (at your option) any later version.
+**
+** This program 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 this program. If not, see .
+*/
+
+// FILE: GlobalLightingModifierUpdate.cpp ////////////////////////////////////////////////////////
+
+#include "PreRTS.h" // This must go first in EVERY cpp file in the GameEngine
+
+#include "Common/Player.h"
+#include "Common/Upgrade.h"
+#include "Common/Xfer.h"
+#include "GameLogic/GameLogic.h"
+#include "GameLogic/Object.h"
+#include "GameLogic/Module/GlobalLightingModifierUpdate.h"
+
+//-------------------------------------------------------------------------------------------------
+static const char* const TheLightingBlendModeNames[] =
+{
+ "MULTIPLY",
+ "ADDITIVE",
+ nullptr
+};
+
+//-------------------------------------------------------------------------------------------------
+GlobalLightingModifierUpdateModuleData::GlobalLightingModifierUpdateModuleData()
+{
+ m_targetColor.red = m_targetColor.green = m_targetColor.blue = 0.0f;
+ m_blendMode = LIGHTINGMOD_MULTIPLY;
+ m_intensity = 1.0f;
+ m_initialDelayFrames = 0; // no delay
+ m_durationFrames = 0; // infinite (stays active until gate drops)
+ m_fadeInFrames = 1;
+ m_fadeOutFrames = 1;
+ // m_requiredUpgrade defaults empty -> always active while alive
+}
+
+//-------------------------------------------------------------------------------------------------
+/*static*/ void GlobalLightingModifierUpdateModuleData::buildFieldParse(MultiIniFieldParse& p)
+{
+ UpdateModuleData::buildFieldParse( p );
+
+ static const FieldParse dataFieldParse[] =
+ {
+ { "TargetColor", INI::parseRGBColor, nullptr, offsetof( GlobalLightingModifierUpdateModuleData, m_targetColor ) },
+ { "BlendMode", INI::parseIndexList, TheLightingBlendModeNames, offsetof( GlobalLightingModifierUpdateModuleData, m_blendMode ) },
+ { "Intensity", INI::parseReal, nullptr, offsetof( GlobalLightingModifierUpdateModuleData, m_intensity ) },
+ { "InitialDelay", INI::parseDurationUnsignedInt, nullptr, offsetof( GlobalLightingModifierUpdateModuleData, m_initialDelayFrames ) },
+ { "Duration", INI::parseDurationUnsignedInt, nullptr, offsetof( GlobalLightingModifierUpdateModuleData, m_durationFrames ) },
+ { "FadeInTime", INI::parseDurationUnsignedInt, nullptr, offsetof( GlobalLightingModifierUpdateModuleData, m_fadeInFrames ) },
+ { "FadeOutTime", INI::parseDurationUnsignedInt, nullptr, offsetof( GlobalLightingModifierUpdateModuleData, m_fadeOutFrames ) },
+ { "RequiredUpgrade", INI::parseAsciiString, nullptr, offsetof( GlobalLightingModifierUpdateModuleData, m_requiredUpgrade ) },
+ { nullptr, nullptr, nullptr, 0 }
+ };
+ p.add(dataFieldParse);
+}
+
+//-------------------------------------------------------------------------------------------------
+GlobalLightingModifierUpdate::GlobalLightingModifierUpdate( Thing *thing, const ModuleData* moduleData ) :
+ UpdateModule( thing, moduleData ),
+ m_weight( 0.0f ), // start faded out; ramps in
+ m_triggered( FALSE ),
+ m_triggerFrame( 0 )
+{
+ GlobalLightingModifierManager::get().registerContributor( this );
+}
+
+//-------------------------------------------------------------------------------------------------
+GlobalLightingModifierUpdate::~GlobalLightingModifierUpdate( void )
+{
+ GlobalLightingModifierManager::get().unregisterContributor( this );
+}
+
+//-------------------------------------------------------------------------------------------------
+Bool GlobalLightingModifierUpdate::computeConditionMet( void ) const
+{
+ const Object* obj = getObject();
+ if( obj == nullptr || obj->isEffectivelyDead() )
+ return FALSE;
+
+ const GlobalLightingModifierUpdateModuleData* d = getGlobalLightingModifierUpdateModuleData();
+ if( d->m_requiredUpgrade.isNotEmpty() )
+ {
+ const UpgradeTemplate* ut = TheUpgradeCenter->findUpgrade( d->m_requiredUpgrade );
+ if( ut == nullptr )
+ return FALSE;
+ if( obj->hasUpgrade( ut ) )
+ return TRUE;
+ const Player* p = obj->getControllingPlayer();
+ if( p != nullptr && p->getCompletedUpgradeMask().testForAll( ut->getUpgradeMask() ) )
+ return TRUE;
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+//-------------------------------------------------------------------------------------------------
+UpdateSleepTime GlobalLightingModifierUpdate::update( void )
+{
+ const GlobalLightingModifierUpdateModuleData* d = getGlobalLightingModifierUpdateModuleData();
+
+ // Determine the timed activation target (0 or 1).
+ Real target;
+ if( !computeConditionMet() )
+ {
+ // gate not satisfied (dead, or required upgrade lost): fade out and allow a future re-trigger
+ m_triggered = FALSE;
+ target = 0.0f;
+ }
+ else
+ {
+ UnsignedInt now = TheGameLogic->getFrame();
+ if( !m_triggered )
+ {
+ m_triggered = TRUE;
+ m_triggerFrame = now; // start of InitialDelay
+ }
+
+ UnsignedInt elapsed = now - m_triggerFrame;
+ if( elapsed < d->m_initialDelayFrames )
+ {
+ target = 0.0f; // still delayed
+ }
+ else
+ {
+ UnsignedInt activeElapsed = elapsed - d->m_initialDelayFrames;
+ if( d->m_durationFrames == 0 || activeElapsed < d->m_durationFrames )
+ target = 1.0f; // active
+ else
+ target = 0.0f; // duration expired -> fade out (stays done while the gate holds)
+ }
+ }
+
+ if( m_weight < target )
+ {
+ Real step = ( d->m_fadeInFrames > 0 ) ? ( 1.0f / (Real)d->m_fadeInFrames ) : 1.0f;
+ m_weight += step;
+ if( m_weight > target )
+ m_weight = target;
+ }
+ else if( m_weight > target )
+ {
+ Real step = ( d->m_fadeOutFrames > 0 ) ? ( 1.0f / (Real)d->m_fadeOutFrames ) : 1.0f;
+ m_weight -= step;
+ if( m_weight < target )
+ m_weight = target;
+ }
+
+ return UPDATE_SLEEP_NONE;
+}
+
+//-------------------------------------------------------------------------------------------------
+void GlobalLightingModifierUpdate::getLightingContribution( RGBColor& outMul, RGBColor& outAdd ) const
+{
+ const GlobalLightingModifierUpdateModuleData* d = getGlobalLightingModifierUpdateModuleData();
+
+ Real s = d->m_intensity * m_weight;
+ if( s < 0.0f )
+ s = 0.0f;
+
+ outMul.red = outMul.green = outMul.blue = 1.0f;
+ outAdd.red = outAdd.green = outAdd.blue = 0.0f;
+
+ const RGBColor& c = d->m_targetColor;
+ switch( d->m_blendMode )
+ {
+ case GlobalLightingModifierUpdateModuleData::LIGHTINGMOD_ADDITIVE:
+ // additive: add the target color scaled by strength
+ outAdd.red = c.red * s;
+ outAdd.green = c.green * s;
+ outAdd.blue = c.blue * s;
+ break;
+
+ case GlobalLightingModifierUpdateModuleData::LIGHTINGMOD_MULTIPLY:
+ default:
+ // multiplicative: lerp each channel from 1 (no change) toward the target color
+ outMul.red = 1.0f + ( c.red - 1.0f ) * s;
+ outMul.green = 1.0f + ( c.green - 1.0f ) * s;
+ outMul.blue = 1.0f + ( c.blue - 1.0f ) * s;
+ break;
+ }
+}
+
+//-------------------------------------------------------------------------------------------------
+void GlobalLightingModifierUpdate::crc( Xfer *xfer )
+{
+ UpdateModule::crc( xfer );
+}
+
+//-------------------------------------------------------------------------------------------------
+/** Xfer method
+ * Version Info:
+ * 1: Initial version
+ * 2: Added timed activation state (m_triggered, m_triggerFrame) */
+//-------------------------------------------------------------------------------------------------
+void GlobalLightingModifierUpdate::xfer( Xfer *xfer )
+{
+ XferVersion currentVersion = 2;
+ XferVersion version = currentVersion;
+ xfer->xferVersion( &version, currentVersion );
+
+ UpdateModule::xfer( xfer );
+
+ xfer->xferReal( &m_weight );
+
+ if( version >= 2 )
+ {
+ xfer->xferBool( &m_triggered );
+ xfer->xferUnsignedInt( &m_triggerFrame );
+ }
+}
+
+//-------------------------------------------------------------------------------------------------
+void GlobalLightingModifierUpdate::loadPostProcess( void )
+{
+ UpdateModule::loadPostProcess();
+}
diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp
index 51263250c6d..684f6bfd12d 100644
--- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp
+++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp
@@ -47,6 +47,7 @@
#include "GameClient/ParticleSys.h"
#include "GameClient/Color.h"
#include "GameClient/View.h"
+#include "GameClient/GlobalLightingModifier.h"
#include "W3DDevice/GameClient/HeightMap.h"
#include "W3DDevice/GameClient/W3DScene.h"
#include "W3DDevice/GameClient/W3DDynamicLight.h"
@@ -988,11 +989,66 @@ void RTS3DScene::Render(RenderInfoClass & rinfo)
{
if (m_customPassMode == SCENE_PASS_DEFAULT)
{
+ // Global lighting modifier (Phase 1: object lighting). If any modifier objects are active,
+ // tint the global directional lights + scene ambient for this frame's object lighting, then
+ // restore afterward. Terrain (baked) is not affected in Phase 1.
+ GlobalLightingModifierManager& lmMgr = GlobalLightingModifierManager::get();
+ Bool lmActive = lmMgr.hasActiveContributors();
+ Vector3 lmSavedAmbient;
+ Vector3 lmSavedDiffuse[LightEnvironmentClass::MAX_LIGHTS];
+ Vector3 lmSavedLightAmbient[LightEnvironmentClass::MAX_LIGHTS];
+ if (lmActive)
+ {
+ RGBColor lmMul, lmAdd;
+ lmMgr.computeCombined(lmMul, lmAdd);
+
+ lmSavedAmbient = Get_Ambient_Light();
+ Vector3 amb = lmSavedAmbient;
+ amb.X = amb.X * lmMul.red + lmAdd.red; if (amb.X < 0.0f) amb.X = 0.0f;
+ amb.Y = amb.Y * lmMul.green + lmAdd.green; if (amb.Y < 0.0f) amb.Y = 0.0f;
+ amb.Z = amb.Z * lmMul.blue + lmAdd.blue; if (amb.Z < 0.0f) amb.Z = 0.0f;
+ Set_Ambient_Light(amb);
+
+ for (Int lmi = 0; lmi < m_numGlobalLights; lmi++)
+ {
+ LightClass* lmL = m_globalLight[lmi];
+ if (lmL == NULL)
+ continue;
+ lmL->Get_Diffuse(&lmSavedDiffuse[lmi]);
+ lmL->Get_Ambient(&lmSavedLightAmbient[lmi]);
+
+ Vector3 d = lmSavedDiffuse[lmi];
+ d.X = d.X * lmMul.red + lmAdd.red; if (d.X < 0.0f) d.X = 0.0f;
+ d.Y = d.Y * lmMul.green + lmAdd.green; if (d.Y < 0.0f) d.Y = 0.0f;
+ d.Z = d.Z * lmMul.blue + lmAdd.blue; if (d.Z < 0.0f) d.Z = 0.0f;
+ lmL->Set_Diffuse(d);
+
+ Vector3 a = lmSavedLightAmbient[lmi];
+ a.X = a.X * lmMul.red + lmAdd.red; if (a.X < 0.0f) a.X = 0.0f;
+ a.Y = a.Y * lmMul.green + lmAdd.green; if (a.Y < 0.0f) a.Y = 0.0f;
+ a.Z = a.Z * lmMul.blue + lmAdd.blue; if (a.Z < 0.0f) a.Z = 0.0f;
+ lmL->Set_Ambient(a);
+ }
+ }
+
//Regular rendering pass with no effects
updatePlayerColorPasses();///@todo: this probably doesn't need to be done each frame.
updateFixedLightEnvironments(rinfo);
Customized_Render(rinfo);
Flush(rinfo);
+
+ if (lmActive)
+ {
+ Set_Ambient_Light(lmSavedAmbient);
+ for (Int lmi = 0; lmi < m_numGlobalLights; lmi++)
+ {
+ LightClass* lmL = m_globalLight[lmi];
+ if (lmL == NULL)
+ continue;
+ lmL->Set_Diffuse(lmSavedDiffuse[lmi]);
+ lmL->Set_Ambient(lmSavedLightAmbient[lmi]);
+ }
+ }
}
else if (m_customPassMode == SCENE_PASS_ALPHA_MASK)
{