Search Unity

Resolved Animating overlay movement in shader - change direction without 'jumping'?

Discussion in 'General Graphics' started by UnbridledGames, Feb 7, 2021.

  1. UnbridledGames

    UnbridledGames

    Joined:
    May 12, 2020
    Posts:
    139
    I'm messing with some vertex shader code. I took a shader I found online and I'm tweaking it. Part of the shader animates the movement of an overlay texture by adding the texture's speed values in the X/Y directions, multiplied by the time:

    uv = uv + distortionSettings.zw * _Time.y;


    I added a script that every few seconds randomly changes the X/Y speed values stored in distortionSettings.zw to a new random value, so the texture randomly changes direction and speed.

    Problem is, the way the movement is handled, most changes cause the overlay to 'jump' as the values _Time.y is being multiplied by change.

    You can't keep a running counter of distance moved in the shader code (it would be updated every pixel drawn if I'm understanding it correctly) and it'd be difficult to keep track in the controlling script. I'd prefer not to have the controlling script update the shader every frame if it can be avoided.

    Is there a better way to handle this inside the shader itself? A way to track the current offset globally? Or a better way to handle the shift so it's not speed * time?
     
  2. bgolus

    bgolus

    Joined:
    Dec 7, 2012
    Posts:
    12,348
    There's no way for a shader to do this on it's own, no. You must use the script to help it out. The easiest approach is indeed to just track an offset directly in C# and update the material every frame. Especially if you're changing the speed frequently.

    However if you really are only changing it occasionally, there is an alternative. At the moment of change you can calculate the current offset, then the new offset, and pass the difference to the shader.

    So, your shader is going to want to look something like this:
    Code (csharp):
    1. uv += frac(_distortionSettings.zw * _Time.y + _distortionOffset);
    Note that much different. You might notice that beyond the offset I added
    frac()
    here. This is important, not for this particular solution, but in general. If you're going to offset a UV by
    _Time
    you want to make sure you wrap a frac() around that offset. Note, not the entire uv, only the offset. The reason is eventually you'll run out floating point precision and the texture will start to look really weird, like it's stretched or point sampled at increasingly lower resolutions.

    But for this particular solution, really all that matters is that added
    _distortionOffset
    .

    The real magic happens in C#.
    _Time.y
    and
    Time.timeSinceLevelLoad
    are equivalent*, so you can calculate the current offset created by
    _distortionSettings.zw * _Time.y
    fairly accurately. Just multiply the
    Vector2
    of the speed by
    Time.timeSinceLevelLoad
    and you've got the previous speed's total offset. Then do the same thing with the new random speed to get the new offset. Then subtract the previous offset from the new offset, and pass that along to the shader.

    Well, almost. I skipped a few minor things.

    First we're missing
    frac()
    , and there's no apparent function in
    Mathf
    or similar that's equivalent. So instead you need to use a modulo of 1.0f. In C# the modulus operator is the
    %
    symbol. So just wrap up your offsets in braces and add
    % 1.0f;
    at the end! (Actually, you can't do that because you can't use
    %
    with a
    Vector2
    , so you have to do it to each component.) And really, the code would work just fine even without it, ignoring the fore mentioned floating point problem.

    Second, the previous speed's total offset is missing whatever offset you passed to the material last! So we need to add that to the value too (before the modulo).

    Third, and last, we don't actually want the previous speed's total offset at the current time, we want it at the previous's frame's time too. Luckily that's easily remedied by subtracting
    Time.deltaTime
    from the
    Time.timeSinceLevelLoad
    . This one is also super minor and if you skipped it might not even be noticeable if your framerate isn't really bad.

    So, all those pieces together gets you something like this:
    Code (csharp):
    1. // on changing the speed
    2. Vector2 previousSpeed = // whatever the last xy speed values sent to the material were
    3. Vector2 previousOffset = // whatever the last offset values sent to the material were
    4.  
    5. Vector2 previousFrameTotalOffset = (previousSpeed * (Time.timeSinceLevelLoad - Time.deltaTime) + previousOffset);
    6. previousFrameTotalOffset.x = previousFrameTotalOffset.x % 1.0f;
    7. previousFrameTotalOffset.y = previousFrameTotalOffset.y % 1.0f;
    8.  
    9. Vector2 newSpeed = // new random speed
    10.  
    11. Vector2 newTotalOffset = newSpeed * Time.timeSinceLevelLoad;
    12. newTotalOffset.x = newTotalOffset.x % 1.0f;
    13. newTotalOffset.y = newTotalOffset.y % 1.0f;
    14.  
    15. Vector2 newOffset = previousFrameTotalOffset - newTotalOffset;
    16.  
    17. // pass newSpeed and newOffset to the material
    * Annoyingly
    _Time.y
    and
    Time.timeSinceLevelLoad
    can sometimes be one frame apart, with
    _Time.y
    being a frame behind. Doesn't seem to always be the case, and I don't think anyone has tracked down exactly when this is a problem. Just know some people have complained about this. Which puts a minor wrench in the presented code. Many people take to not using
    _Time
    at all and instead use
    Shader.SetGlobal
    to set their own global shader time that they can ensure matches up with C#. But, again, this one frame difference is usually small enough that no one will notice. Also note that the scene view time and the game view time are completely unrelated, so just expect it to look wrong in the scene view. Unless you're using your own global time.
     
    Shack_Man likes this.
  3. UnbridledGames

    UnbridledGames

    Joined:
    May 12, 2020
    Posts:
    139
    Thanks for the detailed answer. Was hoping there was an easier way but looks like a script is the best way to do it.