Generates a 3D-printable counterflow core for a heat recovery ventilator (HRV). The core is a gyroid, a TPMS (triply periodic minimal surface). Its wall splits the enclosure into two interleaved channel networks, one for the supply air and one for the exhaust air. Heat passes through the thin wall between them.
The input is an enclosure volume exported from Onshape with its port faces colour-coded. The output is a single watertight mesh, ready for Bambu Studio. It is sized to print in one piece on a Bambu H2C.
Built with PicoGK (voxel geometry kernel) on .NET 9.
For each part, Program.cs runs these steps:
-
Import (
ObjImporter.cs). Reads the OBJ/MTL, converts from metres to mm, and groups triangles by material colour. -
Classify ports (
PortClassifier.cs). Matches each group's diffuse colour against the port palette:Port Colour Kd SupplyIn red 1.000 0.000 0.000SupplyOut orange 0.973 0.529 0.004ExhaustIn blue 0.231 0.380 0.706ExhaustOut cyan 0.616 0.812 0.929Any other colour is treated as enclosure body. An off-palette port is skipped without an error, so use these exact colours.
-
Port slabs (
SlabBuilder.cs). Extrudes each port's faces inward as one closed prism,fPortSealDepthMMdeep. -
Membrane (
Gyroid.cs). Builds the gyroid wall inside the volume. The field is gradient-normalised with one Newton step, which keeps the wall a uniformfWallThicknessMMthick along its normal. -
Port seals. Inside each port's slab, fills the other stream's channels solid, so each port opens only onto its own stream.
-
Skin. Adds a
fSkinThicknessMMouter shell, with cut-outs at the ports. -
Export. Meshes the voxel core, then decimates it (
MeshDecimator.cs, quadric edge collapse to an RMS surface tolerance). Writes STL, 3MF or VDB.
Each step prints a numbered checkpoint. A checkpoint that fails a hard check throws and stops the run:
| Checkpoint | Checks |
|---|---|
| 0 | Toolchain smoke test (writes out/checkpoint0_sphere.stl) |
| 1–3 | Import: group and triangle counts, bounding box, colour → port role |
| 4 | Voxelised volume vs expected (warning only) |
| 5–7 | Port slabs written, ports mapped to faces, membrane and seals built |
| 7b | Every port has at least 5% open area for its assigned stream |
| 8 | Skin covers the body and leaves the ports open |
| 9 | Wall thickness at 200 random points: mean within 2% of nominal, every point within 5% |
| 10 | Decimated mesh has no open, non-manifold or degenerate edges and winds outward; reports volume change and surface deviation |
- Windows. It is the only platform tested so far.
- .NET 9 SDK. PicoGK 2.3.0 targets
net9.0only and is restored from NuGet. - RAM: about 9 GB for the real volume at 0.25 mm. See Performance.
dotnet run -c Release -- [--voxel <mm>] [--only <name>]... [--export stl|3mf|vdb|none]
[--decimate <mm>] [--cell <mm>] [--wall <mm>] [--seal <mm>]| Option | Default | Meaning |
|---|---|---|
--voxel |
0.5 |
Voxel size in mm (production target is 0.15) |
--only |
all parts | Run only parts whose OBJ path contains <name>. Repeatable |
--export |
stl |
Core output: stl, 3mf, vdb, or none |
--decimate |
0.02 |
STL/3MF surface tolerance in mm. 0 keeps the raw voxel mesh |
--cell |
8.0 |
Gyroid wavelength λ in mm |
--wall |
0.8 |
Membrane thickness in mm |
--seal |
0.75 × --cell |
Port seal depth in mm |
--cell is the main lever on output size: triangle count scales as 1/λ²,
and unlike --decimate it costs no geometric accuracy — the wall stays at
nominal thickness. What it costs is heat-exchange area, which scales as 1/λ.
Measured on the 100mm cube at --decimate 0.02: λ 8→12 gave 2.22× fewer
triangles, λ 8→16 gave 4.26×.
Slicers have a practical ceiling on triangle count. Bambu Studio stalls
indefinitely above roughly 1M triangles on this geometry, regardless of
support and infill settings, so the real volume needs --cell 16 to export
something sliceable:
dotnet run -c Release -- --only real --voxel 0.25 --export 3mf --cell 16 --decimate 0.05To build the real enclosure for Bambu Studio:
dotnet run -c Release -- --only real --voxel 0.25 --export 3mfOutput goes to out/<part>/: the core (core.3mf, core.stl or core.vdb)
plus the port slab STLs from Checkpoint 5.
The parts are listed in aParts at the top of Program.cs:
| OBJ | Output |
|---|---|
fixtures/test_cube_100mm.obj |
out/test_cube_100mm |
fixtures/test_cube_filleted_100mm.obj |
out/test_cube_filleted_100mm |
real-HRV-volume/real-HRV-volume.obj |
out/real_hrv_volume |
To add a part, export the enclosure from Onshape as OBJ with the port faces
painted in the palette colours, then add a row to aParts.
The parameters are set in Params.cs:
| Constant | Value | Meaning |
|---|---|---|
fCellSizeMM |
8.0 | Gyroid cell size (λ) |
fWallThicknessMM |
0.8 | Membrane wall thickness |
fPortSealDepthMM |
6.0 | How far each port seal reaches into the core |
fSkinThicknessMM |
1.5 | Outer skin thickness |
fImportScale |
1000 | OBJ metres → mm |
fColorTolerance |
0.15 | Maximum RGB distance for a palette match |
Measured on the real volume (232 × 131 × 140 mm) with a Ryzen 7 5700U and 15 GB RAM:
| Voxel | Output | Triangles | File | Deviation mean / p99 / max | Time | Peak RAM |
|---|---|---|---|---|---|---|
| 0.25 mm | raw STL | 94.0M | 4.7 GB | 0.003 / 0.016 / 0.093 mm | ~11 min | 8.1 GB |
| 0.25 mm | 3MF, 0.02 mm tolerance | 11.1M | 137 MB | 0.014 / 0.041 / 0.127 mm | ~13 min | 8.4 GB |
| 0.5 mm | 3MF, 0.02 mm tolerance | 11.5M | 131 MB | 0.018 / 0.061 / 0.214 mm | — | — |
Notes on these results:
- The measured wall is 0.801 ± 0.002 mm, against a nominal 0.8 mm.
- Decimation changes the volume by less than 0.03%.
- After decimation, the output size depends little on voxel size.
- Meshing is the step that limits memory. At 0.15 mm the real volume needs about 23 GB, so it does not fit on this machine yet. The 100 mm cube does.
Done:
- The full pipeline, Checkpoints 0–10.
- Decimated 3MF export.
- The real volume at 0.25 mm.
Still to do:
- Confirm the 3MF loads and slices well in Bambu Studio.
- Mesh the real volume at 0.15 mm in slabs.
- Load parameters from a config file for parameter sweeps.
See todo.md for the full list, open questions and known PicoGK
issues. One known PicoGK issue is that IntersectImplicit crashes at voxel
sizes ≤ 0.33 mm, which the pipeline works around.
| File | Purpose |
|---|---|
Program.cs |
Command line, pipeline, checkpoints, export |
ObjImporter.cs |
OBJ/MTL parser |
PortClassifier.cs |
Colour → port role |
PortAssertion.cs |
Maps port groups to bounding-box faces |
SlabBuilder.cs |
Closed prism extrusion of port faces |
Gyroid.cs |
Normalised gyroid field and stream implicits |
MeshDecimator.cs |
Parallel quadric decimation, topology checks, 3MF/STL writers |
Params.cs |
Design constants |
fixtures/ |
100 mm test cubes |
real-HRV-volume/ |
The real enclosure export |