# Sensors와 Plugins 설정 기초

#### 센서(Sensor)의 개념

Gazebo에서 센서는 가상 환경에 존재하는 물리적인 센서를 시뮬레이션하기 위한 구성 요소이다. 즉, 실제 센서가 제공하는 물리량(거리, 이미지, 속도, IMU 정보 등)을 소프트웨어적으로 재현한다. 센서는 SDF(Simulation Description Format) 파일에서 `<sensor>` 태그를 통해 정의하며, 특정 링크(link)에 부착해 사용한다. 이는 실제 로봇에 센서를 장착하는 방식과 유사하다.

* **센서 형태**: Gazebo가 기본적으로 지원하는 센서는 카메라, 레이저(LiDAR), 깊이 카메라(Depth Camera), IMU 등 여러 종류가 있다.
* **출력 토픽**: 각 센서는 특정 토픽(ex. `/camera/image_raw`, `/lidar/scan`)을 통해 측정값을 퍼블리시(publish)하며, ROS2에서 이를 받아 활용한다.

일반적으로 센서는 물리 연산 부담이 큰 편이므로, 불필요한 센서를 과도하게 사용하면 시뮬레이션 성능 저하가 발생할 수 있다. 그러므로 필요한 센서만 선택적으로 활성화하고, 필요에 따라 센서의 업데이트 주기(update rate)나 해상도(resolution)를 적절히 조절하는 것이 좋다.

#### 센서 설정 방법

**1. SDF 파일 내 센서 정의**

Gazebo에서 센서를 사용하기 위해서는 URDF나 SDF 파일에 `<sensor>` 태그를 추가해야 한다. 다음은 SDF 파일 예시이다.

```xml
<model name="my_robot">
  <link name="base_link">
    ...
    <sensor name="my_laser" type="gpu_laser">
      <update_rate>30</update_rate>
      <topic>scan</topic>
      <plugin name="GazeboRosLaser" filename="libgazebo_ros_laser.so">
        <ros>
          <namespace>/my_robot</namespace>
          <argument>some_argument</argument>
        </ros>
      </plugin>
    </sensor>
    ...
  </link>
</model>
```

* `type`: 센서 타입을 지정한다. `gpu_laser`, `camera`, `imu` 등
* `update_rate`: 센서가 1초에 데이터를 업데이트하는 횟수
* `plugin`: 센서 동작을 제어하거나, ROS 인터페이스를 연결하기 위해 플러그인을 사용

**2. URDF를 통한 설정**

URDF로 모델을 정의할 경우, `xacro` 파일을 사용해서 `<gazebo>` 태그 아래에 `<sensor>`를 명시할 수 있다. URDF 자체가 센서에 대한 고급 설정을 전부 지원하지 않을 수 있으므로, Gazebo의 고유 기능을 모두 활용하려면 SDF를 사용하는 편이 좋다. 다만 ROS2 공식 튜토리얼이나 xacro를 통한 자동화 관점에서 URDF를 활용하는 예시도 많으므로 필요에 따라 선택하면 된다.

```xml
<robot name="my_robot">
  <link name="base_link">
    <visual> ... </visual>
    <collision> ... </collision>
    <gazebo>
      <sensor type="imu" name="my_imu_sensor">
        <update_rate>100</update_rate>
      </sensor>
    </gazebo>
  </link>
</robot>
```

#### 플러그인(Plugin)의 개념

Gazebo의 플러그인은 Gazebo 엔진의 동작을 확장하거나, 특정 모델 또는 센서의 동작을 세밀하게 제어하기 위한 모듈이다. C++로 작성된 `.so`(shared object) 형태로 로드되며, SDF 혹은 URDF의 `<plugin>` 태그를 통해 로드할 수 있다. ROS2와의 통신(토픽, 서비스, 액션) 기능을 제공하는 ROS2 전용 플러그인도 존재한다.

* **모델 플러그인**: 특정 모델(로봇, 물체 등)에 대한 동작 제어
* **센서 플러그인**: 센서 데이터 처리, 필터링, ROS 토픽 연결
* **월드 플러그인**: 월드(환경) 단위의 기능 제어 (ex. 시뮬레이션 시점에서 환경 이벤트 제어)

플러그인을 통해 다음과 같은 작업이 가능하다.

1. 시뮬레이션 초기화 시점에 특정 알고리즘 실행
2. 시뮬레이션 루프 중 주기적인 콜백 함수 호출
3. 모델 간 상호 작용 정의
4. ROS2 노드, 퍼블리셔, 서브스크라이버 생성 등을 통한 데이터 교환

#### 플러그인 설정 방법

**1. SDF 내 `<plugin>` 태그**

SDF 파일에서는 `<sensor>` 태그 내부나 `<model>` 태그 내부에 `<plugin>`을 삽입한다. 예시는 다음과 같다.

```xml
<model name="my_model">
  <plugin name="my_model_plugin" filename="libmy_model_plugin.so">
    <param1>value1</param1>
    <param2>value2</param2>
  </plugin>
</model>
```

* `name`: 플러그인의 고유 이름
* `filename`: 빌드된 플러그인 라이브러리 이름
* 내부 파라미터(예시: `<param1>` 등)는 플러그인 코드에서 `sdf::Element` API를 통해 읽어온다.

**2. ROS2 전용 플러그인 설정**

ROS2 통신을 연결하기 위한 플러그인은 파일명이 `libgazebo_ros_*.so` 형태인 경우가 많다. 다음은 Laser 센서를 예시로 한 ROS2 플러그인 설정 예시다.

```xml
<sensor name="lidar" type="gpu_laser">
  <update_rate>20</update_rate>
  <plugin name="GazeboRosLaser" filename="libgazebo_ros_laser.so">
    <ros>
      <namespace>/my_robot</namespace>
      <topic_remap>scan:=/lidar_scan</topic_remap>
    </ros>
  </plugin>
</sensor>
```

* `<namespace>`: 플러그인이 퍼블리시/서브스크라이브하는 토픽을 네임스페이스로 묶을 수 있다.
* `<topic_remap>`: ROS2 Topic Remapping을 바로 적용 가능하다.

#### 센서-플러그인 관계

센서와 플러그인의 관계는 크게 두 가지 시나리오가 있다.

1. **센서에 종속된 플러그인**: 센서를 제어하거나 센서 데이터를 ROS2로 보내기 위한 용도로 사용. 예: `libgazebo_ros_laser.so`
2. **모델에 종속된 플러그인**: 센서와 직접적인 연관은 없지만, 센서의 측정값을 받아서 추가 처리(ex. 센서 노이즈 모델링, 시뮬레이션 속 물리 반영 등)를 수행하기도 한다.

센서 플러그인에서 중요한 점은 센서가 데이터를 생성해낼 때마다 플러그인 콜백을 통해 이를 처리하는 구조를 갖춘다는 것이다. 예를 들어 레이저 센서는 주기적으로 레이저 스캔 데이터를 발행하고, 플러그인은 이 데이터를 받아 ROS2 토픽으로 퍼블리시하거나, 추가 연산(예: 필터링)을 수행할 수 있다.

#### 센서 데이터의 기준좌표계 설정

3D 시뮬레이션 상에서 센서는 링크에 장착되는 구조로 표현되며, 센서의 기준좌표계(origin)는 링크의 좌표계를 기준으로 오프셋이 정해진다. 실제 로봇 하드웨어에서 센서를 설치할 때 고려하는 offset과 마찬가지로, 시뮬레이션상에서도 적절하게 좌표계 변환을 해야 한다.

예를 들어, 어떤 센서 좌표계를 $\mathbf{T}*{\text{sensor}}$라 하고, 링크의 좌표계를 $\mathbf{T}*{\text{link}}$라 할 때, 센서 좌표계는 링크 좌표계와 다음과 같은 관계를 가진다.

$$
\mathbf{T}*{\text{sensor}} = \mathbf{T}*{\text{link}} \cdot \mathbf{T}\_{\text{offset}}
$$

* $\mathbf{T}\_{\text{link}}$: 링크의 기준좌표계(로봇 베이스 등)
* $\mathbf{T}\_{\text{offset}}$: 센서를 링크에 부착할 때 발생하는 회전 및 평행이동 변환

이를 고려하여, URDF/SDF에서 `<pose>` 태그를 통해 센서의 위치와 방향을 설정해야 한다.

#### 센서 노이즈 및 모델링

시뮬레이션 환경에서 센서는 이론상 ‘완벽한 측정값’을 제공하지만, 실제 환경에서는 센서 노이즈(Noise)가 존재한다. 따라서 실제 로봇 환경에 근접한 테스트를 위해서는 센서 노이즈를 모델링하는 작업이 중요하다. Gazebo에서는 센서마다 노이즈 파라미터를 지원하거나, 별도의 플러그인을 통해 노이즈를 추가로 주입할 수 있다.

* **레이저(LiDAR) 노이즈**: 레이저 센서에는 거리 측정 오차가 존재하며, 비정상 신호(Outlier)나 무작위 분포(Random Gaussian Noise) 형태로 노이즈를 넣는 설정이 가능하다.
* **IMU 노이즈**: 가속도계 및 자이로스코프 등의 원천 오차, 드리프트(drift), 바이어스(bias) 등 여러 노이즈 모델을 적용할 수 있다.
* **카메라 노이즈**: 이미지 센서에는 감지 화소(Pixel) 단위 노이즈나 광학적 왜곡(Distortion) 등이 존재할 수 있다.

예를 들어, 가우시안 노이즈(Gaussian Noise)를 모델링하기 위해서는 다음과 같이 표준편차($\sigma$)나 평균($\mu$)을 설정할 수 있다.

```xml
<sensor name="my_laser" type="gpu_laser">
  ...
  <noise>
    <type>gaussian</type>
    <mean>0.0</mean>
    <stddev>0.01</stddev>
  </noise>
  ...
</sensor>
```

위 예시에서 `stddev`가 0.01이면, 실제 센서 값에 평균이 0, 표준편차가 0.01인 가우시안 노이즈가 추가된다.

#### 센서 업데이트 주기 최적화

센서 업데이트 주기($f\_{\text{sensor}}$)는 시뮬레이션 성능 및 데이터 품질에 영향을 미치는 중요한 요소이다.

* **시뮬레이션 부하**: 업데이트 주기가 높을수록(예: 60Hz 이상) 센서 관련 연산이 잦아져 CPU/GPU 부하가 커진다.
* **응답성(Responsiveness)**: 주기가 낮으면 실시간성이 떨어진다. 예: 1Hz 센서는 1초에 한 번씩만 측정 데이터를 제공.
* **대역폭(Bandwidth)**: 센서 토픽 메시지가 너무 빈번히 퍼블리시되면 네트워크 트래픽이 증가하고, 처리가 지연될 수 있다.

ROS2 기반 로봇 시스템을 시뮬레이션할 때, 실제 하드웨어 센서의 주기에 맞추는 것이 일반적이다. 예를 들어 IMU 센서가 실제로 200Hz라면 시뮬레이션에서도 유사한 주기를 설정하여 테스트 환경을 현실감 있게 만든다.

정리하자면, 업데이트 주기는 다음 관계로 표현 가능하다.

$$
T\_{\text{sensor}} = \frac{1}{f\_{\text{sensor}}}
$$

여기서 $T\_{\text{sensor}}$는 센서 측정 주기(초)이며, $f\_{\text{sensor}}$는 1초당 측정 횟수(Hz)이다.

#### Gazebo Topic과 ROS2 Topic 연결

기본적으로 Gazebo는 내부적으로 `$~/model/...`, `$~/sensor/...` 등과 같은 Gazebo 전용 Topic을 사용한다. ROS2와 연결하기 위해서는 ROS2 관련 플러그인을 통해 해당 데이터를 ROS2 Topic으로 변환한다.

* **Gazebo Transport**: Gazebo 자체 메시징 시스템
* **ROS2**: DDS 기반의 메시징 시스템
* **Plugin**: 두 메시징 간 브릿지 역할을 수행

다음 예시는 Gazebo Topic `/gazebo/default/my_laser`를 ROS2 Topic `/scan`으로 브릿징하는 레이저 센서 플러그인 설정이다.

```xml
<sensor name="lidar" type="gpu_laser">
  <update_rate>20</update_rate>
  <plugin name="GazeboRosLaser" filename="libgazebo_ros_laser.so">
    <ros>
      <namespace>/my_robot</namespace>
      <topic_remap>scan:=/scan</topic_remap>
    </ros>
  </plugin>
</sensor>
```

여기서 `<topic_remap>`를 통해 Gazebo가 퍼블리시하는 데이터를 `/scan`이라는 ROS2 Topic으로 받아볼 수 있다.

#### 멀티 센서 구성 시 고려사항

로봇 플랫폼에 복수의 센서가 동시에 장착되어 있으면 각 센서의 데이터 스트림이 동시에 발생한다. 다음 사항을 주의 깊게 살펴봐야 한다.

1. **시뮬레이션 속도**: 센서가 많아질수록 Gazebo의 물리 엔진과 렌더링 엔진 모두 부하가 증가한다.
2. **메시지 동기화**: 카메라 이미지와 IMU 데이터를 동기화하려면 ROS2 메시지 타임스탬프를 활용해야 한다.
3. **토픽 충돌**: 같은 이름의 토픽을 여러 센서가 사용하는 경우 충돌이 발생할 수 있으므로, 반드시 토픽 이름을 구분 짓는 것이 좋다.

이를 위해 URDF 혹은 SDF에서 센서마다 `<topic>` 혹은 `<plugin>` 설정 내 `topic_remap` 등을 명시적으로 구분해주는 것이 안전하다.

#### 멀티 GPU 사용 시 주의점

GPU 가속을 적극 활용하는 센서(예: `gpu_laser`, `depth_camera`)가 복수로 존재하거나, 해상도가 높은 카메라를 여러 대 사용하면 GPU 리소스가 빠르게 소진된다. 따라서 아래와 같은 튜닝이 필요할 수 있다.

* **해상도(Resolution) 조정**: 카메라 센서의 해상도나 FPS를 낮춰서 GPU 부담을 줄인다.
* **Field of View(FoV) 제한**: 불필요하게 넓은 시야각을 줄여서 렌더링 부하를 완화한다.
* **Update Rate 조정**: 센서마다 업데이트 주기를 다르게 설정해 GPU 연산 부하를 분산한다.

#### 예시: 카메라와 LiDAR를 함께 사용하는 SDF

아래 예시는 한 로봇 모델에 카메라 센서와 LiDAR 센서를 모두 장착한 SDF 예시이다. 두 센서 모두 ROS2 플러그인을 통해 데이터를 퍼블리시한다.

```xml
<model name="multi_sensor_robot">
  <link name="base_link">
    <sensor name="camera_sensor" type="camera">
      <pose>0 0 0.5 0 0 0</pose>
      <update_rate>20</update_rate>
      <camera>
        <horizontal_fov>1.047</horizontal_fov>
        <image>
          <width>640</width>
          <height>480</height>
          <format>R8G8B8</format>
        </image>
        <clip>
          <near>0.1</near>
          <far>100</far>
        </clip>
      </camera>
      <plugin name="GazeboRosCamera" filename="libgazebo_ros_camera.so">
        <ros>
          <namespace>/multi_sensor_robot</namespace>
          <topic_remap>image:=/camera_image</topic_remap>
        </ros>
      </plugin>
    </sensor>

    <sensor name="lidar_sensor" type="gpu_laser">
      <pose>0 0 0.4 0 0 0</pose>
      <update_rate>10</update_rate>
      <range>
        <min>0.1</min>
        <max>15.0</max>
        <resolution>0.01</resolution>
      </range>
      <noise>
        <type>gaussian</type>
        <mean>0.0</mean>
        <stddev>0.01</stddev>
      </noise>
      <plugin name="GazeboRosLaser" filename="libgazebo_ros_laser.so">
        <ros>
          <namespace>/multi_sensor_robot</namespace>
          <topic_remap>scan:=/lidar_scan</topic_remap>
        </ros>
      </plugin>
    </sensor>
  </link>
</model>
```

* `<pose>`: 센서 장착 위치(링크 기준좌표계에서의 오프셋)
* `<camera>`: 카메라 센서의 시야, 이미지 해상도, 클리핑 범위 설정
* `<range>`: LiDAR 센서의 감지 범위 설정
* `<noise>`: 센서 노이즈 구성

#### 센서 데이터의 TF(Transform) 관리

ROS2에서 센서 데이터를 활용할 때, 좌표계 변환(tf2)이 필수적으로 동반된다. Gazebo 시뮬레이션 환경에서 센서가 측정한 데이터가 실제 로봇에서 기대하는 좌표계(예: `base_link`, `camera_link`)와 적절히 일치해야 한다.

* **센서 링크 정의**: URDF 혹은 SDF에서 센서를 부착한 링크의 이름을 ROS2 패키지에서 TF 트리(tf2)에 반영해야 한다.
* **ROS2 TF 브로드캐스트**: 일반적으로 로봇 디스크립션(URDF/Xacro) 기반으로 `robot_state_publisher` 노드를 띄워서 TF를 퍼블리시한다.
* **센서-링크 offset**: 실제 센서 설치 지점과 로봇 베이스 사이의 오프셋(회전, 병진)을 `pose` 태그로 정확하게 지정해주어야, 센서 측정값을 옳게 해석할 수 있다.

아래는 센서가 퍼블리시하는 ROS2 메시지에 대해 TF가 어떻게 연결되는지를 개략적으로 나타낸 다이어그램이다.

{% @mermaid/diagram content="flowchart LR
subgraph Gazebo Sim
A\["Sensor in SDF/URDF<br>(e.g. camera\_link)"] --> B((Plugin))
end
B --> C\[ROS2 Node in Plugin]

```
subgraph ROS2
C --> D["/tf2 <br>(e.g. base_link->camera_link)"]
C --> E[(Camera Topic)]
end" %}
```

* **A**: Gazebo 내 센서 오브젝트 (SDF/URDF로 정의됨)
* **B**: 센서 플러그인이 동작하는 로직 (C++ `.so` 라이브러리)
* **C**: ROS2 노드 인터페이스 (토픽, TF, 파라미터)
* **D**: TF 트리 (기준 링크(`base_link`)에서 센서 링크(`camera_link`)로의 변환)
* **E**: 카메라 이미지 토픽 등 센서 데이터

#### 센서 플러그인 개발 시 주의사항

사용자가 직접 플러그인을 개발하는 경우, Gazebo 및 ROS2 API 양측을 모두 숙지해야 한다.

1. 빌드 환경:
   * ROS2 버전에 맞는 CMakeLists.txt 작성
   * `gazebo_dev`, `gazebo_ros_pkgs` 등 관련 패키지 의존성 확인
2. 플러그인 클래스 구조:
   * Gazebo C++ Plugin 클래스(예: `ModelPlugin`, `SensorPlugin`)를 상속
   * `Load()`, `OnUpdate()`, `OnNewXYZFrame()` 등의 콜백 함수 오버라이딩
3. ROS2 통신 객체 생성:
   * rclcpp 기반 노드, 퍼블리셔, 서브스크라이버 초기화
   * QoS 설정, 토픽 이름, 메시지 타입 등 주의
4. SDF 파라미터 파싱:
   * `sdf::ElementPtr`로부터 `<param>` 값 읽기

* 파라미터가 없을 시 기본값 적용 로직 구현

아래는 SensorPlugin 예시 코드 스니펫이다.

```cpp
#include <gazebo/common/Plugin.hh>
#include <gazebo/sensors/Sensor.hh>
#include <gazebo/sensors/SensorTypes.hh>
#include <gazebo/sensors/CameraSensor.hh>
#include <rclcpp/rclcpp.hpp>

namespace gazebo
{
  class MyCameraPlugin : public SensorPlugin
  {
  private:
    rclcpp::Node::SharedPtr rosNode_;
    rclcpp::Publisher<sensor_msgs::msg::Image>::SharedPtr imagePub_;
    sensors::CameraSensorPtr parentSensor_;
  
  public:
    MyCameraPlugin() {}
    virtual ~MyCameraPlugin() {}

    void Load(sensors::SensorPtr _sensor, sdf::ElementPtr _sdf)
    {
      // Parent Sensor 할당
      this->parentSensor_ = std::dynamic_pointer_cast<sensors::CameraSensor>(_sensor);
      if (!this->parentSensor_)
      {
        gzerr << "Parent sensor not found!\n";
        return;
      }

      // ROS2 Node 초기화
      if (!rclcpp::ok()) {
        rclcpp::init(0, nullptr);
      }
      this->rosNode_ = rclcpp::Node::make_shared("my_camera_plugin_node");

      // Publisher 생성
      imagePub_ = this->rosNode_->create_publisher<sensor_msgs::msg::Image>("camera_image", 10);

      // Sensor 콜백 등록
      this->parentSensor_->ConnectNewImageFrame(
        std::bind(&MyCameraPlugin::OnNewFrame, this,
                  std::placeholders::_1, std::placeholders::_2,
                  std::placeholders::_3, std::placeholders::_4,
                  std::placeholders::_5));
    }

    void OnNewFrame(const unsigned char * image,
                    unsigned int width, unsigned int height,
                    unsigned int depth, const std::string & format)
    {
      // 여기서 image 데이터를 ROS2 메시지로 변환해 publish
      auto msg = sensor_msgs::msg::Image();
      // Fill msg fields
      imagePub_->publish(msg);
    }
  };

  GZ_REGISTER_SENSOR_PLUGIN(MyCameraPlugin)
}
```

* `Load()` 함수: 플러그인이 센서에 로드될 때 실행되며, 센서 포인터와 SDF 파라미터를 인자로 받는다.
* `ConnectNewImageFrame()`: 카메라 센서에서 새 프레임이 생성될 때마다 호출되는 콜백을 등록한다.
* `OnNewFrame()`: 실제 이미지 데이터를 받아 ROS2 메시지로 변환 후 퍼블리시한다.

#### 디버깅 및 로깅

센서-플러그인 동작이 의도대로 되지 않을 경우, Gazebo와 ROS2 양쪽에서 로그를 확인해야 한다.

* Gazebo 로그:
  * 센서 플러그인 로드 에러, SDF 파싱 에러, 물리 엔진 경고 등은 터미널 콘솔 혹은 `~/.gzserver/log` 디렉토리에 남을 수 있다.
* ROS2 로그:
  * `rclcpp::Logger`를 통한 디버깅 정보, TF 간 연결 문제, Topic mismatch 등이 발생하면 `ros2 topic list`, `ros2 topic echo`, `ros2 run tf2_tools view_frames` 등으로 진단한다.

플러그인 코드에서 `gzerr`, `gzmsg` 등을 통해 Gazebo 쪽 로그를 남길 수 있으며, ROS2 쪽에서는 `RCLCPP_INFO`, `RCLCPP_ERROR` 등을 사용해 로그를 출력할 수 있다.

#### 부록: Model Plugin과 Sensor Plugin 비교

| 구분      | Sensor Plugin                       | Model Plugin                           |
| ------- | ----------------------------------- | -------------------------------------- |
| 적용 대상   | 센서(LiDAR, 카메라, IMU 등)               | 로봇 모델 전체 또는 특정 링크/조인트                  |
| 콜백 주기   | 센서 데이터가 업데이트될 때(이벤트 기반)             | 물리 엔진 업데이트 주기(OnUpdate 이벤트)            |
| ROS2 통신 | 센서 데이터를 토픽으로 퍼블리시, 필요 시 서브스크라이브     | 모델 상태 제어, Joint Control, etc.          |
| 예시      | `GazeboRosCamera`, `GazeboRosLaser` | `GazeboRosControl`, 사용자 정의 ModelPlugin |

#### 고급 센서 플러그인 예시: 딥러닝 기반 물체 인식

일부 상황에서는 시뮬레이션 상의 카메라 데이터를 입력으로 받아 딥러닝 모델(예: 객체 검출, 이미지 분할 등)을 수행하고자 할 수 있다. 이 경우 다음 단계를 거칠 수 있다.

1. **Gazebo 카메라 센서로부터 이미지 획득**
2. **ROS2 토픽을 통해 이미지를 퍼블리시** (예: `/camera/image_raw`)
3. \*\*딥러닝 노드(별도 ROS2 패키지)\*\*에서 해당 이미지를 서브스크라이빙
4. **추론 결과(바운딩 박스, 분할 마스크 등)를 다른 토픽**으로 퍼블리시

직접 플러그인 내부에서 딥러닝 연산을 처리해도 되지만, 시뮬레이션은 주로 물리 엔진과 센서 데이터 제공 역할에 집중하고, 학습이나 추론은 별도의 노드로 분리하는 편이 일반적이다. 이렇게 분리해야 시뮬레이션의 시간 흐름(Real Time Factor)이 연산 지연에 의해 심각하게 왜곡되지 않으며, 분산 처리도 가능해진다.

#### RealSense/Depth 카메라 플러그인

Gazebo에서 Intel RealSense 카메라 같은 깊이 카메라(Depth Camera)를 시뮬레이션하는 플러그인이 별도로 제공되기도 한다. 이를 사용하면 RGB 이미지는 물론 깊이 정보까지 함께 퍼블리시할 수 있다.

```xml
<sensor name="realsense_camera" type="depth">
  <update_rate>30</update_rate>
  <camera name="depth_camera">
    <image>
      <width>640</width>
      <height>480</height>
      <format>R8G8B8</format>
    </image>
    <clip>
      <near>0.2</near>
      <far>10.0</far>
    </clip>
  </camera>
  <plugin name="GazeboRosDepthCamera" filename="libgazebo_ros_depth_camera.so">
    <ros>
      <namespace>/realsense</namespace>
      <topic_remap>image_raw:=/realsense/depth_image</topic_remap>
    </ros>
  </plugin>
</sensor>
```

* **깊이 카메라(Depth Camera)**: 장면의 각 픽셀이 깊이 정보를 포함
* **메시지 타입**: ROS2에서 깊이 이미지는 보통 `sensor_msgs/Image` 형식으로 퍼블리시되며, 인코딩은 `TYPE_32FC1` 등을 사용

#### 세계(World) 플러그인과 센서

월드 플러그인(World Plugin)은 센서 자체가 아닌 ‘환경’에 초점을 맞춘다. 예를 들어, 시뮬레이션 중 특정 시간에 조명(light) 세기를 변경하거나, 가상 장애물을 동적으로 생성/삭제하려면 월드 플러그인을 사용할 수 있다.

* **센서 테스트 시나리오**: 센서가 조명, 안개(fog), 물리 환경 변화 등에 얼마나 민감한지 확인
* **자동 시나리오 생성**: 센서 테스트를 위해 여러 월드 환경(조명 밝기, 지형, 물체 배치 등)을 자동으로 조합해서 여러 케이스를 한 번에 실험

아래는 월드 플러그인을 통해 주기적으로 조명 밝기를 변경하는 예시 스니펫이다.

```xml
<world name="test_world">
  <plugin name="light_changer" filename="liblight_changer.so">
    <change_interval>10.0</change_interval>
  </plugin>
  ...
</world>
```

#### 센서 데이터 녹화(Logging)와 분석

ROS2 환경에서 센서 데이터를 수집해 오프라인으로 분석하거나, 후처리(오프라인 SLAM, 데이터 레이블링 등)를 할 수 있다.

* ros2 bag: ROS2 토픽을 기록할 때 사용하며, 센서 메시지(이미지, 레이저 스캔 등)를 저장할 수 있다.

  ```bash
  ros2 bag record /camera/image_raw /lidar_scan /imu/data
  ```
* **분석 툴**: recorded bag 파일을 다시 재생(`ros2 bag play`)하고, Rviz, Foxglove Studio, Python 노트북 등에서 시각화/분석 가능
* **센서 퀄리티 지표**: 노이즈 레벨, 측정 반복성, 드리프트 정도 등을 오프라인으로 계량화할 수 있다.

#### Gazebo에서의 시각화와 디버그

Gazebo GUI에서 센서 모델을 시각화할 수 있는데, 아래 같은 기능을 활용해 디버깅을 돕는다.

* **센서 광선(Raycast) 확인**: LiDAR나 Sonar와 같은 광선 기반 센서는 시뮬레이터 상에서 광선이 어떻게 발사되는지 확인 가능
* **Camera Overlay**: 카메라 센서의 실제 뷰가 Gazebo GUI 상단에 별도 창으로 표시될 수 있음
* **Plugin 상태 확인**: GUI 플러그인을 사용해 현재 로드된 플러그인 목록, 주기적인 Update 횟수, 센서 데이터 출력 등을 모니터링

만약 GUI에서 제대로 표시되지 않거나, 센서 토픽이 갱신되지 않는다면 SDF/URDF 설정이 잘못되었을 가능성이 크므로, 모델 계층구조와 `<sensor>` 태그의 위치를 다시 점검해야 한다.

#### 추가 고려사항

1. **하드웨어 간 편차(Mock-Up)**: 실제 센서와 다소 다른 성능(동작 범위, 노이즈, 업데이트 지연)이 발생할 수 있으니, 실제 값과 시뮬레이션 값을 비교/보정하는 절차가 필요하다.
2. **실시간성(Real-Time Factor, RTF)**: 센서가 많아지거나 고해상도 카메라를 여러 대 사용하면 RTF가 떨어진다. 시뮬레이션이 실제 시간보다 느리게 돌아갈 수 있으며, 이는 로보틱스 알고리즘의 성능에 영향을 줄 수 있다.
3. **ROS2 QoS 설정**: 센서 데이터는 주로 `Reliability = Best Effort`나 `Durability = Volatile` 같은 QoS를 사용하는데, 네트워크 환경 혹은 멀티 머신 시뮬레이션 시에는 QoS 튜닝이 필요할 수 있다.
