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_tubewrites the bounding box as a visual tube.log_maze_obstaclesemits 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
ObstacleandMazedefined insrc/lib.rsmanage spherical obstacle state and boundary conditions. - Random generation uses
ChaCha8Rngfor 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.rsconnects the maze to the rendering pipeline and depth-camera simulation. - Visualization via Rerun functions in
src/lib.rsenables 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →