From aea8b5632b642112338efbc3cedd3c6932a73f2a Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 9 Aug 2026 11:20:33 +0200 Subject: [PATCH 1/6] Draw Bridge and TerrainLighting/MapData improvements --- .../W3DDevice/GameClient/BaseHeightMap.cpp | 49 ++++++------ .../GameEngine/Include/Common/GlobalData.h | 8 -- .../Code/GameEngine/Include/Common/MapData.h | 9 +++ .../GameLogic/Module/DrawBridgeUpdate.h | 24 +++++- .../GameEngine/Source/Common/GlobalData.cpp | 20 ----- .../Code/GameEngine/Source/Common/MapData.cpp | 33 ++++++-- .../Object/Update/DrawBridgeUpdate.cpp | 80 ++++++++++++++++++- 7 files changed, 164 insertions(+), 59 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp index 3027403ad7d..ce0c5768998 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/BaseHeightMap.cpp @@ -57,6 +57,7 @@ #include #include "Common/GlobalData.h" +#include "Common/MapData.h" #include "Common/PerfTimer.h" #include "Common/Xfer.h" @@ -602,10 +603,10 @@ void BaseHeightMapRenderObjClass::doTheLight(VERTEX_FORMAT *vb, Vector3*light, V // --------------------------------------------- // Height based ambient light factor (for water) // --------------------------------------------- - if (TheGlobalData && - TheGlobalData->m_terrainHeightAmbientLightHeightStart > 0 && - (TheGlobalData->m_terrainHeightAmbientLightHeight1 >= 0 || TheGlobalData->m_terrainHeightAmbientLightHeight2 >= 0) && - vb->z <= TheGlobalData->m_terrainHeightAmbientLightHeightStart + if (TheMapData && + TheMapData->m_terrainHeightAmbientLightHeightStart > 0 && + (TheMapData->m_terrainHeightAmbientLightHeight1 >= 0 || TheMapData->m_terrainHeightAmbientLightHeight2 >= 0) && + vb->z <= TheMapData->m_terrainHeightAmbientLightHeightStart ) { Real col1R, col1G, col1B, col2R, col2G, col2B; //Real factor1 = 0.0; @@ -618,33 +619,33 @@ void BaseHeightMapRenderObjClass::doTheLight(VERTEX_FORMAT *vb, Vector3*light, V Real colB; // case 1: only one color - if (TheGlobalData->m_terrainHeightAmbientLightHeight1 <= 0) + if (TheMapData->m_terrainHeightAmbientLightHeight1 <= 0) { - col1R = TheGlobalData->m_terrainHeightAmbientLightColor2.red; - col1G = TheGlobalData->m_terrainHeightAmbientLightColor2.green; - col1B = TheGlobalData->m_terrainHeightAmbientLightColor2.blue; - height1 = TheGlobalData->m_terrainHeightAmbientLightHeight2; + col1R = TheMapData->m_terrainHeightAmbientLightColor2.red; + col1G = TheMapData->m_terrainHeightAmbientLightColor2.green; + col1B = TheMapData->m_terrainHeightAmbientLightColor2.blue; + height1 = TheMapData->m_terrainHeightAmbientLightHeight2; } - else if (TheGlobalData->m_terrainHeightAmbientLightHeight2 <= 0) { - col1R = TheGlobalData->m_terrainHeightAmbientLightColor1.red; - col1G = TheGlobalData->m_terrainHeightAmbientLightColor1.green; - col1B = TheGlobalData->m_terrainHeightAmbientLightColor1.blue; - height1 = TheGlobalData->m_terrainHeightAmbientLightHeight1; + else if (TheMapData->m_terrainHeightAmbientLightHeight2 <= 0) { + col1R = TheMapData->m_terrainHeightAmbientLightColor1.red; + col1G = TheMapData->m_terrainHeightAmbientLightColor1.green; + col1B = TheMapData->m_terrainHeightAmbientLightColor1.blue; + height1 = TheMapData->m_terrainHeightAmbientLightHeight1; } else { // case 2: both colors - col1R = TheGlobalData->m_terrainHeightAmbientLightColor1.red; - col1G = TheGlobalData->m_terrainHeightAmbientLightColor1.green; - col1B = TheGlobalData->m_terrainHeightAmbientLightColor1.blue; - col2R = TheGlobalData->m_terrainHeightAmbientLightColor2.red; - col2G = TheGlobalData->m_terrainHeightAmbientLightColor2.green; - col2B = TheGlobalData->m_terrainHeightAmbientLightColor2.blue; - height1 = TheGlobalData->m_terrainHeightAmbientLightHeight1; - height2 = TheGlobalData->m_terrainHeightAmbientLightHeight2; + col1R = TheMapData->m_terrainHeightAmbientLightColor1.red; + col1G = TheMapData->m_terrainHeightAmbientLightColor1.green; + col1B = TheMapData->m_terrainHeightAmbientLightColor1.blue; + col2R = TheMapData->m_terrainHeightAmbientLightColor2.red; + col2G = TheMapData->m_terrainHeightAmbientLightColor2.green; + col2B = TheMapData->m_terrainHeightAmbientLightColor2.blue; + height1 = TheMapData->m_terrainHeightAmbientLightHeight1; + height2 = TheMapData->m_terrainHeightAmbientLightHeight2; } - bool multiply = !TheGlobalData->m_terrainHeightAmbientLightAdditive; + bool multiply = !TheMapData->m_terrainHeightAmbientLightAdditive; Real base; if (multiply) base = 1.0; @@ -652,7 +653,7 @@ void BaseHeightMapRenderObjClass::doTheLight(VERTEX_FORMAT *vb, Vector3*light, V base = 0.0; if (vb->z > height1) { - Real t = WWMath::Clamp(invLerp(TheGlobalData->m_terrainHeightAmbientLightHeightStart, height1, vb->z)); + Real t = WWMath::Clamp(invLerp(TheMapData->m_terrainHeightAmbientLightHeightStart, height1, vb->z)); colR = col1R * t + (1.0 - t) * base; colG = col1G * t + (1.0 - t) * base; colB = col1B * t + (1.0 - t) * base; diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 9cff6db11f2..15eb0168afa 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -597,14 +597,6 @@ class GlobalData : public SubsystemInterface Bool m_reverseMoveIgnoreAngleThreshold; ///< if true, a manual REVERSE_MOVE order reverses regardless of heading; if false, only when the goal is behind us Real m_smartGarrisonRange; ///< radius searched for additional transports by the Smart Garrison command - // Water depth lighting - RGBColor m_terrainHeightAmbientLightColor1; - RGBColor m_terrainHeightAmbientLightColor2; - Real m_terrainHeightAmbientLightHeightStart; - Real m_terrainHeightAmbientLightHeight1; - Real m_terrainHeightAmbientLightHeight2; - Bool m_terrainHeightAmbientLightAdditive; - // the trailing '\' is included! const AsciiString &getPath_UserData() const { return m_userDataDir; } diff --git a/GeneralsMD/Code/GameEngine/Include/Common/MapData.h b/GeneralsMD/Code/GameEngine/Include/Common/MapData.h index 07a88767c96..5fc361de23f 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/MapData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/MapData.h @@ -25,7 +25,16 @@ class MapData : public SubsystemInterface Real m_HeightmapScale; Bool m_enableShips; + // Height based terrain ambient light (e.g. water depth lighting). Per-map. + RGBColor m_terrainHeightAmbientLightColor1; + RGBColor m_terrainHeightAmbientLightColor2; + Real m_terrainHeightAmbientLightHeightStart; + Real m_terrainHeightAmbientLightHeight1; + Real m_terrainHeightAmbientLightHeight2; + Bool m_terrainHeightAmbientLightAdditive; + private: + void setDefaults(); static const FieldParse s_MapDataFieldParseTable[]; }; diff --git a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DrawBridgeUpdate.h b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DrawBridgeUpdate.h index f8a6f69aad5..44f96e5569e 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DrawBridgeUpdate.h +++ b/GeneralsMD/Code/GameEngine/Include/GameLogic/Module/DrawBridgeUpdate.h @@ -6,13 +6,13 @@ // INCLUDES /////////////////////////////////////////////////////////////////////////////////////// #include "Common/KindOf.h" +#include "Common/AudioEventRTS.h" #include "GameLogic/Module/UpdateModule.h" // FORWARD REFERENCES ///////////////////////////////////////////////////////////////////////////// class SpecialPowerModule; class ParticleSystem; class FXList; -class AudioEventRTS; enum CommandOption CPP_11(: Int); //------------------------------------------------------------------------------------------------- @@ -26,6 +26,13 @@ class DrawBridgeUpdateModuleData : public ModuleData Real m_openingPushForce; UnsignedInt m_closingDamageTime; + const FXList* m_openingFX; ///< played when the bridge starts opening + const FXList* m_openFX; ///< played when the bridge finishes opening + const FXList* m_closingFX; ///< played when the bridge starts closing + const FXList* m_closedFX; ///< played when the bridge finishes closing + AudioEventRTS m_openingAudio; ///< looped while the bridge is opening + AudioEventRTS m_closingAudio; ///< looped while the bridge is closing + DrawBridgeUpdateModuleData(); static void buildFieldParse(MultiIniFieldParse& p); @@ -62,10 +69,25 @@ class DrawBridgeUpdate : public UpdateModule void pushObjectsOnOpeningDrawbridge( void ); void destroyObjectsUnderClosingDrawbridge (void ); + void stopTransitionAudio( void ); ///< stop any looping opening/closing audio + + enum BridgeTransitionType + { + BRIDGE_TRANSITION_NONE = 0, + BRIDGE_TRANSITION_OPENING, + BRIDGE_TRANSITION_CLOSING, + }; + bool m_bridgeOpened; UnsignedInt m_nextReadyFrame; UnsignedInt m_openingFrame; ///< frame bridge started to open UnsignedInt m_closingDamageFrame; ///< frame damage will be applied when closing + + BridgeTransitionType m_transitionState; ///< opening/closing/none, drives the finish FX and looping audio + UnsignedInt m_transitionDoneFrame; ///< frame the current opening/closing finishes + + AudioEventRTS m_openingAudio; ///< runtime instance of the looping opening audio + AudioEventRTS m_closingAudio; ///< runtime instance of the looping closing audio }; diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 41ce14ca763..6b2ff6a9472 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -583,12 +583,6 @@ GlobalData* GlobalData::m_theOriginal = nullptr; {"HideScorchmarksAboveGround", INI::parseBool, NULL, offsetof(GlobalData, m_hideScorchmarksAboveGround) }, - { "TerrainHeightAmbientLightColor1", INI::parseRGBColor, NULL, offsetof(GlobalData, m_terrainHeightAmbientLightColor1) }, - { "TerrainHeightAmbientLightColor2", INI::parseRGBColor, NULL, offsetof(GlobalData, m_terrainHeightAmbientLightColor2) }, - { "TerrainHeightAmbientLightStart", INI::parseReal, NULL, offsetof(GlobalData, m_terrainHeightAmbientLightHeightStart) }, - { "TerrainHeightAmbientLightHeight1", INI::parseReal, NULL, offsetof(GlobalData, m_terrainHeightAmbientLightHeight1) }, - { "TerrainHeightAmbientLightHeight2", INI::parseReal, NULL, offsetof(GlobalData, m_terrainHeightAmbientLightHeight2) }, - { "TerrainHeightAmbientLightAdditive", INI::parseBool, NULL, offsetof(GlobalData, m_terrainHeightAmbientLightAdditive) }, { nullptr, nullptr, nullptr, 0 } }; @@ -1183,20 +1177,6 @@ GlobalData::GlobalData() m_heightAboveTerrainIncludesWater = false; - m_terrainHeightAmbientLightColor1.red = 0; - m_terrainHeightAmbientLightColor1.green = 0; - m_terrainHeightAmbientLightColor1.blue = 0; - - m_terrainHeightAmbientLightColor2.red = 0; - m_terrainHeightAmbientLightColor2.green = 0; - m_terrainHeightAmbientLightColor2.blue = 0; - - m_terrainHeightAmbientLightHeightStart = -1; - m_terrainHeightAmbientLightHeight1 = -1; - m_terrainHeightAmbientLightHeight2 = -1; - - m_terrainHeightAmbientLightAdditive = false; - } // end GlobalData diff --git a/GeneralsMD/Code/GameEngine/Source/Common/MapData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/MapData.cpp index 01114cd4643..41ef2fbffbc 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/MapData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/MapData.cpp @@ -18,24 +18,47 @@ MapData* TheWriteableMapData = NULL; ///< The current map data singleton { { "HeightMapScale", INI::parseReal, NULL, offsetof( MapData, m_HeightmapScale) }, { "EnableShips", INI::parseBool, NULL, offsetof( MapData, m_enableShips) }, + { "TerrainHeightAmbientLightColor1", INI::parseRGBColor, NULL, offsetof( MapData, m_terrainHeightAmbientLightColor1) }, + { "TerrainHeightAmbientLightColor2", INI::parseRGBColor, NULL, offsetof( MapData, m_terrainHeightAmbientLightColor2) }, + { "TerrainHeightAmbientLightStart", INI::parseReal, NULL, offsetof( MapData, m_terrainHeightAmbientLightHeightStart) }, + { "TerrainHeightAmbientLightHeight1", INI::parseReal, NULL, offsetof( MapData, m_terrainHeightAmbientLightHeight1) }, + { "TerrainHeightAmbientLightHeight2", INI::parseReal, NULL, offsetof( MapData, m_terrainHeightAmbientLightHeight2) }, + { "TerrainHeightAmbientLightAdditive",INI::parseBool, NULL, offsetof( MapData, m_terrainHeightAmbientLightAdditive) }, { NULL, NULL, NULL, 0 } // keep this last }; -MapData::MapData() : SubsystemInterface() +void MapData::setDefaults() { m_HeightmapScale = 1.0f; m_enableShips = false; + + m_terrainHeightAmbientLightColor1.red = 0; + m_terrainHeightAmbientLightColor1.green = 0; + m_terrainHeightAmbientLightColor1.blue = 0; + + m_terrainHeightAmbientLightColor2.red = 0; + m_terrainHeightAmbientLightColor2.green = 0; + m_terrainHeightAmbientLightColor2.blue = 0; + + m_terrainHeightAmbientLightHeightStart = -1; + m_terrainHeightAmbientLightHeight1 = -1; + m_terrainHeightAmbientLightHeight2 = -1; + + m_terrainHeightAmbientLightAdditive = false; +} + +MapData::MapData() : SubsystemInterface() +{ + setDefaults(); } void MapData::init() { - m_HeightmapScale = 1.0f; - m_enableShips = false; + setDefaults(); } void MapData::reset() { - m_HeightmapScale = 1.0f; - m_enableShips = false; + setDefaults(); } void MapData::parseMapDataDefinition(INI* ini) { diff --git a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp index 65fce065d3d..262af185e46 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameLogic/Object/Update/DrawBridgeUpdate.cpp @@ -9,6 +9,7 @@ // INCLUDES /////////////////////////////////////////////////////////////////////////////////////// #include "Common/BitFlagsIO.h" +#include "Common/GameAudio.h" #include "Common/Radar.h" #include "Common/PlayerList.h" #include "Common/ThingTemplate.h" @@ -48,6 +49,11 @@ DrawBridgeUpdateModuleData::DrawBridgeUpdateModuleData() m_closingDuration = 0U; m_openingPushForce = 0.0f; m_closingDamageTime = 0U; + + m_openingFX = nullptr; + m_openFX = nullptr; + m_closingFX = nullptr; + m_closedFX = nullptr; } //------------------------------------------------------------------------------------------------- @@ -61,6 +67,12 @@ DrawBridgeUpdateModuleData::DrawBridgeUpdateModuleData() { "ClosingDuration", INI::parseDurationUnsignedInt, nullptr, offsetof(DrawBridgeUpdateModuleData, m_closingDuration)}, { "OpeningPushForce", INI::parseAccelerationReal, nullptr, offsetof(DrawBridgeUpdateModuleData, m_openingPushForce)}, { "ClosingDamageTime", INI::parseDurationUnsignedInt, nullptr, offsetof(DrawBridgeUpdateModuleData, m_closingDamageTime)}, + { "BridgeOpeningFX", INI::parseFXList, nullptr, offsetof(DrawBridgeUpdateModuleData, m_openingFX)}, + { "BridgeOpenFX", INI::parseFXList, nullptr, offsetof(DrawBridgeUpdateModuleData, m_openFX)}, + { "BridgeClosingFX", INI::parseFXList, nullptr, offsetof(DrawBridgeUpdateModuleData, m_closingFX)}, + { "BridgeClosedFX", INI::parseFXList, nullptr, offsetof(DrawBridgeUpdateModuleData, m_closedFX)}, + { "BridgeOpeningAudio", INI::parseAudioEventRTS, nullptr, offsetof(DrawBridgeUpdateModuleData, m_openingAudio)}, + { "BridgeClosingAudio", INI::parseAudioEventRTS, nullptr, offsetof(DrawBridgeUpdateModuleData, m_closingAudio)}, { nullptr, nullptr, nullptr, 0 } }; p.add(dataFieldParse); @@ -74,12 +86,34 @@ DrawBridgeUpdate::DrawBridgeUpdate(Thing* thing, const ModuleData* moduleData) : m_nextReadyFrame = 0U; m_openingFrame = 0U; m_closingDamageFrame = 0U; + + const DrawBridgeUpdateModuleData* data = getDrawBridgeUpdateModuleData(); + m_transitionState = BRIDGE_TRANSITION_NONE; + m_transitionDoneFrame = 0U; + + // runtime copies of the configured looping audio, tied to this object + m_openingAudio = data->m_openingAudio; + m_openingAudio.setObjectID(getObject()->getID()); + m_closingAudio = data->m_closingAudio; + m_closingAudio.setObjectID(getObject()->getID()); } //------------------------------------------------------------------------------------------------- //------------------------------------------------------------------------------------------------- DrawBridgeUpdate::~DrawBridgeUpdate(void) { + stopTransitionAudio(); +} + +//------------------------------------------------------------------------------------------------- +// Stop any looping opening/closing audio that is currently playing. +//------------------------------------------------------------------------------------------------- +void DrawBridgeUpdate::stopTransitionAudio(void) +{ + if (m_openingAudio.isCurrentlyPlaying()) + TheAudio->removeAudioEvent(m_openingAudio.getPlayingHandle()); + if (m_closingAudio.isCurrentlyPlaying()) + TheAudio->removeAudioEvent(m_closingAudio.getPlayingHandle()); } // ------------------------------------------------------------------------------------------------ @@ -87,6 +121,8 @@ DrawBridgeUpdate::~DrawBridgeUpdate(void) // ------------------------------------------------------------------------------------------------ void DrawBridgeUpdate::onDelete() { + stopTransitionAudio(); + // extend base class UpdateModule::onDelete(); @@ -133,12 +169,30 @@ bool DrawBridgeUpdate::setDrawBridgeState(bool opened, const Object* fromTower) obj->setGeometryInfo(openBridgeGeom); m_openingFrame = TheGameLogic->getFrame(); m_closingDamageFrame = 0U; + + // bridge starts opening: fire the opening FX and loop the opening audio until it finishes + stopTransitionAudio(); + if (data->m_openingFX) + FXList::doFXPos(data->m_openingFX, obj->getPosition()); + m_openingAudio.setPosition(obj->getPosition()); + m_openingAudio.setPlayingHandle(TheAudio->addAudioEvent(&m_openingAudio)); + m_transitionState = BRIDGE_TRANSITION_OPENING; + m_transitionDoneFrame = m_nextReadyFrame; } else { obj->clearAndSetModelConditionState(MODELCONDITION_DOOR_1_OPENING, MODELCONDITION_DOOR_1_CLOSING); obj->setGeometryInfo(obj->getTemplate()->getTemplateGeometryInfo()); m_openingFrame = 0U; // when rapid toggling is possible m_closingDamageFrame = TheGameLogic->getFrame() + data->m_closingDamageTime; + + // bridge starts closing: fire the closing FX and loop the closing audio until it finishes + stopTransitionAudio(); + if (data->m_closingFX) + FXList::doFXPos(data->m_closingFX, obj->getPosition()); + m_closingAudio.setPosition(obj->getPosition()); + m_closingAudio.setPlayingHandle(TheAudio->addAudioEvent(&m_closingAudio)); + m_transitionState = BRIDGE_TRANSITION_CLOSING; + m_transitionDoneFrame = m_nextReadyFrame; } } return true; @@ -150,6 +204,9 @@ void DrawBridgeUpdate::onBridgeDestroyed() { m_openingFrame = 0U; m_closingDamageFrame = 0U; + stopTransitionAudio(); + m_transitionState = BRIDGE_TRANSITION_NONE; + m_transitionDoneFrame = 0U; Object* obj = getObject(); obj->clearModelConditionFlags(MODELCONDITION_DOOR_1_OPENING); obj->clearModelConditionFlags(MODELCONDITION_DOOR_1_CLOSING); @@ -160,6 +217,9 @@ void DrawBridgeUpdate::onBridgeRepaired() { m_openingFrame = 0U; m_closingDamageFrame = 0U; + stopTransitionAudio(); + m_transitionState = BRIDGE_TRANSITION_NONE; + m_transitionDoneFrame = 0U; Object* obj = getObject(); obj->clearModelConditionFlags(MODELCONDITION_DOOR_1_OPENING); obj->clearModelConditionFlags(MODELCONDITION_DOOR_1_CLOSING); @@ -386,6 +446,17 @@ UpdateSleepTime DrawBridgeUpdate::update() m_closingDamageFrame = 0U; } } + + // The opening/closing animation just finished: fire the finished FX and stop the looping audio. + if (m_transitionState != BRIDGE_TRANSITION_NONE && TheGameLogic->getFrame() >= m_transitionDoneFrame) { + const DrawBridgeUpdateModuleData* data = getDrawBridgeUpdateModuleData(); + const FXList* finishedFX = (m_transitionState == BRIDGE_TRANSITION_OPENING) ? data->m_openFX : data->m_closedFX; + if (finishedFX) + FXList::doFXPos(finishedFX, getObject()->getPosition()); + stopTransitionAudio(); + m_transitionState = BRIDGE_TRANSITION_NONE; + m_transitionDoneFrame = 0U; + } return UPDATE_SLEEP_NONE; } @@ -403,12 +474,13 @@ void DrawBridgeUpdate::crc(Xfer* xfer) // Xfer method // Version Info: // 1: Initial version +// 2: Added opening/closing transition state for finish FX + looping audio //------------------------------------------------------------------------------------------------ void DrawBridgeUpdate::xfer(Xfer* xfer) { // version - XferVersion currentVersion = 1; + XferVersion currentVersion = 2; XferVersion version = currentVersion; xfer->xferVersion(&version, currentVersion); @@ -422,6 +494,12 @@ void DrawBridgeUpdate::xfer(Xfer* xfer) xfer->xferUnsignedInt(&m_openingFrame); xfer->xferUnsignedInt(&m_closingDamageFrame); + + if (version >= 2) + { + xfer->xferUser(&m_transitionState, sizeof(m_transitionState)); + xfer->xferUnsignedInt(&m_transitionDoneFrame); + } } //------------------------------------------------------------------------------------------------ From f2d930cf7dd3039440e234a1dbe044b29b960911 Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 9 Aug 2026 14:25:58 +0200 Subject: [PATCH 2/6] Add mode to enable radius decals above water --- .../GameEngine/Include/Common/GlobalData.h | 1 + .../GameEngine/Source/Common/GlobalData.cpp | 2 + .../W3DDevice/GameClient/W3DProjectedShadow.h | 1 + .../GameClient/Shadow/W3DProjectedShadow.cpp | 100 +++++++++++++----- .../W3DDevice/GameClient/Shadow/W3DShadow.cpp | 10 ++ .../Source/W3DDevice/GameClient/W3DScene.cpp | 6 ++ 6 files changed, 91 insertions(+), 29 deletions(-) diff --git a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h index 15eb0168afa..83e8adc2a5a 100644 --- a/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h +++ b/GeneralsMD/Code/GameEngine/Include/Common/GlobalData.h @@ -592,6 +592,7 @@ class GlobalData : public SubsystemInterface DeathTypeFlags m_defaultExcludedDeathTypes; Bool m_heightAboveTerrainIncludesWater; + Bool m_radiusDecalsAboveWater; ///< if true, radius decals (cursors) render over water instead of under it Bool m_hideScorchmarksAboveGround; Bool m_weaponScatterOnWaterSurfaceDefault; ///< default for WeaponTemplate ScatterOnWaterSurface when not set per-weapon Bool m_reverseMoveIgnoreAngleThreshold; ///< if true, a manual REVERSE_MOVE order reverses regardless of heading; if false, only when the goal is behind us diff --git a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp index 6b2ff6a9472..9cd47a63c29 100644 --- a/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp +++ b/GeneralsMD/Code/GameEngine/Source/Common/GlobalData.cpp @@ -580,6 +580,7 @@ GlobalData* GlobalData::m_theOriginal = nullptr; {"DefaultExcludedDeathTypes", INI::parseDeathTypeFlagsList, NULL, offsetof(GlobalData, m_defaultExcludedDeathTypes) }, {"HeightAboveTerrainIncludesWater", INI::parseBool, NULL, offsetof(GlobalData, m_heightAboveTerrainIncludesWater) }, + {"RadiusDecalsAboveWater", INI::parseBool, NULL, offsetof(GlobalData, m_radiusDecalsAboveWater) }, {"HideScorchmarksAboveGround", INI::parseBool, NULL, offsetof(GlobalData, m_hideScorchmarksAboveGround) }, @@ -1176,6 +1177,7 @@ GlobalData::GlobalData() // m_chronoTintStatusType = TINT_STATUS_INVALID; m_heightAboveTerrainIncludesWater = false; + m_radiusDecalsAboveWater = false; } // end GlobalData diff --git a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DProjectedShadow.h b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DProjectedShadow.h index 3fa7bc9b9d5..04fae654146 100644 --- a/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DProjectedShadow.h +++ b/GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DProjectedShadow.h @@ -57,6 +57,7 @@ class W3DProjectedShadowManager : public ProjectedShadowManager void shutdown(void); ///SetTextureStageState(0, D3DTSS_COLOROP, D3DTOP_MODULATE); + m_pDev->SetTextureStageState(0, D3DTSS_COLORARG1, D3DTA_TEXTURE); + m_pDev->SetTextureStageState(0, D3DTSS_COLORARG2, D3DTA_DIFFUSE); + m_pDev->SetTextureStageState(0, D3DTSS_ALPHAOP, D3DTOP_MODULATE); + m_pDev->SetTextureStageState(0, D3DTSS_ALPHAARG1, D3DTA_TEXTURE); + m_pDev->SetTextureStageState(0, D3DTSS_ALPHAARG2, D3DTA_DIFFUSE); + m_pDev->SetTextureStageState(1, D3DTSS_COLOROP, D3DTOP_DISABLE); + m_pDev->SetTextureStageState(1, D3DTSS_ALPHAOP, D3DTOP_DISABLE); + m_pDev->SetRenderState(D3DRS_ALPHATESTENABLE, FALSE); + //Alpha Blended Shadows // m_pDev->SetRenderState( D3DRS_SRCBLEND, D3DBLEND_SRCALPHA ); // m_pDev->SetRenderState( D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA ); @@ -1040,7 +1054,7 @@ void W3DProjectedShadowManager::queueDecal(W3DProjectedShadow *shadow) hmapVertex.X=(float)(i-borderSize)*MAP_XY_FACTOR; hmapVertex.Z=__max((float)hmap->getHeight(i,j)*MAP_HEIGHT_SCALE,layerHeight); - if (TheGlobalData->m_heightAboveTerrainIncludesWater && TheTerrainLogic != nullptr) { + if ((TheGlobalData->m_heightAboveTerrainIncludesWater || TheGlobalData->m_radiusDecalsAboveWater) && TheTerrainLogic != nullptr) { if (Real waterZ = 0; TheTerrainLogic->isUnderwater(hmapVertex.X, hmapVertex.Y, &waterZ)) { if (waterZ > hmapVertex.Z) hmapVertex.Z = waterZ; } @@ -1066,7 +1080,7 @@ void W3DProjectedShadowManager::queueDecal(W3DProjectedShadow *shadow) hmapVertex.X=(float)(i-borderSize)*MAP_XY_FACTOR; hmapVertex.Z=(float)hmap->getHeight(i,j)*MAP_HEIGHT_SCALE+0.01f * MAP_XY_FACTOR; - if (TheGlobalData->m_heightAboveTerrainIncludesWater && TheTerrainLogic != nullptr) { + if ((TheGlobalData->m_heightAboveTerrainIncludesWater || TheGlobalData->m_radiusDecalsAboveWater) && TheTerrainLogic != nullptr) { if (Real waterZ = 0; TheTerrainLogic->isUnderwater(hmapVertex.X, hmapVertex.Y, &waterZ)) { if (waterZ > hmapVertex.Z) hmapVertex.Z = waterZ; } @@ -1444,38 +1458,66 @@ Int W3DProjectedShadowManager::renderShadows(RenderInfoClass & rinfo) flushDecals(lastShadowDecalTexture,lastShadowType); //make sure there are not any unrendered decals left over. TheDX8MeshRenderer.Flush(); //draw all the shadow receiving objects } - if (m_decalList) - { - //keep track of active decal texture so we can render all decals at once. - W3DShadowTexture *lastShadowDecalTexture=nullptr; - ShadowType lastShadowType = SHADOW_NONE; + // Radius decals (the m_decalList) are normally drawn here, before water. When + // TheGlobalData->m_radiusDecalsAboveWater is set they are instead drawn later (after the + // water pass) by a separate renderDecals() call from RTS3DScene::Flush, so they appear over water. + if (!TheGlobalData->m_radiusDecalsAboveWater) + projectionCount += renderDecals(rinfo); + + return projectionCount; +} + +//------------------------------------------------------------------------------------------------- +/** Draw just the radius-decal list (m_decalList). Split out of renderShadows so it can optionally + be drawn after the water pass (see TheGlobalData->m_radiusDecalsAboveWater). */ +//------------------------------------------------------------------------------------------------- +Int W3DProjectedShadowManager::renderDecals(RenderInfoClass & rinfo) +{ + Int projectionCount=0; + + if (!TheTerrainRenderObject || !m_decalList) + return projectionCount; + + //According to Nvidia there's a D3D bug that happens if you don't start with a + //new dynamic VB each frame - so we force a DISCARD by overflowing the counter. + //(also needed because water was drawn between the shadow pass and here) + nShadowDecalVertsInBuf = 0xffff; + nShadowDecalIndicesInBuf = 0xffff; - for( shadow = m_decalList; shadow; shadow = shadow->m_next ) + TheDX8MeshRenderer.Set_Camera(&rinfo.Camera); + + W3DProjectedShadow *shadow; + + //keep track of active decal texture so we can render all decals at once. + W3DShadowTexture *lastShadowDecalTexture=nullptr; + ShadowType lastShadowType = SHADOW_NONE; + + for( shadow = m_decalList; shadow; shadow = shadow->m_next ) + { + if (shadow->m_isEnabled && !shadow->m_isInvisibleEnabled) { - if (shadow->m_isEnabled && !shadow->m_isInvisibleEnabled) - { - if (lastShadowDecalTexture == nullptr) - lastShadowDecalTexture=m_decalList->m_shadowTexture[0]; - if (lastShadowType == SHADOW_NONE) - lastShadowType = m_decalList->m_type; - - if (shadow->m_shadowTexture[0] != lastShadowDecalTexture || - shadow->m_type != lastShadowType) - { flushDecals(lastShadowDecalTexture,lastShadowType); //switched to a new texture, need to render polys using last texture. - lastShadowDecalTexture=shadow->m_shadowTexture[0]; - lastShadowType=shadow->m_type; - } - ///@todo: may need to fix this if shadows are large enough to be seen while object is not visible - if (!(shadow->m_robj && !shadow->m_robj->Is_Really_Visible())) - { //queueSimpleDecal(shadow); - queueDecal(shadow); //only draw shadow if casting object is visible - projectionCount++; - } + if (lastShadowDecalTexture == nullptr) + lastShadowDecalTexture=m_decalList->m_shadowTexture[0]; + if (lastShadowType == SHADOW_NONE) + lastShadowType = m_decalList->m_type; + + if (shadow->m_shadowTexture[0] != lastShadowDecalTexture || + shadow->m_type != lastShadowType) + { flushDecals(lastShadowDecalTexture,lastShadowType); //switched to a new texture, need to render polys using last texture. + lastShadowDecalTexture=shadow->m_shadowTexture[0]; + lastShadowType=shadow->m_type; + } + ///@todo: may need to fix this if shadows are large enough to be seen while object is not visible + if (!(shadow->m_robj && !shadow->m_robj->Is_Really_Visible())) + { //queueSimpleDecal(shadow); + queueDecal(shadow); //only draw shadow if casting object is visible + projectionCount++; } } - - flushDecals(lastShadowDecalTexture,lastShadowType); //make sure there are not any unrendered decals left over. } + + flushDecals(lastShadowDecalTexture,lastShadowType); //make sure there are not any unrendered decals left over. + return projectionCount; } diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp index e5f828619e4..4291f9be3b3 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp @@ -99,6 +99,16 @@ void DoShadows(RenderInfoClass & rinfo, Bool stencilPass) } +// Draw only the radius-decal list. Used to render radius decals AFTER the water pass so they +// appear over water (see TheGlobalData->m_radiusDecalsAboveWater). +// NOTE: does NOT gate on isShadowScene() - the stencil-shadow pass (DoShadows(...,true)) runs +// before water and resets that flag to FALSE, so it is already false by the time we get here. +void DoDecals(RenderInfoClass & rinfo) +{ + if (TheW3DProjectedShadowManager) + TheW3DProjectedShadowManager->renderDecals(rinfo); +} + W3DShadowManager::W3DShadowManager( void ) { DEBUG_ASSERTCRASH(TheW3DVolumetricShadowManager == nullptr && TheW3DProjectedShadowManager == nullptr, diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp index 4a56e2cb750..41d04f77a23 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp @@ -73,6 +73,7 @@ extern void PrepareShadows(); extern void DoTrees(RenderInfoClass & rinfo); extern void DoShadows(RenderInfoClass & rinfo, Bool stencilPass); +extern void DoDecals(RenderInfoClass & rinfo); extern void DoParticles(RenderInfoClass & rinfo); // No texturing, no zbuffer reading/writing, primary gradient, no @@ -876,6 +877,11 @@ void RTS3DScene::Flush(RenderInfoClass & rinfo) WW3D::Render_And_Clear_Static_Sort_Lists(rinfo); //draws things like water + //optionally draw radius decals AFTER water so they show over it (still depth-tested, so objects stay on top). + if (m_customPassMode == SCENE_PASS_DEFAULT && Get_Extra_Pass_Polygon_Mode() == EXTRA_PASS_DISABLE + && TheGlobalData->m_radiusDecalsAboveWater) + DoDecals(rinfo); + if (m_customPassMode == SCENE_PASS_DEFAULT && Get_Extra_Pass_Polygon_Mode() == EXTRA_PASS_DISABLE) flushTranslucentObjects(rinfo); //draw all translucent meshes which don't need per-polygon sorting. From 5a5d88c5908c25b287f457cc76b43cd9dac46cdd Mon Sep 17 00:00:00 2001 From: Andi Date: Sun, 9 Aug 2026 16:08:47 +0200 Subject: [PATCH 3/6] Toggle for FX decals to render above or below water --- .../GameClient/Module/W3DDecalDraw.h | 1 + .../GameClient/Drawable/Draw/W3DDecalDraw.cpp | 3 ++ .../GameEngine/Include/GameClient/Shadow.h | 16 +++++++- .../GameEngine/Include/GameClient/Shadow.h | 16 +++++++- .../W3DDevice/GameClient/W3DProjectedShadow.h | 2 +- .../GameClient/Shadow/W3DProjectedShadow.cpp | 40 ++++++++++++++----- .../W3DDevice/GameClient/Shadow/W3DShadow.cpp | 2 +- .../Source/W3DDevice/GameClient/W3DScene.cpp | 6 +-- 8 files changed, 69 insertions(+), 17 deletions(-) diff --git a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DDecalDraw.h b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DDecalDraw.h index a7e781bf799..c203c809fbd 100644 --- a/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DDecalDraw.h +++ b/Core/GameEngineDevice/Include/W3DDevice/GameClient/Module/W3DDecalDraw.h @@ -49,6 +49,7 @@ class W3DDecalDrawModuleData : public ModuleData ShadowType m_type; /// type of projection Real m_decalSizeX; /// 1/(world space extent of texture in x direction) Real m_decalSizeY; /// 1/(world space extent of texture in y direction) + Bool m_renderAboveWater; /// if true, this decal draws above water (else below, shadow-like) W3DDecalDrawModuleData(); ~W3DDecalDrawModuleData(); diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp index 5e06850a625..9416e33e916 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp @@ -48,6 +48,7 @@ //------------------------------------------------------------------------------------------------- W3DDecalDrawModuleData::W3DDecalDrawModuleData() { + m_renderAboveWater = FALSE; // default: below water (shadow-like) } //------------------------------------------------------------------------------------------------- @@ -69,6 +70,7 @@ void W3DDecalDrawModuleData::buildFieldParse(MultiIniFieldParse& p) { "FadeInTime", INI::parseDurationUnsignedInt, nullptr, offsetof(W3DDecalDrawModuleData, m_fadeInTime) }, { "SizeX", INI::parseReal, nullptr, offsetof(W3DDecalDrawModuleData, m_decalSizeX) }, { "SizeY", INI::parseReal, nullptr, offsetof(W3DDecalDrawModuleData, m_decalSizeY) }, + { "RenderAboveWater", INI::parseBool, nullptr, offsetof(W3DDecalDrawModuleData, m_renderAboveWater) }, { nullptr, nullptr, nullptr, 0 } }; p.add(dataFieldParse); @@ -128,6 +130,7 @@ void W3DDecalDraw::init_shadow() shadowInfo.m_sizeY = data->m_decalSizeY; shadowInfo.m_offsetX = 0.0f; // TODO shadowInfo.m_offsetY = 0.0f; // TODO + shadowInfo.m_waterRenderMode = data->m_renderAboveWater ? SHADOW_WATER_ABOVE : SHADOW_WATER_BELOW; //shadowInfo.m_hasDynamicLength = FALSE; DEBUG_ASSERTCRASH(m_shadow == nullptr, ("m_shadow is not null")); diff --git a/Generals/Code/GameEngine/Include/GameClient/Shadow.h b/Generals/Code/GameEngine/Include/GameClient/Shadow.h index cacf40256a8..33f032e0887 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Shadow.h +++ b/Generals/Code/GameEngine/Include/GameClient/Shadow.h @@ -62,6 +62,14 @@ static const char* const TheShadowNames[] = #define MAX_SHADOW_LIGHTS 1 //maximum number of shadow casting light sources in scene - support for more than 1 has been dropped from most code. +// Per-decal ordering relative to water (evaluated only by the Zero Hour render path; inert here). +enum ShadowWaterMode +{ + SHADOW_WATER_DEFAULT = 0, //follow the global RadiusDecalsAboveWater flag + SHADOW_WATER_ABOVE, //always draw above water + SHADOW_WATER_BELOW //always draw below water (shadow-like) +}; + class RenderObjClass; //forward reference class RenderCost; //forward reference @@ -83,6 +91,7 @@ class Shadow m_sizeY = 0.0f; m_offsetX = 0.0f; m_offsetY = 0.0f; + m_waterRenderMode = SHADOW_WATER_DEFAULT; } char m_ShadowName[64]; //when set, overrides the default model shadow (used mostly for Decals). @@ -93,9 +102,13 @@ class Shadow Real m_sizeY; //world size of decal projection Real m_offsetX; //world shift along x axis Real m_offsetY; //world shift along y axis + Int m_waterRenderMode; //ShadowWaterMode: above/below/default water ordering }; - Shadow(void) : m_diffuse(0xffffffff), m_color(0xffffffff), m_opacity (0x000000ff), m_localAngle(0.0f) {} + Shadow(void) : m_diffuse(0xffffffff), m_color(0xffffffff), m_opacity (0x000000ff), m_localAngle(0.0f), m_waterRenderMode(SHADOW_WATER_DEFAULT) {} + + void setWaterRenderMode(Int mode) { m_waterRenderMode = mode; } + Int getWaterRenderMode(void) const { return m_waterRenderMode; } ///getWaterRenderMode()) + { + case SHADOW_WATER_ABOVE: return TRUE; + case SHADOW_WATER_BELOW: return FALSE; + default: return TheGlobalData->m_radiusDecalsAboveWater; + } +} + /**Decals have a low poly count so its better to render large numbers at once. This system will queue them up until the buffers fill up. It will then flush the buffer (draw decals) and be ready for new decals. This is an optimized system that only uses the render objects bounding box to determine shadow visibility. @@ -1054,7 +1069,7 @@ void W3DProjectedShadowManager::queueDecal(W3DProjectedShadow *shadow) hmapVertex.X=(float)(i-borderSize)*MAP_XY_FACTOR; hmapVertex.Z=__max((float)hmap->getHeight(i,j)*MAP_HEIGHT_SCALE,layerHeight); - if ((TheGlobalData->m_heightAboveTerrainIncludesWater || TheGlobalData->m_radiusDecalsAboveWater) && TheTerrainLogic != nullptr) { + if ((TheGlobalData->m_heightAboveTerrainIncludesWater || decalDrawsAboveWater(shadow)) && TheTerrainLogic != nullptr) { if (Real waterZ = 0; TheTerrainLogic->isUnderwater(hmapVertex.X, hmapVertex.Y, &waterZ)) { if (waterZ > hmapVertex.Z) hmapVertex.Z = waterZ; } @@ -1080,7 +1095,7 @@ void W3DProjectedShadowManager::queueDecal(W3DProjectedShadow *shadow) hmapVertex.X=(float)(i-borderSize)*MAP_XY_FACTOR; hmapVertex.Z=(float)hmap->getHeight(i,j)*MAP_HEIGHT_SCALE+0.01f * MAP_XY_FACTOR; - if ((TheGlobalData->m_heightAboveTerrainIncludesWater || TheGlobalData->m_radiusDecalsAboveWater) && TheTerrainLogic != nullptr) { + if ((TheGlobalData->m_heightAboveTerrainIncludesWater || decalDrawsAboveWater(shadow)) && TheTerrainLogic != nullptr) { if (Real waterZ = 0; TheTerrainLogic->isUnderwater(hmapVertex.X, hmapVertex.Y, &waterZ)) { if (waterZ > hmapVertex.Z) hmapVertex.Z = waterZ; } @@ -1458,20 +1473,19 @@ Int W3DProjectedShadowManager::renderShadows(RenderInfoClass & rinfo) flushDecals(lastShadowDecalTexture,lastShadowType); //make sure there are not any unrendered decals left over. TheDX8MeshRenderer.Flush(); //draw all the shadow receiving objects } - // Radius decals (the m_decalList) are normally drawn here, before water. When - // TheGlobalData->m_radiusDecalsAboveWater is set they are instead drawn later (after the - // water pass) by a separate renderDecals() call from RTS3DScene::Flush, so they appear over water. - if (!TheGlobalData->m_radiusDecalsAboveWater) - projectionCount += renderDecals(rinfo); + // Draw the below-water subset of the decal list here (before water). The above-water subset is + // drawn later, after the water pass, by a second renderDecals() call from RTS3DScene::Flush. + projectionCount += renderDecals(rinfo, false); return projectionCount; } //------------------------------------------------------------------------------------------------- -/** Draw just the radius-decal list (m_decalList). Split out of renderShadows so it can optionally - be drawn after the water pass (see TheGlobalData->m_radiusDecalsAboveWater). */ +/** Draw the decal list (m_decalList), limited to decals whose effective water ordering matches + aboveWaterPass. Called once before water (aboveWaterPass=false) and once after (true), so decals + can be ordered above or below water per-decal. */ //------------------------------------------------------------------------------------------------- -Int W3DProjectedShadowManager::renderDecals(RenderInfoClass & rinfo) +Int W3DProjectedShadowManager::renderDecals(RenderInfoClass & rinfo, Bool aboveWaterPass) { Int projectionCount=0; @@ -1494,6 +1508,10 @@ Int W3DProjectedShadowManager::renderDecals(RenderInfoClass & rinfo) for( shadow = m_decalList; shadow; shadow = shadow->m_next ) { + // only draw the decals belonging to this pass (above vs below water) + if (decalDrawsAboveWater(shadow) != aboveWaterPass) + continue; + if (shadow->m_isEnabled && !shadow->m_isInvisibleEnabled) { if (lastShadowDecalTexture == nullptr) @@ -1582,6 +1600,7 @@ Shadow* W3DProjectedShadowManager::addDecal(Shadow::ShadowTypeInfo *shadowInfo) shadow->setTexture(0,st); ///@todo: Fix projected shadows to allow multiple lights shadow->m_type = shadowType; /// type of projection shadow->m_allowWorldAlign=allowWorldAlign; /// wrap shadow around world geometry - else align perpendicular to local z-axis. + shadow->setWaterRenderMode(shadowInfo->m_waterRenderMode); shadow->m_oowDecalSizeX = 1.0f/decalSizeX; //one over width shadow->m_oowDecalSizeY = 1.0f/decalSizeY; //one over height @@ -1689,6 +1708,7 @@ Shadow* W3DProjectedShadowManager::addDecal(RenderObjClass *robj, Shadow::Shadow shadow->setTexture(0,st); ///@todo: Fix projected shadows to allow multiple lights shadow->m_type = shadowType; /// type of projection shadow->m_allowWorldAlign=allowWorldAlign; /// wrap shadow around world geometry - else align perpendicular to local z-axis. + shadow->setWaterRenderMode(shadowInfo->m_waterRenderMode); AABoxClass box; diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp index 4291f9be3b3..fecb249f974 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DShadow.cpp @@ -106,7 +106,7 @@ void DoShadows(RenderInfoClass & rinfo, Bool stencilPass) void DoDecals(RenderInfoClass & rinfo) { if (TheW3DProjectedShadowManager) - TheW3DProjectedShadowManager->renderDecals(rinfo); + TheW3DProjectedShadowManager->renderDecals(rinfo, true); //above-water subset } W3DShadowManager::W3DShadowManager( void ) diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp index 41d04f77a23..51263250c6d 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DScene.cpp @@ -877,9 +877,9 @@ void RTS3DScene::Flush(RenderInfoClass & rinfo) WW3D::Render_And_Clear_Static_Sort_Lists(rinfo); //draws things like water - //optionally draw radius decals AFTER water so they show over it (still depth-tested, so objects stay on top). - if (m_customPassMode == SCENE_PASS_DEFAULT && Get_Extra_Pass_Polygon_Mode() == EXTRA_PASS_DISABLE - && TheGlobalData->m_radiusDecalsAboveWater) + //draw the above-water decal subset AFTER water so those decals show over it (still depth-tested, so + //objects stay on top). Which decals qualify is decided per-decal (global flag + per-decal water mode). + if (m_customPassMode == SCENE_PASS_DEFAULT && Get_Extra_Pass_Polygon_Mode() == EXTRA_PASS_DISABLE) DoDecals(rinfo); if (m_customPassMode == SCENE_PASS_DEFAULT && Get_Extra_Pass_Polygon_Mode() == EXTRA_PASS_DISABLE) From b3869d11b1a06f6b16ee61e7a873f769de9dbf60 Mon Sep 17 00:00:00 2001 From: Andi Date: Mon, 10 Aug 2026 19:28:19 +0200 Subject: [PATCH 4/6] disable Shadow decal type for Decaldraw --- .../GameClient/Drawable/Draw/W3DDecalDraw.cpp | 19 ++++++++++++++- .../GameClient/Shadow/W3DProjectedShadow.cpp | 24 +++++++++++++------ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp index 9416e33e916..aefab313950 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp @@ -125,7 +125,24 @@ void W3DDecalDraw::init_shadow() strlcpy(shadowInfo.m_ShadowName, data->m_textureName.str(), ARRAY_SIZE(shadowInfo.m_ShadowName)); shadowInfo.allowUpdates = FALSE; //shadow image will never update shadowInfo.allowWorldAlign = TRUE; //shadow image will wrap around world objects - shadowInfo.m_type = data->m_type; + + // W3DDecalDraw is a standalone FX decal, not a real object shadow. Only the decal-use blend types + // are valid here; SHADOW_DECAL (and other shadow/projection/volume types) route into object-shadow + // code paths that assume a shadow-casting object + runtime shadow texture, which crashes for stacked + // FX decals. Coerce anything else to SHADOW_ALPHA_DECAL. + ShadowType decalType; + if (data->m_type == SHADOW_ADDITIVE_DECAL) + decalType = SHADOW_ADDITIVE_DECAL; + else if (data->m_type == SHADOW_ALPHA_DECAL) + decalType = SHADOW_ALPHA_DECAL; + else + { + DEBUG_CRASH(("W3DDecalDraw: Style must be SHADOW_ALPHA_DECAL or SHADOW_ADDITIVE_DECAL (got 0x%x); " + "SHADOW_DECAL and shadow/projection types are not supported for FX decals and can crash. " + "Coercing to SHADOW_ALPHA_DECAL.", (Int)data->m_type)); + decalType = SHADOW_ALPHA_DECAL; + } + shadowInfo.m_type = decalType; shadowInfo.m_sizeX = data->m_decalSizeX; shadowInfo.m_sizeY = data->m_decalSizeY; shadowInfo.m_offsetX = 0.0f; // TODO diff --git a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DProjectedShadow.cpp b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DProjectedShadow.cpp index 58762b84e65..61f3860f5e6 100644 --- a/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DProjectedShadow.cpp +++ b/GeneralsMD/Code/GameEngineDevice/Source/W3DDevice/GameClient/Shadow/W3DProjectedShadow.cpp @@ -1025,10 +1025,21 @@ void W3DProjectedShadowManager::queueDecal(W3DProjectedShadow *shadow) Int numVerts = vertsPerRow *vertsPerColumn; //number of terrain vertices Int numIndex=(endX - startX) * (endY-startY)*6; //6 indices per terrain cell (2 triangles). + // Skip a pathologically large decal that can't fit a single buffer (would overrun the DISCARD lock). + if (numVerts > SHADOW_DECAL_VERTEX_SIZE || numIndex > SHADOW_DECAL_INDEX_SIZE) + return; + SHADOW_DECAL_VERTEX* pvVertices; UnsignedShort *pvIndices; - if (nShadowDecalVertsInBuf > (SHADOW_DECAL_VERTEX_SIZE-numVerts)) //check if room for model verts + // Decide a single flush for the whole decal: if EITHER the vertex or index buffer would overflow, + // flush and discard BOTH together. Checking them independently lets the two buffers desync (a decal + // uses ~6x more indices than verts, so the index buffer overflows first), which corrupts the shared + // batch bookkeeping and eventually makes DrawIndexedPrimitive read past the vertex buffer -> crash. + Bool needFlush = (nShadowDecalVertsInBuf > (SHADOW_DECAL_VERTEX_SIZE - numVerts)) || + (nShadowDecalIndicesInBuf > (SHADOW_DECAL_INDEX_SIZE - numIndex)); + + if (needFlush) { //flush the buffer by drawing the contents and re-locking again flushDecals(shadow->m_shadowTexture[0], shadow->m_type); if (shadowDecalVertexBufferD3D->Lock(0,numVerts*sizeof(SHADOW_DECAL_VERTEX),(unsigned char**)&pvVertices,D3DLOCK_DISCARD) != D3D_OK) @@ -1114,16 +1125,15 @@ void W3DProjectedShadowManager::queueDecal(W3DProjectedShadow *shadow) shadowDecalVertexBufferD3D->Unlock(); - if (nShadowDecalIndicesInBuf > (SHADOW_DECAL_INDEX_SIZE-numIndex)) //check if room for model verts - { //flush the buffer by drawing the contents and re-locking again - flushDecals(shadow->m_shadowTexture[0], shadow->m_type); - + // Use the SAME flush decision as the vertex buffer above so both buffers reset in lockstep. + // flushDecals + the vertex/batch-counter resets already happened in the vertex block; here we + // only need to discard-lock the index buffer and reset the index counters. + if (needFlush) + { if (shadowDecalIndexBufferD3D->Lock(0,numIndex*sizeof(short),(unsigned char**)&pvIndices,D3DLOCK_DISCARD) != D3D_OK) return; nShadowDecalStartBatchIndex=0; - nShadowDecalPolysInBatch=0; //reset number of polys in texture batch - nShadowDecalVertsInBatch=0; nShadowDecalIndicesInBuf=0; } else From f4e46a29448bba28957d704d8c6d895fe2896ade Mon Sep 17 00:00:00 2001 From: Andi Date: Tue, 11 Aug 2026 15:50:09 +0200 Subject: [PATCH 5/6] Additive decal shading fixes --- .../GameEngine/Include/GameClient/Shadow.h | 18 ++++++++++++------ .../GameEngine/Include/GameClient/Shadow.h | 18 ++++++++++++------ 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/Generals/Code/GameEngine/Include/GameClient/Shadow.h b/Generals/Code/GameEngine/Include/GameClient/Shadow.h index 33f032e0887..29783b39ee8 100644 --- a/Generals/Code/GameEngine/Include/GameClient/Shadow.h +++ b/Generals/Code/GameEngine/Include/GameClient/Shadow.h @@ -188,9 +188,12 @@ inline void Shadow::setOpacity(Int value) if (m_type & SHADOW_ADDITIVE_DECAL) { Real fvalue=(Real)m_opacity/255.0f; - m_diffuse=REAL_TO_INT(((Real)(m_color & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 8) & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 16) & 0xff) * fvalue)); + // Premultiply each channel by opacity and pack into its correct byte (D3DCOLOR 0xAARRGGBB). + // Additive blend (ONE/ONE) ignores alpha, so fade is done by scaling RGB toward black. + Int r = REAL_TO_INT((Real)((m_color >> 16) & 0xff) * fvalue); + Int g = REAL_TO_INT((Real)((m_color >> 8) & 0xff) * fvalue); + Int b = REAL_TO_INT((Real)( m_color & 0xff) * fvalue); + m_diffuse = (r << 16) | (g << 8) | b; } } } @@ -208,9 +211,12 @@ inline void Shadow::setColor(Color value) if (m_type & SHADOW_ADDITIVE_DECAL) { Real fvalue=(Real)m_opacity/255.0f; - m_diffuse=REAL_TO_INT(((Real)(m_color & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 8) & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 16) & 0xff) * fvalue)); + // Premultiply each channel by opacity and pack into its correct byte (D3DCOLOR 0xAARRGGBB). + // Additive blend (ONE/ONE) ignores alpha, so fade is done by scaling RGB toward black. + Int r = REAL_TO_INT((Real)((m_color >> 16) & 0xff) * fvalue); + Int g = REAL_TO_INT((Real)((m_color >> 8) & 0xff) * fvalue); + Int b = REAL_TO_INT((Real)( m_color & 0xff) * fvalue); + m_diffuse = (r << 16) | (g << 8) | b; } } } diff --git a/GeneralsMD/Code/GameEngine/Include/GameClient/Shadow.h b/GeneralsMD/Code/GameEngine/Include/GameClient/Shadow.h index 1b6ff6fa544..9e1cb49b13d 100644 --- a/GeneralsMD/Code/GameEngine/Include/GameClient/Shadow.h +++ b/GeneralsMD/Code/GameEngine/Include/GameClient/Shadow.h @@ -193,9 +193,12 @@ inline void Shadow::setOpacity(Int value) if (m_type & SHADOW_ADDITIVE_DECAL) { Real fvalue=(Real)m_opacity/255.0f; - m_diffuse=REAL_TO_INT(((Real)(m_color & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 8) & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 16) & 0xff) * fvalue)); + // Premultiply each channel by opacity and pack into its correct byte (D3DCOLOR 0xAARRGGBB). + // Additive blend (ONE/ONE) ignores alpha, so fade is done by scaling RGB toward black. + Int r = REAL_TO_INT((Real)((m_color >> 16) & 0xff) * fvalue); + Int g = REAL_TO_INT((Real)((m_color >> 8) & 0xff) * fvalue); + Int b = REAL_TO_INT((Real)( m_color & 0xff) * fvalue); + m_diffuse = (r << 16) | (g << 8) | b; } } } @@ -213,9 +216,12 @@ inline void Shadow::setColor(Color value) if (m_type & SHADOW_ADDITIVE_DECAL) { Real fvalue=(Real)m_opacity/255.0f; - m_diffuse=REAL_TO_INT(((Real)(m_color & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 8) & 0xff) * fvalue)) - |REAL_TO_INT(((Real)((m_color >> 16) & 0xff) * fvalue)); + // Premultiply each channel by opacity and pack into its correct byte (D3DCOLOR 0xAARRGGBB). + // Additive blend (ONE/ONE) ignores alpha, so fade is done by scaling RGB toward black. + Int r = REAL_TO_INT((Real)((m_color >> 16) & 0xff) * fvalue); + Int g = REAL_TO_INT((Real)((m_color >> 8) & 0xff) * fvalue); + Int b = REAL_TO_INT((Real)( m_color & 0xff) * fvalue); + m_diffuse = (r << 16) | (g << 8) | b; } } } From 65edd6fb558235c0d86ca10974d6dc55a0622fcd Mon Sep 17 00:00:00 2001 From: Andi Date: Thu, 13 Aug 2026 10:36:48 +0200 Subject: [PATCH 6/6] decal extensions --- Core/GameEngine/Source/GameClient/FXList.cpp | 24 ++++++++++++------- .../GameClient/Drawable/Draw/W3DDecalDraw.cpp | 11 ++++++--- .../GameEngine/Source/GameClient/Drawable.cpp | 4 ++++ 3 files changed, 28 insertions(+), 11 deletions(-) diff --git a/Core/GameEngine/Source/GameClient/FXList.cpp b/Core/GameEngine/Source/GameClient/FXList.cpp index 5251fc4e244..782db55df3a 100644 --- a/Core/GameEngine/Source/GameClient/FXList.cpp +++ b/Core/GameEngine/Source/GameClient/FXList.cpp @@ -481,11 +481,8 @@ class DecalFXNugget : public FXNugget DecalFXNugget() { - m_templateName.set("GenericDecal"); // TODO - //m_templateName = AsciiString::TheEmptyString; - //m_textureName = AsciiString::TheEmptyString; - //m_opacity = 1.0; ///< value between 0 and 1 - //m_color = 0; ///< color in ARGB format. (Alpha is ignored). + // m_templateNames left empty; doFXPos falls back to "GenericDecal" if none listed. + m_scale.setRange(1.0f, 1.0f, GameClientRandomVariable::CONSTANT); // default = no scale variance m_lifetime = 0; /* m_fadeOutTime = 0; m_fadeInTime = 0; @@ -514,7 +511,12 @@ class DecalFXNugget : public FXNugget } } - Drawable* drawable = TheThingFactory->newDrawable(TheThingFactory->findTemplate(m_templateName)); + // pick one of the listed decal templates at random (fall back to GenericDecal if none listed) + AsciiString tmplName = m_templateNames.empty() + ? AsciiString("GenericDecal") + : m_templateNames[GameClientRandomValue(0, (Int)m_templateNames.size() - 1)]; + + Drawable* drawable = TheThingFactory->newDrawable(TheThingFactory->findTemplate(tmplName)); if (!drawable) return; @@ -531,6 +533,10 @@ class DecalFXNugget : public FXNugget if (m_randomAngle) drawable->setOrientation(GameClientRandomValueReal(0, PI * 2)); + // apply per-spawn random uniform scale variance (default range 1..1 = no change); + // W3DDecalDraw multiplies its decal size by the drawable's instance scale. + drawable->setInstanceScale(drawable->getInstanceScale() * m_scale.getValue()); + drawable->setExpirationDate(TheGameLogic->getFrame() + m_lifetime); } else @@ -555,7 +561,8 @@ class DecalFXNugget : public FXNugget { static const FieldParse myFieldParse[] = { - { "DecalName", INI::parseAsciiString, nullptr, offsetof(DecalFXNugget, m_templateName) }, + { "DecalName", INI::parseAsciiStringVectorAppend, nullptr, offsetof(DecalFXNugget, m_templateNames) }, + { "Scale", INI::parseGameClientRandomVariable, nullptr, offsetof(DecalFXNugget, m_scale) }, { "Lifetime", INI::parseDurationUnsignedInt, nullptr, offsetof(DecalFXNugget, m_lifetime) }, { "Offset", INI::parseCoord3D, nullptr, offsetof(DecalFXNugget, m_offset) }, { "Angle", INI::parseReal, nullptr, offsetof(DecalFXNugget, m_angle) }, @@ -571,7 +578,8 @@ class DecalFXNugget : public FXNugget } private: - AsciiString m_templateName; + std::vector m_templateNames; ///< one is picked at random per spawn ("DecalName", repeatable) + GameClientRandomVariable m_scale; ///< random uniform size factor per spawn ("Scale = low high") UnsignedInt m_lifetime; Coord3D m_offset; Real m_angle; diff --git a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp index aefab313950..7c832358f14 100644 --- a/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp +++ b/Core/GameEngineDevice/Source/W3DDevice/GameClient/Drawable/Draw/W3DDecalDraw.cpp @@ -143,8 +143,10 @@ void W3DDecalDraw::init_shadow() decalType = SHADOW_ALPHA_DECAL; } shadowInfo.m_type = decalType; - shadowInfo.m_sizeX = data->m_decalSizeX; - shadowInfo.m_sizeY = data->m_decalSizeY; + // honor the drawable's per-instance scale so FX-nugget scale variance affects decal size + Real scale = getDrawable()->getInstanceScale(); + shadowInfo.m_sizeX = data->m_decalSizeX * scale; + shadowInfo.m_sizeY = data->m_decalSizeY * scale; shadowInfo.m_offsetX = 0.0f; // TODO shadowInfo.m_offsetY = 0.0f; // TODO shadowInfo.m_waterRenderMode = data->m_renderAboveWater ? SHADOW_WATER_ABOVE : SHADOW_WATER_BELOW; @@ -169,8 +171,11 @@ void W3DDecalDraw::init_renderBox(const Matrix3D* transformMtx) { const W3DDecalDrawModuleData* data = getW3DDecalDrawModuleData(); + // honor the drawable's per-instance scale so FX-nugget scale variance affects decal size + Real scale = getDrawable()->getInstanceScale(); + Vector3 center = { 0, 0, 0 }; - Vector3 extent = { data->m_decalSizeX, data->m_decalSizeY, 1.0f }; + Vector3 extent = { data->m_decalSizeX * scale, data->m_decalSizeY * scale, 1.0f }; m_renderBox = NEW OBBoxRenderObjClass( OBBoxClass(center, extent) diff --git a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp index d2e38d18e3f..0bf2279aad5 100644 --- a/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp +++ b/GeneralsMD/Code/GameEngine/Source/GameClient/Drawable.cpp @@ -3598,6 +3598,10 @@ void Drawable::drawEnthusiastic(const IRegion2D* healthBarRegion) // // only display if have enthusiasm + // hardcoded fix to prevent projectiles that got the bonus from the launcher from showing the icon + if (obj->isKindOf(KINDOF_PROJECTILE)) + return; + if( obj->testWeaponBonusCondition( WEAPONBONUSCONDITION_ENTHUSIASTIC ) == TRUE && healthBarRegion != nullptr ) {