diff --git a/lunar_terrain/Dockerfile b/lunar_terrain/Dockerfile new file mode 100644 index 00000000..a6394016 --- /dev/null +++ b/lunar_terrain/Dockerfile @@ -0,0 +1,155 @@ +# Copyright 2021 Open Source Robotics Foundation, Inc. +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# A Docker configuration script to build the Space ROS image. +# +# The script provides the following build arguments: +# +# VCS_REF - The git revision of the Space ROS source code (no default value). +# VERSION - The version of Space ROS (default: "preview") + +FROM osrf/space-ros:humble-2024.10.0 + +# Define arguments used in the metadata definition +ARG VCS_REF +ARG VERSION="preview" + +# Specify the docker image metadata +LABEL org.label-schema.schema-version="1.0" +LABEL org.label-schema.name="Lunar Terrain" +LABEL org.label-schema.description="Lunar terrain demo on the Space ROS platform" +LABEL org.label-schema.vendor="Open Robotics" +LABEL org.label-schema.version=${VERSION} +LABEL org.label-schema.url="https://github.com/space-ros" +LABEL org.label-schema.vcs-url="https://github.com/space-ros/docker" +LABEL org.label-schema.vcs-ref=${VCS_REF} + +# Clone all space-ros sources +RUN mkdir ${SPACEROS_DIR}/src \ + && vcs import ${SPACEROS_DIR}/src < ${SPACEROS_DIR}/exact.repos + +# Define a few key variables +ENV DEMO_DIR=${HOME_DIR}/demos_ws +ENV GZ_VERSION=garden +ENV GZ_PARTITION=spaceros +ENV GZ_SIM_RESOURCE_PATH=/home/spaceros-user/demos_ws/src/simulation/models/lunar_terrain/models +ENV ROS_DISTRO=humble + +# Disable prompting during package installation +ARG DEBIAN_FRONTEND=noninteractive + +# Update the ROS package keys +ADD --chmod=644 https://raw.githubusercontent.com/ros/rosdistro/master/ros.key /usr/share/keyrings/ros-archive-keyring.gpg + +# Make sure the latest versions of packages are installed +# Using Docker BuildKit cache mounts for /var/cache/apt and /var/lib/apt ensures that +# the cache won't make it into the built image but will be maintained between steps. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + sudo apt-get update +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + sudo apt-get dist-upgrade -y +RUN rosdep update + +# Install the various build and test tools +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + sudo apt install -y \ + build-essential \ + clang-format \ + cmake \ + git \ + libbullet-dev \ + python3-colcon-common-extensions \ + python3-flake8 \ + python3-pip \ + python3-pytest-cov \ + python3-rosdep \ + python3-setuptools \ + python3-vcstool \ + wget + +RUN sudo add-apt-repository ppa:kisak/kisak-mesa + +RUN sudo apt install xterm -y + +# Get rosinstall_generator +# Using Docker BuildKit cache mounts for /var/cache/apt and /var/lib/apt ensures that +# the cache won't make it into the built image but will be maintained between steps. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + sudo apt-get update -y && sudo apt-get install -y python3-rosinstall-generator + +# Install Gazebo Garden +# Using Docker BuildKit cache mounts for /var/cache/apt and /var/lib/apt ensures that +# the cache won't make it into the built image but will be maintained between steps. +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + sudo wget https://packages.osrfoundation.org/gazebo.gpg -O /usr/share/keyrings/pkgs-osrf-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/pkgs-osrf-archive-keyring.gpg] http://packages.osrfoundation.org/gazebo/ubuntu-stable $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/gazebo-stable.list > /dev/null \ + && sudo apt-get update -y && sudo apt-get install -y gz-garden && sudo apt-get install -y git-lfs && git lfs install + +# Generate repos file for demo dependencies, excluding packages from Space ROS core. +COPY --chown=${USERNAME}:${USERNAME} demo-pkgs.txt /tmp/ +COPY --chown=${USERNAME}:${USERNAME} excluded-pkgs.txt /tmp/ +RUN rosinstall_generator \ + --rosdistro ${ROS_DISTRO} \ + --deps \ + --exclude-path ${SPACEROS_DIR}/src \ + --exclude $(cat /tmp/excluded-pkgs.txt) -- \ + -- $(cat /tmp/demo-pkgs.txt) \ + > /tmp/demo_generated_pkgs.repos + +RUN mkdir -p ${DEMO_DIR}/src +WORKDIR ${DEMO_DIR} +RUN vcs import src < /tmp/demo_generated_pkgs.repos + +# Install system dependencies +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + /bin/bash -c 'source ${SPACEROS_DIR}/install/setup.bash' \ + && rosdep install --from-paths ${SPACEROS_DIR}/src src --ignore-src --rosdistro ${ROS_DISTRO} -r -y --skip-keys "console_bridge generate_parameter_library fastcdr fastrtps rti-connext-dds-5.3.1 urdfdom_headers rmw_connextdds ros_testing rmw_connextdds rmw_fastrtps_cpp rmw_fastrtps_dynamic_cpp composition demo_nodes_py lifecycle rosidl_typesupport_fastrtps_cpp rosidl_typesupport_fastrtps_c ikos diagnostic_aggregator diagnostic_updater joy qt_gui rqt_gui rqt_gui_py" + +# Get the source for the dependencies +# RUN vcs import src < /tmp/demo_generated_pkgs.repos +COPY --chown=${USERNAME}:${USERNAME} demo_manual_pkgs.repos /tmp/ +RUN vcs import src < /tmp/demo_manual_pkgs.repos && /bin/bash -c 'source "${SPACEROS_DIR}/install/setup.bash"' + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + sudo apt-get update -y \ +&& /bin/bash -c 'source "${SPACEROS_DIR}/install/setup.bash"' \ +&& rosdep install --from-paths src --ignore-src -r -y --rosdistro ${ROS_DISTRO} + +# Copy the demo source code +COPY --chown=${USERNAME}:${USERNAME} lunar_sun_gz_plugin src/lunar_sun_gz_plugin +COPY --chown=${USERNAME}:${USERNAME} lunar_terrain_gz_bringup src/lunar_terrain_gz_bringup +COPY --chown=${USERNAME}:${USERNAME} lunar_terrain_gz_worlds src/lunar_terrain_gz_worlds + +# Build the demo +RUN /bin/bash -c 'source ${SPACEROS_DIR}/install/setup.bash \ + && colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release' + +# Create the render group if it doesn't exist +RUN sudo groupadd -f render + +# Add the user to the render group so that the user can access /dev/dri/renderD128 +RUN sudo usermod -aG render $USERNAME + +# Setup the entrypoint +COPY --chown=${USERNAME}:${USERNAME} ./entrypoint.sh / +# RUN chown appuser:appuser /entrypoint.sh && chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] +CMD ["bash"] diff --git a/lunar_terrain/LunarSim.jpg b/lunar_terrain/LunarSim.jpg new file mode 100644 index 00000000..077cfa97 Binary files /dev/null and b/lunar_terrain/LunarSim.jpg differ diff --git a/lunar_terrain/README.md b/lunar_terrain/README.md new file mode 100644 index 00000000..6eac1a83 --- /dev/null +++ b/lunar_terrain/README.md @@ -0,0 +1,79 @@ +# Lunar Terrain Demo +[![Licence](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\ +Alt text + +This folder contains three ROS2 packages to enable simulation in a lunar environment, including a lunar gazebo world using a Digital Elevation Model (DEM) and a plugin for a dynamic sun model based on Ephemeris data. + +# Space ROS Lunar Terrain Demo Docker Image + +The Space ROS Lunar Terrain Demo docker image uses the spaceros docker image (*osrf/space-ros:latest*) as its base image. +The Dockerfile installs all of the prerequisite system dependencies along with the demo source code, then builds the Space ROS Lunar Sim Demo. + +This demo includes a Gazebo simulation of the lunar environment (specfically around the Shackleton crater near the south pole). It uses +Digital Elevation Models (DEMs) from the Lunar Orbiter Laser Altimeter (LOLA) to accurately simulate the lunar surface in a specific region. It also contains a dynamic model of the Sun that moves according to Ephemeris data. + +## Building the Demo Docker + +The demo image builds on top of the `spaceros` image. +To build the docker image, first ensure the `spaceros` base image is available either by [building it locally](https://github.com/space-ros/space-ros) or pulling it. + +Then build `lunar_terrain` demo images: + +```bash +cd lunar_terrain +./build.sh +``` + +## Running the Demo Docker + +run the following to allow GUI passthrough: +```bash +xhost +local:docker +``` + +Then run: + +```bash +./run.sh +``` + +Depending on the host computer, you might need to remove the ```--gpus all``` flag in ```run.sh```, which uses your GPUs. + +## Running the Demo + +Launch the demo: + +```bash +source install/setup.bash +ros2 launch lunar_terrain_gz_bringup lunar_terrain_world.launch.py +``` + +This will launch the gazebo lunar world, spawn the rover and start teleop. This will be a new terminal window which enables you to control the rover as per the instructions in the terminal window. + + +## lunar_sun_gz_plugin +This package contains a gazebo plugin to move an actor and create a light source at the location of the actor. +The plugin must be added to an actor named `animated_sun`, which can be done as follows: +``` + + + Other tags ... + +``` +The suns trajectory is based on the `horizons_az_el.csv` file. Detailed documentation on updating this can be found on the space-ros lunar_terrain docs page. + +You can adjust the sun’s position update frequency, which currently occurs every hour, by modifying the waypoint_duration variable in lunar_sun.cpp: +``` + std::chrono::_V2::steady_clock::duration waypointDuration = + std::chrono::hours(1); +``` + +## lunar_terrain_gz_worlds +This package contains the lunar_terrain world files, including the world sdf, DEM files, textures and tools for creating textures. It also contains the `display.launch.py` launch file, which launches gazebo with the lunar_terrain world by default (but does not spawn the rover). For detailed documentation on updating DEM model and textures see the space-ros lunar_terrain docs page. + +## lunar_terrain_gz_bringup +This package contains launch and configuration files to launch the lunar_terrain world and spawn [leo_rover](https://github.com/LeoRover) with keyboard controls. The `spawn_leo_robot.launch.py` spawns the leo model, starts`gz_bridge` and `key_teleop`. The `gz_bridge` parameters can be modified in the `leo_ros_gz_bridge.yaml` file in the config directory. + +Challenge Name: NASA Space ROS Sim Summer Sprint Challenge \ +Team Lead Freelancer Username: elementrobotics \ +Submission Title: LunarSim \ No newline at end of file diff --git a/lunar_terrain/build.sh b/lunar_terrain/build.sh new file mode 100755 index 00000000..75c716b6 --- /dev/null +++ b/lunar_terrain/build.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash + +ORG=openrobotics +IMAGE=space_robots_lunar_terrain +TAG=latest + +VCS_REF="" +VERSION=preview + +# Exit script with failure if build fails +set -eo pipefail + +echo "" +echo "##### Building Space ROS Demo Docker Image #####" +echo "" + +docker build -t $ORG/$IMAGE:$TAG \ + --build-arg VCS_REF="$VCS_REF" \ + --build-arg VERSION="$VERSION" . + +echo "" +echo "##### Done! #####" diff --git a/lunar_terrain/demo-pkgs.txt b/lunar_terrain/demo-pkgs.txt new file mode 100644 index 00000000..810d8895 --- /dev/null +++ b/lunar_terrain/demo-pkgs.txt @@ -0,0 +1,4 @@ +leo_gz_plugins +rviz2 +xacro +teleop_twist_keyboard \ No newline at end of file diff --git a/lunar_terrain/demo_manual_pkgs.repos b/lunar_terrain/demo_manual_pkgs.repos new file mode 100644 index 00000000..9303f68a --- /dev/null +++ b/lunar_terrain/demo_manual_pkgs.repos @@ -0,0 +1,26 @@ +repositories: + actuator_msgs: + type: git + url: https://github.com/rudislabs/actuator_msgs.git + version: main + ros_gz: + type: git + url: https://github.com/gazebosim/ros_gz.git + version: humble + vision_msgs: + type: git + url: https://github.com/ros-perception/vision_msgs.git + version: humble + gps_msgs: + type: git + url: https://github.com/swri-robotics/gps_umd.git + path: gps_msgs + version: 113782d + leo_common-ros2: + type: git + url: https://github.com/LeoRover/leo_common-ros2.git + version: humble + simulation: + type: git + url: https://github.com/space-ros/simulation.git + version: main \ No newline at end of file diff --git a/lunar_terrain/entrypoint.sh b/lunar_terrain/entrypoint.sh new file mode 100755 index 00000000..d1b51f26 --- /dev/null +++ b/lunar_terrain/entrypoint.sh @@ -0,0 +1,6 @@ +#!/bin/bash +set -e + +# Setup the Demo environment +source "${DEMO_DIR}/install/setup.bash" +exec "$@" diff --git a/lunar_terrain/excluded-pkgs.txt b/lunar_terrain/excluded-pkgs.txt new file mode 100644 index 00000000..e279257d --- /dev/null +++ b/lunar_terrain/excluded-pkgs.txt @@ -0,0 +1,10 @@ +fastcdr +fastrtps +fastrtps_cmake_module +generate_parameter_library +rmw_connextdds +rmw_fastrtps_cpp +rmw_fastrtps_dynamic_cpp +rmw_fastrtps_shared_cpp +rosidl_typesupport_fastrtps_c +rosidl_typesupport_fastrtps_cpp diff --git a/lunar_terrain/lunar_sun_gz_plugin/CMakeLists.txt b/lunar_terrain/lunar_sun_gz_plugin/CMakeLists.txt new file mode 100644 index 00000000..6e0ff86f --- /dev/null +++ b/lunar_terrain/lunar_sun_gz_plugin/CMakeLists.txt @@ -0,0 +1,55 @@ +cmake_minimum_required(VERSION 3.8) +project(lunar_sun_gz_plugin) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +find_package(ament_cmake REQUIRED) + +add_library(lunar_sun_gz_plugin SHARED +src/lunar_sun.cpp) + +if(DEFINED ENV{GZ_VERSION} AND "$ENV{GZ_VERSION}" STREQUAL "garden") + find_package(gz-plugin2 REQUIRED COMPONENTS register) + set(GZ_PLUGIN_VER ${gz-plugin2_VERSION_MAJOR}) + + find_package(gz-sim7 REQUIRED) + set(GZ_SIM_VER ${gz-sim7_VERSION_MAJOR}) + + target_link_libraries(lunar_sun_gz_plugin + gz-sim${GZ_SIM_VER}::gz-sim${GZ_SIM_VER} + gz-plugin${GZ_PLUGIN_VER}::gz-plugin${GZ_PLUGIN_VER} + ) +else() + #If GZ_VERSION is not set default to Fortress + find_package(ignition-plugin1 REQUIRED COMPONENTS register) + set(IGN_PLUGIN_VER ${ignition-plugin1_VERSION_MAJOR}) + + find_package(ignition-gazebo6 REQUIRED) + set(IGN_GAZEBO_VER ${ignition-gazebo6_VERSION_MAJOR}) + + add_definitions(-DUSE_IGNITION) + + target_link_libraries(lunar_sun_gz_plugin + ignition-gazebo${IGN_GAZEBO_VER}::ignition-gazebo${IGN_GAZEBO_VER} + ignition-plugin${IGN_PLUGIN_VER}::ignition-plugin${IGN_PLUGIN_VER} + ) +endif() + +set_property(TARGET lunar_sun_gz_plugin PROPERTY CXX_STANDARD 17) + +install( + TARGETS + lunar_sun_gz_plugin + DESTINATION lib +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + ament_lint_auto_find_test_dependencies() +endif() + +ament_environment_hooks("${CMAKE_CURRENT_SOURCE_DIR}/hooks/gz_sim_system_plugin_path.dsv") + +ament_package() diff --git a/lunar_terrain/lunar_sun_gz_plugin/hooks/gz_sim_system_plugin_path.dsv b/lunar_terrain/lunar_sun_gz_plugin/hooks/gz_sim_system_plugin_path.dsv new file mode 100644 index 00000000..0cefc7b7 --- /dev/null +++ b/lunar_terrain/lunar_sun_gz_plugin/hooks/gz_sim_system_plugin_path.dsv @@ -0,0 +1 @@ +prepend-non-duplicate;GZ_SIM_SYSTEM_PLUGIN_PATH;lib \ No newline at end of file diff --git a/lunar_terrain/lunar_sun_gz_plugin/horizons_az_el.csv b/lunar_terrain/lunar_sun_gz_plugin/horizons_az_el.csv new file mode 100644 index 00000000..7ebf139b --- /dev/null +++ b/lunar_terrain/lunar_sun_gz_plugin/horizons_az_el.csv @@ -0,0 +1,98 @@ +Date__(UT)__HR:MN,Azimuth,Elevation +2024-Aug-12 00:00,29.321225,1.240311 +2024-Aug-12 01:00,28.812252,1.253037 +2024-Aug-12 02:00,28.303282,1.265572 +2024-Aug-12 03:00,27.794317,1.277915 +2024-Aug-12 04:00,27.285356,1.290064 +2024-Aug-12 05:00,26.776399,1.302019 +2024-Aug-12 06:00,26.267446,1.313778 +2024-Aug-12 07:00,25.758497,1.325342 +2024-Aug-12 08:00,25.249553,1.336709 +2024-Aug-12 09:00,24.740613,1.347878 +2024-Aug-12 10:00,24.231678,1.358848 +2024-Aug-12 11:00,23.722748,1.369619 +2024-Aug-12 12:00,23.213822,1.380190 +2024-Aug-12 13:00,22.704902,1.390561 +2024-Aug-12 14:00,22.195986,1.400730 +2024-Aug-12 15:00,21.687075,1.410696 +2024-Aug-12 16:00,21.178170,1.420460 +2024-Aug-12 17:00,20.669270,1.430020 +2024-Aug-12 18:00,20.160376,1.439375 +2024-Aug-12 19:00,19.651487,1.448526 +2024-Aug-12 20:00,19.142604,1.457471 +2024-Aug-12 21:00,18.633726,1.466209 +2024-Aug-12 22:00,18.124855,1.474741 +2024-Aug-12 23:00,17.615989,1.483065 +2024-Aug-13 00:00,17.107130,1.491182 +2024-Aug-13 01:00,16.598277,1.499090 +2024-Aug-13 02:00,16.089430,1.506788 +2024-Aug-13 03:00,15.580590,1.514277 +2024-Aug-13 04:00,15.071756,1.521556 +2024-Aug-13 05:00,14.562929,1.528624 +2024-Aug-13 06:00,14.054109,1.535481 +2024-Aug-13 07:00,13.545297,1.542127 +2024-Aug-13 08:00,13.036491,1.548560 +2024-Aug-13 09:00,12.527692,1.554781 +2024-Aug-13 10:00,12.018901,1.560790 +2024-Aug-13 11:00,11.510118,1.566585 +2024-Aug-13 12:00,11.001342,1.572166 +2024-Aug-13 13:00,10.492574,1.577533 +2024-Aug-13 14:00,9.983814,1.582687 +2024-Aug-13 15:00,9.475062,1.587625 +2024-Aug-13 16:00,8.966318,1.592349 +2024-Aug-13 17:00,8.457582,1.596857 +2024-Aug-13 18:00,7.948855,1.601151 +2024-Aug-13 19:00,7.440137,1.605228 +2024-Aug-13 20:00,6.931427,1.609089 +2024-Aug-13 21:00,6.422726,1.612735 +2024-Aug-13 22:00,5.914034,1.616164 +2024-Aug-13 23:00,5.405351,1.619376 +2024-Aug-14 00:00,4.896678,1.622372 +2024-Aug-14 01:00,4.388014,1.625151 +2024-Aug-14 02:00,3.879359,1.627713 +2024-Aug-14 03:00,3.370714,1.630058 +2024-Aug-14 04:00,2.862079,1.632186 +2024-Aug-14 05:00,2.353454,1.634096 +2024-Aug-14 06:00,1.844839,1.635790 +2024-Aug-14 07:00,1.336234,1.637265 +2024-Aug-14 08:00,0.827640,1.638524 +2024-Aug-14 09:00,0.319056,1.639565 +2024-Aug-14 10:00,359.810483,1.640389 +2024-Aug-14 11:00,359.301920,1.640996 +2024-Aug-14 12:00,358.793368,1.641385 +2024-Aug-14 13:00,358.284827,1.641557 +2024-Aug-14 14:00,357.776298,1.641512 +2024-Aug-14 15:00,357.267779,1.641249 +2024-Aug-14 16:00,356.759272,1.640770 +2024-Aug-14 17:00,356.250777,1.640074 +2024-Aug-14 18:00,355.742293,1.639162 +2024-Aug-14 19:00,355.233820,1.638033 +2024-Aug-14 20:00,354.725360,1.636688 +2024-Aug-14 21:00,354.216912,1.635126 +2024-Aug-14 22:00,353.708475,1.633349 +2024-Aug-14 23:00,353.200051,1.631356 +2024-Aug-15 00:00,352.691640,1.629147 +2024-Aug-15 01:00,352.183240,1.626724 +2024-Aug-15 02:00,351.674854,1.624086 +2024-Aug-15 03:00,351.166480,1.621233 +2024-Aug-15 04:00,350.658118,1.618166 +2024-Aug-15 05:00,350.149770,1.614885 +2024-Aug-15 06:00,349.641434,1.611390 +2024-Aug-15 07:00,349.133112,1.607682 +2024-Aug-15 08:00,348.624803,1.603762 +2024-Aug-15 09:00,348.116507,1.599629 +2024-Aug-15 10:00,347.608225,1.595284 +2024-Aug-15 11:00,347.099956,1.590728 +2024-Aug-15 12:00,346.591700,1.585961 +2024-Aug-15 13:00,346.083458,1.580983 +2024-Aug-15 14:00,345.575230,1.575795 +2024-Aug-15 15:00,345.067016,1.570397 +2024-Aug-15 16:00,344.558816,1.564790 +2024-Aug-15 17:00,344.050630,1.558975 +2024-Aug-15 18:00,343.542457,1.552951 +2024-Aug-15 19:00,343.034299,1.546721 +2024-Aug-15 20:00,342.526155,1.540283 +2024-Aug-15 21:00,342.018026,1.533639 +2024-Aug-15 22:00,341.509911,1.526789 +2024-Aug-15 23:00,341.001810,1.519735 +2024-Aug-16 00:00,340.493724,1 diff --git a/lunar_terrain/lunar_sun_gz_plugin/package.xml b/lunar_terrain/lunar_sun_gz_plugin/package.xml new file mode 100644 index 00000000..6a9d21a7 --- /dev/null +++ b/lunar_terrain/lunar_sun_gz_plugin/package.xml @@ -0,0 +1,31 @@ + + + + lunar_sun_gz_plugin + 1.0.0 + + Plugins for dynamic sun in lunar world + + Munir Azme + Apache License 2.0 + + ament_cmake + + gz-plugin2 + gz-sim7 + + gz-plugin + gz-sim6 + gz-plugin + gz-sim6 + + ament_lint_auto + ament_cmake_copyright + ament_cmake_cpplint + ament_cmake_lint_cmake + ament_cmake_uncrustify + ament_cmake_xmllint + + ament_cmake + + \ No newline at end of file diff --git a/lunar_terrain/lunar_sun_gz_plugin/src/lunar_sun.cpp b/lunar_terrain/lunar_sun_gz_plugin/src/lunar_sun.cpp new file mode 100644 index 00000000..bfa8e530 --- /dev/null +++ b/lunar_terrain/lunar_sun_gz_plugin/src/lunar_sun.cpp @@ -0,0 +1,215 @@ +// Copyright 2024 Element Robotics Pty Ltd +// +// 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 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef USE_IGNITION +namespace gazebo = ignition::gazebo; +#else +namespace gazebo = gz::sim; +#endif + +class LunarSun : public gazebo::System, + public gazebo::ISystemConfigure, + public gazebo::ISystemUpdate { + // Model entity + gazebo::World world_{gazebo::kNullEntity}; + + + // Joint entities + gazebo::Entity actorEntity, lightEntity; + + // Whether the system has been properly configured + bool configured_{false}; + +public: + void Configure(const gazebo::Entity& entity, + const std::shared_ptr& sdf, + gazebo::EntityComponentManager& ecm, + gazebo::EventManager& /*eventMgr*/) override { + this->world_ = gazebo::World(entity); + + this->actorEntity = + ecm.EntityByComponents(gazebo::components::Name("animated_sun")); + + LoadCSV("/home/spaceros-user/demos_ws/src/lunar_sun_gz_plugin/horizons_az_el.csv"); + + ignition::math::Pose3d startPose(0.0, 0.0, 0.0, 0.0, 0.0, 0.0); + // Position the light above the ground + ecm.CreateComponent(this->actorEntity, + gazebo::components::Pose(startPose)); + ecm.CreateComponent(this->actorEntity, + gazebo::components::TrajectoryPose( + this->trajectory[0])); + configured_ = true; + } + + void Update(const gazebo::UpdateInfo& info, + gazebo::EntityComponentManager& ecm) override { + // Get actor pose + auto actor = gazebo::Actor(this->actorEntity); + auto actorPosition = actor.WorldPose(ecm); + + + // Get the current simulation time + this->currentSimTime = info.simTime; + + // Calculate the time elapsed since the last update + std::chrono::_V2::steady_clock::duration elapsedTime = + this->currentSimTime - this->startWaypointTime; + + double secondElapsed = + std::chrono::duration_cast(elapsedTime).count(); + + if (elapsedTime >= waypointDuration) { + // Move to the next waypoint + this->startWaypointTime = this->currentSimTime; + this->trajectoryIndex++; + if (this->trajectoryIndex >= this->trajectory.size()) { + this->trajectoryIndex = 0; + } + // Set the actor's trajectory pose + actor.SetTrajectoryPose(ecm, this->trajectory[this->trajectoryIndex]); + ecm.SetChanged(this->actorEntity, gazebo::components::TrajectoryPose::typeId, + gazebo::ComponentState::PeriodicChange); + + ignition::msgs::Light lightMsg; + lightMsg.set_name("sunlight"); + lightMsg.set_type(ignition::msgs::Light::DIRECTIONAL); + + // Set the pose in the light message + // Directly set the position components + ignition::msgs::Pose* poseLight2 = lightMsg.mutable_pose(); + poseLight2->mutable_position()->set_x( + this->trajectory[this->trajectoryIndex].X()); + poseLight2->mutable_position()->set_y( + this->trajectory[this->trajectoryIndex].Y()); + poseLight2->mutable_position()->set_z( + this->trajectory[this->trajectoryIndex].Z()); + + // Directly set the orientation components (quaternion) + poseLight2->mutable_orientation()->set_w(1.0); // No rotation + poseLight2->mutable_orientation()->set_x(0.0); + poseLight2->mutable_orientation()->set_y(0.0); + poseLight2->mutable_orientation()->set_z(0.0); + + // Set light properties + lightMsg.mutable_diffuse()->set_r(0.8); + lightMsg.mutable_diffuse()->set_g(0.8); + lightMsg.mutable_diffuse()->set_b(0.8); + lightMsg.mutable_diffuse()->set_a(1.0); + + lightMsg.mutable_specular()->set_r(0.05); + lightMsg.mutable_specular()->set_g(0.05); + lightMsg.mutable_specular()->set_b(0.05); + lightMsg.mutable_specular()->set_a(1.0); + + lightMsg.mutable_direction()->set_x( + this->trajectory[this->trajectoryIndex].X()); + lightMsg.mutable_direction()->set_y( + -this->trajectory[this->trajectoryIndex].Y()); + lightMsg.mutable_direction()->set_z( + -this->trajectory[this->trajectoryIndex].Z()); + + lightMsg.set_range(20); + lightMsg.set_cast_shadows(true); + lightMsg.set_intensity(200.0); + + // Publish the light message to the light config service + bool result; + ignition::msgs::Boolean res; + ignition::transport::Node node; + constexpr unsigned int timeout = 5000; + bool executed = node.Request("/world/dem_heightmap/light_config", lightMsg, + timeout, res, result); + + ecm.SetChanged(this->lightEntity, gazebo::components::Pose::typeId, + gazebo::ComponentState::PeriodicChange); + ecm.SetChanged(this->lightEntity, gazebo::components::Light::typeId, + gazebo::ComponentState::PeriodicChange); + } else { + return; + } + } + +private: + void LoadCSV(const std::string& filename) { + double originx; + double originy; + double originz; + std::ifstream file(filename); + std::string line; + + if (std::getline(file, line)) { + // Skip the first line + } + + if (std::getline(file, line)) { + std::stringstream ss(line); + std::string origTimeStr, origAzimuthStr, origElevationStr; + std::getline(ss, origTimeStr, ','); + std::getline(ss, origAzimuthStr, ','); + std::getline(ss, origElevationStr, ','); + double origAzimuth = std::stod(origAzimuthStr); + double origElevation = std::stod(origElevationStr); + originx = 10000 * cos(origAzimuth * M_PI / 180.0) * + cos(origElevation * M_PI / 180.0); + originy = 10000 * sin(origAzimuth * M_PI / 180.0) * + cos(origElevation * M_PI / 180.0); + originz = sin(origElevation * M_PI / 180.0); + } + + while (std::getline(file, line)) { + std::stringstream ss(line); + std::string timeStr, azimuthStr, elevationStr; + std::getline(ss, timeStr, ','); + std::getline(ss, azimuthStr, ','); + std::getline(ss, elevationStr, ','); + double azimuth = std::stod(azimuthStr); + double elevation = std::stod(elevationStr); + + // Convert azimuth and elevation to radians and then to a Pose + ignition::math::Vector3d position( + 100000 * cos(azimuth * M_PI / 180.0) * cos(elevation * M_PI / 180.0), + 100000 * sin(azimuth * M_PI / 180.0) * cos(elevation * M_PI / 180.0), + (100000 * sin(elevation * M_PI / 180.0)) - 10000); + + ignition::math::Pose3d pose(position, ignition::math::Quaterniond::Identity); + this->trajectory.push_back(pose); + } + } + + std::vector trajectory; + int trajectoryIndex = 0; + std::chrono::_V2::steady_clock::duration startWaypointTime = + std::chrono::steady_clock::duration::zero(); + std::chrono::_V2::steady_clock::duration currentSimTime; + std::chrono::_V2::steady_clock::duration waypointDuration = + std::chrono::hours(1); +}; + +IGNITION_ADD_PLUGIN(LunarSun, gazebo::System, LunarSun::ISystemConfigure, + LunarSun::ISystemUpdate) diff --git a/lunar_terrain/lunar_terrain_gz_bringup/CMakeLists.txt b/lunar_terrain/lunar_terrain_gz_bringup/CMakeLists.txt new file mode 100644 index 00000000..1dab55ca --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_bringup/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.8) +project(lunar_terrain_gz_bringup) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) +# uncomment the following section in order to fill in +# further dependencies manually. +# find_package( REQUIRED) + +install( + DIRECTORY + launch + config + DESTINATION share/${PROJECT_NAME} +) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + # the following line skips the linter which checks for copyrights + # comment the line when a copyright and license is added to all source files + set(ament_cmake_copyright_FOUND TRUE) + # the following line skips cpplint (only works in a git repo) + # comment the line when this package is in a git repo and when + # a copyright and license is added to all source files + set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +ament_package() diff --git a/lunar_terrain/lunar_terrain_gz_bringup/config/leo_ros_gz_bridge.yaml b/lunar_terrain/lunar_terrain_gz_bringup/config/leo_ros_gz_bridge.yaml new file mode 100644 index 00000000..4f98732d --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_bringup/config/leo_ros_gz_bridge.yaml @@ -0,0 +1,21 @@ +--- +- ros_topic_name: "/cmd_vel" + ros_type_name: "geometry_msgs/msg/Twist" + gz_topic_name: "/cmd_vel" + gz_type_name: "ignition.msgs.Twist" + direction: ROS_TO_GZ +- gz_topic_name: "/imu/data_raw" + gz_type_name: "ignition.msgs.IMU" + ros_topic_name: "/imu/data_raw" + ros_type_name: "sensor_msgs/msg/Imu" + direction: GZ_TO_ROS +- gz_topic_name: "/joint_states" + gz_type_name: "ignition.msgs.Model" + ros_topic_name: "/joint_states" + ros_type_name: "sensor_msgs/msg/JointState" + direction: GZ_TO_ROS +- gz_topic_name: "/odom" + gz_type_name: "ignition.msgs.Odometry" + ros_topic_name: "/odom" + ros_type_name: "nav_msgs/msg/Odometry" + direction: GZ_TO_ROS \ No newline at end of file diff --git a/lunar_terrain/lunar_terrain_gz_bringup/launch/lunar_terrain_world.launch.py b/lunar_terrain/lunar_terrain_gz_bringup/launch/lunar_terrain_world.launch.py new file mode 100644 index 00000000..78ad54d9 --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_bringup/launch/lunar_terrain_world.launch.py @@ -0,0 +1,84 @@ +# Copyright 2023 Fictionlab sp. z o.o. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +import os +import launch +from ament_index_python.packages import get_package_share_directory + +from launch import LaunchDescription +from launch.actions import DeclareLaunchArgument +from launch.actions import IncludeLaunchDescription +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch.substitutions import LaunchConfiguration +from launch_ros.actions import Node + + +def generate_launch_description(): + # Setup project paths + pkg_project_gazebo = get_package_share_directory("lunar_terrain_gz_bringup") + pkg_project_worlds = get_package_share_directory("lunar_terrain_gz_worlds") + + # #Tenzin: TODO - research what this actually does and how we should be specifying it + robot_ns = DeclareLaunchArgument( + "robot_ns", + default_value="", + description="Robot namespace", + ) + + # Setup to launch the simulator and Gazebo world + gz_sim = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(pkg_project_worlds, "launch", "display.launch.py") + ), + ) + + spawn_robot = IncludeLaunchDescription( + PythonLaunchDescriptionSource( + os.path.join(pkg_project_gazebo, "launch", + "spawn_leo_robot.launch.py") + ), + launch_arguments={"robot_ns": LaunchConfiguration("robot_ns"),}.items(), + ) + + # Bridge ROS topics and Gazebo messages for establishing communication + topic_bridge = Node( + package="ros_gz_bridge", + executable="parameter_bridge", + name="clock_bridge", + arguments=[ + "/clock@rosgraph_msgs/msg/Clock[ignition.msgs.Clock]", + ], + parameters=[ + { + "qos_overrides./tf_static.publisher.durability": "transient_local", + } + ], + output="screen", + ) + + return LaunchDescription( + [ + robot_ns, + gz_sim, + spawn_robot, + topic_bridge, + ] + ) diff --git a/lunar_terrain/lunar_terrain_gz_bringup/launch/spawn_leo_robot.launch.py b/lunar_terrain/lunar_terrain_gz_bringup/launch/spawn_leo_robot.launch.py new file mode 100644 index 00000000..c4e8f528 --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_bringup/launch/spawn_leo_robot.launch.py @@ -0,0 +1,159 @@ +# Copyright 2023 Fictionlab sp. z o.o. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +import os + +from ament_index_python.packages import get_package_share_directory +from launch import LaunchContext, LaunchDescription +from launch.actions import DeclareLaunchArgument, OpaqueFunction +from launch.substitutions import LaunchConfiguration +from launch.actions import IncludeLaunchDescription +from launch_xml.launch_description_sources import XMLLaunchDescriptionSource +from launch.substitutions import PathJoinSubstitution +from launch_ros.substitutions import FindPackageShare +from launch_ros.actions import Node +import xacro + + +def spawn_robot(context: LaunchContext, namespace: LaunchConfiguration): + pkg_project_description = get_package_share_directory("leo_description") + + lunar_terrain_gz_bringup = get_package_share_directory("lunar_terrain_gz_bringup") + robot_ns = context.perform_substitution(namespace) + + robot_desc = xacro.process( + os.path.join( + pkg_project_description, + "urdf", + "leo_sim.urdf.xacro", + ), + mappings={"robot_ns": robot_ns}, + ) + + if robot_ns == "": + robot_gazebo_name = "leo_rover" + node_name_prefix = "" + else: + robot_gazebo_name = "leo_rover_" + robot_ns + node_name_prefix = robot_ns + "_" + + # Launch robot state publisher node + robot_state_publisher = Node( + namespace=robot_ns, + package="robot_state_publisher", + executable="robot_state_publisher", + name="robot_state_publisher", + output="both", + parameters=[ + {'use_sim_time': LaunchConfiguration('use_sim_time')}, + {"robot_description": robot_desc}, + ] + ) + + # Spawn a robot inside a simulation + leo_rover = Node( + namespace=robot_ns, + package="ros_gz_sim", + executable="create", + name="ros_gz_sim_create", + output="both", + arguments=[ + "-topic", + "robot_description", + "-name", + robot_gazebo_name, + "-x", + LaunchConfiguration('x_pose'), + "-y", + LaunchConfiguration('y_pose'), + "-z", + LaunchConfiguration('z_pose'), + ], + ) + + # Bridge ROS topics and Gazebo messages for establishing communication + topic_bridge = Node( + package="ros_gz_bridge", + executable="parameter_bridge", + name=node_name_prefix + "parameter_bridge", + parameters=[ + { + "config_file": os.path.join(lunar_terrain_gz_bringup, "config", "leo_ros_gz_bridge.yaml"), + "qos_overrides./tf_static.publisher.durability": "transient_local", + } + ], + output="screen", + ) + + + # Launch key_teleop.launch.xml from leo_teleop package + key_teleop_launch = IncludeLaunchDescription( + XMLLaunchDescriptionSource( + PathJoinSubstitution( + [FindPackageShare('leo_teleop'), + 'launch', 'key_teleop.launch.xml'] + ) + ) + ) + + return [ + robot_state_publisher, + leo_rover, + topic_bridge, + key_teleop_launch + ] + + +def generate_launch_description(): + name_argument = DeclareLaunchArgument( + "robot_ns", + default_value="", + description="Robot namespace", + ) + + x_arg = DeclareLaunchArgument( + 'x_pose', + default_value='0', + description="The x_pose of the rover's starting position" + ) + + y_arg = DeclareLaunchArgument( + 'y_pose', + default_value='0', + description="The y_pose of the rover's starting position" + ) + + z_arg = DeclareLaunchArgument( + 'z_pose', + default_value='560', + description="The z_pose of the rover's starting position" + ) + + namespace = LaunchConfiguration("robot_ns") + + return LaunchDescription( + [ x_arg, + y_arg, + z_arg, + name_argument, + OpaqueFunction(function=spawn_robot, args=[namespace]) + ] + ) diff --git a/lunar_terrain/lunar_terrain_gz_bringup/package.xml b/lunar_terrain/lunar_terrain_gz_bringup/package.xml new file mode 100644 index 00000000..953e4af2 --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_bringup/package.xml @@ -0,0 +1,37 @@ + + + lunar_terrain_gz_bringup + 0.0.0 + Bringup package for the lunar_terrain simulation in ROS 2 + elero-3 + Apache License 2.0 + + ament_cmake + + ament_index_python + robot_state_publisher + ros_gz_bridge + ros_gz_image + ros_gz_sim + xacro + tf2_ros + + leo_description + lunar_terrain_gz_plugins + leo_gz_plugins + lunar_terrain_gz_worlds + + ament_lint_auto + ament_cmake_black + ament_cmake_copyright + ament_cmake_lint_cmake + ament_cmake_mypy + ament_cmake_xmllint + + ament_lint_auto + ament_lint_common + + + ament_cmake + + diff --git a/lunar_terrain/lunar_terrain_gz_worlds/CMakeLists.txt b/lunar_terrain/lunar_terrain_gz_worlds/CMakeLists.txt new file mode 100644 index 00000000..72464ac9 --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_worlds/CMakeLists.txt @@ -0,0 +1,34 @@ +cmake_minimum_required(VERSION 3.8) +project(lunar_terrain_gz_worlds) + +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) +endif() + +# find dependencies +find_package(ament_cmake REQUIRED) +# uncomment the following section in order to fill in +# further dependencies manually. +# find_package( REQUIRED) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + # the following line skips the linter which checks for copyrights + # comment the line when a copyright and license is added to all source files + set(ament_cmake_copyright_FOUND TRUE) + # the following line skips cpplint (only works in a git repo) + # comment the line when this package is in a git repo and when + # a copyright and license is added to all source files + set(ament_cmake_cpplint_FOUND TRUE) + ament_lint_auto_find_test_dependencies() +endif() + +# Install launch files. +install( + DIRECTORY launch + DESTINATION share/${PROJECT_NAME}/ +) + +ament_environment_hooks("${CMAKE_CURRENT_SOURCE_DIR}/hooks/gz_sim_resource_path.dsv.in") + +ament_package() diff --git a/lunar_terrain/lunar_terrain_gz_worlds/hooks/gz_sim_resource_path.dsv.in b/lunar_terrain/lunar_terrain_gz_worlds/hooks/gz_sim_resource_path.dsv.in new file mode 100644 index 00000000..0858a47d --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_worlds/hooks/gz_sim_resource_path.dsv.in @@ -0,0 +1 @@ +prepend-non-duplicate;GZ_SIM_RESOURCE_PATH;share/@PROJECT_NAME@/worlds;share/@PROJECT_NAME@/models \ No newline at end of file diff --git a/lunar_terrain/lunar_terrain_gz_worlds/launch/display.launch.py b/lunar_terrain/lunar_terrain_gz_worlds/launch/display.launch.py new file mode 100644 index 00000000..c13c683d --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_worlds/launch/display.launch.py @@ -0,0 +1,93 @@ +import launch +from launch.actions import ( + ExecuteProcess, + DeclareLaunchArgument, + RegisterEventHandler, + SetEnvironmentVariable, +) +from launch.conditions import IfCondition +from launch.event_handlers import OnProcessExit +from launch.substitutions import ( + Command, + FindExecutable, + LaunchConfiguration, + NotSubstitution, + AndSubstitution, + PathJoinSubstitution +) +from launch.launch_description_sources import PythonLaunchDescriptionSource +from launch_ros.actions import Node +from launch_ros.substitutions import FindPackageShare +import launch_ros +import os + + +def generate_launch_description(): + # robot_model = launch_ros.substitutions.FindPackageShare(package="leo_description").find("leo_description") + pkg_share = FindPackageShare("simulation") + gz_verbosity = LaunchConfiguration("gz_verbosity") + headless = LaunchConfiguration("headless") + world = LaunchConfiguration('world') + + world_path = PathJoinSubstitution([pkg_share, "models", "lunar_terrain", "world", world]) + + # gazebo have to be executed with shell=False, or test_launch won't terminate it + # see: https://github.com/ros2/launch/issues/545 + # This code is form taken ros_gz_sim and modified to work with shell=False + # see: https://github.com/gazebosim/ros_gz/blob/ros2/ros_gz_sim/launch/gz_sim.launch.py.in + gz_env = {'GZ_SIM_SYSTEM_PLUGIN_PATH': + ':'.join([os.environ.get('GZ_SIM_SYSTEM_PLUGIN_PATH', default=''), + os.environ.get('LD_LIBRARY_PATH', default='')]), + 'GZ_PARTITION': os.environ.get('GZ_PARTITION', default='')} + gazebo = [ + ExecuteProcess( + condition=launch.conditions.IfCondition(headless), + cmd=['ruby', FindExecutable(name="gz"), 'sim', '-r', '-v', gz_verbosity, '-s', '--headless-rendering', world_path], + output='screen', + additional_env=gz_env, # type: ignore + shell=False, + ), + ExecuteProcess( + condition=launch.conditions.UnlessCondition(headless), + cmd=['ruby', FindExecutable(name="gz"), 'sim', '-r', '-v', gz_verbosity, world_path], + output='screen', + additional_env=gz_env, # type: ignore + shell=False, + ) + ] + + return launch.LaunchDescription( + [ + + DeclareLaunchArgument( + name="headless", + default_value="False", + description="Start GZ in headless mode and don't start RViz (overrides use_rviz)", + ), + DeclareLaunchArgument( + name="use_sim_time", + default_value="True", + description="Flag to enable use_sim_time", + ), + DeclareLaunchArgument( + "gz_verbosity", + default_value="3", + description="Verbosity level for Gazebo (0~4).", + ), + DeclareLaunchArgument( + "gz_args", + default_value="", + description="Extra args for Gazebo (ie. '-s' for running headless)", + ), + DeclareLaunchArgument( + name="log_level", + default_value="info", + description="The level of logging that is applied to all ROS 2 nodes launched by this script.", + ), + DeclareLaunchArgument( + 'world', + default_value="dem_moon.sdf", + description="The world file to be used from the worlds directory" + ) + ] + gazebo + ) \ No newline at end of file diff --git a/lunar_terrain/lunar_terrain_gz_worlds/package.xml b/lunar_terrain/lunar_terrain_gz_worlds/package.xml new file mode 100644 index 00000000..22b677c6 --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_worlds/package.xml @@ -0,0 +1,21 @@ + + + lunar_terrain_gz_worlds + 0.0.0 + Gazebo Worlds for Moon World Simulation using Ignition Gazebo + Element Robotics + Apache License 2.0 + + ament_cmake + ament_cmake_python + + ament_lint_auto + ament_lint_common + + ros_ign_gazebo + leo_description + + + ament_cmake + + diff --git a/lunar_terrain/lunar_terrain_gz_worlds/scripts/blend_normals.py b/lunar_terrain/lunar_terrain_gz_worlds/scripts/blend_normals.py new file mode 100644 index 00000000..2f9a2901 --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_worlds/scripts/blend_normals.py @@ -0,0 +1,101 @@ +# Copyright 2024 Element Robotics Pty Ltd +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import numpy as np +from PIL import Image +import OpenEXR +import Imath +import sys + +def load_exr_normal_map(path): + # Open the EXR file + file = OpenEXR.InputFile(path) + + # Get the header and data window to determine the image size + header = file.header() + dw = header['dataWindow'] + size = (dw.max.x - dw.min.x + 1, dw.max.y - dw.min.y + 1) + + # Define the channels to extract (R, G, B) + channels = ['R', 'G', 'B'] + + # Extract and convert each channel + pt = Imath.PixelType(Imath.PixelType.FLOAT) + data = [np.frombuffer(file.channel(c, pt), dtype=np.float32).reshape(size[1], size[0]) for c in channels] + + # Stack the channels into an (H, W, 3) numpy array and normalize to [-1, 1] + normal_map = np.stack(data, axis=-1) + normal_map = normal_map * 2.0 - 1.0 + + return normal_map + +def load_normal_map(path): + img = Image.open(path).convert('RGB') + normals = np.array(img).astype(np.float32) / 255.0 + normals = normals * 2 - 1 # Map from [0, 1] to [-1, 1] + return normals + +def save_normal_map(normals, path): + normals = (normals + 1) / 2 * 255 # Map from [-1, 1] to [0, 1] + normals = normals.astype(np.uint8) + img = Image.fromarray(normals) + img.save(path) + print(f"Normal map saved as {path}") + +def blend_normals(low_res_normals, high_res_normals): + # Perform per-pixel normal blending + combined_normals = np.zeros_like(low_res_normals) + + # Blend the x and y components + combined_normals[..., 0] = low_res_normals[..., 0] * high_res_normals[..., 2] + high_res_normals[..., 0] * low_res_normals[..., 2] + combined_normals[..., 1] = low_res_normals[..., 1] * high_res_normals[..., 2] + high_res_normals[..., 1] * low_res_normals[..., 2] + + # Blend the z component + combined_normals[..., 2] = low_res_normals[..., 2] * high_res_normals[..., 2] - (low_res_normals[..., 0] * high_res_normals[..., 0] + low_res_normals[..., 1] * high_res_normals[..., 1]) + + # Normalize the result to ensure unit length + norm_magnitudes = np.sqrt(np.sum(np.square(combined_normals), axis=-1, keepdims=True)) + combined_normals /= norm_magnitudes + return combined_normals + +def resample_normal_map(normal_map, target_size): + img = Image.fromarray(((normal_map + 1) / 2 * 255).astype(np.uint8)) + img_resized = img.resize(target_size, Image.BICUBIC) + resized_normal_map = np.array(img_resized).astype(np.float32) / 255.0 + return resized_normal_map * 2 - 1 # Convert back to [-1, 1] + +def tile_normal_map(normal_map, target_shape): + tile_x = target_shape[1] // normal_map.shape[1] + 1 + tile_y = target_shape[0] // normal_map.shape[0] + 1 + tiled_map = np.tile(normal_map, (tile_y, tile_x, 1)) + return tiled_map[:target_shape[0], :target_shape[1]] + +if __name__ == '__main__': + if len(sys.argv) != 4: + print("Usage: python blend_normals.py ") + sys.exit(1) + low_res_path = sys.argv[1] + high_res_path = sys.argv[2] + output_path = sys.argv[3] + # Load your low-resolution and high-resolution EXR normal maps + low_res_normals = load_normal_map(low_res_path) + high_res_normals = load_exr_normal_map(high_res_path) + + # Resample or tile the high-resolution normal map to match the low-res map size + low_res_normals_resized = resample_normal_map(low_res_normals, high_res_normals.shape[:2]) + + # Blend the normal maps + combined_normals = blend_normals(low_res_normals_resized, high_res_normals) + + # Save the resulting combined normal map + save_normal_map(combined_normals, output_path) diff --git a/lunar_terrain/lunar_terrain_gz_worlds/scripts/crop_dem.py b/lunar_terrain/lunar_terrain_gz_worlds/scripts/crop_dem.py new file mode 100644 index 00000000..1f40eef6 --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_worlds/scripts/crop_dem.py @@ -0,0 +1,66 @@ +# Copyright 2024 Element Robotics Pty Ltd +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import rasterio +from rasterio.windows import Window +import numpy as np +import sys + +def crop_dem(input_tif, output_tif, crop_window): + """ + Crop a DEM TIFF file and save the cropped result. + + Parameters: + - input_tif: Path to the input TIFF file. + - output_tif: Path to save the cropped TIFF file. + - crop_window: Tuple of (row_start, row_stop, col_start, col_stop) defining the crop area. + """ + + with rasterio.open(input_tif) as src: + # Define the cropping window + window = Window(crop_window[2], crop_window[0], crop_window[3] - crop_window[2], crop_window[1] - crop_window[0]) + + # Read the data from the defined window + data = src.read(window=window) + + # Define metadata for the cropped file + meta = src.meta.copy() + meta.update({ + 'height': window.height, + 'width': window.width, + 'transform': src.window_transform(window) + }) + + # Write the cropped data to a new TIFF file + with rasterio.open(output_tif, 'w', **meta) as dst: + dst.write(data) + + # Find the maximum height in the cropped region + max_height = np.nanmax(data) + min_height = np.nanmin(data) + print(f"Height diff in cropped region: {max_height - min_height}") + + +if __name__ == '__main__': + if len(sys.argv) != 6: + print("Usage: python crop_dem.py ") + sys.exit(1) + input_tif = sys.argv[1] + output_tif = sys.argv[2] + row_start = int(sys.argv[3]) + length = int(sys.argv[5]) + row_stop = int(row_start + length) + col_start = int(sys.argv[4]) + col_stop = int(col_start + length) + crop_window = (row_start, row_stop, col_start, col_stop) + crop_dem(input_tif, output_tif, crop_window) \ No newline at end of file diff --git a/lunar_terrain/lunar_terrain_gz_worlds/scripts/surface_normals.py b/lunar_terrain/lunar_terrain_gz_worlds/scripts/surface_normals.py new file mode 100644 index 00000000..8c017d6f --- /dev/null +++ b/lunar_terrain/lunar_terrain_gz_worlds/scripts/surface_normals.py @@ -0,0 +1,56 @@ +# Copyright 2024 Element Robotics Pty Ltd +# +# 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 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import numpy as np +import rasterio +from rasterio.plot import show +from PIL import Image +import sys + +if __name__ == '__main__': + if len(sys.argv) != 3: + print("Usage: python surface_normals.py ") + sys.exit(1) + # Step 1: Load the DEM from the TIFF file + dem_path = sys.argv[1] + output_image_path = sys.argv[2] + with rasterio.open(dem_path) as src: + dem = src.read(1) # Read the first band + transform = src.transform + + # Step 2: Calculate the gradients in x and y directions + x, y = np.gradient(dem, transform[0], transform[4]) + + # Step 3: Calculate the surface normals + normals = np.zeros((dem.shape[0], dem.shape[1], 3)) + normals[..., 0] = -x # -dz/dx + normals[..., 1] = -y # -dz/dy + normals[..., 2] = 1 # dz/dz = 1 + + # Step 4: Normalize the normals + norm_magnitudes = np.sqrt(normals[..., 0]**2 + normals[..., 1]**2 + normals[..., 2]**2) + normals[..., 0] /= norm_magnitudes + normals[..., 1] /= norm_magnitudes + normals[..., 2] /= norm_magnitudes + + # Step 5: Map normals to RGB values + # Normalize to range [0, 255] for PNG saving + normals_rgb = (normals + 1) / 2 * 255 + normals_rgb = normals_rgb.astype(np.uint8) + + # Step 6: Save as a PNG image + img = Image.fromarray(normals_rgb) + img.save(output_image_path) + + print(f"Surface normals saved as {output_image_path}") diff --git a/lunar_terrain/run.sh b/lunar_terrain/run.sh new file mode 100755 index 00000000..ae941ae8 --- /dev/null +++ b/lunar_terrain/run.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +# Runs a docker container with the image created by build.bash +# Requires: +# docker +# an X server + +IMG_NAME=openrobotics/space_robots_lunar_terrain + +# Replace `/` with `_` to comply with docker container naming +# And append `_runtime` +CONTAINER_NAME="$(tr '/' '_' <<< "$IMG_NAME")" + +# Start the container +docker run --rm -it --name "$CONTAINER_NAME" --network host \ + -e DISPLAY -e TERM -e QT_X11_NO_MITSHM=1 "$IMG_NAME"