-
Notifications
You must be signed in to change notification settings - Fork 1
feat(skinning): Skinning v2 — DQS display toggle, evaluation suite, Slice-C decision (#819 Slices C/D/E) #830
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
78f87b6
8dc4b2e
527f9a0
6e82afe
2418ed0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -148,3 +148,4 @@ __pycache__/ | |
| /monk_*.png | ||
| /multiview_bake_*.png | ||
| /multiview_bake_*i.png | ||
| !docs/SKINNING_QUALITY.md | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| # Skinning quality: metrics, acceptance suite, and the Mixamo comparison protocol | ||
|
|
||
| Issue [#819](https://github.com/fernandotonon/QtMeshEditor/issues/819) (Skinning v2, Slice E). | ||
| The algorithms these measure: **GeodesicVoxel** (default, Slice A), | ||
| **InverseDistance** (#402 legacy / fallback), the Slice-B post-passes, | ||
| and the **Dual-Quaternion display toggle** (Slice D). | ||
|
|
||
| ## Metrics | ||
|
|
||
| All computed by `qtmesh skin <file> --evaluate [--json]` on the asset's | ||
| **existing** weights (whoever authored them), and by the pure-data | ||
| `SkinMetrics` / `SkinWeightsPost` APIs in the acceptance tests. | ||
|
|
||
| | Metric | Definition | Good looks like | | ||
| |---|---|---| | ||
| | Influence histogram | vertices per influence count (0–8), average, max | max ≤ 4 (hardware-skinning convention) | | ||
| | Weight smoothness | Laplacian energy: mean over mesh edges of ‖w_u − w_v‖² | lower = smoother falloffs; hard 0/1 borders score ~2 per edge | | ||
| | Bleed fraction | share of committed (vertex, bone) weights whose bone is **not geodesically local** to the vertex (GeodesicVoxelBind field) | 0 — geodesic-voxel weights are 0 by construction | | ||
| | Volume preservation | LBS-deformed / rest mesh volume under a specified joint rotation (`SkinMetrics::deformLBS` + `meshVolume`) | ≥ 0.9 at a 90° single-joint bend | | ||
|
|
||
| ## Acceptance suite (CI) | ||
|
|
||
| Procedural, hermetic fixtures in `src/SkinMetrics_test.cpp` / | ||
| `src/GeodesicVoxelBind_test.cpp` (no binary assets, no downloads): | ||
|
|
||
| - **Two-limb proximity pair** (boxes 0.5 units apart, one bone each): | ||
| geodesic-voxel bleed = **0** (asserted exactly); the inverse-distance | ||
| contrast on the same fixture must show > 0.1 bleed — if it stops | ||
| bleeding the fixture has lost its meaning. | ||
| - **U-shape bend-not-chord**: weights follow the geodesic path, never | ||
| the Euclidean chord between arm tips. | ||
| - **Cracked (non-watertight) box**: weights match the closed box — | ||
| voxel-scale hole closing. | ||
| - **90° elbow bend** (capsule tube, radius 0.5 × length 4, two bones, | ||
| smoothing 8): volume ratio ≥ **0.9**. Reference run: **0.911** | ||
| (rest 2.828, bent 2.577). For twists beyond what any weight map can | ||
| fix, use the Dual-Quaternion display toggle (Slice D). | ||
| - **Smoothing property**: Laplacian relaxation strictly reduces the | ||
| weight-field energy. | ||
|
|
||
| ## Evaluating an asset | ||
|
|
||
| ```bash | ||
| qtmesh skin character.fbx --evaluate # text report | ||
| qtmesh skin character.fbx --evaluate --json # machine-readable | ||
| qtmesh skin character.fbx --evaluate --voxel-res 128 # finer bleed field | ||
| ``` | ||
|
|
||
| Sample output (bandit.fbx, 90k verts, 119 bones, skinned by | ||
| `qtmesh skin --algo geodesic-voxel`): | ||
|
|
||
| ``` | ||
| Skin Evaluation | ||
| =============== | ||
| Vertices: 90573 | ||
| Bones: 119 | ||
| Avg influences: 3.47 | ||
| Max influences: 4 | ||
| Smoothness energy: 0.004... (lower = smoother falloffs) | ||
| Bleed fraction: 0.0055 (weights not geodesically local) | ||
| ``` | ||
|
Comment on lines
+52
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Add a language identifier to the sample-output fence. The unlabelled fence triggers MD040. Use 🧰 Tools🪛 markdownlint-cli2 (0.22.1)[warning] 52-52: Fenced code blocks should have a language specified (MD040, fenced-code-language) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| ## Mixamo comparison protocol (manual — no Adobe assets in-repo) | ||
|
|
||
| Adobe's Mixamo auto-rigger is the industry reference for one-click | ||
| skinning. The protocol compares our weights against it on the same | ||
| mesh without committing any Adobe-derived asset: | ||
|
|
||
| 1. Take a CC0 humanoid (e.g. a [Quaternius](https://quaternius.com) | ||
| character), export it **unrigged** as FBX. | ||
| 2. Upload to [mixamo.com](https://www.mixamo.com), auto-rig with the | ||
| default skeleton (no animation), download as FBX Binary, T-pose. | ||
| Keep the file local — it is Adobe-processed and must not be | ||
| committed. | ||
| 3. Rig + skin the same unrigged mesh with ours: | ||
| `qtmesh rig model.fbx --skeleton humanoid --skin -o ours.fbx` | ||
| (or `qtmesh skin` if you already have a matching skeleton). | ||
| 4. Compare: `qtmesh skin ours.fbx --compare mixamo_ref.fbx --json` | ||
| — vertices are matched by position (Mixamo re-exports reorder | ||
| them), bones by name; the report gives mean/max per-vertex weight | ||
| L1 difference and the top-10 differing bones. | ||
| 5. Also run `--evaluate` on both files and compare the metric table. | ||
|
|
||
| Interpretation: bone names differ between our templates and Mixamo's | ||
| (`mixamorig:*`) — rename with `qtmesh anim --rename` or map manually; | ||
| unmatched bone names inflate the L1 diff. The #819 target: UniRigML | ||
| within 10% of the Mixamo reference on the `--evaluate` metrics, GVB | ||
| within 20%. | ||
|
|
||
| The env-gated regression test (`SkinEvaluateTest.CompareAgainstReference`) | ||
| runs the comparison automatically when `QTMESH_SKIN_REF_FBX` (the | ||
| Mixamo file) and `QTMESH_SKIN_OURS_FBX` are set; it is skipped | ||
| otherwise, so CI stays hermetic. | ||
|
|
||
| ### Recorded run | ||
|
|
||
| _Pending a manual Mixamo export (requires an Adobe account). Record | ||
| the `--compare` and `--evaluate` outputs here when first run._ | ||
|
|
||
| ## Dual-quaternion display (Slice D) | ||
|
|
||
| `Animation mode → Skinning → Display: Linear | Dual Quaternion`, or | ||
| MCP `set_skinning_display {mode: "dual-quaternion"}`. Kills the | ||
| candy-wrapper collapse on twists that no weight map can fix. Display | ||
| only: exporters consume the unchanged vertex weights — engines | ||
| re-skin with their own blend. Entities above the RTSS bone cap (96) | ||
| stay on the default path. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2308,6 +2308,81 @@ Rectangle { | |
| : "Select a skinned mesh (with a skeleton) first." | ||
| } | ||
| } | ||
|
|
||
| // #819 Slice D: per-entity skinning display mode. | ||
| // Dual Quaternion (RTSS hardware skinning) preserves | ||
| // volume on twists — no candy-wrapper collapse. Display | ||
| // only: exported weights are unchanged. | ||
| Row { | ||
| id: skinDispRow | ||
| spacing: 6 | ||
| visible: SkinWeightsController.hasSkinnedSelection | ||
|
|
||
| // Re-read the active mode when the selection changes. | ||
| property string activeMode: SkinWeightsController.skinningDisplayMode() | ||
| Connections { | ||
| target: SkinWeightsController | ||
| function onSelectionChanged() { | ||
| skinDispRow.activeMode = | ||
| SkinWeightsController.skinningDisplayMode() | ||
| } | ||
|
Comment on lines
+2321
to
+2328
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Refresh the active mode when runtime/MCP changes it. This handler only reacts to selection changes, but 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| Text { | ||
| anchors.verticalCenter: parent.verticalCenter | ||
| text: "Display:" | ||
| color: PropertiesPanelController.textColor | ||
| font.pixelSize: 10 | ||
| opacity: 0.8 | ||
| } | ||
|
|
||
| Repeater { | ||
| model: [ | ||
| { label: "Linear", mode: "linear" }, | ||
| { label: "Dual Quaternion", mode: "dual-quaternion" } | ||
| ] | ||
|
|
||
| Rectangle { | ||
| id: dispBtn | ||
| required property var modelData | ||
| readonly property bool active: | ||
| skinDispRow.activeMode === modelData.mode | ||
| width: dispLabel.implicitWidth + 14 | ||
| height: 22 | ||
| radius: 3 | ||
| color: active ? PropertiesPanelController.highlightColor | ||
| : (dispMa.containsMouse | ||
| ? PropertiesPanelController.headerColor | ||
| : PropertiesPanelController.inputColor) | ||
| border.color: PropertiesPanelController.borderColor | ||
| border.width: 1 | ||
|
|
||
| Text { | ||
| id: dispLabel | ||
| anchors.centerIn: parent | ||
| text: dispBtn.modelData.label | ||
| color: PropertiesPanelController.textColor | ||
| font.pixelSize: 10 | ||
| } | ||
| MouseArea { | ||
| id: dispMa | ||
| anchors.fill: parent | ||
| hoverEnabled: true | ||
| cursorShape: Qt.PointingHandCursor | ||
| onClicked: { | ||
| if (SkinWeightsController.setSkinningDisplayMode( | ||
| dispBtn.modelData.mode)) | ||
| skinDispRow.activeMode = dispBtn.modelData.mode | ||
| } | ||
| ToolTip.visible: containsMouse | ||
| ToolTip.delay: 500 | ||
| ToolTip.text: dispBtn.modelData.mode === "dual-quaternion" | ||
| ? "Render with dual-quaternion hardware skinning — preserves volume on twists (no candy-wrapper). Display only: exported weights are unchanged; engines re-skin with their own blend." | ||
| : "Render with the default linear-blend skinning path." | ||
| } | ||
|
Comment on lines
+2345
to
+2382
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Make the display controls keyboard- and screen-reader-accessible. These new controls are raw 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -39,6 +39,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| #include "UvProject.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "QuadRetopo.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "SkinWeights.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "SkinEvaluate.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "AutoRig.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "ImageTo3D/MeshGenPredictor.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| #include "ImageTo3D/TripoSGPredictor.h" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -842,6 +843,13 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| " on volume-less meshes). Weights are Laplacian-smoothed + pruned\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " (--smooth-iterations, 0 = off). Mesh must have a skeleton attached.\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " --merge keeps existing weights instead of replacing them.\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " skin <file> --evaluate [--voxel-res N] [--json]\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " Skin-quality metrics on the EXISTING weights (#819): influence\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " histogram, Laplacian smoothness energy, geodesic bleed fraction.\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " skin <file> --compare <reference> [--json]\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " Per-vertex weight diff vs a reference-skinned copy of the same asset\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " (e.g. Mixamo) — vertices matched by position, bones by name. See\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " docs/SKINNING_QUALITY.md for the comparison protocol.\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " morph <file> --list [--json] List morph targets / blend shapes on a mesh. (Set/add/delete\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " land in follow-up slices once authoring is in place.)\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " nodeanim <file> --list [--json] List node-animation clips on a scene (props, doors, machinery,\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8843,6 +8851,8 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| QString algoName = QStringLiteral("geodesic-voxel"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| int voxelRes = 64; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| int smoothIterations = 3; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| bool evaluateMode = false; // #819 Slice E: metrics, no write | ||||||||||||||||||||||||||||||||||||||||||||||||||
| QString comparePath; // #819 Slice E: reference-skin diff | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| for (int i = 1; i < argc; ++i) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const QString arg = QString::fromLocal8Bit(argv[i]); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8908,6 +8918,10 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| maxDistance = v; continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (arg == "--evaluate") { evaluateMode = true; continue; } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (arg == "--compare" && i + 1 < argc) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| comparePath = QString::fromLocal8Bit(argv[++i]); continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!arg.startsWith("-") && inputPath.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| inputPath = arg; continue; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8919,11 +8933,14 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| "[--algo geodesic-voxel|inverse-distance|unirig] " | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "[--max-influences N] [--falloff F] [--max-distance D] " | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "[--voxel-res N] [--smooth-iterations N] " | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "[--skip-unweighted] [--merge] -o <out> [--json]" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| "[--skip-unweighted] [--merge] -o <out> [--json]\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " qtmesh skin <file> --evaluate [--voxel-res N] [--json]\n" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| " qtmesh skin <file> --compare <reference> [--json]" | ||||||||||||||||||||||||||||||||||||||||||||||||||
| << Qt::endl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (outputPath.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const bool analysisMode = evaluateMode || !comparePath.isEmpty(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (outputPath.isEmpty() && !analysisMode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err() << "Error: -o <output> required." << Qt::endl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 2; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8932,6 +8949,10 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| if (!fi.exists()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err() << "Error: file not found: " << inputPath << Qt::endl; return 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!comparePath.isEmpty() && !QFileInfo::exists(comparePath)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err() << "Error: reference file not found: " << comparePath << Qt::endl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!initOgreHeadless()) return 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| SentryReporter::addBreadcrumb( | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -8963,6 +8984,55 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Ogre::Entity* entity = meshEntities.first(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ── #819 Slice E: metrics / reference-comparison modes ───────── | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Both analyse the EXISTING weights; nothing is written. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (analysisMode) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| QJsonObject evalReport; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| QString evalError; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!comparePath.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| SentryReporter::addBreadcrumb(QStringLiteral("ai.assist.skin.compare"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| QString("skin --compare .%1").arg(fi.suffix())); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // Import the reference into the same scene and diff the | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // sets to find its entity. | ||||||||||||||||||||||||||||||||||||||||||||||||||
| SentryReporter::addBreadcrumb(QStringLiteral("file.import"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| QString("Importing %1").arg(QFileInfo(comparePath).absoluteFilePath())); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| MeshImporterExporter::importer( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| {QFileInfo(comparePath).absoluteFilePath()}); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| Ogre::Entity* refEntity = nullptr; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| for (Ogre::Entity* e : Manager::getSingleton()->getEntities()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (e && e->getMovableType() == "Entity" | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Check failure on line 9003 in src/CLIPipeline.cpp
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| && !meshEntities.contains(e)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| refEntity = e; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| break; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+9002
to
+9008
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Add multi-entity guard for the reference file. The reference entity is found by picking the first new 🛡️ Proposed multi-entity guard Ogre::Entity* refEntity = nullptr;
+ int newCount = 0;
for (Ogre::Entity* e : Manager::getSingleton()->getEntities()) {
if (e && e->getMovableType() == "Entity"
&& !meshEntities.contains(e)) {
- refEntity = e;
- break;
+ if (!refEntity) refEntity = e;
+ ++newCount;
}
}
+ if (newCount > 1) {
+ err() << "Error: reference " << comparePath
+ << " contains multiple mesh entities. `qtmesh skin --compare` "
+ "currently supports one entity per file."
+ << Qt::endl;
+ return 1;
+ }
if (!refEntity) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!refEntity) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err() << "Error: failed to load reference " << comparePath | ||||||||||||||||||||||||||||||||||||||||||||||||||
| << Qt::endl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| evalReport = SkinEvaluate::compare(entity, refEntity, &evalError); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| SentryReporter::addBreadcrumb(QStringLiteral("ai.assist.skin.evaluate"), | ||||||||||||||||||||||||||||||||||||||||||||||||||
| QString("skin --evaluate .%1 voxelRes=%2") | ||||||||||||||||||||||||||||||||||||||||||||||||||
| .arg(fi.suffix()).arg(voxelRes)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| evalReport = SkinEvaluate::evaluate(entity, voxelRes, &evalError); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (evalReport.isEmpty()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| err() << "Error: " << (evalError.isEmpty() | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ? QStringLiteral("evaluation failed") : evalError) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| << Qt::endl; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (jsonOutput) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| cliWrite(QString::fromUtf8( | ||||||||||||||||||||||||||||||||||||||||||||||||||
| QJsonDocument(evalReport).toJson(QJsonDocument::Indented)) + "\n"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| cliWrite(SkinEvaluate::reportToText(evalReport)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| SkinWeightsOptions opts; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| opts.maxInfluencesPerVertex = maxInfluences; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| opts.falloff = falloff; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reconcile the bleed metric definition with the sample output.
The table says GeodesicVoxel bleed is
0“by construction,” but the documented GeodesicVoxel sample reports0.0055. Clarify whether post-processing/smoothing can introduce non-local weights, then align the definition, acceptance threshold, and sample output.Also applies to: 59-60
🤖 Prompt for AI Agents