Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions gz_waves_provider_fft/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
cmake_minimum_required(VERSION 3.16)
project(gz_waves_provider_fft)

if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
endif()

if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()

find_package(ament_cmake REQUIRED)
# gz_waves's exported library links gz-sim PUBLIC, so gz-sim::core must be
# resolvable before find_package(gz_waves). The core also provides
# WavesSystemBase, the base for the fft system plugin below.
find_package(gz_sim_vendor REQUIRED)
find_package(gz-sim REQUIRED)
find_package(gz_waves REQUIRED) # IWaveField + WaveParameters + base
find_package(gz_plugin_vendor REQUIRED)
find_package(gz-plugin REQUIRED COMPONENTS register)
find_package(gz_math_vendor REQUIRED)
find_package(gz-math REQUIRED) # gz::math::Vector3d in the interface
find_package(gz_common_vendor REQUIRED)
find_package(gz-common REQUIRED) # gzerr/gzmsg console
# FFTWaveSimulation's public header returns Eigen::MatrixXd grids, so Eigen is
# a direct dependency.
find_package(Eigen3 REQUIRED)
# Apache-2.0 Horvath spectrum library — REQUIRED, consumed as a regular system
# package (installed from HonuRobotics/encinowaves, not vendored in-tree). The
# fft engine is the EncinoWaves spectral synthesizer (TMA/JONSWAP/PM spectra +
# directional spreading + dispersion), selected via
# <spectrum>/<spreading>/<dispersion>. Its CMake config transitively pulls in
# Eigen3 / TBB / Imath.
find_package(EncinoWaves REQUIRED)

# --------------------------------------------------------------------------
# FFT wave-field engine (libgz-waves-provider-fft): a plain IWaveField
# implementation backed by the EncinoWaves spectral library.
# It is LINKED directly by the fft system plugin (server) and the water visual
# (GUI), not discovered by a runtime loader, so it carries no GZ_ADD_PLUGIN and
# is exported as a normal library target. MakeFFTWaveField() is the factory the
# consumers register under the "fft" token.
# --------------------------------------------------------------------------
add_library(gz-waves-provider-fft SHARED
src/FFTWaveSimulation.cc
)
target_include_directories(gz-waves-provider-fft PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_include_directories(gz-waves-provider-fft PRIVATE
${gz_waves_INCLUDE_DIRS})
# Eigen is PUBLIC: the engine's public header returns Eigen::MatrixXd grids, so
# anything that includes it (the system plugin, the water visual) needs Eigen.
target_link_libraries(gz-waves-provider-fft
PUBLIC Eigen3::Eigen gz-math::gz-math
PRIVATE EncinoWaves::EncinoWaves gz-common::gz-common)

# --------------------------------------------------------------------------
# FFT wave source system (gz-sim-waves-fft-system): a WavesSystemBase subclass
# that builds the FFT engine. THIS is the gz-plugin SDF loads by filename; it
# links the engine directly, so there is no runtime engine loader on the server.
# --------------------------------------------------------------------------
add_library(gz-sim-waves-fft-system SHARED
src/FftWavesSystem.cc
)
target_include_directories(gz-sim-waves-fft-system PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
${gz_waves_INCLUDE_DIRS})
target_link_libraries(gz-sim-waves-fft-system
PRIVATE
gz_waves::gz_waves
gz-waves-provider-fft
gz-sim::core
gz-plugin::register)

# --------------------------------------------------------------------------
# FFT GUI registrar (gz-sim-waves-fft-gui): loaded in the GUI process alongside
# WaterVisual. It registers the "fft" factory so the visual can rebuild its own
# thread-private engine from the replicated recipe via CreateWaveSimulation.
# This is what lets gz_waves_rendering stay provider-agnostic (core-only): the
# engine dependency lives here, the GUI-side mirror of gz-sim-waves-fft-system.
# --------------------------------------------------------------------------
add_library(gz-sim-waves-fft-gui SHARED
src/FftWavesGui.cc
)
target_include_directories(gz-sim-waves-fft-gui PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
${gz_waves_INCLUDE_DIRS})
target_link_libraries(gz-sim-waves-fft-gui
PRIVATE
gz_waves::gz_waves
gz-waves-provider-fft
gz-sim::core
gz-plugin::register)

install(
TARGETS gz-waves-provider-fft
EXPORT export_gz_waves_provider_fft
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
RUNTIME DESTINATION bin
)
install(
TARGETS gz-sim-waves-fft-system gz-sim-waves-fft-gui
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
install(DIRECTORY include/ DESTINATION include)

ament_export_targets(export_gz_waves_provider_fft HAS_LIBRARY_TARGET)
ament_export_include_directories(include)
ament_export_dependencies(gz_math_vendor Eigen3)

if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
set(ament_cmake_copyright_FOUND TRUE)
set(ament_cmake_cpplint_FOUND TRUE)
set(ament_cmake_uncrustify_FOUND TRUE)
ament_lint_auto_find_test_dependencies()

# Links the engine directly and registers its "fft" factory in-binary (see
# kFftRegistered in the test), so the CreateWaveSimulation tests resolve via
# the registry — no dlopen, no GzPluginHook.
find_package(ament_cmake_gtest REQUIRED)
ament_add_gtest(fft_test test/fft_test.cc)
target_include_directories(fft_test PRIVATE
${gz_waves_INCLUDE_DIRS})
target_link_libraries(fft_test
gz_waves::gz_waves gz-waves-provider-fft)
endif()

ament_package()
189 changes: 189 additions & 0 deletions gz_waves_provider_fft/include/gz/sim/waves/FFTWaveSimulation.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/*
* Copyright (C) 2026 Honu Robotics
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*/

#ifndef GZ_SIM_WAVES_FFTWAVESIMULATION_HH_
#define GZ_SIM_WAVES_FFTWAVESIMULATION_HH_

#include <cstddef>
#include <cstdint>
#include <memory>

#include <Eigen/Dense>

#include "gz/sim/waves/WaveSimulation.hh"

namespace gz::sim::waves
{

struct WaveParameters;

/// \brief Stochastic FFT-based wave field engine backed by the Apache-2.0 EncinoWaves
/// spectral library (Horvath 2015): the inverse 2D FFT of an empirically
/// modelled directional spectrum (TMA/JONSWAP/PM + directional spreading +
/// dispersion), selected via the <spectrum>/<spreading>/<dispersion> params.
///
/// The output is a periodic tile of size `tileSize × tileSize` metres.
/// Queries outside the tile are wrapped via `fmod`. Each call to `Update`
/// regenerates the grid for that time; per-point queries (`Elevation`,
/// `ParticleVelocity`, ...) bilinear-sample the stored grid.
class FFTWaveSimulation final : public IWaveField
{
/// \brief Default-construct an unconfigured field. Call `SetParameters`
/// before `Update`/sampling. Used by the engine factory (`MakeFFTWaveField`).
public: FFTWaveSimulation();

/// \brief Construct from spectrum / wind parameters.
/// \param[in] _params Wave parameters; uses `direction` as the wind heading
/// and derives wind speed from `period` (deep-water PMS relation:
/// V19 ≈ 0.879·g/omegaP). `gain` scales spectrum amplitudes uniformly.
/// \param[in] _tileSize Physical tile extent in metres; the wave field is
/// periodic with this period along both x and y.
/// \param[in] _gridSize Resolution per axis (must be a power of two for
/// the FFT path; 64, 128, 256 typical).
/// \param[in] _seed RNG seed for the Gaussian-distributed amplitudes; same
/// seed → same wave field bit-for-bit across runs.
public: FFTWaveSimulation(const WaveParameters &_params,
double _tileSize,
std::size_t _gridSize,
std::uint32_t _seed);

/// \brief Destructor (out-of-line so the EncinoState pimpl stays in the .cc).
public: ~FFTWaveSimulation() override;

// Documentation inherited
public: void SetParameters(const WaveParameters &_params) override;
// Documentation inherited
public: double Elevation(double _x, double _y, double _t) const override;
// Documentation inherited
public: gz::math::Vector3d ParticleVelocity(
double _x, double _y, double _t) const override;
// Documentation inherited
public: gz::math::Vector3d Normal(
double _x, double _y, double _t) const override;
// Documentation inherited
public: double Jacobian(double _x, double _y, double _t) const override;
// Documentation inherited
public: void Update(double _simTime) override;
// Documentation inherited
public: std::string_view Kind() const override { return "fft"; }
// Documentation inherited
public: std::optional<TileSize> Bounds() const override
{
return TileSize{this->tileSize, this->tileSize};
}
// Documentation inherited
public: const WaveField2D *Field() const override;

// ---- Accessors for unit tests + visual heightmap upload ----------------

/// \brief Grid resolution per axis (power of two).
public: std::size_t GridSize() const { return this->gridSize; }
/// \brief Physical tile extent per axis [m]; the field is periodic with it.
public: double TileSizeMeters() const { return this->tileSize; }

/// \brief Surface elevation field η(x, y, t), refreshed by Update().
public: const Eigen::MatrixXd &HeightGrid() const
{
return this->heightGrid;
}
/// \brief Horizontal x-displacement field Dx(x, y, t), refreshed by Update().
/// Multiplied by a "choppiness" factor in the visual shader to sharpen
/// crests (Tessendorf 2001, eq. 29). Same grid layout as `HeightGrid()`.
public: const Eigen::MatrixXd &DispXGrid() const
{
return this->dispXGrid;
}
/// \brief Horizontal y-displacement field Dy(x, y, t).
public: const Eigen::MatrixXd &DispYGrid() const
{
return this->dispYGrid;
}

/// \brief Bilinear sample of `_grid` at the given world (x, y), wrapping
/// queries outside the tile to its periodic image.
/// \param[in] _grid The grid to sample.
/// \param[in] _x World x coordinate [m].
/// \param[in] _y World y coordinate [m].
/// \return The bilinearly-interpolated grid value.
private: double BilinearSample(const Eigen::MatrixXd &_grid,
double _x, double _y) const;

// Configuration. Default member-inits keep a default-constructed instance
// benign until SetParameters() runs (the factory path).

/// \brief Physical tile extent per axis [m].
private: double tileSize{200.0};
/// \brief Grid resolution per axis (power of two).
private: std::size_t gridSize{128};
/// \brief Wind speed [m/s], derived from `period`.
private: double windSpeed{0.0};
/// \brief Spectrum amplitude gain.
private: double gain{1.0};
/// \brief Startup-ramp time constant τ [s].
private: double tau{2.0};

/// \brief Surface elevation grid η (real domain), regenerated by Update().
private: Eigen::MatrixXd heightGrid;
/// \brief Horizontal x-displacement grid Dx (Tessendorf eq. 29); used by the
/// visual shader for choppy, asymmetric crests.
private: Eigen::MatrixXd dispXGrid;
/// \brief Horizontal y-displacement grid Dy.
private: Eigen::MatrixXd dispYGrid;
/// \brief Folding/whitecap metric (Encino path): per-cell minimum eigenvalue
/// of the displacement Jacobian, 1 = flat. Sampled by Jacobian() → FoamMask().
private: Eigen::MatrixXd minEGrid;

/// \brief Water-particle velocity grids [m/s] — the Eulerian time derivative
/// of the displacement field (∂Dx/∂t, ∂Dy/∂t, ∂η/∂t), computed each Update by
/// finite-differencing a scratch propagation at t+dt. Sampled by
/// ParticleVelocity().
private: Eigen::MatrixXd velXGrid;
/// \brief Water-particle velocity grid, y component [m/s].
private: Eigen::MatrixXd velYGrid;
/// \brief Water-particle velocity grid, z (vertical) component [m/s].
private: Eigen::MatrixXd velZGrid;

/// \brief Column-major view into the grids above, returned by Field() as the
/// engine-agnostic rendering contract. Repopulated on each call from the
/// current grid data() — Update reassigns the grids, so a cached pointer
/// would dangle. Mutable because Field() is const.
private: mutable WaveField2D field;

/// \brief Forward declaration of the EncinoWaves Update state. Defined
/// entirely in FFTWaveSimulation.cc so the vendored EncinoWaves headers don't
/// leak into this public include surface.
private: struct EncinoState;
/// \brief The EncinoWaves-backed Update state (pimpl).
private: std::unique_ptr<EncinoState> encino;

/// \brief Physics-based amplitude calibration for the Encino path. Encino's
/// intrinsic field variance is far larger than a physical sea state at our
/// wind speeds, so we measure its intrinsic RMS once at construction and
/// store the factor that rescales the significant wave height to the
/// fully-developed Pierson-Moskowitz relation (Hs = 0.21·V19.5²/g). Applied
/// to η/Dx/Dy every Update.
private: double encinoScale{1.0};

/// \brief Sim time of the last Update(). The field is deterministic in time,
/// so Update() short-circuits on a repeat call — letting several consumers
/// (the source system plus each WaveBuoyancy that advances its own instance)
/// converge on one FFT per tick. -1 = never updated (sim time is always ≥ 0).
private: double lastUpdateT{-1.0};
};

/// \brief Factory: a default-constructed FFT wave-field engine (apply
/// `SetParameters` before use). Registered under the "fft" token so
/// `CreateWaveSimulation` can rebuild the engine from a serialized `Wavefield`
/// component on the GUI side.
std::shared_ptr<IWaveField> MakeFFTWaveField();

} // namespace gz::sim::waves

#endif // GZ_SIM_WAVES_FFTWAVESIMULATION_HH_
40 changes: 40 additions & 0 deletions gz_waves_provider_fft/package.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>gz_waves_provider_fft</name>
<version>0.0.0</version>
<description>
Stochastic FFT wave-field engine for gz_waves, backed by the Apache-2.0
EncinoWaves spectral library: TMA/JONSWAP/Pierson-Moskowitz spectra with
directional spreading and dispersion, selected via the spectrum, spreading
and dispersion SDF parameters. Provides the IWaveField engine library and
the gz-sim-waves-fft-system plugin.
</description>
<maintainer email="cen.aguero@gmail.com">Carlos Agüero</maintainer>
<license>Apache-2.0</license>

<buildtool_depend>ament_cmake</buildtool_depend>

<depend>gz_waves</depend>
<depend>gz_sim_vendor</depend>
<depend>gz_math_vendor</depend>
<depend>gz_common_vendor</depend>
<depend>gz_plugin_vendor</depend>
<depend>eigen</depend>
<!-- The Apache-2.0 EncinoWaves spectral library is a required dependency the
FFT engine is built on. It is consumed as a regular system package via
find_package(EncinoWaves), built and installed from
https://github.com/HonuRobotics/encinowaves (no rosdep key, so it is not
listed here). Its CMake config transitively requires Eigen / TBB / Imath,
declared here so rosdep installs them. -->
<depend>libtbb-dev</depend>
<depend>libimath-dev</depend>

<test_depend>ament_cmake_gtest</test_depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>

<export>
<build_type>ament_cmake</build_type>
</export>
</package>
Loading