Skip to content

Conversation

@Caball009
Copy link

@Caball009 Caball009 commented Dec 8, 2025

Fixes issue: #1953

This PR makes two visual changes relating to the opacity step of detected stealth models.

There are 2 commits, with 2 slightly different implementations. The first was an attempt to make the code more optimized. It has a downside: if the render frame rate is fluctuating a lot, the opacity step can be slightly irregular sometimes but nothing crazy. The second calculates the opacity step scalar when rendering. This gives the best results but is more costly.

Before

  1. Stealth detected models were flickering when the render frame rate exceeds 30:
stealth_detected_model_flickering_before.mp4
  1. Stealth detected models were pulsating too quickly when units / buildings were detected by multiple units / special powers:
stealth_detected_model_pulsating_before.mp4

After

  1. Opacity step should now look the same for (almost) all render frame rates:
stealth_detected_model_flickering_after.mp4
  1. Multiple detections make (almost) no difference to the pulsating models.
stealth_detected_model_pulsating_after.mp4

TODO

  • Decide on implementation
  • Replicate in Generals

… from render update

Scalar value is calculated ahead of rendering.
… from render update

Scalar value is NOT calculated ahead of rendering.
@Caball009 Caball009 added Minor Severity: Minor < Major < Critical < Blocker Rendering Is Rendering related labels Dec 8, 2025
@Caball009 Caball009 linked an issue Dec 8, 2025 that may be closed by this pull request
@MrS-ibra
Copy link

MrS-ibra commented Dec 9, 2025

This also fixes #1719 , they are the same issue

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused by the 2 commits. Perhaps open 2 Pulls with each proposal?

Overall, I do not understand why the m_secondMaterialPassOpacity evolution is made so complicated.

// TheSuperHackers @tweak The opacity step is now decoupled from the render update.
// minOpacity = (X ^ (framerate / updatesPerSec)) -> e.g. [ 0.05 = X ^ (100 / 2) ] -> [ X = 0.941845 ] -> [ 0.941845 ^ 50 = 0.05 ]
constexpr const Real minOpacity = 0.05f;
constexpr const Real updatesPerSec = 2.0f; // (LOGICFRAMES_PER_MSEC_REAL / data->m_updateRate (15))
Copy link

@xezon xezon Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the value code commented? Edit: This needs to be fixed

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another question: Does this still work correctly when pausing the game?

Copy link
Author

@Caball009 Caball009 Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, the opacity keeps decreasing and at some point it gets below the minimum of 0.001 and so it gets set to 0. At that point the regular model is shown instead. This'll need fixing.

if (theirDraw && !them->isKindOf(KINDOF_MINE))
{
theirDraw->setSecondMaterialPassOpacity( 1.0f );
constexpr const Real minOpacity = 0.05f;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this value should be part of drawable data? It now exists at 2 places and it needs to be in sync?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, having it in two places is unfortunate. I think making it a part of the drawable date should work.

@Caball009
Copy link
Author

Caball009 commented Dec 9, 2025

I am confused by the 2 commits.

The second commit is a less optimized but simpler implementation of the first commit, that's all.

Perhaps open 2 Pulls with each proposal?

I think we can forget about the first commit if we decide that calculating the scalar value on the fly is acceptable.

Overall, I do not understand why the m_secondMaterialPassOpacity evolution is made so complicated.

Right, I'll add an explanation. This was the original behavior assuming 30 logic frames and 30 render frames per second.

  1. StealthDetectorUpdate::update tends to be called every 15 logic frames (dependent on StealthDetectorUpdateModuleData::m_updateRate) for every stealth detector and sets the opacity value back to 1.0.
  2. Drawable::draw multiplies the opacity by MATERIAL_PASS_OPACITY_FADE_SCALAR (0.8).
  3. For a single stealth detector, the lowest opacity value is equal to 0.8 ^ 15 = 0.035184.
  4. With 70 render frames, this becomes 0.8 ^ 35 = 0.000405, which is below the minimum threshold VERY_TRANSPARENT_MATERIAL_PASS_OPACITY (0.001) and so the original model starts to become visible.

It's not overly complicated, but since the opacity decreases logarithmically, we need to calculate the right scalar value.

Where does this number come from?

See point 3. I've increased it slightly to a more sensible number.

Copy link

@xezon xezon left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I understand the logic now and it looks correct. We need to improve this change a bit.

constexpr const Real minOpacity = 0.05f;
constexpr const Real resetThreshold = 2 * minOpacity;

// TheSuperHackers @tweak Reset opacity only below threshold to prevent models flickering from multiple detections or special powers.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean multiple detectors will reduce the m_secondMaterialPassOpacity faster?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the opposite. Multiple detectors (and also increased logic frame rate) would set the opacity back to 1.0f faster / more often without this check.

Copy link

@xezon xezon Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok I understand. Would it perhaps be possible to couple the reset at exactly 15 frames (aka updateRate), instead of testing for reaching this min limit? Maybe build a logic frame counter for it?

Then we also do not need MinOpacity at 2 places.

Copy link
Author

@Caball009 Caball009 Dec 13, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That'd mean the pulsating effect increases as the logic frame rate increases; e.g. at 60 logic frames the effect goes through 4 cycles per second instead of 2.
Does that make sense for things that are purely visual? I'm aware that some other visual effects currently behave this way (such as the clouds, water, trees, and the lights around the supply dock), but I'm not sure if that's desirable.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right that makes no sense. We do not want it to be bound to plain logic frames. But you can scale the updateRate by LOGICFRAMES_PER_SECOND/BaseFps, which will be 15*2 at 60 logic fps. I think it would be interesting to know if there are issues with pausing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Minor Severity: Minor < Major < Critical < Blocker Rendering Is Rendering related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enemy stealthed units/buildings reveal animation flicker or animate too quickly at higher render FPS

3 participants