0

Context

I've been trying to create a buoyancy script that samples the position of a point, tests if it's under a certain level (the "water level"), and adds a force on that position based on depth. Separately, I worked on creating a nice looking water shader in Shadergraph, and had the bright idea to add in waves using the Simple Noise node + vertex displacement.

However, the only way (I could think of) to use those displaced values as the float "water level" was to rewrite the entire node tree in C#, and use that to sample the "water level" at that position.

Problem

For some reason, the final displaced mesh and the calculated positions are different, causing the buoyancy script to assume that the "water level" is higher/lower than it is. The difference isn't large, so I'm assuming there's an error somewhere within either the C# Node Graph or C# Simple Noise translation.

Is that correct? If so, where and what's my misunderstanding? If not, what else could have gone wrong?

Approach

Node Graph

Image of the node graph for the wave vector displacement

*If you need zoomed in pictures, let me know!

All things considered, it's relatively simple. It:

  1. Takes the world position as a UV, and offsets and tiles it.
  2. Feeds the UV to a Simple Noise node, and multiplies the noise by a strength.
  3. Clamps the output.
  4. Repeats 1-3 again and adds both together for more detail.
  5. Replaces the Y value of the vertex position with the combined wave value.

C# Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WaveHeightCalculator : MonoBehaviour
{
    // Step 1
    [SerializeField] Material _waterMaterial;

    [Header("Waves")]
    [SerializeField] float _waveTiling;
    [SerializeField] float _waveOffset;
    [SerializeField] float _waveMin;
    [SerializeField] float _waveMax;
    [Header("Small Waves")]
    [SerializeField] float _wavesSmallScale;
    [SerializeField] float _wavesSmallStrength;
    [SerializeField] Vector2 _wavesSmallVelocity;
    [Header("Large Waves")]
    [SerializeField] float _wavesLargeScale;
    [SerializeField] float _wavesLargeStrength;
    [SerializeField] Vector2 _wavesLargeVelocity;
    
    // Step 2
    private void OnValidate()
    {
        _waterMaterial = GetComponent<Renderer>().sharedMaterial;
        SetVariables();
    }
    void SetVariables()
    {
        _waveTiling = _waterMaterial.GetFloat("_Wave_Tiling");
        _waveOffset = _waterMaterial.GetFloat("_Wave_Offset");
        _waveMin = _waterMaterial.GetFloat("_Wave_Min");
        _waveMax = _waterMaterial.GetFloat("_Wave_Max");
        _wavesSmallScale = _waterMaterial.GetFloat("_Waves_Small_Scale");
        _wavesSmallStrength = _waterMaterial.GetFloat("_Waves_Small_Strength");
        _wavesSmallVelocity = _waterMaterial.GetVector("_Waves_Small_Velocity");
        _wavesLargeScale = _waterMaterial.GetFloat("_Waves_Large_Scale");
        _wavesLargeStrength = _waterMaterial.GetFloat("_Waves_Large_Strength");
        _wavesLargeVelocity = _waterMaterial.GetVector("_Waves_Large_Velocity");
    }

    // Step 3
    public float GetWaveHeightAtPosition(Vector3 position)
    {
        Vector2 noiseMapUV;
        noiseMapUV = new Vector2(position.x, position.z) * _waveTiling;
        // Calculate Small Waves
        Vector2 wavesSmallUVOffset = (Time.time / 20) * _wavesSmallVelocity;
        float noiseValueAtUVPlusOffset = UnitySimpleNoiseAtUV(noiseMapUV + wavesSmallUVOffset, _wavesSmallScale);
        float wavesSmall = noiseValueAtUVPlusOffset * _wavesSmallStrength;
        // Calculate Large Waves
        Vector2 wavesLargeUVOffset = (Time.time / 20) * _wavesLargeVelocity;
        noiseValueAtUVPlusOffset = UnitySimpleNoiseAtUV(noiseMapUV + wavesLargeUVOffset, _wavesLargeScale);
        float wavesLarge = noiseValueAtUVPlusOffset * _wavesLargeStrength;
        // Combine
        float waveHeight = wavesSmall + wavesLarge;
        // Clamp
        waveHeight = Mathf.Clamp(waveHeight, _waveMin, _waveMax);
        // Offset
        waveHeight += _waveOffset;
        return waveHeight;
    }

In the C# script, a couple of things are going on. Here's my thought process for it:

  1. It assigns the relevant material properties to member variables.
  2. It sets those variables in the OnValidate() function.
  3. It uses those variables to calculate the wave value; equivalent to the "water level".

The script also contains and relies on my best attempt at translating the Simple Noise node from "Show Generated Code", which looked liked this.

Generated Code

inline float Unity_SimpleNoise_RandomValue_float (float2 uv)
{
    float angle = dot(uv, float2(12.9898, 78.233));
    #if defined(SHADER_API_MOBILE) && (defined(SHADER_API_GLES) || defined(SHADER_API_GLES3) || defined(SHADER_API_VULKAN))
        // 'sin()' has bad precision on Mali GPUs for inputs > 10000
        angle = fmod(angle, TWO_PI); // Avoid large inputs to sin()
    #endif
    return frac(sin(angle)*43758.5453);
}

inline float Unity_SimpleNnoise_Interpolate_float (float a, float b, float t)
{
    return (1.0-t)*a + (t*b);
}


inline float Unity_SimpleNoise_ValueNoise_float (float2 uv)
{
    float2 i = floor(uv);
    float2 f = frac(uv);
    f = f * f * (3.0 - 2.0 * f);

    uv = abs(frac(uv) - 0.5);
    float2 c0 = i + float2(0.0, 0.0);
    float2 c1 = i + float2(1.0, 0.0);
    float2 c2 = i + float2(0.0, 1.0);
    float2 c3 = i + float2(1.0, 1.0);
    float r0 = Unity_SimpleNoise_RandomValue_float(c0);
    float r1 = Unity_SimpleNoise_RandomValue_float(c1);
    float r2 = Unity_SimpleNoise_RandomValue_float(c2);
    float r3 = Unity_SimpleNoise_RandomValue_float(c3);

    float bottomOfGrid = Unity_SimpleNnoise_Interpolate_float(r0, r1, f.x);
    float topOfGrid = Unity_SimpleNnoise_Interpolate_float(r2, r3, f.x);
    float t = Unity_SimpleNnoise_Interpolate_float(bottomOfGrid, topOfGrid, f.y);
    return t;
}
void Unity_SimpleNoise_float(float2 UV, float Scale, out float Out)
{
    float t = 0.0;

    float freq = pow(2.0, float(0));
    float amp = pow(0.5, float(3-0));
    t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;

    freq = pow(2.0, float(1));
    amp = pow(0.5, float(3-1));
    t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;

    freq = pow(2.0, float(2));
    amp = pow(0.5, float(3-2));
    t += Unity_SimpleNoise_ValueNoise_float(float2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;

    Out = t;
}

    /* WARNING: $splice Could not find named fragment 'CustomInterpolatorPreVertex' */

    // Graph Vertex
    // GraphVertex: <None>

    /* WARNING: $splice Could not find named fragment 'CustomInterpolatorPreSurface' */

    // Graph Pixel
    struct SurfaceDescription
{
float4 Out;
};

Translated Code

    float float_frac(float x) { return x - Mathf.Floor(x);}
    Vector2 frac(Vector2 x) { return x - new Vector2(Mathf.Floor(x.x), Mathf.Floor(x.y));}
    float sin(float x) { return Mathf.Sin(x);}
    float dot(Vector2 a, Vector2 b) { return a.x * b.x + a.y * b.y;}
    float float_floor(float x) { return Mathf.Floor(x);}
    Vector2 floor(Vector2 x) { return new Vector2(Mathf.Floor(x.x), Mathf.Floor(x.y));}
    float float_abs(float x) { return Mathf.Abs(x);}
    Vector2 abs(Vector2 x) { return new Vector2(Mathf.Abs(x.x), Mathf.Abs(x.y));}
    float pow (float x, float y) { return Mathf.Pow(x, y);}

    float Unity_SimpleNoise_RandomValue_float (Vector2 uv)
    {
        float angle = dot(uv, new Vector2(12.9898f, 78.233f));
        return float_frac(sin(angle) * 43758.5453f);
    }
    float Unity_SimpleNnoise_Interpolate_float (float a, float b, float t)
    {
        return (1.0f - t) * a + (t * b);
    }
    float Unity_SimpleNoise_ValueNoise_float (Vector2 uv)
    {
        Vector2 i = floor(uv);
        Vector2 f = frac(uv);
        f = (f * f) * (new Vector2 (3.0f, 3.0f) - new Vector2(2.0f, 2.0f) * f);

        uv = abs(frac(uv) - new Vector2 (0.5f, 0.5f));
        Vector2 c0 = i + new Vector2(0.0f, 0.0f);
        Vector2 c1 = i + new Vector2(1.0f, 0.0f);
        Vector2 c2 = i + new Vector2(0.0f, 1.0f);
        Vector2 c3 = i + new Vector2(1.0f, 1.0f);
        float r0 = Unity_SimpleNoise_RandomValue_float(c0);
        float r1 = Unity_SimpleNoise_RandomValue_float(c1);
        float r2 = Unity_SimpleNoise_RandomValue_float(c2);
        float r3 = Unity_SimpleNoise_RandomValue_float(c3);

        float bottomOfGrid = Unity_SimpleNnoise_Interpolate_float(r0, r1, f.x);
        float topOfGrid = Unity_SimpleNnoise_Interpolate_float(r2, r3, f.x);
        float t = Unity_SimpleNnoise_Interpolate_float(bottomOfGrid, topOfGrid, f.y);
        return t;
    }
    float UnitySimpleNoiseAtUV(Vector2 UV, float Scale)
    {
        float t = 0.0f;

        float freq = pow(2.0f, 0);
        float amp = pow(0.5f, 3-0);
        t += Unity_SimpleNoise_ValueNoise_float(new Vector2(UV.x*Scale/freq, UV.y*Scale/freq))*amp;

        freq = pow(2.0f, 1);
        amp = pow(0.5f, 3-1);
        t += Unity_SimpleNoise_ValueNoise_float(new Vector2(UV.x * Scale / freq, UV.y * Scale / freq)) * amp;

        freq = pow(2.0f, 2);
        amp = pow(0.5f, 3-2);
        t += Unity_SimpleNoise_ValueNoise_float(new Vector2(UV.x * Scale / freq, UV.y * Scale / freq)) * amp;

        return t;
    }
feenix
  • 1
  • 1

1 Answers1

0

I have the same premise as you, and in fact used your translation of the simple noise node to get my calculation working.

Here's a few things I did differently that might have caused the calculation difference.

I used Time.timeSinceLevelLoad rather than Time.time, as that's the one that the shader graph apparently uses as well (See here). Using Time.time may have caused some values to differ between frames so that might be the cause of some discrepancy.

I also used TransformPoint on the final Y coordinate I got in order to use the world coordinate of my water plane's vertex rather than its local coordinate. With my plane at (0,0,0), the shader causes the local y-value of the wave in the plane to be around y = 0.6, but in world space that y-value is actually around y=5.9 instead. So I needed that transformation as otherwise it give me the water plane's local height.

Here's my water height function, using shader from here.

public float GetWaterHeight(Vector3 position)
{
    //need to do the shader's y position calculation to get the height of the vertex at this position
    var uvInput = waveTile * (waveTileFactor * (new Vector2(position.x, position.z)));
    Debug.Log("Uv Input: " + uvInput);
    var offsetInput = waveSpeed * ((Time.timeSinceLevelLoad * new Vector2(0, 1)) / 20);
    Debug.Log("Offset Input: " + offsetInput + " with time as " + Time.timeSinceLevelLoad);
    var tilingOutput = uvInput * new Vector2(1, 1) + offsetInput;
    var noiseOutput = UnitySimpleNoiseAtUV(tilingOutput, waveScale);
    Debug.Log("Noise gen: " + noiseOutput + " from tilingOutput " + tilingOutput);
    var finalY = waveStrength * noiseOutput;

    //var pixelValue = oceanTex.GetPixelBilinear(transform.position.y, transform.position.z).b;
    return ocean.position.y + ocean.TransformPoint(new Vector3(0, finalY, 0)).y;
}

The rest of the code (for the simple noise function and whatnot) is the same as yours.

spyguy001
  • 71
  • 6