Skip to content
jonri edited this page Oct 28, 2023 · 6 revisions

Godot Hydrodynamics module

Overview

Hydro is a module for Godot 3 and 4 which allows rigid bodies to behave realistically in water. When an object enters the water, three forces will be calculated:

  • Buoyancy - the pressure of the water causes an object to float.
  • Drag - an object moving through the water will slow down. The amount of drag is dependent on the object's shape.
  • Lift - an object moving through the water at an angle will generate lift, the way a boat lifts out of the water. This is also dependent on the object's shape.

In addition to applying these forces, this module also does the following:

  • Allows you to plug in your own wave-generation function to have your objects bob in the ocean.
  • Allows you to set a flow direction for features such as rivers.
  • Contains convenience nodes for you to apply thrust and steering, making it easy to create a boat or other watercraft.

A simple demo project is included with the source code. You can also find more advanced examples in the Showcase.

Installation

For performance reasons, this is a native module that must be built from source with Godot.

  1. Get the source code for Godot: https://docs.godotengine.org/en/stable/development/compiling/getting_source.html
  2. Enter the source code directory, and go to the modules directory. Clone this project into a folder called "hydro": git clone https://github.com/godot-extended-libraries/hydro.git
  3. Build Godot for your platform: https://docs.godotengine.org/en/stable/development/compiling/introduction_to_the_buildsystem.html

Usage

With this module, Godot will contain a several new Node types. HydroRigidBody will do everything a normal RigidBody does, and adds the hydrodynamic forces on top of it.

WaterArea3D defines the area in which hydrodynamic forces will be calculated.

To customize the ocean height or add waves to your WaterArea3D, attach a script to your WaterArea3D and add a new function:

func _get_water_heights(positions):
	var ret : PoolVector3Array
	for p in positions:
		ret.append(Vector3(p.x, 42, p.z))
	return ret

This example will set the water's global Y position to 42. You could also hook the Y value into another function that generates waves.

The HydroRigidBody will expect one mesh to be a direct child. This mesh will be the only one used to calculate the buoyancy and related forces. This mesh may be concave, but it must be closed and not intersect itself. Buoyancy is related to volume, and an open shape does not have a volume. If you have other display meshes that you want to come along for the ride, place them as a child of the main one. For example, you could place your boat hull as the main mesh, and then have your deck fittings, outboard motor, etc as child meshes. If you want a hollow boat, this can be done as well using some shader tricks.

For developing a pilotable watercraft, there are several additional convenience nodes:

Troubleshooting

Debugging forces

If you have a complex object that is not floating properly, you can add an ImmediateGeometry node as a child of the HydroRigidBody, and it will automatically be used for debugging. While your game is running, this will display a wireframe of the submerged portion of your hull, a line representing each of the forces that are generated, and a diamond shape representing the wave heights immediately surrounding your object. You can use these to diagnose whether something is out of place.

Tips and tricks

  • Make sure your shape is totally closed.
  • Make sure all your normals face outwards. If you see one force line facing the opposite direction of the rest, it's a good bet you need to check that face.
  • Make sure you set a realistic mass. For reference, a 1m cube of styrofoam weighs 50 kg, so dropping in a default 2x2x2 cube and leaving the mass set at the default 1 kg will not work very well.
  • If you want an object to start in the water, place it as close to its "rest" height on the water as possible. Starting a little too high is usually better than starting too low.
  • Make sure that your water height function returns the desired height in global coordinates, and that your wave height does not exceed the bounds of its enclosing WaterArea3D.

Known issues

  • Dropping an object into a WaterArea3D from a large height can cause an inaccurately large buoyancy force to be generated, rocketing the object back out of the water. In reality, a dropped object would lose a lot of its velocity to the impact with the water and this is not currently simulated.
  • Objects with very large or small masses can end up with inaccuracies as well. Sometimes increasing the physics step frequency can help, but this may be related to floating-point precision too. It is yet to be determined whether double-precision will help with this.

Testing

The stresstest.gd script can be used to generate and run a test in which one or more cubes will drop through a WaterArea3D.

You can configure the test using the following arguments:

    --cubes=n
        The number of cubes to generate
    --density=n
        Sets the density of the cubes.  Must be larger than 1.0 in order to sink.
    --depth=n
        Exit when all cubes sink to this depth
    --faces=n
        The number of faces to generate per-cube.  For exact results, use 6 times a square number.
        For example, 3 * 3 * 6 = 54 faces.
    --size=n
        The size of each cube

After the test is complete, some statistics will be printed to the console. These tests may be useful for performance profiling, benchmarking, or regression testing.

Example Command:

bin/godot.x11.opt.tools.64.llvm -s modules/hydro/tests/stresstest.gd --cubes=1000 --faces=54 --size=1 --depth=5 --density 1.5

Technical References

If you are interested in how the physics work, here are some resources I found useful or insightful along the way:

While my approach has some differences, the writeup in the final two links above does an excellent job explaining and visualizing the physics involved.