Figuring out terrain water wave height?

I’ve seen tons of questions and discussion related to finding how to calculate terrain water wave height. However, as far as I can tell nobody has actually got it working perfectly and without any hiccups.

I wanted to create this thread to get some discussion going towards solving this once and for all as a quick search will tell you a number of people are interested in this.

However, before getting into that I want to review a few useful things that have already been found. My hope is that this gets us all on some baseline level of knowledge.

The first bit of knowledge is this tweet:

I was able to reproduce it by adjusting the formula slightly: 1.8 * math.sin(TAU/85 * x + PI2) * math.sin(TAU/85 * z)

Note that this only seems to work with Terrain.WaterWaveSize = 1

The second bit of knowledge comes from this post:

A lot of useful stuff here, but definitely not 100% accurate and even if it was, it has a weird clause where you have to load in with wave speed at zero. Also, much like the first example it only seems accurate for certain values.

You can see examples of both these techniques in this place file:

Terrain water.rbxl (26.3 KB)

The last useful thing I’ve seen was tipped off to me by @Maximum_ADHD. If you look in the shader.pack files with a text editor you can find some machine generated shader code.

It’s not easily readable, but it may help us figure out some small details we would have had to guess otherwise.

So I guess here’s a summary of things that need to be solved:

  1. With WaterWaveSpeed = 0 can we find the height at any x and z combo for any Terrain.WaterWaveSize?
  2. Using @blobbyblob’s technique can we solve for when values of WaterWaveSpeed > 0?
  3. Is it possible to move away from blobby’s technique of forcing us to load in with WaterWaveSpeed = 0?

Number 3 may not be possible, but given what people have solved thus far I think 1 & 2 should be possible.


I just did some investigating of the decompiled shader code, here’s what I’ve found so far:

float _700 = _530.y + (((sin(((_665 - _667) * CB3[0].x) - CB3[0].y) + sin(((_665 + _667) * CB3[0].x) + CB3[0].y)) * CB3[0].z) * (_546 * clamp(1.0 - (dot(_530 - CB0[7].xyz, -CB0[6].xyz) * CB0[23].y), 0.0, 1.0)));

We know this line controls the water height since it has the sin functions, but it’s in a strange form that I hadn’t seen before. Anywho, I roughly translated this equation into something I could graph, here’s what I got:

constants A, B, C
water_y = (sin(((z - x) * A) - B) + sin(((z + x) * A) + B)) * C

If you look at the graphing window in the picture I posted, we can see that this odd equation gets the same wave pattern that we expect, so it is correct. Messing with the 3 constants, I found that A is the wavelength, B is the wave shift (so our time constant), C is our wave amplitude coefficient.

In other words, the variable that is influenced by time is CB3[0].y, or the Y value of the first vector4 in our uniform array of 3. From my limited knowledge/memory of glsl, uniforms are values defined in memory by a specific name which can be accessed by shaders, so if we could decompile Roblox source code, we could potentially search for the term CB3 and find some corresponding logic that would suggest where the time value is being fetched.


I wonder if the time constant is elapsedTime()?
(cc @zeuxcg)

1 Like

If this were true, you’d expect every time you make a frequency change in the waves (WaveWaterSpeed), it’d jump to a new phase. Basically, the waves would flicker.

It has to be some counter which takes into account the wave speed.


We’d also need to take into account the Y value of any bit of terrain water in a world. Otherwise, this would only work on y=0

Sampling Terrain Water Height at Any Position - Help and Feedback / Cool Creations - DevForum | Roblox


Excellent work! I just assumed it would be impossible to match up with Roblox’s internal clock used for the waves. It’s funny how close we were to cracking this all those years ago, the only piece we were missing was treating time as a counter, instead of some global time value. Ironically the answer was staring us in the face…

Now if only we could figure out what graphic setting the client is on, so we can properly adjust our game logic to account for waves existing or not existing based on graphics level. Too bad the API that does exist does not return any useful information when the setting is set to Automatic…