A fully parametric VTOL designed in OpenSCAD, built for vase mode 3D printing. The project generates a complete aircraft: wings with internal structure, a Bézier-profiled fuselage, motor arms, winglets, ailerons, and a center body.
The development of the OpenGeoDrone was funded by the French National Research Agency (ANR, Agence Nationale de la Recherche) and the German Research Foundation (DFG, Deutsche Forschungsgemeinschaft) within the framework of the Franco-German research project FlyHigh (ANR FlyHigh ANR-24-CE92-0015 and GR 5407/2-1)
- Overview
- Repository Structure
- Print Tutorial
- OpenSCAD Quick Start
- How the Wings Are Built — The Vase Mode Method
- How the Fuselage Is Built — C1 Quintic Bézier
- Parts to Print
- Key Parameters Reference
- Slicer Setup
- Performance Tips
- Credits and Prior Work
- License
OpenGeoDrone is a VTOL UAV designed entirely in OpenSCAD. The goal is to produce aerodynamically efficient, lightweight, and printable parts using the spiral vase mode of FDM slicers (also called "spiralize outer contour"). This mode prints the outer skin as a single continuous spiral — no retractions, no seams, very low weight.
The aircraft is parametric end-to-end: wing span, chord, sweep, airfoil, motor position, spar count, aileron geometry, fuselage shape, and more are all driven by variables in a single file (OpenGeoDrone.scad). Changing one number regenerates every dependent part automatically.
VTOL, Vertical Take-Off and Landing :
The aircraft takes off and lands using its four hovering motors. It flies like a plane thanks to a pusher motor and two elevons.
Default aircraft specs (all configurable):
| Specification | Value |
|---|---|
| Half-wingspan | 510 mm (total ~1200 mm + center body) |
| Mass without battery | 1.066kg |
| Max payload mass | 1.15kg with 60% throttle in hovering |
| Flight endurance | 25min in plane mode with 90s Hovering with 4S 4Ah battery |
| Min plane flying speed | 14m/s |
| Hoovering | 34% throttle |
| Root chord | 210 mm |
| **Airfoil ** | MH61 (cambered, suitable for slow stable RC flight) |
| carbon fiber spars | 5 (2 per half-wing + 1) |
| Motors | 4 hovering motors on tilted arms + one rear pusher |
| Elevons | 2 for pitch and roll control |
| Center part dimensions | 275mmx90mm |
| Wings easy to remove | no cables |
| Inertial Matrix | coming soon |
| Hovering power with 4S | 400W |
| Plane power with 4S | 90W |
Differents parts:
Every parts of the plane are printable. STL files are accessible in stl_print_part directory and the bambulab preset for X1C and H2D are available in bambulab_preset directory. It is also possible to print directly the project with the 3mf format in the 3mf_print_part directory.
Use a 0.4mm nozzle for all prints.
It will load automatically the Bambulab preset on H2D or X1C.You can find this files in the folder 3mf_print_part
For X1C or H2D :
Use PLA Aero for :
- X1C_Left_Root_part
- X1C_Left_Mid_Aileron_part
- X1C_Right_Root_part
- X1C_Right_Mid_Aileron_part
Use PETG Basic for :
- X1C_Motor_Arms_and_sides
- X1C_Fuselage_and_Clamps
- X1C_Center_part
You choose of course the part corresponding to your printer.
For PLA Aero Wings, use the 0.20mm_Standard-PLA_Aero_VaseMode_XXX.json upon your printer and print the following parts.
PLA Aero Wings :
- Left_Root_part
- Right_Root_part
- Left_Mid_Aileron_part
- Right_Mid_Aileron_part
For Side parts, Clamps parts and Center body below, you can print it in PETG Basic in standard mode (0.20mm Standard)
Side parts :
- Left_Motor_arm_back
- Left_Motor_arm_front
- Right_Motor_arm_back
- Right_Motor_arm_front
- Left_Servo_horn
- Right_Servo_horn
Clamps parts :
- 6 x Clamp_fixation_big
Center body :
- Center_part
- Fuselage_bottom_back_part
- Fuselage_front_part
- Fuselage_upper_part
- Rear_motor_part
- Carbon fiber tube ⌀5.5 mm
- Carbon fiber tube ⌀6.5 mm
- Flight controller (e.g. Tawaki)
- 2 Servo
- RC
- Pitot tube
- GPS module and Modem to Ground station if needed
- 4 Motors for hoovering (F40)
- 1 ESC 4 to 1
- 2 Propellers 7x4 and 2 reverse Propellers 7x4
- 1 Motor for pusher (low kV < 1200)
- 1 ESC for pusher
- 1 Propeller 9x6
- TODO
Vase_wing_openscad/
│
├── OpenGeoDrone.scad # Main entry point — all parameters and top-level assembly
│
├── lib/
│ ├── Wing-Creator.scad # Wing shell geometry (airfoil lofting, sweep, washout)
│ ├── Grid-Structure.scad # Internal rib and spar grid structure
│ ├── Grid-Void-Creator.scad # Clearance voids ensuring vase mode continuity
│ ├── Rib-Void-Creator.scad # Optional weight-reduction holes in ribs
│ ├── Spar-Hole.scad # Carbon spar tube holes and retaining rings
│ ├── Aileron-Creator.scad # Aileron geometry, pin holes, command pin voids
│ ├── Motor-arm.scad # Motor arm tube, tilt, attachment to wing
│ ├── Winglet-Creator.scad # Winglet geometry and attachment interface
│ ├── Center-part.scad # Center body / fuselage assembly
│ ├── Servo-Hole.scad # Servo pocket geometry
│ ├── Helpers.scad # Utility functions (chord interpolation, spline, etc.)
│ ├── Tools.scad # General geometry helpers
│ └── openscad-airfoil/ # Airfoil coordinate libraries (MH45, NACA0008, etc.)
│
├── bambulab_preset/ # Ready-to-import slicer presets for Bambu Lab printers
├── git-images/ # Screenshots of rendered wings for this README
├── stl_generator.sh # Bash script to batch-export all STL files via OpenSCAD CLI
└── README.md # This file
The single file to open is OpenGeoDrone.scad. All parameters live at the top of that file. The lib/ modules are included automatically — you never need to edit them directly.
Standard OpenSCAD is very slow on this model. Use the development snapshot with the Manifold geometry engine, which is approximately 100× faster:
- Download a development snapshot: openscad.org/downloads.html#snapshots
- Open Edit → Preferences → Features
- Enable the Manifold checkbox
File → Open → OpenGeoDrone.scad
At the top of OpenGeoDrone.scad, set exactly one part flag to true:
// --- Choose the part to export ---
Root_part = true; // Wing root section (fuselage side)
Mid_Aileron_part = false; // Mid section + aileron as one piece
Tip_part = false; // Wing tip section (+ winglet if enabled)
Motor_arm_full = false; // Motor arm (front + back together)
Motor_arm_front = false; // Front half of motor arm only
Motor_arm_back = false; // Rear half of motor arm only
Servo_horn = false; // Servo horn
Center_part = false; // Center body / fuselage
Rear_motor_part = false; //Part for Rear motor attach
Clamp_fixation_big = false; // Clamp to fix parts together (ie wings to center_part or motor_arm)
Clamp_fixation_small = false; // Clamp to fix parts together (ie wing_tips to center_part or motor_arm)
Fuselage_front_part = false; // Fuselage
Fuselage_bottom_back_part = false; // Fuselage
Fuselage_upper_part = false; // Fuselage
Full_fuselage = false; // Fuselage parts all together
Full_system = false; // Full assembly preview (not for printing)
// --- Choose the side ---
Left_side = true; // Generate left wing half
Right_side = false; // Generate right wing half (mirrored)Press F6 to render, then File → Export → Export as STL.
To export as STL all parts at once, use the batch script:
./stl_generator.shParts will be exported in folder stl_output
Vase mode (also called "spiralize outer contour") is a slicer setting that prints only the outer wall as a single continuous upward spiral — no infill, no top/bottom layers, no retractions. For a wing this is ideal: the airfoil skin becomes a single-layer seamless tube that is light, smooth, and aerodynamically clean.
The geometry must be designed specifically for this mode. At every layer height, the cross-section of the model must form a single closed contour — if the slicer sees multiple disconnected loops, it cannot maintain the spiral and the print fails. Every design decision in this project (grid voids, spar hole placement, aileron connection geometry) exists to satisfy this constraint.
The wing shape starts with a 2D airfoil polygon. OpenGeoDrone uses profiles from the openscad-airfoil library, which stores airfoil coordinates as OpenSCAD path arrays. These were pre-processed from UIUC .dat files using AeroSandbox to increase point density, resulting in smoother printed curves.
The default airfoil is MH61, a cambered profile well-suited for slow stable RC flight. The winglet uses NACA0008, a symmetric thin profile. Up to three different airfoils can be blended along the span (root, mid, tip) using slice_transitions.
The wing outer skin is generated by CreateWing() in Wing-Creator.scad. The airfoil cross-section is placed and transformed at wing_sections spanwise stations (default: 50). At each station z, the section is:
- Scaled to the local chord length, following either a trapezoidal or elliptic planform (
wing_modeparameter) - Swept in X by a user-defined spline (
lead_edge_sweepparameter) — this shifts the leading edge rearward as span increases, giving the wing its arrow shape - Curved in Y by an optional spline (
lead_edge_curve_y) — used for dihedral - Washed out by rotating the section about a pivot point (
washout_pivot_percparameter) to progressively reduce angle of attack toward the tip, which improves stall behavior
The elliptic chord distribution uses a super-ellipse formula:
chord(z) = root_chord × (1 − (z / span)^p)^(1/p)
where p = elliptic_param.
- A value of 2 gives a true ellipse
- Values > 2 give a squarer tip
- Values < 2 give a sharper tip
Each station is a near-zero-thickness extruded polygon. OpenSCAD's hull() operation connects consecutive stations into a smooth lofted surface — the wing outer skin.
The wing skin alone is too flexible. An internal rib-and-spar grid is built by Grid-Structure.scad and subtracted from the wing volume, creating a periodic internal skeleton visible through the skin after printing.
Two grid modes are available:
Mode 1 — Diamond grid: A diagonal lattice scaled by grid_size_factor. Lightweight and uniform, but without defined spar paths.
Mode 2 — Spar + cross-rib grid (default): Longitudinal spar walls at the same chord percentages as the carbon tubes, connected by rib_num transverse ribs. This is a structurally coherent option — the grid walls and the tube channels form a unified load path.
After the grid is built, CreateGridVoid() cuts narrow channels along every grid wall. This is the key to vase mode compatibility: the channels reduce each internal wall to a thin fin rather than a solid block, ensuring the slicer always sees a single outermost contour at every layer.
Three carbon fiber tube channels are cut through the wing by Spar-Hole.scad:
| Spar | Position from leading edge | Tube diameter |
|---|---|---|
| Spar 1 | 15% of chord | 5.5 mm |
| Spar 2 | 37% of chord | 5.5 mm |
| Spar 3 | 75% of chord | 6.5 mm |
Spar 1 and 2 holes are angled to follow the leading edge sweep, so the carbon tube runs geometrically parallel to the wing planform. Spar 3 is perpendicular to the root face and bigger diameter to increase the structure rigidity and create a block mechanism with a triangle system.
Around each hole, a ring of 12 small retaining circles (spar_circle_holder = 0.25 mm) creates a snap-fit friction interface that holds the tube in place after insertion without glue.
The same holes continue into the center body to connect both wing halves through the fuselage.
OpenSCAD prints the required tube lengths to the console at render time:
[SPAR] Spar 1 at 15% from LE is XXX mm length.
[SPAR] Spar 2 at 37% from LE is XXX mm length.
[SPAR] Spar 3 at 75% from LE is XXX mm length.
Cut your carbon tubes to these exact lengths before assembly.
Elevons are sticked to the mid part :

The connection between the mid part and the aileron is performed by a thin layer and tht actuation is done by the servo horn part connected to your servo:
Clamp are used to hold parts together, insert them slowly into the 6 places :
The fuselage / center body profile is defined by a quintic Bézier curve (degree 5, 6 control points) that describes the body radius along the longitudinal axis. This gives a smooth, aerodynamic shape with guaranteed C1 continuity at both endpoints — the tangent is well-defined at the nose and tail, preventing any abrupt transitions.
The six control points are derived from three base radii and five shape factors:
| Point | Value | Role |
|---|---|---|
| P0 | R_front |
Nose radius |
| P1 | R_front + P1_factor × (R_max − R_front) |
Nose tangent — controls C1 smoothness at nose |
| P2 | R_front + P2_factor × (R_max − R_front) |
Controls widening speed |
| P3 | R_tail + P3_factor × (R_max − R_tail) |
Controls how long the body stays wide |
| P4 | R_tail + P4_factor × (R_max − R_tail) |
Tail tangent — controls C1 smoothness at tail |
| P5 | R_tail + P5_factor × (R_max − R_tail) |
Tail radius |
The curve is numerically normalized so that max(R(t)) is equal to center_part width, regardless of what factor values are chosen.
Each longitudinal station generates a 2D polygon using an asymmetric super-ellipse:
ca = cos(a), sa = sin(a)
x = rx · sign(ca) · |ca|^(2/nx)
y = ry · sign(sa) · |sa|^(2/ny)
The vertical radius ry uses two independent scaling factors:
- Upper half (
sa ≥ 0):ry = rx × fuselage_scale_y_top - Lower half (
sa < 0):ry = rx × fuselage_scale_y_bottom
This makes the fuselage asymmetric vertically — for example, flatter on the bottom for a landing skid while remaining more rounded on top. Both halves meet seamlessly at y = 0.
The exponents in fuselage_ellipse_param = [nx, ny] control the cross-section shape:
n = 2→ standard ellipsen = 4→ rounded rectanglen → ∞→ rectangle
fuselage_sections() produces an array of 101 pairs [z, rx] by evaluating the Bézier curve at num = 100 equally spaced parameter values. Each pair becomes one frame — a thin 2D polygon at position z along the body axis.
The fuselage is then assembled in by chaining hull() between every pair of consecutive frames, producing 100 smooth segments that form a seamless body skin.
Finally, the module wraps the fuselage in a global hull() together with the inboard wing root cross-sections, creating a smooth aerodynamic fairing between fuselage and wing with no sharp steps or manual fillets.
| Parameter | Default | Description |
|---|---|---|
wing_mm |
500 | Half-wingspan in mm |
wing_root_chord_mm |
180 | Root chord length in mm |
wing_tip_chord_mm |
110 | Tip chord (trapezoidal mode only) |
wing_mode |
2 | 1 = trapezoidal, 2 = elliptic |
elliptic_param |
3.5 | Elliptic exponent (2 = true ellipse, >2 = squarer tip) |
wing_sections |
50 | Spanwise resolution — higher = smoother, slower |
wing_root_mm |
215 | Spanwise length of root section |
wing_mid_mm |
245 | Spanwise length of mid section |
AC_CG_margin |
10% | CG target margin aft of aerodynamic center |
| Parameter | Default | Description |
|---|---|---|
center_width |
80 mm | Maximum fuselage width |
center_length |
275 mm | Fuselage total length |
R_front |
1 mm | Nose radius |
R_max |
center_width/2 |
Maximum body radius (normalized exactly) |
R_tail |
5 mm | Tail exit radius |
fuselage_scale_y_top |
1.2 | Upper half vertical scaling ratio |
fuselage_scale_y_bottom |
1 | Lower half vertical scaling ratio |
fuselage_ellipse_param |
[7, 4.8] |
Cross-section super-ellipse exponents [nx, ny] |
num |
100 | Number of longitudinal Bézier frames |
| Parameter | Default | Description |
|---|---|---|
spar_hole_perc |
15% | Spar 1 chord position from leading edge |
spar_hole_perc_2 |
37% | Spar 2 chord position |
spar_hole_perc_3 |
75% | Spar 3 chord position |
spar_hole_size |
5.6 mm | Spar 1 & 2 tube outer diameter |
spar_hole_size_3 |
6.65 mm | Spar 3 tube outer diameter |
spar_circles_nb |
12 | Retaining ring segment count |
spar_circle_holder |
0.25 mm | Retaining ring interference radius |
| Parameter | Default | Description |
|---|---|---|
motor_arm_length_front |
170 mm | Front tube length |
motor_arm_length_back |
210 mm | Rear tube length |
motor_arm_tilt_angle |
20° | Motor tilt angle |
motor_arm_height |
19 mm | Arm cross-section height |
ellipse_maj_ax |
9 mm | Arm tube major axis radius |
ellipse_min_ax |
13 mm | Arm tube minor axis radius |
| Parameter | Default | Description |
|---|---|---|
draft_quality |
false |
Set to true for fast coarse preview |
$fa |
5° | Maximum angle between mesh segments |
$fs |
1 mm | Maximum segment length |
TODO
Import the presets from the bambulab_preset/ folder. If the import fails, check that the preset version matches your printer firmware and update the version field in the JSON if needed.
Known issue: Bambu Studio can crash when slicing large models due to an NVIDIA driver conflict. See this forum thread for the fix.
- PLA Aero : for wings
- PETG : for all other parts
Render is very slow:
- Enable the Manifold engine in OpenSCAD preferences
- Set
draft_quality = truefor a fast preview pass - Lower
wing_sectionsif you see the warning:"Normalized tree is growing past 200000 elements. Aborting normalization."
Spar hole not appearing in the model:
- The spar hole is too far from any skin edge — the vase circuit connection path is too long for the slicer to close
- Toggle
spar_flip_side_1,spar_flip_side_2, orspar_flip_side_3to route the connection toward the nearest edge instead
Aileron binds or is too tight after printing:
- Increase
y_offset_aileron_to_wingslightly (try 0.8–1.0 mm) - Adjust
ailerons_pin_hole_dilatation_offset_PLA(increase slightly if pin is too tight)
Adding custom inserts inside the wing:
- Always cut a clearance void around any added component before subtracting it from the wing
- Without the void, the insert will conflict with adjacent rib walls and break the vase contour
This project builds on and extends the following open-source work:
- Beachless/Vase-Wing — the original vase mode wing generator for OpenSCAD that directly inspired this project
- BouncyMonkey — Propeller Generator — wing lofting and construction technique adapted here
- guillaumef/openscad-airfoil — Perl script and SCAD library for converting airfoil
.datfiles into OpenSCAD polygon paths - peterdsharpe/AeroSandbox — used to resample airfoil coordinate files to higher density for smoother printed curves
- UIUC Airfoil Database — source of MH61 and NACA airfoil coordinate data
GPL-3.0 — see LICENSE for details.





















