Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Calculating waves in C# (to get water height) [Resolved]

Discussion in 'Scripting' started by Meta_Bird, Nov 8, 2022.

  1. Meta_Bird

    Meta_Bird

    Joined:
    Apr 25, 2020
    Posts:
    15
    I have a vertex displacement shader (in shader graph) that is working adequately to displace a surface to create ocean waves (its a basic Gerstner-like wave function).

    I'm now trying to create an identical version of the math within the shader in a script so that I can get the height of the water at any position.

    What I can't make sense of is that it looks to me like the script should generate the same results as the shader but the two quickly diverge.

    The Displacement method generates 5 new displacements (Vector3s) based on 5 different sets of parameters grabbed from the material (direction, amplitude and time), adds them together, and adds them to the original position.

    If I run the Displacement method and the shader once they both produce the identical result (I'm using a debug node in shader graph to test this). But quickly the script generates Vector3 displacements that the majority of time are positive on the Y axis. And so, according to the script the water height is pretty much constantly rising. Whereas the shader definitely does not behave this way.

    Evidently my math isn't good enough to understanding what is causing this, nor is my understanding of shaders and coding good enough to figure out why the two are producing different results.

    I appreciate this is a lot to ask. If anyone has any tips or pointers, that would be very much appreciated.



    For reference, here is the script:

    private void FixedUpdate()
    {
    testVector = Displacement(testVector);
    }

    private Vector3 Displacement(Vector3 position)
    {
    displacement1 = Wave(position, forwardBack1, amplitude1, timeScales1 * Time.time);
    displacement2 = Wave(position, forwardBack2, amplitude2, timeScales2 * Time.time);
    displacement3 = Wave(position, leftRight1, amplitude3, timeScales3 * Time.time);
    displacement4 = Wave(position, leftRight2, amplitude4, timeScales4 * Time.time);
    displacement5 = Wave(position, diagonal, amplitude5, timeScale5 * Time.time);

    newDisplacement = ((displacement1 + displacement2) + (displacement3 + displacement4)) + displacement5;
    newPosition = newDisplacement + position;

    return newPosition;
    }

    private Vector3 Wave(Vector3 position, Vector3 direction, float amplitude, float time)
    {
    float theta = Theta(position, direction, time);

    // Calculate X
    float x = Mathf.Sin(theta) * WaveInput(direction, direction.x, amplitude);
    float negateX = -1 * x;

    // Calculate Y
    float y = Mathf.Cos(theta) * amplitude;

    // Calculate Z
    float z = Mathf.Sin(theta) * WaveInput(direction, direction.z, amplitude);
    float negateZ = -1 * z;

    return new Vector3(negateX, y, negateZ);
    }

    private float WaveInput(Vector3 direction, float axis, float amplitude)
    {
    double d = amplitude / Math.Tanh(direction.magnitude * depth);
    float dToFloat = Convert.ToSingle(d);
    float input = (axis / direction.magnitude) * dToFloat;
    return input;
    }

    private float Theta(Vector3 position, Vector3 direction, float time)
    {
    float x = (direction.x * position.x) + (direction.z * position.z);
    float theta = (x - (Frequency(direction) * time)) - phase;
    return theta;
    }

    private float Frequency(Vector3 v)
    {
    float vectorLength = v.magnitude;
    float x = Convert.ToSingle((gravity * vectorLength) * Math.Tanh(vectorLength * depth));
    float frequency = Mathf.Sqrt(x);
    return frequency;
    }


    And the shader
    As input, the shader uses:
    • the absolute world position of the vertices,
    • 5 different "directions" (Vector3s),
    • 5 different "amplitudes" (floats)
    • 5 different "time" values multiplied by Time
    It adds together 5 "Waves" generated by the Wave subshader and adds that to the position. Then converts the new absolute world position back to object position.

    The Wave (and WaveInput) Subshader

    upload_2022-11-8_11-20-39.png

    Theta
    upload_2022-11-8_11-12-11.png

    Frequency
    upload_2022-11-8_11-22-0.png
     

    Attached Files:

  2. Niter88

    Niter88

    Joined:
    Jul 24, 2019
    Posts:
    112
    You could do the math on CPU and use it on both.
    You could use a Compute Shader to do the math on the GPU and get the data back to the CPU (faster)

    Best to avoid doing the same calculations on CPU and GPU. Things are a little different on one another.
     
  3. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,722
    I disagree with this.

    First: you absolutely cannot do the calculation on the CPU and use it on the GPU for waves. The fact is that the GPU is going to be calculating wave height for every vertex in the mesh. It would not be efficient to do that much calculation on the CPU.

    The CPU will only need to do the calculation for specific points of interest you care about (for example the position of individual boats on the water). This is manageable for the CPU.

    As for doing the calculation on the GPU and transferring it back to the CPU, that will most likely be much, much slower than simply repeating the calculation as needed for specific points of interest. It will also require you to write a compute shader. Furthermore there won't be a good way to request wave heights for specific positions, so you will need to get the entire wave height array or whatever, which is way more data than you need.

    Therefore I advocate for duplicating the calculation as needed. That being said I'm not sure exactly where your calculations are diverging. I'd check on things like making sure you're using radians for trig functions etc.
     
    Niter88, Bunny83 and Meta_Bird like this.
  4. Meta_Bird

    Meta_Bird

    Joined:
    Apr 25, 2020
    Posts:
    15
    Thanks for the input!
    Ideally, I'd like to stick with the existing approach.

    I had the same thought re trig functions and I think I've accounted for it. (My understanding is Mathf.cos and Mathf.sin and the Cos and Sin shader nodes all take radians as their input float. And (i believe) what I'm passing into them is identical in the shader and the script).

    I feel like I am overlooking something dumb and I can't see the forest for the trees here.
    Maybe something stupid like misunderstanding the output of the Position node.
     
  5. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    Yes. I did something similar for log polar grid. Though CPU does need only a portion of the math, and as you said it rarely needs to do more than a couple of points at a time. Whereas the shader is chewing through the entire screen the whole time. My case was slightly specific, but that's a good advice in general.
     
    PraetorBlue likes this.
  6. karliss_coldwild

    karliss_coldwild

    Joined:
    Oct 1, 2020
    Posts:
    530
    This is just a guess but are you sure you have to update the position in a feedback loop feeding in the wave position from previous step? From what I understand the vertex shader always gets the base position from original mesh as input instead of modified position from previous frame.

    So try
    Code (CSharp):
    1. private void FixedUpdate()
    2. {
    3.     waveTop = Displacement(input); // where input comes from mouse, keyboard or whatever your logic but don't assign result back to input
    4. }
    5. // instead of
    6. private void FixedUpdate()
    7. {
    8.      testVector = Displacement(testVector); // next iteration will get modified testVector instead of x/z position at base level
    9. }
    If the code is really meant to update position in a feedback loop, I would suggest looking for a different wave formula. In theory integral of sin(x) is still somekind of sinusoidal with some offsets and should remain in fixed range. But in practice due to floating point precision limits and also potential minor variations in timestep, I wouldn't be surprised if it drifts over time. Especially when evaluating on two different kind of floating point units (GPU and CPU).
     
    Meta_Bird and PraetorBlue like this.
  7. Meta_Bird

    Meta_Bird

    Joined:
    Apr 25, 2020
    Posts:
    15
    Yes! Thank you karliss_codwild!

    I had tried:
    Code (CSharp):
    1.         private void FixedUpdate()
    2.         {
    3.             testVector += Displacement(startingPos);
    4.         }
    And this didn't seem to work.
    But following your input I got the floating object to ask this script for the wave height based on the object's starting position and this works exactly as I had hoped. Thank you!


     
    Last edited: Nov 9, 2022
    Niter88 likes this.
  8. orionsyndrome

    orionsyndrome

    Joined:
    May 4, 2014
    Posts:
    3,043
    You are right about that, but that's easily solvable by introducing time as a shader parameter. CPU should handle time and drive the shader, I think that's the easiest and most reliable setup.
     
    Niter88 and PraetorBlue like this.