From ebe8d9784098606d84cccf91e377398a47fb2189 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sun, 11 Oct 2026 04:06:07 +0200 Subject: [PATCH] Research: spike run 7, Instant Meshes with the SDF's cross field and a hard rim The fidelity wall was the SDF program's own fit, not the remesher; the hard crease with Newton projection gives a crisp rim; flow and UVs remain open. Keeps the ~100-line Instant Meshes patch (BSD fork, against 7b31608). Co-Authored-By: Claude Opus 5.5 --- .../mesh-probe-run7-im-field/RESULT.md | 169 +++++++++++ .../instant-meshes-field.patch | 275 ++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 docs/research/mesh-probe-run7-im-field/RESULT.md create mode 100644 docs/research/mesh-probe-run7-im-field/instant-meshes-field.patch diff --git a/docs/research/mesh-probe-run7-im-field/RESULT.md b/docs/research/mesh-probe-run7-im-field/RESULT.md new file mode 100644 index 0000000..b0288ce --- /dev/null +++ b/docs/research/mesh-probe-run7-im-field/RESULT.md @@ -0,0 +1,169 @@ +# Run 7: an Instant Meshes fork driven by the SDF (cross field, hard rim crease, per-vertex size) + +Agent `spike-im-field-features`, session `self-2026-10-10-ag2`, 2026-10-11. Ruling `brush-tree:ruling:spike-instant-meshes-field-features`, bar `brush-tree:ruling:shippable-part-bar`. Scratch only; no repo touched; operator GUI Blender (PID 36324) untouched; all my processes ended. + +## Verdict + +1. **The diagnosis was wrong.** The ~0.49 % chamfer wall is not lattice size. It is how well run 4's SDF program fits the Tripo target. + - Measured against the SDF itself, every Instant Meshes output, plain included, sits at 0.01-0.07 % chamfer and 0.99+ top silhouette IoU. + - Scored against Tripo, every variant lands at 0.48-0.55 % whatever the field, constraints or density. + - The program alone scores 0.520 (run 4). No remesher can beat that against Tripo. +2. **The real thin-feature defect was in the program.** Its fitted beak was a pinched neck (half-width 0.006) and an inverted station. That gave: + - 119 non-manifold input edges; + - ~300 extra orientation singularities (374 against 62); + - every hole in the IM outputs (16-122 open edges). + + Reverting those two stations to run 2's description values (`in/params7.json`) leaves every output with 0 holes, 0 non-manifold edges and 1 shell. +3. **Hard rim crease (step 2) is what bought rim fidelity.** It works through position and orientation constraints on the exact crease plus Newton projection: at 8k quads the rim p95 error falls 10x, from 0.20 % to 0.02 % of the diagonal, and the rim renders crisp with a one-segment bevel. It needs the lattice finer than the rim wall (0.035-0.08 high). At 2k quads it only halves the error. +4. **The exact-Hessian cross field (step 1) buys alignment and costs poles.** Median misalignment to the principal directions drops from 13° to 3.4°, but poles rise from 31 to 52 per 1000 v. +5. **The size field (1b) made topology worse.** The isotropic per-vertex scale cost 3-4x poles and up to 1074 open edges for no fidelity gain at equal density. Anisotropic scale is too deep to patch (see 1b). +6. **Step 3 was not run.** Fidelity at the rim was already met, and the wing joins the body through a smooth blend (k_wing 0.12), not a crease, so there is no curve to stitch along. +7. **Her bar is half met.** + - **Met:** watertight, 1 shell, pure-quad base, crisp beveled rim with face-weighted normals. + - **Not met:** + - Flow: loops are not designed, none closes, and poles are 50-69 per 1000 v against v7's 47. + - UVs: 10 islands with no crumbs, but area distortion of 0.67-0.76 against v7's 0.076. The automatic seams join wing top and bottom wherever the snapped rim has gaps. + - Automated remeshing of an SDF composition is now a fidelity solution, not yet a topology-and-UV solution. + +## The fork + +`C:\Tools\instant-meshes-fork` is a copy of `instant-meshes-src` at 7b31608, built out of tree in `C:\Tools\instant-meshes-fork-build` with `/m:4`. Patch: `instant-meshes-field.patch` (100 lines added, 10 removed, plus run 6's glfw CMP0042 line). +- `--field `: a text file of records `x y z qx qy qz qw ox oy oz ow s`. + - Each solver vertex takes the nearest record through a uniform-grid lookup, so subdivision does not break indexing. + - q and qw feed `CQ/CQw`. o and ow feed `CO/COw`, the same constraint path `-b` uses. Then `propagateConstraints`. + - `s > 0` gives a per-vertex target edge length. +- Per-vertex scale: `MultiResolutionHierarchy::mS`, one vector per level, built from `toUpper` averages. + - `optimize_positions_impl` uses the edge scale s_ij = (s_i + s_j)/2 in the compat functor and s_i in the rounding. + - `extract_graph` uses s_ij for its edge classification and s_i for its cluster weights. + - The degenerate-triangle cleanup uses min(s). +- Upstream bug fixed: `propagateConstraints` projected `cq - v` where it meant `co - v` (`hierarchy.cpp`). +- Runtime is 2.4-5 s per run. + +## Inputs + +`tools/prep.py` and `prep2.py` produce them: +- Surface nets at h 0.005, Newton-projected: 68k vertices for the repaired program, 71k for the fitted one. +- The field is run 4's: exact-Hessian principal directions, anisotropy confidence, Ohtake ridges, and the program's crease. +- New in `tools/feat.py`: the exact crease. Gauss-Newton on {slab = 0, planform = 0}, kept where the wing is the active surface. + +Field variants: +- **field2:** curvature weight 0.5·cw where cw > 0.3, else free; ridges 0.5; crease 1. +- **creaseonly:** constraints only on the rim (2,865 vertices, orientation along the crease and position on it). +- **crease2:** field2 plus the rim constraints. + +Post-process (`tools/post.py`): output vertices within 0.35·s of the crease are snapped onto the exact crease, and the rest are Newton-projected onto f = 0 (the `_p` meshes). + +## Measurements + +All rows below are on the repaired program, flags `-d -r 4 -p 4 -c 40 -S 2`; `out/post3.log`. + +Fidelity is in % of the bbox diagonal: +- "SDF ch." is chamfer against the dense SDF zero set. +- "rim p95" is the 95th-percentile distance from 2,202 exact crease samples to the mesh, raw then projected. +- "top IoU" is the top-view silhouette against the SDF. +- Tripo is run 4's scorer. + +| mesh (IM -f) | quads | poles/1000v | open/nonmanifold | misalign to principal dir, med / p90 | rim edge misalign med / p90 | SDF ch. raw → proj | rim p95 raw → proj | top IoU proj | Tripo ch. / sil | +|---|---|---|---|---|---|---|---|---|---| +| plain f500 | 2020 | 51.4 | 0/0 | 14.5° / 35.9° | 2.9° / 9.1° | 0.046 → 0.033 | 0.644 → 0.541 | 0.9962 | 0.547 / 0.905 | +| field2 f500 | 2010 | 79.0 | 0/0 | 4.9° / 21.3° | 2.4° / 6.7° | 0.043 → 0.031 | 0.552 → 0.513 | 0.9967 | 0.547 / 0.903 | +| creaseonly f500 | 2038 | 67.2 | 0/0 | 17.3° / 37.0° | 2.3° / 6.4° | 0.036 → 0.030 | 0.334 → 0.151 | 0.9961 | 0.545 / 0.904 | +| crease2 f500 | 2086 | 92.4 | 0/0 | 4.9° / 25.0° | 2.0° / 7.2° | 0.038 → 0.030 | 0.459 → 0.302 | 0.9966 | 0.546 / 0.904 | +| plain f2000 | 7978 | **30.7** | 0/0 | 13.2° / 34.7° | 2.2° / 6.2° | 0.011 → 0.008 | 0.257 → 0.202 | 0.9990 | 0.555 / 0.902 | +| field2 f2000 | 8132 | 51.5 | 0/0 | **3.4° / 16.1°** | 2.1° / 5.3° | 0.011 → 0.008 | 0.274 → 0.201 | 0.9991 | 0.554 / 0.903 | +| creaseonly f2000 | 8052 | 41.1 | 0/0 | 15.6° / 37.6° | 1.2° / 4.1° | 0.010 → 0.008 | 0.123 → **0.020** | 0.9990 | 0.554 / 0.903 | +| crease2 f2000 | 8242 | 57.6 | 0/0 | 3.7° / 15.9° | **1.1° / 4.0°** | 0.010 → 0.008 | 0.161 → 0.045 | 0.9991 | 0.553 / 0.904 | + +### On the fitted program (beak defect present; `out/post1.log`, `post2.log`) + +The same pattern holds, plus holes: +- plain f2000: 39.8 poles, 70 open edges, rim p95 0.262 after projection. +- creaseonly f2000: 48.3 poles, rim p95 0.023. + +The open edges sit mostly on the body at the beak, for example 57 body / 28 wing / 12 pods. + +### Step 1b, size field + +Per vertex, s = min(0.35/|kmax|, inscribed radius, s0), clamped to [0.3, 1]·s0, with 0.6·s0 on the crease and a gradation limit of 0.3·|edge| (`fieldS`, `creaseS`). Fitted program: + +| mesh | quads | poles/1000v | open edges | SDF ch. proj | rim p95 proj | +|---|---|---|---|---|---| +| fieldS f500 | 13009 | 115.2 | 134 | 0.010 | 0.235 | +| creaseS f500 | 13229 | 120.7 | 178 | 0.010 | **0.014** | +| plain f3300 (equal density) | 13024 | **33.7** | 104 | 0.010 | 0.198 | +| creaseS f2000 | 26381 | 121.6 | **1074** | 0.009 | 0.214 | +| creaseG f2000 (gentle: ≥ 0.6·s0, gradation 0.15) | 15203 | 78.6 | 250 | 0.009 | 0.006 | + +- Why the size field fails: grading the lattice spacing breaks the integer-offset consistency between neighbours, which shows up as poles and holes. Fidelity at equal density is unchanged. +- Thickness never limits the wing body: it is 0.035-0.08 thick against s = 0.025 at 8k quads. The only sub-lattice thing on the wing is the rim wall at 2k quads, and the crease constraint already handles it at 8k. +- Anisotropic scale was not attempted, because the patch would have to go deep: + - The functions are `position_round_3/4`, `position_floor_3/4`, `compat_position_{extrinsic,intrinsic}_{3,4}` and their `_index` variants (field.cpp ~290-610), `optimize_positions_impl`, `extract_graph`'s classify_edges, and `extract_faces`' scale thresholds. + - Every one rounds q and n×q by a single scale. + - A (s_u, s_v) pair must also swap when the 4-RoSy matching rotates q by 90°, but `compat_orientation_*` returns rotated vectors, not the rotation index. So the pair cannot follow without changing that interface across the solver and the extractor. + - The anisotropic follow-up for that is Panozzo et al. 2014 frame fields with a non-orthogonal parametrisation. That is a different solver, not a patch. + +### Lowered and measured as run 2 did + +`tools/lower7.py` is run 4's `lower.py` with these changes: +- no Catmull-Clark and no re-projection; +- holes filled (none left on the repaired program); +- seams at part-label boundaries, where the labels are majority-smoothed and split at the mirror; +- seams on edges over 60° and on the exact crease. + +It keeps the one-segment bevel at 30° with width 0.004, then Weighted Normal by face area. Measured with run 4's `measure.py`; Tripo score from `score.py`; `out/chain7n.log`, `chain7m.log`. + +| | L7nplain (plain f2000, unprojected) | **L7ncreaseM (creaseonly f2000)** | L7nfieldM (crease2 f2000) | L7nfield500M (crease2 f500) | run 3 v7 (designed cage) | run 6 plain IM f2000 raw | +|---|---|---|---|---|---|---| +| faces q/t/n, max ngon | 8382/89/150, 6 | 8468/97/155, 6 | 8635/114/178, 6 | 2311/82/116, 6 | 9297/196/180, 6 | 8027/0/0 | +| shells / open edges | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 0 | 1 / 50 | +| poles/1000v / valence ≥ 6 | **38.4** / 0 | 50.2 / 4 | 69.0 / 5 | 114.1 / 7 | 47.1 / 0 | 36.8 | +| longest loop / closed ≥ 20 / share in loops ≥ 20 | 253 open / 2 / **0.764** | 274 open / 1 / 0.707 | 105 open / 1 / 0.593 | 94 open / 0 / 0.363 | 148 closed / 30 / 0.755 | 284 open / 0 | +| exported sliver share / excluding bevel strips | 0.1 % / 0 | 1.8 % / 0 | 6.9 % / 1 | 15.7 % / 2 | 24 % / 254 | n/a | +| dihedral > 60° / > 80° left | 28 / 9 | 16 / 3 | 11 / 4 | 22 / 4 | 546 / 316 | n/a | +| UV islands / ≥ 1 % / crumbs | 11 / 6 / 1 | 10 / 6 / 0 | 10 / 6 / 0 | 11 / 6 / 1 | 3 / 3 / 0 | n/a | +| UV area distortion mean \|log2\| / outside 0.5-2x | 1.074 / 25 % | 0.764 / 21 % | 0.668 / 17 % | 0.473 / 7 % | **0.076 / 0.8 %** | n/a | +| UV angle error / wasted | 2.07° / 67 % | 1.50° / 60 % | 1.88° / 70 % | 2.91° / 63 % | 2.61° / 59 % | n/a | +| Tripo chamfer / F@2% / sil | 0.552 / 0.950 / 0.907 | 0.553 / 0.950 / 0.905 | 0.552 / 0.951 / 0.904 | 0.546 / 0.953 / 0.904 | **0.228 / 0.990 / 0.967** | 0.498 / 0.954 / 0.900 | + +- **The rim is the visible win.** In L7nplain, IM rounds the rim off the surface-nets staircase. Few edges pass 30°, so almost nothing is beveled, and it shades faceted (sheet row 5). The crease rows (6-8) have a straight, crisp beveled rim that shades clean with face-weighted normals. The rim wall is 1-2 quads high, with some poles on it. +- **Topology.** Pure-quad bases, watertight, near-zero slivers outside the bevel strips. +- **Flow is generic field flow.** Loops are long but open and not designed. +- **The cross field's poles survive lowering.** L7nfieldM has 69 poles per 1000 v against 50 for crease-only and 38 for plain. +- **UVs are the clear failure.** + - The part-label, mirror and hard-edge seams leave each wing half as one chart, top and bottom joined through gaps in the snapped crease: only ~300 of the rim vertices land on the exact crease. + - Area distortion is 6-10x v7's. A cut along the exact crease did not change the seams. + - Fixing this needs either IM vertices placed on the whole crease (so it becomes a closed seam loop) or a distortion-driven seam method. Examples: OptCuts (Li, Kaufman, Kim, Sorkine-Hornung 2018) or Autocuts (Poranne et al. 2017); neither was tried. +- **Tripo fidelity of every lowered row is the program's.** The 0.55 is the repaired beak; it was 0.51 with the fitted beak. v7 wins on Tripo because its cage was fitted to Tripo, not to this SDF. + +## What each step bought + +| step | bought | cost | where it broke | +|---|---|---|---| +| 1 cross field (field2) | misalignment 13° → 3.4° median; rim edges 2.2° → 1.1° | poles +68 % (31 → 52 per 1000 v); singularities 62 → 120 | no fidelity gain. The unfiltered field (`field`, every vertex weighted) doubled poles. | +| 1b size field | nothing at equal density | poles 3-4x, holes | integer lattice offsets break under spacing gradients | +| 2 rim crease + projection | rim p95 0.20 → 0.02 % at 8k quads; crisp bevelable rim | poles +34 % (31 → 41) | below s ≈ rim wall height (2k quads) it only halves the error. Snapped coverage of the crease is partial (~300 vertices). | +| beak repair (program fix) | 0 holes, 0 non-manifold edges, singularities 374 → 62 | Tripo chamfer +0.05 | degenerate primitives in the program are the real "thin feature" hazard | +| 3 wing as own patch | not run | | no crease between wing and body to stitch along (smooth blend). Fidelity was already met. | + +**Next, if pursued:** +1. Make the crease constraint cover the whole curve, so the rim becomes a closed seam loop. Seed lattice vertices along the curve at s spacing as hard COw rather than nearest-vertex hints. +2. Add a validator that rejects degenerate SDF primitives: inverted or near-zero stations. +3. Use a seam method for UVs. +4. To get designed flow, refit run 3's designed cage to the SDF (run 4's refit) rather than an IM field. On this evidence that is still the only path to closed loops and v7-class UVs. + +## Files + +Root `...\scratchpad\mesh-probe\run7-im-field\`. +- **Sheets:** `sheet-main.png` and `sheet-closeup.png`. + - Main sheet columns: 3/4 front, side, 3/4 rear, wire, UV. + - Close-up columns: rim close-up, rim wire, top wire. + - Rows (in both): 1 Tripo target, 2 SDF zero set (repaired), 3 run 3 v7, 4 run 6 plain IM f2000, 5 L7nplain, 6 L7ncreaseM, 7 L7nfieldM, 8 L7nfield500M. +- **Patch:** `instant-meshes-field.patch`. +- **Tools** (`tools/`): + - `prep.py`, `prep2.py`, `feat.py`, `post.py`, `lower7.py`; + - `runim.sh`, `chain7.sh`, `bl.sh` (wait-first Blender wrapper), `build.sh`. +- **Logs:** `out/*.log`; `post1-3.log` hold the per-mesh JSON. +- **Meshes and renders:** + - `out/.obj` and the projected `_p.obj`/`.stl`; + - `out/L7*.blend`, `.metrics.json`, `.score.txt` and renders. +- **Inputs:** `in/`, for the repaired program (`params7.json`); `in_fit/` holds the fitted-program inputs. diff --git a/docs/research/mesh-probe-run7-im-field/instant-meshes-field.patch b/docs/research/mesh-probe-run7-im-field/instant-meshes-field.patch new file mode 100644 index 0000000..c5a439b --- /dev/null +++ b/docs/research/mesh-probe-run7-im-field/instant-meshes-field.patch @@ -0,0 +1,275 @@ +Submodule ext/nanogui contains modified content +Submodule ext/glfw contains modified content +diff --git a/ext/nanogui/ext/glfw/CMakeLists.txt b/ext/nanogui/ext/glfw/CMakeLists.txt +index 8b861820..be54a918 100644 +--- a/ext/nanogui/ext/glfw/CMakeLists.txt ++++ b/ext/nanogui/ext/glfw/CMakeLists.txt +@@ -7,7 +7,7 @@ cmake_minimum_required(VERSION 2.8.11.2) + if (NOT CMAKE_VERSION VERSION_LESS "3.0") + # Until all major package systems have moved to CMake 3, + # we stick with the older INSTALL_NAME_DIR mechanism +- cmake_policy(SET CMP0042 OLD) ++ cmake_policy(SET CMP0042 NEW) + endif() + + if(POLICY CMP0022) +diff --git a/src/batch.cpp b/src/batch.cpp +index b35769d..d5d8e36 100644 +--- a/src/batch.cpp ++++ b/src/batch.cpp +@@ -21,12 +21,79 @@ + #include "normal.h" + #include "extract.h" + #include "bvh.h" ++#include ++#include ++ ++/* Fork: external constraint field. Text file: a count n, then n records ++ x y z qx qy qz qw ox oy oz ow s ++ (q: tangent direction + weight in [0,1] blended into the orientation solve; ++ o: a point the lattice line along q must pass through + weight, as Instant Meshes' boundary ++ constraints use it; s: target edge length at the point, <= 0 for the global scale). ++ Each solver vertex takes the nearest record (uniform-grid lookup), so the records need not ++ match the vertex indexing after subdivision. */ ++static void apply_field_file(MultiResolutionHierarchy &mRes, const std::string &path, int rosy, int posy) { ++ std::ifstream is(path); ++ if (!is) throw std::runtime_error("Could not open field file " + path); ++ size_t n; is >> n; ++ std::vector R(n * 12); ++ for (size_t k = 0; k < n * 12; ++k) is >> R[k]; ++ if (!is) throw std::runtime_error("Field file is truncated: " + path); ++ Vector3f lo = Vector3f::Constant(1e30f), hi = Vector3f::Constant(-1e30f); ++ for (size_t r = 0; r < n; ++r) for (int k = 0; k < 3; ++k) { lo[k] = std::min(lo[k], R[r*12+k]); hi[k] = std::max(hi[k], R[r*12+k]); } ++ Float cell = std::max((hi - lo).maxCoeff() / std::cbrt((Float) n) * 1.5f, (Float) 1e-6f); ++ auto key = [&](const Vector3f &p, int dx, int dy, int dz) { ++ int64_t x = (int64_t) std::floor((p.x() - lo.x()) / cell) + dx, y = (int64_t) std::floor((p.y() - lo.y()) / cell) + dy, ++ z = (int64_t) std::floor((p.z() - lo.z()) / cell) + dz; ++ return (x * 73856093) ^ (y * 19349663) ^ (z * 83492791); ++ }; ++ std::unordered_multimap grid; ++ for (size_t r = 0; r < n; ++r) grid.emplace(key(Vector3f(R[r*12], R[r*12+1], R[r*12+2]), 0, 0, 0), (uint32_t) r); ++ mRes.clearConstraints(); ++ const MatrixXf &V = mRes.V(), &N = mRes.N(); ++ VectorXf S0(V.cols()); bool anyS = false; ++ uint32_t nq = 0, no = 0; ++ for (uint32_t i = 0; i < V.cols(); ++i) { ++ Vector3f p = V.col(i); Float best = 1e30f; int64_t br = -1; ++ for (int ring = 1; ring <= 3 && br < 0; ++ring) ++ for (int dx = -ring; dx <= ring; ++dx) for (int dy = -ring; dy <= ring; ++dy) for (int dz = -ring; dz <= ring; ++dz) { ++ auto range = grid.equal_range(key(p, dx, dy, dz)); ++ for (auto it = range.first; it != range.second; ++it) { ++ const Float *q = &R[it->second * 12]; ++ Float d = (Vector3f(q[0], q[1], q[2]) - p).squaredNorm(); ++ if (d < best) { best = d; br = it->second; } ++ } ++ } ++ if (br < 0) { S0[i] = mRes.scale(); continue; } ++ const Float *q = &R[br * 12]; ++ Vector3f n_i = N.col(i), cq(q[3], q[4], q[5]); ++ cq -= n_i * n_i.dot(cq); ++ if (q[6] > 0 && cq.squaredNorm() > 1e-12f) { ++ mRes.CQ().col(i) = cq.normalized(); mRes.CQw()[i] = std::min(q[6], (Float) 1); ++nq; ++ if (q[10] > 0) { mRes.CO().col(i) = Vector3f(q[7], q[8], q[9]); mRes.COw()[i] = std::min(q[10], (Float) 1); ++no; } ++ } ++ S0[i] = q[11] > 0 ? q[11] : mRes.scale(); anyS |= q[11] > 0; ++ } ++ mRes.propagateConstraints(rosy, posy); ++ cout << "Field file: " << n << " records, " << nq << " orientation and " << no << " position constraints"; ++ if (anyS) { ++ std::vector &S = mRes.Slevels(); S.clear(); S.push_back(S0); ++ for (int l = 0; l + 1 < mRes.levels(); ++l) { ++ const MatrixXu &up = mRes.toUpper(l); VectorXf next(up.cols()); ++ for (uint32_t i = 0; i < up.cols(); ++i) ++ next[i] = up(1, i) == INVALID ? S[l][up(0, i)] : 0.5f * (S[l][up(0, i)] + S[l][up(1, i)]); ++ S.push_back(next); ++ } ++ cout << ", per-vertex scale " << S0.minCoeff() << " .. " << S0.maxCoeff(); ++ } ++ cout << endl; ++} + + void batch_process(const std::string &input, const std::string &output, + int rosy, int posy, Float scale, int face_count, + int vertex_count, Float creaseAngle, bool extrinsic, + bool align_to_boundaries, int smooth_iter, int knn_points, +- bool pure_quad, bool deterministic) { ++ bool pure_quad, bool deterministic, ++ const std::string &field_file) { + cout << endl; + cout << "Running in batch mode:" << endl; + cout << " Input file = " << input << endl; +@@ -157,6 +224,9 @@ void batch_process(const std::string &input, const std::string &output, + mRes.propagateConstraints(rosy, posy); + } + ++ if (!field_file.empty() && !pointcloud) ++ apply_field_file(mRes, field_file, rosy, posy); ++ + if (bvh) { + bvh->setData(&mRes.F(), &mRes.V(), &mRes.N()); + } else if (smooth_iter > 0) { +diff --git a/src/batch.h b/src/batch.h +index e9d7baa..298ea5c 100644 +--- a/src/batch.h ++++ b/src/batch.h +@@ -19,4 +19,5 @@ extern void batch_process(const std::string &input, const std::string &output, + int rosy, int posy, Float scale, int face_count, + int vertex_count, Float creaseAngle, bool extrinsic, + bool align_to_boundaries, int smooth_iter, +- int knn_points, bool dominant, bool deterministic); ++ int knn_points, bool dominant, bool deterministic, ++ const std::string &field_file = ""); +diff --git a/src/extract.cpp b/src/extract.cpp +index 5a38653..34ea42f 100644 +--- a/src/extract.cpp ++++ b/src/extract.cpp +@@ -44,6 +44,8 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in + + const MatrixXf &Q = mRes.Q(), &O = mRes.O(), &N = mRes.N(), &V = mRes.V(); + const VectorXf &COw = mRes.COw(); ++ const VectorXf &S = mRes.S(); ++ const bool varS = S.size() == V.cols(); + const AdjacencyMatrix &adj = mRes.adj(); + + Timer<> timer; +@@ -75,7 +77,8 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in + std::pair shift = compat_position( + V.col(i), N.col(i), Q_rot.first, O.col(i), + V.col(j), N.col(j), Q_rot.second, O.col(j), +- scale, inv_scale, &error); ++ varS ? 0.5f * (S[i] + S[j]) : scale, ++ varS ? 2.0f / (S[i] + S[j]) : inv_scale, &error); + + Vector2i absDiff = (shift.first-shift.second).cwiseAbs(); + +@@ -280,7 +283,8 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in + continue; + uint32_t j = it->second; + +- Float weight = std::exp(-(O.col(i)-V.col(i)).squaredNorm() * inv_scale * inv_scale * 9); ++ const Float is_i = varS ? 1.0f / S[i] : inv_scale; ++ Float weight = std::exp(-(O.col(i)-V.col(i)).squaredNorm() * is_i * is_i * 9); + if (COw.size() != 0 && COw[i] != 0) { + tbb::spin_mutex::scoped_lock lock(mutex); + crease_out.insert(j); +@@ -325,7 +329,7 @@ extract_graph(const MultiResolutionHierarchy &mRes, bool extrinsic, int rosy, in + bool changed_inner; + do { + changed_inner = false; +- Float thresh = 0.3f * scale; ++ Float thresh = 0.3f * (varS ? S.minCoeff() : scale); /* fork: smallest local spacing */ + + std::vector> candidates; + for (uint32_t i_id=0; i_id static inline Float opt + const AdjacencyMatrix &adj = mRes.adj(level); + const MatrixXf &N = mRes.N(level), &Q = mRes.Q(level), &V = mRes.V(level); + const Float scale = mRes.scale(), inv_scale = 1.0f / scale; ++ const VectorXf &S = mRes.S(level); ++ const bool varS = S.size() == V.cols(); + const std::vector *phase = nullptr; + const MatrixXf &CQ = mRes.CQ(level); + const MatrixXf &CO = mRes.CO(level); +@@ -933,8 +935,9 @@ template static inline Float opt + q_j.normalize(); + #endif + ++ const Float s_ij = varS ? 0.5f * (S[i] + S[j]) : scale; + std::pair value = compat_functor( +- v_i, n_i, q_i, sum, v_j, n_j, q_j, o_j, scale, inv_scale); ++ v_i, n_i, q_i, sum, v_j, n_j, q_j, o_j, s_ij, 1.0f / s_ij); + + sum = value.first*weight_sum + value.second*weight; + weight_sum += weight; +@@ -955,7 +958,8 @@ template static inline Float opt + } + + if (weight_sum > 0) +- O.col(i) = round_functor(sum, q_i, n_i, v_i, scale, inv_scale); ++ O.col(i) = varS ? round_functor(sum, q_i, n_i, v_i, S[i], 1.0f / S[i]) ++ : round_functor(sum, q_i, n_i, v_i, scale, inv_scale); + } + }; + +diff --git a/src/hierarchy.cpp b/src/hierarchy.cpp +index 2882a67..7bca4ea 100644 +--- a/src/hierarchy.cpp ++++ b/src/hierarchy.cpp +@@ -813,7 +813,7 @@ void MultiResolutionHierarchy::propagateConstraints(int rosy, int posy) { + } + if (co != Vector3f::Zero()) { + Vector3f n = N_next.col(i), v = V_next.col(i); +- co -= n.dot(cq - v) * n; ++ co -= n.dot(co - v) * n; /* fork: upstream projected cq - v */ + } + #if 0 + cqw *= 0.5f; +diff --git a/src/hierarchy.h b/src/hierarchy.h +index 2b3c337..ee36928 100644 +--- a/src/hierarchy.h ++++ b/src/hierarchy.h +@@ -76,6 +76,9 @@ public: + inline uint32_t size(int level = 0) const { return mV[level].cols(); } + + inline Float scale() const { return mScale; } ++ /* Fork: optional per-vertex target edge length (one vector per level; empty = global scale) */ ++ inline const VectorXf &S(int level = 0) const { static const VectorXf empty; return level < (int) mS.size() ? mS[level] : empty; } ++ inline std::vector &Slevels() { return mS; } + inline void setScale(Float scale) { mScale = scale; } + inline int iterationsQ() const { return mIterationsQ; } + inline void setIterationsQ(int iterationsQ) { mIterationsQ = iterationsQ; } +@@ -125,6 +128,7 @@ public: + bool mFrozenQ, mFrozenO; + ordered_lock mMutex; + Float mScale; ++ std::vector mS; + int mIterationsQ; + int mIterationsO; + uint32_t mTotalSize; +diff --git a/src/main.cpp b/src/main.cpp +index a2f6f11..439b11f 100644 +--- a/src/main.cpp ++++ b/src/main.cpp +@@ -29,7 +29,7 @@ int main(int argc, char **argv) { + int rosy = 4, posy = 4, face_count = -1, vertex_count = -1; + uint32_t knn_points = 10, smooth_iter = 2; + Float crease_angle = -1, scale = -1; +- std::string batchOutput; ++ std::string batchOutput, fieldFile; + #if defined(__APPLE__) + bool launched_from_finder = false; + #endif +@@ -44,6 +44,12 @@ int main(int argc, char **argv) { + deterministic = true; + } else if (strcmp("--intrinsic", argv[i]) == 0 || strcmp("-i", argv[i]) == 0) { + extrinsic = false; ++ } else if (strcmp("--field", argv[i]) == 0) { ++ if (++i >= argc) { ++ cerr << "Missing field file argument!" << endl; ++ return -1; ++ } ++ fieldFile = argv[i]; + } else if (strcmp("--boundaries", argv[i]) == 0 || strcmp("-b", argv[i]) == 0) { + align_to_boundaries = true; + } else if (strcmp("--threads", argv[i]) == 0 || strcmp("-t", argv[i]) == 0) { +@@ -154,6 +160,7 @@ int main(int argc, char **argv) { + cout << " -S, --smooth Number of smoothing & ray tracing reprojection steps (default: 2)" << endl; + cout << " -D, --dominant Generate a tri/quad dominant mesh instead of a pure tri/quad mesh" << endl; + cout << " -i, --intrinsic Intrinsic mode (extrinsic is the default)" << endl; ++ cout << " --field Per-point constraints: x y z qx qy qz qw ox oy oz ow s (see batch.cpp)" << endl; + cout << " -b, --boundaries Align to boundaries (only applies when the mesh is not closed)" << endl; + cout << " -r, --rosy Specifies the orientation symmetry type (2, 4, or 6)" << endl; + cout << " -p, --posy Specifies the position symmetry type (4 or 6)" << endl; +@@ -177,7 +184,7 @@ int main(int argc, char **argv) { + batch_process(args[0], batchOutput, rosy, posy, scale, face_count, + vertex_count, crease_angle, extrinsic, + align_to_boundaries, smooth_iter, knn_points, +- !dominant, deterministic); ++ !dominant, deterministic, fieldFile); + return 0; + } catch (const std::exception &e) { + cerr << "Caught runtime error : " << e.what() << endl;