Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 

Repository files navigation

USV Simulation ROS 2 - Istanbul Bosphorus

picture

Youtube Demo Video

A browser-based, ROS 2 compatible simulation of an autonomous Unmanned Surface Vehicle (USV) operating in a stylized model of the Istanbul Bosphorus. The vehicle is driven by a from-scratch rigid-body hydrodynamics model, exposes a full ROS 2 sensor and command interface over rosbridge, and includes an in-browser mission planner that lets the operator draw a route and launch autonomous line-following with a single button.


Table of Contents

  1. Vehicle Model and Physics
  2. Installation
  3. Running the Simulation
  4. ROS 2 Interface: Message Types and Flow
  5. Autonomous Mode and the Mission Planner
  6. Manual Control and Camera
  7. Environment Editor
  8. Visualization in RViz2
  9. Project Structure
  10. Troubleshooting

Coordinate conventions

All ROS output follows REP-103 (ENU / FLU). Internally the renderer uses the Three.js convention, and conversions are applied at the sensor boundary:

Quantity Three.js world ROS (ENU / FLU)
Axes +X East, +Y Up, +Z South East = x, North = -z, Up = y
Vector (x, y, z) (x, -z, y)
Orientation quaternion (qx,qy,qz,qw) (qx, -qz, qy, qw)
Body frame +X forward, +Z starboard FLU: forward = +X, left = -Z, up = +Y
Heading object.rotation.y ENU yaw (0 = East, CCW positive)

The GPS origin (the world origin) is fixed at latitude 41.0451 N, longitude 29.0330 E, in the middle of the strait.


Vehicle Model and Physics

The vessel

The controlled vehicle is a fast interceptor-class USV (inspired by vehicles such as the ULAQ class). It is a monohull with an enclosed, uncrewed foredeck, a sensor mast, and twin stern propulsion. The visual model is the "Cora" USV mesh from the OSRF VORC project (see Model Credits).

The physical hull used for buoyancy and drag is a box approximation of the vessel:

Parameter Value Notes
Length (L) 11.0 m overall
Beam (B) 3.4 m overall
Hull depth 1.6 m keel to deck
Mass 5 500 kg equilibrium draft approximately 0.26 m
Block coefficient (Cb) 0.55 planing-style hull
Max thrust per propeller 25 000 N thrust-to-weight approximately 0.9 g
Top speed ~15 m/s approximately 30 knots
Max yaw rate 0.9 rad/s at full differential thrust
Thruster response tau = 0.25 s first-order lag

All of these values live in a single file, web/js/config.js, which is the central tuning point for physics, sensors, environment, and rendering.

The sea surface (Gerstner waves)

The ocean is a sum of four Gerstner wave components. Each component is defined by a direction, amplitude, wavelength, and steepness; its angular frequency is derived from the deep-water dispersion relation omega = sqrt(g * k), with k = 2*pi / wavelength.

Crucially, the same wave formula and the same parameters are evaluated in two places:

  • On the CPU (web/js/waves.js) for the physics. getHeight(x, z, t) returns the true surface elevation, inverting the horizontal Gerstner displacement with three fixed-point iterations.
  • On the GPU (web/js/ocean.js) in a custom vertex shader for the visuals.

Because both use identical parameters, the vessel physically rides the exact wave it appears to sit on: pitch, roll, and heave are emergent, not scripted.

Rigid-body dynamics

Every fixed physics step accumulates forces and torques in the world frame, then integrates with semi-implicit Euler. There is no third-party physics engine; the dynamics are written directly in web/js/physics.js.

  1. Buoyancy (multi-point). A 5 x 3 grid of sample points is distributed over the hull bottom. Each point is transformed into the world frame, and its submersion depth is measured against the Gerstner surface at that instant. Each submerged point contributes an upward Archimedes force rho * g * h * cellArea applied at the point, which produces both heave support and the restoring torque that keeps the vessel upright and makes it pitch and roll with the sea.

  2. Hydrodynamic drag. Damping is applied in the body frame with independent linear and quadratic coefficients for surge (forward), sway (lateral), and heave (vertical), scaled by the submerged fraction. Rotational drag is applied about the body roll, yaw, and pitch axes in the same way. The quadratic surge term is what caps the top speed near 15 m/s.

  3. Righting torque. In addition to the distributed buoyancy, an explicit restoring torque proportional to roll and pitch angle adds stability and prevents capsizing in steep waves.

  4. Propulsion. Two stern thrusters produce force along the body +X axis, each applied at its offset from the center of gravity (port at -Z, starboard at +Z). Thrust follows the command through a first-order lag (tau = 0.25 s), and a thruster only produces force while it is actually submerged.

  5. Contact. Hull corner points are tested against the analytic terrain height field for grounding, and a bounding sphere is rejected against environment object colliders (boxes, cylinders, spheres), so the vessel cannot drive through bridge piers, docked ships, or the shore.

  6. Integration. Linear: v += (F / m) * dt, then position += v * dt, with a safety clamp on speed. Angular: world torque is rotated into the body frame, divided by a box inertia tensor, integrated, and converted back; the orientation quaternion is advanced by q += 0.5 * omega_world * q * dt and renormalized. A NaN guard resets the vehicle if the state ever becomes non-finite.

Differential-thrust command mapping

The vehicle is steered by differential thrust. A geometry_msgs/Twist on /usv/cmd_vel is mapped to normalized thruster commands:

forward = clamp(linear.x  / max_speed,   -1, 1)     # max_speed = 15 m/s
turn    = clamp(angular.z / max_yaw_rate, -1, 1)     # max_yaw_rate = 0.9 rad/s

port_thruster     = clamp(forward - turn, -1, 1)
starboard_thruster = clamp(forward + turn, -1, 1)

A positive angular.z (counter-clockwise, i.e. a turn to port) makes the starboard thruster push harder, which is the physically correct sense. Thruster commands can also be sent directly via /usv/thrusters.

Deterministic, hidden-tab-safe stepping

Physics runs at a fixed 60 Hz, driven by a setInterval heartbeat inside an inline Web Worker. This decouples the physics and ROS publishing rates from the render loop (requestAnimationFrame), so the simulation keeps stepping at the correct rate even when the browser tab is in the background and rendering is throttled.


Installation

Prerequisites

  • ROS 2 Humble (Ubuntu 22.04 recommended)
  • rosbridge_suite (ros-humble-rosbridge-suite)
  • RViz2 (ros-humble-rviz2), optional but recommended
  • Python 3.10+
  • A WebGL-capable browser (Chrome or Firefox)

Install the ROS dependencies with apt:

sudo apt update
sudo apt install ros-humble-rosbridge-suite ros-humble-rviz2

If rosbridge is not available as a package on your system, the helper script scripts/setup_rosbridge.sh builds it from source into ~/rosbridge_ws. Note that it also removes the standalone bson pip package if present, because it shadows PyMongo's bson module and breaks rosbridge.

Build the ROS 2 package

git clone <this-repository> tekne_simulation
cd tekne_simulation

# Build the usv_sim package
source /opt/ros/humble/setup.bash
cd ros2_ws
colcon build --symlink-install
cd ..

The helper scripts/build_ros2.sh performs the same build. The environment helper scripts/env.sh sources ROS 2, rosbridge, and this workspace in one step; if your ROS 2 or rosbridge installations live somewhere other than the defaults, edit the paths at the top of that script.


Running the Simulation

You need two things running: the web server that serves the 3D client, and the ROS 2 stack that bridges it to the network.

Quick start (single command)

scripts/run_all.sh

This starts the web server in the background and brings up the full ROS stack (rosbridge + monitor + viz + mission_manager) in the foreground. It is everything needed for both manual and autonomous operation, including the mission planner's "Start Autonomous" button. Press Ctrl-C to stop; the background web server is torn down automatically.

Then open http://localhost:8080 in a browser. The status indicator in the top-left corner turns green ("ROS connected") once the bridge is reachable.

Alternative: separate terminals

# Terminal 1: serve the web client on http://localhost:8080
scripts/serve_web.sh

# Terminal 2: full ROS stack (rosbridge + monitor + viz + mission_manager)
scripts/mission.sh

usv_bringup.launch.py (used by scripts/rviz.sh and the manual/teleop workflows) brings up only rosbridge and the monitor. The mission planner's autonomous launch requires the mission_manager node, which is included in mission_planner.launch.py and in run_all.sh.

scripts/serve_web.py serves the client with a Cache-Control: no-store header so that code changes are always picked up on reload during development.

Only one browser tab should publish at a time. The client uses a single-instance lock, but it only coordinates tabs of the same origin. If two simulator tabs are connected to the same bridge, both publish odometry and the ROS side receives interleaved, conflicting state. Keep a single simulator tab open.


ROS 2 Interface: Message Types and Flow

Topics

Topic Type Direction Rate
/usv/gps sensor_msgs/NavSatFix sim -> ROS 5 Hz
/usv/imu sensor_msgs/Imu sim -> ROS 50 Hz
/usv/odom nav_msgs/Odometry sim -> ROS 20 Hz
/usv/velocity geometry_msgs/TwistStamped sim -> ROS 20 Hz
/usv/scan sensor_msgs/LaserScan sim -> ROS 10 Hz
/tf tf2_msgs/TFMessage sim -> ROS 20 Hz
/usv/cmd_vel geometry_msgs/Twist ROS -> sim -
/usv/thrusters std_msgs/Float32MultiArray ROS -> sim -
/usv/reset std_msgs/Empty ROS -> sim -
/usv/mission/waypoints nav_msgs/Path web -> ROS -
/usv/mission/command std_msgs/String (JSON) web -> ROS -
/usv/mission/status std_msgs/String ROS -> web 1 Hz
/usv/current_wp std_msgs/Int32 ROS -> RViz -
/usv/path nav_msgs/Path ROS -> RViz 2 Hz
/usv/markers visualization_msgs/MarkerArray ROS -> RViz 2 Hz

Sensor details

  • GPS (NavSatFix). Latitude and longitude are derived from the ENU position relative to the fixed origin, with Gaussian noise (sigma = 0.8 m) and a filled position covariance.
  • IMU (Imu). Orientation is the ENU attitude quaternion; angular velocity and linear acceleration are reported in the FLU body frame. The linear acceleration is proper acceleration, so at rest the accelerometer reads +g along the body-up axis, as a real device would. Per-field Gaussian noise is applied.
  • Odometry (Odometry). Pose is reported in the map frame (ENU); the twist is in the body frame (FLU). No noise is added to odometry.
  • LaserScan (LaserScan). A stabilized 360-degree planar lidar mounted 2.6 m above the waterline, range 1.5 to 200 m, 360 rays. Rays are cast horizontally (independent of vessel roll and pitch) by ray-marching against the analytic terrain height field and testing analytic colliders (rotated boxes, cylinders, spheres). A range of 0.0 denotes no return.
  • TF. A dynamic map -> base_link transform, plus static base_link -> {gps_link, imu_link, lidar_link} transforms so TF consumers can resolve every sensor frame. A base_link_stab frame (vehicle XY position, level orientation) is also published; RViz's chase camera targets it so that wave-induced roll, pitch, and heave do not shake the view.

Command examples

# Drive forward at ~2.5 m/s
ros2 topic pub -r 10 /usv/cmd_vel geometry_msgs/msg/Twist '{linear: {x: 2.5}}'

# Forward with a turn to port (positive angular.z = counter-clockwise)
ros2 topic pub -r 10 /usv/cmd_vel geometry_msgs/msg/Twist '{linear: {x: 4.0}, angular: {z: 0.4}}'

# Drive the thrusters directly, [port, starboard], range -1..1
ros2 topic pub -r 10 /usv/thrusters std_msgs/msg/Float32MultiArray '{data: [0.9, 0.3]}'

# Reset the vehicle to its spawn point
ros2 topic pub --once /usv/reset std_msgs/msg/Empty '{}'

Commands are subject to a 1.5 s safety timeout: if no command arrives within that window, the thrusters are zeroed. This is why the teleop node republishes continuously at 10 Hz.

Message flow

Sensors:   simulator  --(NavSatFix/Imu/Odometry/LaserScan/TF)-->  rosbridge  -->  ROS nodes
Commands:  ROS nodes  --(Twist / Float32MultiArray / Empty)------>  rosbridge  -->  simulator
Mission:   browser UI --(Path + String command)----------------->  mission_manager
           mission_manager --(spawns)--> autonomous controller --(Twist)--> simulator

Autonomous Mode and the Mission Planner

Autonomy is designed so that the route is never hard-coded: the operator draws it in the browser and the ROS side follows it. There are two moving parts, the in-browser planner and the mission_manager node.

1. Planning the route (browser)

Press M to enter the Mission Planner. The camera switches to a top-down view of the strait.

  • Click on the water to drop numbered waypoints. A cyan route line connects them in order.
  • Click an existing waypoint to remove it; drag to pan; scroll to zoom.
  • Optionally tick "Loop route" to make the vehicle cycle the route continuously.

As the route changes, the planner publishes it as a nav_msgs/Path (in the map / ENU frame) on /usv/mission/waypoints. This is picked up by the visualization node so the planned route appears live in RViz as well. The relevant client code is web/js/mission.js and web/js/ros.js.

2. Launching (browser to mission_manager)

Pressing Start Autonomous publishes the current route once more, then sends a JSON command on /usv/mission/command:

{ "action": "start", "loop": false, "cruise": 6.0 }

The mission_manager node (ros2_ws/src/usv_sim/usv_sim/mission_manager.py) receives this and:

  1. Spawns the autonomous controller with the operator's waypoints passed as a parameter, e.g. ros2 run usv_sim autonomous --ros-args -p waypoints:=[e1, n1, e2, n2, ...] -p loop:=false -p cruise_speed:=6.0.
  2. Opens RViz2 with the bundled configuration if it is not already running.
  3. Publishes progress on /usv/mission/status (running, idle, stopped, no_waypoints), which the planner panel displays.

Because a browser cannot spawn desktop processes, mission_manager is the component that actually starts the controller and RViz on the operator's behalf. It spawns each child in its own process group and, on Stop, signals the whole group so the controller (and the node it launches) shuts down cleanly; it also publishes a zero Twist as a safety measure.

3. Following the route (autonomous controller)

The controller (ros2_ws/src/usv_sim/usv_sim/autonomous_controller.py) uses Line-of-Sight (LOS) guidance, the standard approach for marine path following. Rather than steering at the next waypoint (which cuts corners and drifts off the intended track), it steers toward the line segment between the previous and current waypoint.

For a segment from A to B, given the vehicle position:

path_angle    = atan2(By - Ay, Bx - Ax)
cross_track    = -(x - Ax) * sin(path_angle) + (y - Ay) * cos(path_angle)
along_track    =  (x - Ax) * cos(path_angle) + (y - Ay) * sin(path_angle)
desired_course = path_angle + atan2(-cross_track, lookahead)     # lookahead = 45 m

The desired course drives the cross-track error to zero, so the vehicle converges onto the line and stays on it. Steering is a PD law on the heading error:

angular.z = kp_yaw * heading_error - kd_yaw * yaw_rate           # kp_yaw = 1.0, kd_yaw = 0.55

The derivative term (damping on the measured yaw rate, taken from the odometry twist) suppresses the oscillation that a proportional-only controller produces on a fast, high-inertia hull. Forward speed is scaled down for large heading errors and large cross-track errors, so the vehicle slows into turns. A waypoint is considered reached when the vehicle is within tolerance of it or has passed it along the track, which prevents the controller from circling a point it cannot exactly hit.

Obstacle avoidance runs on top of guidance: the nearest return within a +/- 40-degree frontal sector of the lidar, closer than avoid_distance, adds a steering bias away from the obstacle and reduces speed.

The controller publishes geometry_msgs/Twist on /usv/cmd_vel (which flows back through rosbridge to the simulator) and the active waypoint index on /usv/current_wp (used by RViz to color the target waypoint). Tuning parameters (lookahead, kp_yaw, kd_yaw, cruise_speed, wp_tolerance, avoid_distance, loop) are all ROS parameters.

Verified path-following accuracy on a multi-turn route is approximately 0.7 m average cross-track error (4.4 m maximum, at the sharpest corner).

One-command launch

source scripts/env.sh
ros2 launch usv_sim mission_planner.launch.py
# brings up rosbridge + monitor + viz + mission_manager
# then, in the browser: press M, draw a route, press Start Autonomous

scripts/mission.sh wraps this command.

Fixed route (without the planner)

A self-contained autonomous mission with a built-in channel-centerline route is also available:

source scripts/env.sh
ros2 launch usv_sim autonomous_mission.launch.py

Manual Control and Camera

With a simulator tab focused (and the editor and planner closed), the vehicle can be driven from the keyboard:

Key Action
W / S forward / reverse thrust
A / D port / starboard turn
C cycle camera mode (follow / orbit / free)
E toggle the environment editor
M toggle the mission planner
R reset the vehicle to spawn
H show / hide the help panel

Keyboard commands are overridden by ROS commands and vice versa; releasing the keys zeroes the manual thrust.

The teleop node offers keyboard control from a terminal instead, publishing to /usv/cmd_vel at 10 Hz:

source scripts/env.sh
ros2 run usv_sim teleop

Environment Editor

Press E to open the scene editor. The scene is fully customizable and persists across sessions.

  • Pick an object type from the palette and click on the water to place it.
  • Click an object to select it; drag to move, Q/E to rotate, +/- to scale, Delete to remove.
  • Save to the browser (localStorage), or download / upload the scene as JSON.

Object types include red and green channel buoys, a container ship, a small ship, a sailboat, a tugboat, docks, a lighthouse, crates, a platform, and a custom GLB by URL. The starting scene is defined in web/environment.json; its schema is:

{
  "objects": [
    { "type": "buoy_red", "x": 44, "z": 1250, "yawDeg": 0, "scale": 1, "name": "optional label" },
    { "type": "custom_glb", "x": 0, "z": 500, "url": "https://.../model.glb", "targetLength": 30 }
  ]
}

Coordinates are in simulator world meters (x = East, z = South, so ROS North = -z). Floating objects ride the waves; fixed objects (docks, lighthouses) sit on the terrain. Every object contributes a collider that the vehicle and the lidar both see.


Visualization in RViz2

scripts/rviz.sh
# or: source scripts/env.sh && ros2 launch usv_sim rviz.launch.py

The bundled configuration (ros2_ws/src/usv_sim/rviz/usv.rviz) shows:

  • Odometry (USV) - live position and heading, with a hull box.
  • Waypoints & Route - waypoint spheres (green = upcoming, orange = active target, gray = passed) with labels, and the route line.
  • Traveled Path - the track the vehicle has actually followed.
  • Lidar - the 360-degree scan returns.
  • TF Frames and a 100 m grid.

The chase camera targets the base_link_stab frame, so the view follows the vehicle smoothly without inheriting wave motion. A saved "Bird's Eye" view shows the whole map from above.

The Mission Planner opens RViz automatically when you press Start Autonomous, so a separate launch is usually unnecessary.


Project Structure

tekne_simulation/
  web/                        Browser client (served statically, no build)
    index.html                App shell, import map, loading splash
    environment.json          Default scene definition
    js/
      config.js               Central configuration (physics, sensors, visuals, topics)
      waves.js                CPU Gerstner wave field
      ocean.js                GPU ocean surface (shader, same wave params)
      terrain.js              Stylized Bosphorus terrain, bridges, landmarks, colliders
      physics.js              Rigid-body USV dynamics
      sensors.js              Sensor synthesis + ENU/FLU conversions
      ros.js                  rosbridge connection, publishers/subscribers, mission API
      boat.js                 Vehicle visual model + wake
      environment.js          Scene object registry, placement, colliders
      editor.js               In-browser scene editor
      mission.js              Top-down mission planner
      ui.js                   HUD (status, telemetry, help)
      main.js                 Bootstrap, fixed-step loop, camera, input
    vendor/                   Three.js and roslib (vendored, no CDN)
  ros2_ws/src/usv_sim/        ROS 2 (ament_python) package
    usv_sim/
      monitor.py              Subscribes all sensors, prints rates and status
      teleop_keyboard.py      Terminal keyboard teleoperation
      autonomous_controller.py  LOS path-following controller
      viz.py                  RViz helper: path, markers, stabilized camera frame
      mission_manager.py      Receives UI route + command, launches controller and RViz
    launch/                   Launch files (bringup, autonomous, rviz, mission_planner)
    rviz/usv.rviz             RViz configuration
  scripts/                    env, serve, build, rviz, mission, run_all, setup_rosbridge

Troubleshooting

"ROS offline" in the browser. Make sure rosbridge is running and reachable on port 9090. The client reconnects automatically every 2 seconds.

rosbridge fails to start / serialization errors. A standalone bson pip package shadows PyMongo's bson module and breaks rosbridge. Remove it: pip uninstall -y bson (keep PyMongo).

The vehicle position jumps around, or RViz shakes / drifts. Two simulator tabs are connected to the same bridge and both are publishing odometry. Close all but one simulator tab. You can confirm with ros2 topic echo /client_count (should read 1) and ros2 topic hz /usv/odom (should read approximately 20 Hz, not 40).

Code changes do not appear. The browser is caching the ES modules. Hard-reload (Ctrl+Shift+R). The provided serve_web.py sets no-store headers to avoid this.

Port already in use. Check for stale processes on 8080 (web) or 9090 (rosbridge): ss -tlnp | grep -E '8080|9090'.

ROS 2 installed somewhere non-standard. scripts/env.sh sources ROS 2, rosbridge, and this workspace. Edit the paths at the top of the script if your installations differ from the defaults.

About

Browser-based ROS 2 simulation of an autonomous surface vehicle navigating a physically simulated Istanbul Bosphorus — from-scratch rigid-body hydrodynamics, WebGL rendering, LOS path following.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages