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
+[](https://opensource.org/licenses/Apache-2.0)\
+
+
+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"