Research: mesh probe run 2, which lowering gives shippable geometry

The compact Dexter Quiet description lowered three ways (OpenSCAD plus
automatic cleanup, a bmesh script, Geometry Nodes) and judged against the
operator's bar. Serves the brush-tree target revision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-10 23:37:23 +02:00
commit 91babd43cc

View file

@ -0,0 +1,226 @@
# Mesh probe run 2: is the lowered geometry shippable?
Agent `probe-shippable-lowering`, session `self-2026-10-10-ag2`, 2026-10-10. Disposable scratch; no repo edited.
Root: this directory (`...\scratchpad\mesh-probe\run2-shippable\`).
Operator question: "Sure, it can fit geometry, especially with 'real OpenSCAD', but is it good geometry? Do we
end up with something we'll want to ship, if we go this way? Because I'm not sure we wouldn't be better off
building with, say, Blender's geonodes."
Her bar (brush-tree:ruling:shippable-part-bar): clean, unbroken topology flow; UVs that keep the parts of one
object together, with minimal distortion; hard edges sharpened by a one-segment bevel shaded with face-weighted
normals.
Input: `docs/research/mesh-probe-run1/dexter-quiet-compact.scad` from the brush-tree branch, copied here as
`A.scad`. It is run 1's round 3: superellipse stations, n = 3. All three lowerings use its numbers:
- planform, 17 points;
- wing plane z = a + b|x| + c·y, with wing_t 0.045;
- trailing scallops;
- 12 tail and body stations;
- pods as hulls of two spheres;
- the chin fin.
Python copy of the numbers: `tools/desc.py`.
## Contact sheets (rows top to bottom: Tripo target, A, B, C; orange squares = row number)
- **`sheet-main.png`**. Columns: 3/4 front, side and 3/4 rear in metallic grey under studio light, then a
wireframe overlay, then the UV layout. In the UV column, colour marks the island and red marks overlap. The
target has no UVs, so its UV tile is black. This is the one to judge at a glance.
- **`sheet-closeup.png`**. Columns: close-up of the wing edge and wing-body junction (shaded, then wireframe),
and a top view with wireframe. The bevel highlights and the faceting are visible here.
- **`sheet-edit.png`**. Rows: A, A_edit, B, B_edit, C, C_edit. The edit is nose_k 1.0 to 1.2 plus a new
wing_sweep of 0.1.
- `sheet-top.png`: top views, shaded and wireframe.
- Per-lowering frames are in `out/<X>.v0-4.png`, `out/<X>.w0,w2,w4.png` and `out/<X>.uv.png`. Metrics are in
`out/<X>.metrics.json`, scores in `out/<X>.score.txt`, and the .blend files in `out/`.
Renders are Eevee (Blender 5.2.2). The look:
- Principled metallic 1.0, roughness 0.3, grey;
- Blender's bundled `studio.exr` world plus one big soft area key;
- orthographic camera, dark ground composited.
This approximates her test render; it is not her scene.
## The three lowerings
- **A, OpenSCAD lowering with the best automatic cleanup** (`tools/lower_a.py`):
- OpenSCAD 2026.09.23 with Manifold renders the file (2 s, 2836 triangles, genus 0).
- Blender then runs: merge by distance, tris-to-quads, limited dissolve at 2°, Bevel (1 segment, by angle
30°, width 0.004), Smart UV Project (66°) and Weighted Normal (face area, keep sharp).
- A2 also tried QuadriFlow, see "Could not test".
- **B, Blender built by script** (`tools/lower_b.py`, 174 lines):
- Body: tail and body stations as one quad loft, 36 verts per ring, with 2 Catmull-Rom rings inserted per
span. The ends get Coons-patch quad caps.
- Wing: one quad sheet across the whole span, with columns at the planform breakpoints and at the
trailing-edge kink, 16 chord rows, the scallop as the trailing edge, and a closed side band.
- Exact Boolean only for the wing into the body.
- Pods are quad capsules and the chin fin is a prism. Both are left as intersecting shells, not
booleaned.
- Seams go on edges over 30°, the body side lines and the cap rings. Then unwrap (angle based), average the
island scale, pack, Bevel (1 segment, 30°), Weighted Normal.
- **C, Geometry Nodes generated by Python** (`tools/lower_c.py`, 172 lines). It is the same construction as
one node group, `DexterQuiet`, with 199 nodes and 271 links. Its 16 parameters are modifier inputs:
- nose_root, nose_k, wing_t, wing_a/b/c, wing_sweep, scallop_r/cx/cy, sect_n, ring_n, loft_res,
wing_cuts, chord_rows, seam_angle.
- Station and planform data live in Index Switch nodes.
- Body: Points, then Points to Curves (Catmull-Rom), then Curve to Mesh with a superellipse profile, then
Set Position from the hw/zc/hh attributes.
- Wing: a Grid whose columns sample the leading-edge curve, then Extrude Mesh, joined with a flipped copy.
- Mesh Boolean (Manifold solver) for the wing into the body.
- Pods are UV spheres reshaped into capsules; the fin is a convex hull.
- UVs come from the UV Unwrap node, with the seam field = edge angle > 30° or a `cut_e` edge flag, then
Pack UV Islands.
- Modifier stack: GN, then Bevel (1 segment, 30°), then Weighted Normal.
## Measurements
| | A (OpenSCAD + cleanup) | B (bmesh script) | C (geonodes) |
|---|---|---|---|
| Faces: quads / tris / ngons | 1146 / 125 / 168 | 3039 / 49 / 215 | 3979 / 127 / 229 |
| Largest ngon | **59** (wing top/bottom dissolved to single faces) | 11 (boolean seam) | 36 (loft end caps; GN has no grid fill) |
| Poles (valence ≠ 4) per 1000 verts | **411.7** | 100.1 | 98.0 |
| Poles of valence 6 or more | 10 | 0 | 4 (UV-sphere pod tips) |
| Longest edge loop (edges) | **8** | 86 | 35 |
| Closed loops ≥ 20 edges | 0 | 41 | 57 |
| Share of edges in loops ≥ 20 edges | **0.013** | 0.528 | 0.605 |
| Non-manifold edges / shells | 0 / 1 | 0 / 4 (pods and fin intersect) | 0 / 4 |
| Exported triangles | 3392 | 6914 | 9022 |
| Sliver tris (< 10°) after export triangulation | 2082 (61%) | 2112 (31%) | 2537 (28%) |
| …excluding faces of bevel width | 491 | 336 | 157 |
| Dihedral > 60° / > 80° left (unbevelled hard edges) | 86 / 15 | 83 / 7 | 77 / 9 |
| UV islands: total / ≥ 1% of area / < 4 faces | 51 / 13 / 15 | 23 / 11 / 2 | 21 / 11 / 2 |
| UV area distortion, mean \|log2\|, area-weighted | 0.120 | 0.113 | 0.118 |
| UV area outside 0.5-2x | 0.1% | 0.7% | 1.1% |
| UV angle error, mean | 2.74° | **1.04°** | 1.14° |
| UV space wasted | 54% | 47% | 61% |
| Chamfer % diag / F@2% / mean sil-IoU (run-1 `mp score`) | 0.852 / 0.901 / 0.919 | 0.841 / 0.899 / 0.920 | 0.840 / 0.900 / 0.920 |
Raw OpenSCAD output scores 0.852 / 0.901 / 0.919. Cleanup lost no shape, and B and C lose none against A.
Reading the numbers:
- **Fidelity is the description's, not the lowering's.** All three are within 0.012 chamfer. This is the
same 0.852 that run 1's round 3 scored. No lowering makes the description closer to the target.
- **A fails the topology bar, and cleanup cannot fix it.** Hull-of-disks lofting plus CSG emits long fan
triangles. Join-triangles and limited dissolve turn them into a patchwork with no loops longer than 8
edges. The wing top and bottom become single ngons of up to 59 sides. 61% of the exported triangles are
slivers. On the close-up, the body hull's facets show through the weighted normals as flat shading
patches.
- **A's UVs look fine on paper** (area 0.12), because Smart UV cuts the surface into 51 islands, 15 of them
crumbs under 4 faces. That breaks "parts of one object kept together". See its UV tile.
- **B and C meet the topology-flow bar on the loft and the wing.** Body rings are closed 36-edge loops from
stinger to beak, and the wing is a quad grid.
- **Where B and C miss the topology bar:**
- The wing-body boolean seam leaves 215-229 ngons and most of the poles. That is the one place a hand
modeller would retopologise.
- The pods intersect the body as separate shells instead of being merged.
- C's loft caps are 36-gons and its pod tips are pole fans. Geometry Nodes has no grid-fill or Coons
patch, so B's quad caps have no direct node equivalent.
- **The remaining slivers in B and C are mostly the bevel strips themselves.** A 0.004 strip against
0.025 edges is inherently thin when triangulated; her bevel style has the same property. Excluding faces
of bevel width, C has 157 and B 336, from the thin rings near the stinger and beak tips.
- **The bevel bar is met on the outline in all three:** one segment, a bright highlight on the wing edge
band, weighted normals on.
- **It is not met at the wing-body junction**, where the crease is under 30° over much of its length, so
it gets no bevel. That is a hard intersection line, not a fillet. The edge counts above 60° (77-86) are
similar in all three, so the leftover hard edges are in the description (fin, pod ends, crease), not in
the lowering.
- **The normal-deviation metric** (mean angle between custom and face normals, 3.6-4.4° in all three) is
not diagnostic. The largest faces here sit on the curved body, which is legitimately curved. I have
dropped it as evidence.
- **The UV bar is met by B and C.** Each has about 11 islands of 1% of area or more:
- body top, body bottom;
- wing top and wing bottom per side;
- the wing edge band;
- pods, caps, fin.
Angle error is about 1° and area distortion about 0.11. Packing wastes 47-61%, which is plain
pack_islands; no trim or texel-density decisions were made.
## Editability (nose_k 1.0 to 1.2, plus a wing_sweep 0.1 that the description lacked)
| | What the edit is | Rerun | Topology after the edit |
|---|---|---|---|
| A | 2 `.scad` lines (one value, plus a `multmatrix` shear on the planform inside `wing_side`) | OpenSCAD 2 s + cleanup script ~3 s | It changes. Faces 1439 to 1434, ngons 168 to 173, islands 51 to 48. Every edit re-rolls the patchwork, and any hand fix-up in Blender is lost. |
| B | `DESC_OVERRIDES` or `desc.py` values; sweep is 1 line in the wing loop | Script ~3.6 s, produces a new .blend | Stable. Loops 86 to 86, islands 23 to 23, +4 faces at the boolean seam. Hand work is lost on rerun. |
| C | Two modifier input values. Sweep was one exposed input plus 2 math nodes. | Live in the modifier panel. Script build ~3 s. | Stable. Loops 35 to 35, islands 21 to 21, +2 faces. Hand work can only be stacked as later modifiers; editing the result means applying the modifier. |
`sheet-edit.png` shows the edits. They are small on screen: the nose grows from 0.52 to 0.552 and the wing
tips sweep back about 0.05.
## B and C converge
Same fidelity (0.841 / 0.840). Same UV quality (angle error 1.04° / 1.14°, area 0.113 / 0.118). Same island
structure, and the same misses at the boolean seam. The differences are resolution choices and two Geometry
Nodes gaps:
- the caps are ngons, and the pods are UV spheres with pole fans;
- there is no bevel node, so the bevel is a modifier after the tree, and the unwrap happens before the
bevel. The bevel faces inherit UVs from the neighbouring face, and that measured fine.
**Geonodes is a practical lowering target the model can drive.** One generator script, about 170 lines,
produced a working parametric tree. I hit four Blender 5.2 API traps before it ran:
1. The Index Switch has a virtual extend socket.
2. Several enum properties became menu sockets that take UI labels: `'Angle Based'`, not `'ANGLE_BASED'`.
3. Modifier inputs are set through `mod.properties.inputs[id]["value"]`. `mod[id]` is gone.
4. **The GN Mesh Boolean with the Exact solver corrupted named attributes.** Wing vertices came out with
`cut` and `endcap` = 1, values the wing never had, even after storing explicit zeros. The Manifold
solver propagates them correctly.
Point-attribute seams also broke where the boolean split an edge, which merged the body's top and bottom UV
islands. The fix was carrying the seam as an edge attribute, `cut_e`. I checked it with
`tools/c_opunwrap.py`: the same geometry unwrapped by the operator gave the identical bad result, so this
was a seam-definition issue, not the UV node.
**Readability:** the tree's node positions are auto-placed with no frames, so as a canvas it is spaghetti.
The readable artefact is the Python generator. A tree she would want to open needs frames per part
(body / wing / pods / UV) and a layout pass, which I did not do.
## Conclusion on her question
**OpenSCAD as the lowering does not give geometry she would ship, and automatic cleanup cannot rescue
it.** Its output has already lost the construction intent: which vertices are a station ring, which faces
are the wing sheet. The topology numbers above are the evidence: 412 poles per 1000 vertices, a longest loop
of 8 edges, 59-gons, 61% slivers, and 51 UV crumbs.
**Blender lowering (B or C) from the same description meets her bar where the description has clean
construction:** the body loft and the wing sheet. Both misses come from the construction, not the lowering
tool:
- the boolean seam where the wing enters the body (ngons and poles);
- intersecting pods and fin.
Neither B nor C is a finished part yet. The description itself is crude, so the part is too: a
constant-thickness slab wing, no crease fillet, blob pods, 0.84% chamfer.
**Recommendation: the hybrid.** The model writes the description, in OpenSCAD or anything else that carries
the parameters and the construction intent: stations, planform, plane, part list. Blender lowers it by
construction, not by CSG output.
- **Geonodes (C)** is the better target for her. It is equal in quality to B and live-editable from the
modifier panel, which is her tool.
- **B's bmesh script** is the fallback for constructions nodes cannot express, such as quad caps or
grid fill.
- **"Real OpenSCAD" can stay as the authoring and fitting surface** only if the lowering reads its
parameters and not its mesh. Lowering the CSG tree itself reproduces A.
Confidence: moderate for "A cannot meet the bar". That follows from what the output loses, and the numbers
are large. Low to moderate for "C is shippable with work":
- one hull, one description;
- construction and seam choices are mine;
- no human retopologised the seam for comparison;
- the eye on the renders was mine, not hers.
## Could not test / caveats
- **QuadriFlow, A2's best automatic retopology**, refused in background mode. It reported "needs to be
manifold" even on bmesh-manifold input, and again after a voxel remesh. A voxel remesh alone (0.004)
gives 114k faces and 380 poles per 1000 vertices, and destroys the hard edges that bevel-by-angle needs.
I did not try QuadriFlow in the GUI. `tools/lower_a2.py` and `out/A2.*` hold the failed attempt.
- Her studio scene, texture baking and decals, export into the game engine, and LOD were not tested.
Packing efficiency was not tuned.
- Interpenetrating pods were a choice. Booleaning them in would add more seam ngons of the kind the wing
produced.
- The A cleanup parameters (dissolve 2°, join 10°/40°, Smart UV 66°) were set once, not swept.
- Tools: `tools/run.sh measure|render|target|score <name>`, `measure.py`, `render.py` and `sheet.py`. The
scorer is run 1's `mp`, rebuilt into `tools/mp-target`; that 156 MB build directory has been deleted, so
rebuild it before rescoring. `dbg*.py` are throwaway diagnostics.
- No processes of mine are running. The operator's GUI Blender (PID 36324) was never touched. I waited out
another agent's background Blender smoke and a cargo build before my first jobs.