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:
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:
// on changing the speed
Vector2 previousSpeed = // whatever the last xy speed values sent to the material were
Vector2 previousOffset = // whatever the last offset values sent to the material were
Vector2 previousFrameTotalOffset = (previousSpeed * (Time.timeSinceLevelLoad - Time.deltaTime) + previousOffset);
previousFrameTotalOffset.x = previousFrameTotalOffset.x % 1.0f;
previousFrameTotalOffset.y = previousFrameTotalOffset.y % 1.0f;
Vector2 newSpeed = // new random speed
Vector2 newTotalOffset = newSpeed * Time.timeSinceLevelLoad;
newTotalOffset.x = newTotalOffset.x % 1.0f;
newTotalOffset.y = newTotalOffset.y % 1.0f;
Vector2 newOffset = previousFrameTotalOffset - newTotalOffset;
// 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.