Asset QA (review gate)
Never assume a generated or edited mesh is correct. Check it with numbers, then look at it.
Procedure
- Automated check, passing one asset's objects together:
scripts/qa_check.py -- --objects Body Head Hat --profile gameProfiles:game(default),render(lenient),print(watertight required),roblox(20k tris/mesh, watertight, no n-gons, ≤4 influences). - Fix every
errorsentry (table below), then re-run until"pass": true. Each warning must be either fixed or explicitly accepted, with the reason reported to the user. - Visual check, always:
- Live: MCP
lookwithmode="angles"on the targets, once inmaterial/solidshading and once inwireframe. Usexrayfor internals. - Headless:
scripts/review_shots.py -- --objects ... --mode random(one color per object, which makes detached pieces obvious), then--mode wirefor topology. Open the returned PNG.
- Live: MCP
- Report: tris per object, issues fixed, and remaining accepted warnings.
What each finding means → fix
| Finding | Cause | Fix |
|---|---|---|
zfighting A<->B |
two surfaces facing the same way within --eps (1 mm) that overlap |
delete the hidden duplicate, or offset ≥ 2–5 mm along the normal, or boolean/merge them |
back_to_back |
faces touching with opposite normals (boxes glued face-to-face) | delete the hidden faces (wasted tris, bake artifacts) |
floating |
island/object not touching any other part within --tol |
move it to make contact (snap), bridge it, or join and merge. If floating is intended (sparks, UI), check it separately |
enclosed |
part fully inside another, not touching (eyes, internal mechanics) | usually fine. Confirm it's intended |
inside_out_islands |
closed part with normals pointing in | mesh.normals_make_consistent(inside=False) on that part |
inconsistent_normals_edges |
neighbouring faces wound in opposite directions | recalc normals; if it persists, look for non-manifold seams |
nonmanifold_edges / wire_edges |
>2 faces per edge, or stray edges | dissolve/delete the extra faces; delete_loose |
boundary_edges |
open holes (bad for print/Roblox/bakes) | fill (fill_holes/edge_face_add) or cap |
degenerate_faces / zero_length_edges |
collapsed geometry | dissolve_degenerate(threshold=1e-4) |
duplicate_verts |
unwelded seams | remove_doubles(threshold=1e-4) (Merge by Distance); check UV seams after |
ngons |
faces with >4 sides | triangulate/quadrangulate locally; n-gons are fine only on flat hard-surface faces that never deform |
| unapplied/negative scale | object transform | transform_apply(rotation=True, scale=True); negative scale flips normals on export |
uv_maps: 0 / zero-area UV / overlap |
missing or bad unwrap | blender-retopology-uv |
| unweighted / >4 influences | skinning | blender-character-rigging (weights_check.py) |
ground_z ≠ 0 |
asset floats or sinks | move the origin to the bottom and set z = 0 (props) |
Manual checks the script can't fully judge
- Silhouette and proportions at the target camera distance. Compare with references and real-world size (
ob.dimensions). - Shading artifacts: smoothing on hard edges (needs sharp edges/weighted normals), and pinching at poles on curved surfaces.
- Intersections that look wrong (a leg clipping a seat), even though they're "attached".
- Interior/hidden geometry that wastes tris (use
xray). - Deformation: pose the rig at extremes (elbow 140°, knee 150°, shoulder raised) and look for collapsing volume.
- Material assignment: every face has the intended material, with no default grey slots.
Tuning
--epsis the z-fight distance. Raise it to 0.005 for large environment scenes, and lower it to 0.0002 for tiny jewelry.--tolis the contact tolerance for detached parts. Raise it for chunky kitbash.--no-modifierschecks the base mesh. By default the evaluated (modifier) result is checked, which is what gets exported.- Above
--max-tris(300k), the z-fight and floating checks are skipped. Check high-poly sources per part instead.