How do I create buoyancy for a water shader

:information_source: Attention Topic was automatically imported from the old Question2Answer platform.
:bust_in_silhouette: Asked By fUNNY-fnaf

Hi, I’m currently working on a sailing game, but I’m having trouble figuring out how to create a buoyancy script for the boat.
I’ve tried looking into collision of different kinds in order to check when the boat is beneath the water and therefore needs to be pushed back up. I’ve tried using a raycast, I’ve even tried to get all the vertices from the mesh and use that, but non of it has worked.

I’m using this shader
Here’s the shader code for those who don’t feel like clicking the link…

shader_type spatial;

uniform vec4 _WATER_COLOUR1 : hint_color = vec4(0.04, 0.38, 0.88, 1.0);
uniform vec4 _WATER_COLOUR2 : hint_color = vec4(0.04, 0.35, 0.78, 1.0);
uniform vec4 _FOAM_COLOUR : hint_color = vec4(0.8125, 0.9609, 0.9648, 1.0);

uniform float _DISTORTION_SPEED = 2.0;
uniform float _HEIGHT = 2.0;

uniform vec2 _TILE = vec2(5.0, 5.0);
uniform vec2 _WAVE_SIZE = vec2(2.0, 2.0);
uniform float _WAVE_SPEED = 1.5;

const float PI = 3.14159265359;


float random(vec2 uv) {
    return fract(sin(dot(uv.xy, vec2(12.9898,78.233))) * 43758.5453123);
}

float noise(vec2 uv) {
    vec2 uv_index = floor(uv);
    vec2 uv_fract = fract(uv);

    // The four corners of the 2D tile
    float a = random(uv_index);
    float b = random(uv_index + vec2(1.0, 0.0));
    float c = random(uv_index + vec2(0.0, 1.0));
    float d = random(uv_index + vec2(1.0, 1.0));

    vec2 blur = smoothstep(0.0, 1.0, uv_fract);

    return mix(a, b, blur.x) +
        (c - a) * blur.y * (1.0 - blur.x) +
        (d - b) * blur.x * blur.y;
}

float fbm(vec2 uv) {
    int octaves = 6;
    float amplitude = 0.5;
    float frequency = 3.0;
    float value = 0.0;

    for (int i = 0; i < octaves; i++) {
	    value += amplitude * noise(frequency * uv);
	    amplitude *= 0.5;
	    frequency *= 2.0;
    }
    return value;
}

float circ(vec2 pos, vec2 c, float s) {
    c = abs(pos - c);
c = min(c, 1.0 - c);

return smoothstep(0.0, 0.002, sqrt(s) - sqrt(dot(c, c))) * -1.0;
}

float waterlayer(vec2 uv) {
    uv = mod(uv, 1.0); // Clamp to [0..1]
    float ret = 1.0;
ret += circ(uv, vec2(0.37378, 0.277169), 0.0268181);
ret += circ(uv, vec2(0.0317477, 0.540372), 0.0193742);
ret += circ(uv, vec2(0.430044, 0.882218), 0.0232337);
ret += circ(uv, vec2(0.641033, 0.695106), 0.0117864);
ret += circ(uv, vec2(0.0146398, 0.0791346), 0.0299458);
ret += circ(uv, vec2(0.43871, 0.394445), 0.0289087);
ret += circ(uv, vec2(0.909446, 0.878141), 0.028466);
ret += circ(uv, vec2(0.310149, 0.686637), 0.0128496);
ret += circ(uv, vec2(0.928617, 0.195986), 0.0152041);
ret += circ(uv, vec2(0.0438506, 0.868153), 0.0268601);
ret += circ(uv, vec2(0.308619, 0.194937), 0.00806102);
ret += circ(uv, vec2(0.349922, 0.449714), 0.00928667);
ret += circ(uv, vec2(0.0449556, 0.953415), 0.023126);
ret += circ(uv, vec2(0.117761, 0.503309), 0.0151272);
ret += circ(uv, vec2(0.563517, 0.244991), 0.0292322);
ret += circ(uv, vec2(0.566936, 0.954457), 0.00981141);
ret += circ(uv, vec2(0.0489944, 0.200931), 0.0178746);
ret += circ(uv, vec2(0.569297, 0.624893), 0.0132408);
ret += circ(uv, vec2(0.298347, 0.710972), 0.0114426);
ret += circ(uv, vec2(0.878141, 0.771279), 0.00322719);
ret += circ(uv, vec2(0.150995, 0.376221), 0.00216157);
ret += circ(uv, vec2(0.119673, 0.541984), 0.0124621);
ret += circ(uv, vec2(0.629598, 0.295629), 0.0198736);
ret += circ(uv, vec2(0.334357, 0.266278), 0.0187145);
ret += circ(uv, vec2(0.918044, 0.968163), 0.0182928);
ret += circ(uv, vec2(0.965445, 0.505026), 0.006348);
ret += circ(uv, vec2(0.514847, 0.865444), 0.00623523);
ret += circ(uv, vec2(0.710575, 0.0415131), 0.00322689);
ret += circ(uv, vec2(0.71403, 0.576945), 0.0215641);
ret += circ(uv, vec2(0.748873, 0.413325), 0.0110795);
ret += circ(uv, vec2(0.0623365, 0.896713), 0.0236203);
ret += circ(uv, vec2(0.980482, 0.473849), 0.00573439);
ret += circ(uv, vec2(0.647463, 0.654349), 0.0188713);
ret += circ(uv, vec2(0.651406, 0.981297), 0.00710875);
ret += circ(uv, vec2(0.428928, 0.382426), 0.0298806);
ret += circ(uv, vec2(0.811545, 0.62568), 0.00265539);
ret += circ(uv, vec2(0.400787, 0.74162), 0.00486609);
ret += circ(uv, vec2(0.331283, 0.418536), 0.00598028);
ret += circ(uv, vec2(0.894762, 0.0657997), 0.00760375);
ret += circ(uv, vec2(0.525104, 0.572233), 0.0141796);
ret += circ(uv, vec2(0.431526, 0.911372), 0.0213234);
ret += circ(uv, vec2(0.658212, 0.910553), 0.000741023);
ret += circ(uv, vec2(0.514523, 0.243263), 0.0270685);
ret += circ(uv, vec2(0.0249494, 0.252872), 0.00876653);
ret += circ(uv, vec2(0.502214, 0.47269), 0.0234534);
ret += circ(uv, vec2(0.693271, 0.431469), 0.0246533);
ret += circ(uv, vec2(0.415, 0.884418), 0.0271696);
ret += circ(uv, vec2(0.149073, 0.41204), 0.00497198);
ret += circ(uv, vec2(0.533816, 0.897634), 0.00650833);
ret += circ(uv, vec2(0.0409132, 0.83406), 0.0191398);
ret += circ(uv, vec2(0.638585, 0.646019), 0.0206129);
ret += circ(uv, vec2(0.660342, 0.966541), 0.0053511);
ret += circ(uv, vec2(0.513783, 0.142233), 0.00471653);
ret += circ(uv, vec2(0.124305, 0.644263), 0.00116724);
ret += circ(uv, vec2(0.99871, 0.583864), 0.0107329);
ret += circ(uv, vec2(0.894879, 0.233289), 0.00667092);
ret += circ(uv, vec2(0.246286, 0.682766), 0.00411623);
ret += circ(uv, vec2(0.0761895, 0.16327), 0.0145935);
ret += circ(uv, vec2(0.949386, 0.802936), 0.0100873);
ret += circ(uv, vec2(0.480122, 0.196554), 0.0110185);
ret += circ(uv, vec2(0.896854, 0.803707), 0.013969);
ret += circ(uv, vec2(0.292865, 0.762973), 0.00566413);
ret += circ(uv, vec2(0.0995585, 0.117457), 0.00869407);
ret += circ(uv, vec2(0.377713, 0.00335442), 0.0063147);
ret += circ(uv, vec2(0.506365, 0.531118), 0.0144016);
ret += circ(uv, vec2(0.408806, 0.894771), 0.0243923);
ret += circ(uv, vec2(0.143579, 0.85138), 0.00418529);
ret += circ(uv, vec2(0.0902811, 0.181775), 0.0108896);
ret += circ(uv, vec2(0.780695, 0.394644), 0.00475475);
ret += circ(uv, vec2(0.298036, 0.625531), 0.00325285);
ret += circ(uv, vec2(0.218423, 0.714537), 0.00157212);
ret += circ(uv, vec2(0.658836, 0.159556), 0.00225897);
ret += circ(uv, vec2(0.987324, 0.146545), 0.0288391);
ret += circ(uv, vec2(0.222646, 0.251694), 0.00092276);
ret += circ(uv, vec2(0.159826, 0.528063), 0.00605293);

    return max(ret, 0.0);
}

// Procedural texture generation for the water
vec3 water(vec2 uv, vec3 cdir, float iTime)
{
    uv *= vec2(0.25);
    uv += fbm(uv) * 0.2;

    // Parallax height distortion with two directional waves at
    // slightly different angles.
    vec2 a = 0.025 * cdir.xz / cdir.y; // Parallax offset
    float h = sin(uv.x + iTime); // Height at UV
    uv += a * h;
    h = sin(0.841471 * uv.x - 0.540302 * uv.y + iTime);
    uv += a * h;

    // Texture distortion
    float d1 = mod(uv.x + uv.y, 2.0*PI);
    float d2 = mod((uv.x + uv.y + 0.25) * 1.3, 6.0*PI);
    d1 = iTime * 0.07 + d1;
    d2 = iTime * 0.5 + d2;
    vec2 dist = vec2(
	    sin(d1) * 0.15 + sin(d2) * 0.05,
	    cos(d1) * 0.15 + cos(d2) * 0.05
    );

    vec3 ret = mix(_WATER_COLOUR1.rgb, _WATER_COLOUR2.rgb, waterlayer(uv + dist.xy));
    ret = mix(ret, _FOAM_COLOUR.rgb, waterlayer(vec2(1.0) - uv - dist.yx));
    return ret;
}


void vertex(){
    float time = TIME * _WAVE_SPEED;
    vec2 uv = UV * _WAVE_SIZE;
    float d1 = mod(uv.x + uv.y, 2.0*PI);
    float d2 = mod((uv.x + uv.y + 0.25) * 1.3, 6.0*PI);
    d1 = time * 0.07 + d1;
    d2 = time * 0.5 + d2;
    vec2 dist = vec2(
	    sin(d1) * 0.15 + sin(d2) * 0.05,
	    cos(d1) * 0.15 + cos(d2) * 0.05
    );

    VERTEX.y += dist.y * _HEIGHT;
}

void fragment()
{
    vec2 uv = UV;
    ALBEDO = vec3(water(uv * _TILE, vec3(0,1,0), TIME * _DISTORTION_SPEED));
}
:bust_in_silhouette: Reply From: aXu_AP

Let’s split this problem into 2 phases:

1. Get heightmap of the water
Since the water is displaced in vertex shader, the changes aren’t applied onto the mesh. But you can simulate the vertex shader in your code (it doesn’t seem too complicated, one can easily to translate it into gdscript or C#). Make it into function get_water_height(position_uv : Vector2) -> float:

2. Apply buoyancy force
Now that you can get water height of arbitrary position, you can check whether the boat is under water in multiple points, and apply upward force. This force should be relative to the depth. Something like this:

# point is global coordinate
func apply_buoyancy(point : Vector3) -> void:
	var depth = get_water_height(Vector2(point.x, point.z)) - point.y
	if depth > 0:
		add_force(Vector3.UP * depth, point - global_transform.origin)

I tried making a spatial node with multiple Position3Ds as its children in grid formation and moved them to the bottom of the boat. Then I looped trough the positions and applied the function above. With a bit of fine tuning, I think it will be quite believable boat sim :slight_smile:

I made a small test project and it worked out well (albeit I didn’t make the wave function, just plane of water).

EDIT: I couldn’t resist trying out it with wave shader, and it works great! Some notes for implementation:

First, you need to change shader’s inbuilt TIME to parameter which gets passed in every frame. This is because you need to know what time is it in boat script, and secondly, you don’t want the waves to continue under pause.

Second thing is a small nitpick, but my code isn’t the most accurate - it works reasonable enough for partly submerged objects but if the object sinks far enough buoyancy gets too strong - it should be constant for fully submerged object. Realistically one would need to apply not upward force for all the points, but pressure towards the normal of the surface of the object. I didn’t get arount to implement it, since setting up those points with normals manually would be a pain, but it should result in more realistic physics. Easy hack around it could be to limit buoyancy force to the height of the object.

Third thing you might want to do is to apply extra force downwards under the boat. This makes the boat more stable and reflects real boats, which have heavy keel underside.

First of all, thank you so much for the great answer…
Second of all, I tried recreating the shader vertex code but I couldn’t really get it working. My main problem with recreating the code was getting the height in the exact position, I tried replacing the UV.xy in the line with the players xy vector…

func calculate_height(x, y):
var _t = time * _WAVE_SPEED
var uv = Vector2(x, y) * _WAVE_SIZE

var d1 = fmod(uv.x + uv.y, 2.0*PI)
var d2 = fmod((uv.x + uv.y + 0.25) * 1.3, 6.0*PI)

d1 = _t * 0.07 + d1;
d2 = _t * 0.5 + d2;

var dist = Vector2(
	sin(d1) * 0.15 + sin(d2) * 0.05,
	cos(d1) * 0.15 + cos(d2) * 0.05
)

return dist.y * _HEIGHT

fUNNY-fnaf | 2021-10-24 17:05

I had hard time understanding what the vertex shader was doing so I rewrote it and then translated to gdscript. It’s quite similiar, but a bit different:

# gdscript
func wave(phase : float, speed : float, amplitude : float) -> float:
	return sin(time * speed + phase) * amplitude;

func get_height(position : Vector2) -> float:
	var wave_size = material.get_shader_param("wave_size")
	var x = position.x * wave_size.x
	var y = position.y * wave_size.y
	# Angle each wave differently for interesting interactions
	var w1 = x + y * .1
	var w2 = x + y * .2
	var w3 = x + y
	# Use speeds and amplitudes which aren't easily refactorable
	var dist = wave(w1, .32, .40) + wave(w2, 0.5, 0.33) + wave(w3, .43, .27)
	return dist * material.get_shader_param("height")

In the vertex shader I set uv to global coordinates of the vertex, planning to move the ocean with camera (endless ocean!)

// shader
float wave(float phase, float speed, float amplitude){
	return sin(time * speed + phase) * amplitude;
}

void vertex(){
	// Get global coordinates
	UV = (WORLD_MATRIX * vec4(VERTEX, 1.0)).xz * wave_size;
	// Angle each wave differently for interesting interactions
	float w1 = UV.x + UV.y * .1;
	float w2 = UV.x + UV.y * .2;
	float w3 = UV.x + UV.y;
	// Use speeds and amplitudes which aren't easily refactorable
	float dist = wave(w1, .32, .40) + wave(w2, 0.5, 0.33) + wave(w3, .43, .27);
	VERTEX.y = dist * height;
}

aXu_AP | 2021-10-24 18:30

Ah okay, that makes a lot of sense.
Again, thanks for the extremely helpful answer

fUNNY-fnaf | 2021-10-24 18:58

I’m glad that I could help. Just say if you run into new problems :slight_smile:

aXu_AP | 2021-10-24 19:33