How the Maze Obstacle System Works in Peng: Implementation and Physics

The Maze obstacle system in Peng is a deterministic 3D simulation component that generates, updates, and renders moving spherical obstacles within bounded volumes, implemented in src/lib.rs using the Obstacle and Maze structs.

The Peng repository provides a quadrotor simulation environment with dynamic obstacle generation for testing collision avoidance algorithms. The Maze obstacle system creates a configurable 3D space filled with moving spherical objects that bounce off virtual walls, providing reproducible scenarios for drone navigation testing according to the makeecat/peng source code.

Core Data Structures

The implementation centers on two primary types defined in src/lib.rs: the Obstacle struct representing individual spheres, and the Maze struct managing the collection and boundary conditions.

The Obstacle Struct

Defined at lines 2671–2779 in src/lib.rs, the Obstacle struct holds the state of a single moving sphere:

pub struct Obstacle {
    pub position: Vector3<f32>,
    pub velocity: Vector3<f32>,
    pub radius: f32,
}

Each obstacle maintains its position as a 3D vector, velocity for movement per simulation step, and radius defining its collision size.

The Maze Struct

The Maze struct (lines 2718–2732 in src/lib.rs) serves as the container and simulation controller:

pub struct Maze {
    pub lower_bounds: [f32; 3],
    pub upper_bounds: [f32; 3],
    pub obstacles: Vec<Obstacle>,
    pub obstacles_velocity_bounds: [f32; 3],
    pub obstacles_radius_bounds: [f32; 2],
    pub rng: ChaCha8Rng,
}

Key fields include:

  • Bounds arrays defining the axis-aligned bounding box
  • Obstacles vector storing all active spheres
  • Velocity bounds specifying maximum speed per axis (±value)
  • Radius bounds constraining obstacle sizes
  • ChaCha8Rng providing deterministic random number generation

Initialization and Construction

The Maze::new method (lines 2746–2763 in src/lib.rs) handles construction:

pub fn new(
    lower_bounds: [f32; 3],
    upper_bounds: [f32; 3],
    num_obstacles: usize,
    obstacles_velocity_bounds: [f32; 3],
    obstacles_radius_bounds: [f32; 2],
) -> Self {
    let mut maze = Maze {
        lower_bounds,
        upper_bounds,
        obstacles: Vec::new(),
        obstacles_velocity_bounds,
        obstacles_radius_bounds,
        rng: ChaCha8Rng::from_rng(&mut rand::rng()),
    };
    maze.generate_obstacles(num_obstacles);
    maze
}

The constructor stores the configuration and immediately calls generate_obstacles to fill the obstacles vector.

Random Obstacle Generation

The generate_obstacles method (lines 2734–2755 in src/lib.rs) creates the initial obstacle distribution:

pub fn generate_obstacles(&mut self, num_obstacles: usize) {
    self.obstacles = (0..num_obstacles)
        .map(|_| {
            // Random position inside the maze volume
            let position = Vector3::new(
                self.rng.random_range(self.lower_bounds[0]..self.upper_bounds[0]),
                self.rng.random_range(self.lower_bounds[1]..self.upper_bounds[1]),
                self.rng.random_range(self.lower_bounds[2]..self.upper_bounds[2]),
            );
            // Random symmetric velocity per axis
            let v_bounds = self.obstacles_velocity_bounds;
            let velocity = Vector3::new(
                self.rng.random_range(-v_bounds[0]..v_bounds[0]),
                self.rng.random_range(-v_bounds[1]..v_bounds[1]),
                self.rng.random_range(-v_bounds[2]..v_bounds[2]),
            );
            // Random radius within the configured range
            let r_bounds = self.obstacles_radius_bounds;
            let radius = self.rng.random_range(r_bounds[0]..r_bounds[1]);

            Obstacle::new(position, velocity, radius)
        })
        .collect();
}

This method samples position, velocity, and radius uniformly from their respective bounds, ensuring each obstacle starts with valid parameters.

Physics Update and Collision Detection

The update_obstacles method (lines 2796–2816 in src/lib.rs) advances the simulation:

pub fn update_obstacles(&mut self, dt: f32) {
    self.obstacles.iter_mut().for_each(|obstacle| {
        // Simple Euler integration
        obstacle.position += obstacle.velocity * dt;

        // Bounce off each wall when the sphere would cross the bounds.
        for i in 0..3 {
            if obstacle.position[i] - obstacle.radius < self.lower_bounds[i]
                || obstacle.position[i] + obstacle.radius > self.upper_bounds[i]
            {
                obstacle.velocity[i] *= -1.0; // invert the component on that axis
            }
        }
    });
}

The update loop performs Euler integration to move obstacles linearly, then checks each axis for boundary violations. When a sphere’s surface would go outside the maze box, the corresponding velocity component is flipped, creating perfectly elastic bounces.

Integration with the Simulation Loop

In src/main.rs, the maze is instantiated from the YAML configuration and updated every simulation step:

let mut maze = Maze::new(
    config.maze.lower_bounds,
    config.maze.upper_bounds,
    config.maze.num_obstacles,
    config.maze.obstacles_velocity_bounds,
    config.maze.obstacles_radius_bounds,
);

...

loop {
    // Advance obstacle motion
    maze.update_obstacles(quad.time_step);
    // … other simulation updates …
}

The same maze instance is passed to the depth‑camera renderer and to the ray‑casting routine, so every rendered depth image reflects the current positions of the moving obstacles.

Visualization and Logging

Peng can log the maze geometry (the enclosing tube) and the current obstacle states to a Rerun recording stream:

  • log_maze_tube writes the bounding box as a visual tube.
  • log_maze_obstacles emits each obstacle’s position and radius as a 3‑D point cloud.

Both functions read directly from maze.lower_bounds, maze.upper_bounds, and maze.obstacles (see src/lib.rs around lines 3100‑3170).

Practical Code Examples

Below are short snippets that demonstrate how a user could create and manipulate a maze outside the main binary (e.g. in a test or a REPL).

use peng_quad::{Maze, Obstacle};
use nalgebra::Vector3;

// 1️⃣ Create a maze with a 2 m³ volume and 10 random obstacles.
let mut maze = Maze::new(
    [-1.0, -1.0, -1.0],   // lower bounds
    [1.0, 1.0, 1.0],      // upper bounds
    10,                    // number of obstacles
    [0.2, 0.2, 0.2],       // max velocity per axis
    [0.05, 0.15],          // radius range
);

// 2️⃣ Inspect a randomly generated obstacle.
let first = &maze.obstacles[0];
println!(
    "Obstacle #0 – pos: ({:.2}, {:.2}, {:.2}), vel: ({:.2}, {:.2}, {:.2}), r: {:.2}",
    first.position.x, first.position.y, first.position.z,
    first.velocity.x, first.velocity.y, first.velocity.z,
    first.radius,
);

// 3️⃣ Step the simulation forward by 0.02 s.
maze.update_obstacles(0.02);

// 4️⃣ Manually add a deterministic obstacle.
let fixed = Obstacle::new(
    Vector3::new(0.0, 0.0, 0.0), // stationary at the centre
    Vector3::zeros(),
    0.1,
);
maze.obstacles.push(fixed);

Summary

  • The Maze obstacle system in Peng provides a deterministic, reproducible environment for testing quadrotor collision avoidance.
  • Core types Obstacle and Maze defined in src/lib.rs manage spherical obstacle state and boundary conditions.
  • Random generation uses ChaCha8Rng for reproducible initial conditions, sampling position, velocity, and radius within configured bounds.
  • Physics updates employ Euler integration with perfectly elastic bouncing against axis-aligned bounding walls.
  • Integration in src/main.rs connects the maze to the rendering pipeline and depth-camera simulation.
  • Visualization via Rerun functions in src/lib.rs enables real-time debugging of obstacle positions.

Frequently Asked Questions

How does Peng ensure reproducible maze configurations?

Peng uses a ChaCha8Rng random number generator seeded from the global RNG during Maze::new. Because the generator is deterministic, providing the same seed produces identical obstacle positions, velocities, and radii across simulation runs, essential for reproducible robotics experiments.

What collision detection method does the Maze obstacle system use?

The system implements sphere-to-wall collision detection in Maze::update_obstacles. After each Euler integration step, the code checks if obstacle.position[i] - obstacle.radius falls below lower_bounds[i] or if obstacle.position[i] + obstacle.radius exceeds upper_bounds[i] for each axis. When violated, the corresponding velocity component flips, creating perfectly elastic bounces.

Can obstacles be added manually after maze initialization?

Yes. While Maze::generate_obstacles creates random obstacles during construction, the obstacles field is a public Vec<Obstacle>. Users can push manually constructed Obstacle instances using Obstacle::new(position, velocity, radius), allowing deterministic test scenarios alongside randomly generated environments.

How is the maze visualized during simulation?

Peng integrates with the Rerun visualization framework through functions defined around lines 3100–3170 in src/lib.rs. The log_maze_tube function renders the bounding box as a tube structure, while log_maze_obstacles emits each obstacle’s position and radius as a 3D point cloud, enabling real-time debugging of obstacle motion and collision boundaries.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →