Synthetic LiDAR from Cached 3D Mesh Urban Obstacle Avoidance with BendyRuler -- Balliyang

Hi everyone,

Updated Video:

I’ve been working on something a bit unusual and wanted to share where it’s at. Fair warning — I’m not a firmware developer by trade, and some of what follows might make the proper devs wince, and yeah lots of AI. But it flies, and I think the idea has legs, so here goes.

Original Video

**How it started — a backup camera that got ideas above its station**

This whole thing began as a virtual camera for QGroundControl. I wanted a synthetic 3D view rendered from cached Google 3D Tiles — basically a “what the drone sees” window in the GCS using the mesh data downloaded. Just a nice-to-have backup perspective for situational awareness, night flying or fog flying.

Then one evening it hit me: if I can render the 3D world from the drone’s point of view, I can cast rays into it. And if I can cast rays, I can generate OBSTACLE_DISTANCE messages. And if I can do that… I can feed BendyRuler.

So that’s what I did. Probably should have thought harder about whether I *should* before I figured out that I *could*.

**What it actually is**

**Balliyang** (Bat in Wiradjuri tradition) is a QGroundControl custom build plugin that:

  1. Downloads and caches Google 3D Tiles for any area, or you can add your own mesh

  2. Renders them in an offscreen OpenGL context

  3. Casts 36 rays at 4Hz across multiple pitch layers (level, ±30°, ±60°) from the drone’s position through the cached mesh

  4. Sends the results as MAVLink OBSTACLE_DISTANCE messages back to the flight controller

  5. ArduPilot’s BendyRuler then uses this data for path planning

The virtual camera feed shows up as a small 3D window in the fly view, and there’s a elliptical proximity HUD overlaid on it.

Pros:

  • Runs on the flight controller — no companion computer dependency, no single point of failure, no expensive heavy Lidar.

  • a great way to tune behavior for real lidar without risking it.

  • Works in AUTO, GUIDED, and STABILISED/LOITER modes

  • Pre-cached mesh means no internet required in flight

  • Lightweight — 36 rays at 4Hz is trivial compute

  • Uses standard MAVLink OBSTACLE_DISTANCE — no ArduPilot core changes needed

  • Works anywhere Google 3D Tiles has coverage (most cities worldwide) you can provide your own Mesh for wares it does not have.

  • Provides collision prevention even for manual flight

Cons:

  • Mesh is only as current as the last download — new construction or temporary obstacles won’t be there

  • Trees and vegetation are captured in the mesh but their shapes are approximate

  • This is NOT a replacement for real sensors — it’s a supplement for known, static, mapped obstacles

**BendyRuler — standing on the shoulders of giants**

I want to be really clear: BendyRuler is a genuinely clever piece of engineering. The multi-step lookahead, the bearing resistance, the way it balances progress against safety — Randy Mackay designed that architecture gave it real thought, and it shows. The core algorithm is elegant. Sorry I bastardised it.

What I found was that BendyRuler needed some tuning and a few additions to handle dense urban environments where you’ve got 20-metre corridors between buildings, tight corners, and obstacles on all sides. The kind of environment it probably wasn’t originally designed for — because until now, nobody was feeding it synthetic LiDAR data from cached mesh in downtown Sydney.

The main changes I made (all contained within BendyRuler and WPNav_OA, no core ArduPilot changes):

- **Tunable scoring weights** — 7 new OA_BR_W_* parameters so the balance between progress, margin, deficit penalty, pitch preference, and braking can be adjusted via MAVLink without recompiling

- **Updated the OA path planner to run at 4Hz** instead of 1Hz, matching the LiDAR scan rate

- **Stronger bearing resistance** — if the current path has positive margin, commit to it. Don’t change direction unless the path is actually blocked. This was the single biggest improvement for corridor navigation

- **Progressive braking** — linear speed reduction as margin decreases, with a 30% minimum so the drone never crawls to a stop in a wide corridor

- **Direct path override** — when the straight line to the waypoint is clear, skip the search entirely and fly direct. Prevents scoring noise from causing wandering in open space

- **Pitch layer restrictions** — level flight preferred, climbing only as a last resort, no descending into the city (rooftops look “open” from above but you end up in the building zone)

- **Hard stop** — if margin reaches zero in all directions, hold position. Never push through a wall – Probably need to add a back up logic.

- **Velocity feedforward and destination smoothing** in WPNav_OA to prevent the 4Hz update rate from causing trajectory jitter

- **Position prediction** in the ray caster — compensates for ~250ms of MAVLink lag by predicting the drone’s position forward using velocity before casting rays

I also spent an embarrassing amount of time on yaw control before learning that the best approach was to barely touch it. The drone navigates by translating, not by rotating. Heavy-handed yaw control just caused oscillation. The final solution is a smoothed trajectory-following yaw with asymmetric speed filtering — it took about 15 attempts to get right, and I’m still not 100% sure I understand why the final version works.

**The video**

This is the drone navigating a mission through Sydney CBD — from Hyde Park through the streets around Macquarie Street to Sydney Hospital and back. Three waypoints through urban corridors. No manual intervention once the mission starts. It’s not perfect — you’ll see it hesitate at the first building approach and there’s some oscillation in tight spots — but it completes the route without hitting anything, which is more than I expected when I started this.

**Not just AUTO — works in GUIDED and STABILISED too**

Worth mentioning: because this feeds standard OBSTACLE_DISTANCE messages to the flight controller, it works across flight modes. In AUTO it provides the path planning you see in the video. In GUIDED it prevents collisions while flying to a commanded position. And in STABILISED/LOITER, the simple avoidance layer (AC_Avoid) uses the same proximity data to stop the pilot flying into buildings — the drone just refuses to go closer. You get collision prevention for free in every mode, with full autonomous navigation in AUTO.

**Where this could go — companion computer pipeline**

The current setup runs the ray casting on the GCS (my MacBook), which obviously isn’t going to work if you lose link.

But the path forward is clear:

**Near term**: Move the ray caster to a companion computer (Raspberry Pi 5 or 4). The cached mesh lives on local storage, the ray casting is lightweight (36 rays at 4Hz is trivial), and the OBSTACLE_DISTANCE messages go to the FC over serial. No internet required in flight — the mesh is pre-cached. This gives you local position-aware obstacle avoidance at very low computational cost.

**Medium term**: Live mesh generation from onboard cameras. Generating a dense point cloud in real-time, and cache it for later.

**The honest truth about limits**: I think I’ve taken BendyRuler about as far as it can go for dense urban navigation. It’s a reactive planner — it probes directions, scores them, picks the best. It works brilliantly for what it was designed for, and with the additions above it handles corridors surprisingly well. But for really complex urban environments — think multiple turns, dead ends, narrow gaps — you probably need a proper path planner that builds a route through the mesh ahead of time. Something like a visibility graph or RRT* through the 3D obstacle space. BendyRuler would still be valuable as the real-time local avoidance layer, but the strategic routing would come from the companion computer.

The downside of a full path planner: it’s a lot more computation, it needs the full mesh in memory, and it introduces a dependency on the companion computer being alive and healthy. BendyRuler running on the FC with synthetic LiDAR has a beautiful simplicity — the FC makes all decisions, no single point of failure. A companion-computer path planner trades that simplicity for capability. There’s probably a right answer for different use cases.

**Thanks**

To the ArduPilot team for building something I could hack on. To Randy Mackay for creating BendyRuler — seriously, the bones of that algorithm are sound, and it took me a while to appreciate how much the architecture was doing. To Rishabh for adding the vertical search, the bearing resistance, and 3D obstacle support — the bearing resistance in particular turned out to be one of the most important pieces for urban corridor navigation. And to the community for being the kind of place where someone can post "I pointed a virtual camera at Google Maps and taught a drone to fly through Sydney with machine learning " without getting laughed out of the room.

Oh I’m looking for a job BTW.

Happy to answer questions, share code, or accept suggestions for what I’m doing wrong (there’s probably a lot).

Cheers, Leon

@rmackay9

8 Likes

that is very cool, would it work with something like a boat on a river?

Possibly. things to think about. River levels rise and fall, tides move in and out, and obstacles change more frequently. Real-time LiDAR data would be needed. Boats are not constrained by weight tho. so….

1 Like

Some more progress, it now knows when it’s stuck in a corridor and can back out and find another way.

1 Like

Hi @Leon_Dwyer1,

Really well done! I hope you don’t mind I’ve moved this to the “Blog” category so that it will appear on the front page of ardupilot.org.

BTW, the original design of bendy ruler came from @tridge as part of the CanberraUAV team. I adopted it and made it work for Copter and Rover.

Thanks again!

1 Like

I’d assume almost anything has his hands on it. He’s made ArduPilot’s software what it is, and every time. I use rsync, I thank @tridge for making my life so much easier.

I’ve made a lot more progress since the post. I’m now running machine learning cases to optimise the scoring weights for smoother, faster navigation. Once I have those results, I’ll look at which layers can be simplified — there are several that work together, and each one adds parameters to Bendy. I wince at adding parameters to ArduPilot, so I want to find the minimum set that actually matters.

One big win: I’ve solved that behaviour where Bendy forces a landing when it can’t find a way forward. And from what I can see, the compute load is reasonable on an F7. It’ll probably kill an F4, though.

On accuracy — the mesh altitude is currently calibrated from the arming point, which fixes a lot of the vertical error. But horizontal GPS error still remains, and mesh accuracy has a big impact on how well this works. Eventually, I’d like a horizontal calibration routine, a real rangefinder pointing forward — point at a known feature, measure, point at another, measure, then compute the offset.

A real rangefinder pointing forward would also catch uncatalogued objects in the direct path and, as a ground-truth safety net, when the mesh can’t be trusted.

My real goal is to build from the bottom up. The plan: a companion computer that builds a mesh on the fly, caches it, and does proper route planning, with BendyRuler on the FC as the fallback if that fails. A redundant navigation stack — real-world low-cost sensors, banked knowledge of the world, real-time awareness on the companion computer, and graceful degradation down to on-FC avoidance when it needs to.

3 Likes

This is awesome!

# Balliyang Update — Path Planning, BendyRuler Bug Fixes, and a Video With No Audio

G’day all,

Quick update on the Balliyang synthetic LiDAR project. It’s been a big couple of days — found some genuine ArduPilot bugs, got 3D path planning working inside QGC, and submitted a couple of upstream PRs. Also recorded a demo video with my mic off, because apparently that’s how I roll.

## What’s New

### 3D Path Planning in QGC

The big one. Balliyang now plans obstacle-avoiding paths through the cached 3D mesh and uploads them as spline waypoint missions — all automatically from inside QGC. I plan to make a Misstionplanner plugin at some point.

The workflow:

1. Load the 3D tiles (Google Photorealistic 3D Tiles, or your mesh)

2. Upload your mission

3. Synthetic LiDAR detects the mission, validates it against the mesh

4. If obstacles are in the way → plans around them using A* grid search with clearance-weighted corridors

5. Uploads the refined mission with `NAV_SPLINE_WAYPOINT` items

6. Drone flies smooth curves through the city

The path planner uses the same BVH ray-casting engine that drives the proximity scanner — proven, fast, no separate process needed. It runs on a background thread so QGC stays responsive.

The cyan path shows up in both the 3D world view (as a ribbon with direction chevrons) and on the 2D fly map.

### BendyRuler Bug: `dest_to_next_dest_clear` Never Set

Found a genuine bug in `AP_OAPathPlanner.cpp`. When BendyRuler is the active OA planner (`OA_TYPE=1`), `dest_to_next_dest_clear` is initialised `false` in `avoidance_thread()` and never set `true` — only Dijkstra’s planner touches it.

The problem: `AC_WPNav_OA::update_wpnav()` checks this flag every iteration and calls `force_stop_at_next_wp()` when it’s false. That clears `fast_waypoint` and sets the destination speed to zero. End result: the drone stops and hovers at every single waypoint, even in completely clear airspace with no obstacles anywhere nearby.

The fix is one check — if OA returns `OA_NOT_REQUIRED`, the path is clear by definition:

```cpp

if (res == OA_NOT_REQUIRED) {

dest_to_next_dest_clear = true;

}

```

**PR submitted**: [ArduPilot #33178]( AC_Avoidance: set dest_to_next_dest_clear when OA not required by leonfliesthings-dev · Pull Request #33178 · ArduPilot/ardupilot · GitHub )

I reckon this affects anyone using BendyRuler with multi-waypoint missions. If your drone’s been stopping at every WP and you couldn’t figure out why — this might be it.

### GUIDED Mode + OA: Nobody Home

EDIT: my assumption below is wrong.

Discovered that GUIDED mode’s `Pos` submode (which is what “Fly Here” uses in most GCS apps) sends the target position straight to `pos_control->input_pos_NED_m()`, completely bypassing `wp_nav`. Since BendyRuler lives inside the `AC_WPNav_OA` wrapper, it never gets a chance to run.

So you can have `OA_TYPE=1`, proximity sensors active, obstacles everywhere — and “Fly Here” will send your drone straight into a wall.

The fix calls the OA path planner on the target position before sending it to pos_control. If BendyRuler detects an obstacle, it adjusts the target. If not, the original target passes through unchanged.

**PR submitted**: [ArduPilot #33179]( mode_guided: run OA path planner in Pos submode by leonfliesthings-dev · Pull Request #33179 · ArduPilot/ardupilot · GitHub )

Edit - PR removed.
Default is 0. Bit 6 (value 64) controls “Waypoint navigation used for position targets”. When UNSET (0), GUIDED Pos uses pos_control directly. When SET (64), it uses wp_nav which includes BendyRuler OA.

### S-Curves vs Classic Splines

Spent a fair while debugging why spline waypoints weren’t flying smooth curves. Turns out ArduPilot 4.1+ replaced classic splines with S-Curves for trajectory generation. `NAV_SPLINE_WAYPOINT` (cmd 82) still uses Hermite cubic splines for the path shape, but the velocity/acceleration profile is now S-Curve based. They’re fundamentally different systems — cmd 82 produces actual curved paths, cmd 16 produces straight lines with velocity smoothing at corners.

Understanding this properly was key to getting the path planner to generate waypoints that ArduPilot actually flies well. The expert analysis (which I’ll write up separately if people are interested) covers the Hermite math, velocity vector computation, SPLINE_FACTOR scaling, and optimal waypoint placement.

### QGC Spline Curve Display

QGC shows straight lines between spline waypoints, even though ArduPilot flies smooth curves. Added a Catmull-Rom interpolation overlay to the fly map that shows the approximate spline curve. Small thing, but makes it much easier to see what the drone will actually fly.

**PR submitted**: [QGC #14433]( FlyView: display Catmull-Rom curve for spline waypoints by leonfliesthings-dev · Pull Request #14433 · mavlink/qgroundcontrol · GitHub )

### BendyRuler Weight Tuning (ML)

Ran 27 automated SITL training runs through Sydney CBD to optimise the BendyRuler scoring weights. The best result (run018, gen3/ind6): 0 collisions, 3.29m minimum margin, completed all waypoints in 420s. Key finding: much higher progress weight (W_PROG 0.8→2.64) and much lower deficit penalty (W_DEF 2.0→0.36) produces smoother urban navigation. The drone pushes harder to make forward progress and doesn’t panic about detours.

## What’s Next

- **BendyRuler velocity monitor** — instead of hard on/off, have BendyRuler look ahead along the planned spline path and manage speed proportionally to obstacle distance. Like adaptive cruise control for drones.

- **Path quality tuning** — the A* grid planner works but could be smarter about altitude (going over low buildings vs around) and corner smoothing.

- **Jetson companion computer** — the long-term plan. 4x MIPI cameras building live mesh, visual localisation for GPS-free navigation. But BendyRuler + cached mesh first.

## Hardware

Still targeting Raspberry Pi 4 for cached mesh, Pi 5 for development, Jetson for production. The path planner runs entirely inside QGC for now — no companion computer needed. When the Jetson arrives, the real-time GUIDED mode control moves there.

Keen to hear if anyone else has hit the `dest_to_next_dest_clear` bug. I suspect it’s been quietly making BendyRuler look worse than it actually is for a while.

Cheers,

Leon

1 Like

Well done Leon, this is so smart from conception. To use a publicly available dataset that would have cost a lot to acquire and train your model on it, it’s brilliant.