Docs / Cookbooks

The Meshy → meshcheck loop

Generate with Meshy, validate with meshcheck, read the failing checks, regenerate. A closed loop an agent can run unattended.

This is a pattern for wiring a text-to-3D generator to a deterministic QA gate. The shape is the same for any generator; only the generation call changes. The point: an agent decides whether to accept, retry, or escalate a model from the report alone — no human in the loop.

The report blocks below are real meshcheck output. The generator step stands in for your Meshy call — swap in your own GLB and the meshcheck calls are identical. Each report is reproducible against the named corpus fixture with meshcheck-corpus check <file> --profile web (see the reproduce lines).

The loop

  1. Generate. Ask Meshy for an asset and download the GLB to disk.
  2. Validate. validate_model({ path, profile: "web" }). This runs checks_only — the fast, 1-credit path that skips the renderer. Add mode: "full" when you also want screenshots.
  3. Branch on the verdict.
    • pass — accept the asset.
    • warn — accept, but record the warnings for review.
    • fail — read the failing checks, translate them into generation constraints, and retry.
  4. Inspect (optional). Once the measurable checks pass, ask /inspect whether the model actually looks like what you asked for. This is the one non-deterministic step; keep it separate from the gate.
  5. Stop on pass, or after a retry budget (say, three attempts), then escalate to a human.

What a failing report tells the agent

meshcheck failures are structured, so the agent maps a check ID to a concrete next action instead of guessing:

Failing check What it measured The retry constraint
PERF-001 Triangle count over the profile’s max_tris Lower the target poly count, or enable decimation
MAT-001 Texture dimension over max_texture_dim Request a smaller texture resolution
GEO-003 Flipped winding past the threshold Re-export with consistent winding / recompute normals
UV-001 Textured mesh missing TEXCOORD_0 Regenerate with UVs, or drop the texture

Worked example: a budget failure

Attempt one comes back over the web triangle budget. This is real output for the corpus fixture Box__explode_tris.glb (an over-tessellated export standing in for a raw generation):

validate_model({ path: "./attempt-1.glb", profile: "web" })
→ verdict: "fail"
  PERF-001  fail   196,608 triangles exceeds profile budget of 25,000
  PERF-002  warn   196,620 vertices exceeds profile budget of 40,000
  (all other checks pass or skip)
# reproduce
meshcheck-corpus check corpus/broken/Box__explode_tris.glb --profile web

The agent reads PERF-001, sets a triangle target (or enables decimation), and regenerates. Attempt two is within budget — real output for the clean Box.glb:

validate_model({ path: "./attempt-2.glb", profile: "web" })
→ verdict: "pass"
  summary: 0 errors, 0 warnings, 9 info, 14 skipped
  PERF-001  pass
# reproduce
meshcheck-corpus check corpus/clean/Box.glb --profile web

(The 14 skipped are the render checks and any UV/material checks that don’t apply — this run was checks_only, so the RND-* family reports skipped rather than running the renderer.)

A texture-budget failure looks the same. Real output for Duck__upsize_textures.glb on the web profile:

validate_model({ path: "./textured.glb", profile: "web" })
→ verdict: "fail"
  MAT-001  fail   largest texture is 8192px, exceeding the profile budget of 2048px
  MAT-002  warn   estimated texture memory 357,019,156 bytes exceeds the profile budget of 33,554,432 bytes

The agent’s retry constraint is unambiguous: request a texture at or under 2048px.

Add a semantic check with /inspect

The checks above are deterministic measurements — they never judge whether the model looks right. Once the budget checks pass, /inspect closes that gap by asking a vision model, over the standard render angles, whether the asset matches your prompt:

inspect_model({
  path: "./attempt-2.glb",
  check: "VIS-001",
  prompt: "low-poly wooden crate, game prop"
})

The response is schema-constrained and flagged non-deterministic (this is the only such surface — it costs 5 credits and carries "deterministic": false and the model version):

{
  "inspect_id": "ins_...",
  "deterministic": false,
  "model": "claude-haiku-4-5",
  "answer": {
    "match": true,
    "confidence": "high",
    "mismatches": [],
    "notes": "..."
  }
}

Confidence is categorical (low | medium | high), never a number — numbers would imply a precision a vision opinion doesn’t have. Keep the measurable and the semantic judgments separate: the first is deterministic and gates the pipeline; the second is advisory.

Notes

  • The full report (mode: "full") carries the renderer’s screenshots — six standard angles on signed, expiring URLs. An agent can hand those straight to /inspect, or reuse them with render_id.
  • Because /validate is deterministic, cache the report by the model’s sha256 and skip re-validating an unchanged asset.