Collision mesh generator: Godot, Unreal, Unity
Pricing
from $50.00 / 1,000 collision set delivereds
Collision mesh generator: Godot, Unreal, Unity
Convex collision hulls from GLB or FBX. Godot -convcolonly, Unreal UCX FBX, Unity hulls GLB. Measures how much surface still passes through.
Pricing
from $50.00 / 1,000 collision set delivereds
Rating
0.0
(0)
Developer
Jean-Baptiste Couche
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
Generate collision meshes, then measure the file that ships
Point it at a .glb, .gltf or .fbx and get back collision your engine
already reads: Godot -convcolonly nodes, Unreal UCX_ meshes, one node
per hull for Unity MeshColliders — plus how much of the surface a body could
still pass through, measured on the file inside the ZIP rather than on the
hulls in memory.
V-HACD, per object rather than per file. Headless Blender 4.5 LTS on the FBX legs. The source format is detected from the file's bytes, not its extension. No UI, no login, no seat: one HTTP call per asset, or the same JSON handed to an agent over MCP.
Running V-HACD is the easy part
Every wrapper does that. What almost nobody does is re-open the file they just wrote.
Fidelity is normally measured against the hulls held in RAM, moments before
an exporter touches them. That is the wrong end of the pipeline: if the GLB
writer drops a node or resolves an instance transform differently on the way
out, the file disagrees with the report and nothing notices. So hulls.glb
is reloaded from disk on every job, placed into world space and re-measured
against the source surface, and that is the number the report leads
with. The in-memory hulls are measured on the same sample points as a
cross-check:
7 assets × 2 fit modes, plus 3 deployed runsshipped p95 overshoot − in-memory p95 overshoot delta 0.0000, every time
Past 0.25 percentage points the verdict becomes REVIEW_EXPORT, ZIP still
attached — a suspect measurement is a reason to look, not to withhold a
delivery. A check that passes either way protects nothing, so the Docker
build hands that same comparison a deliberately mismatched space and refuses
to finish unless it fails.
Godot collision suffixes: -convcol is not -convcolonly
Measured against Godot 4.7.1's own importer rather than read off the documentation page:
| suffix | CollisionShape3D | shape type | visible meshes |
|---|---|---|---|
-col | 16 | ConcavePolygonShape3D | 17 |
-convcol | 16 | ConvexPolygonShape3D | 17 |
-colonly | 16 | ConcavePolygonShape3D | 1 |
-convcolonly | 16 | ConvexPolygonShape3D | 1 |
All four produce 16 CollisionShape3D nodes, so all four look like they
worked. -convcol also leaves every hull as a visible MeshInstance3D:
sixteen faceted blobs drawn over the model, no error anywhere.
Where the suffix is written matters just as much as which suffix it is. A glTF node carries a name, and so does the mesh it points at — different strings, and Blender writes the mesh datablock name into the second one. Same importer, same suffix, one variable:
-convcolonly written on | Collision shapes | Visible meshes |
|---|---|---|
| the node name | 16 | 1 |
| the mesh name | 0 | 17 |
| both | 16 | 1 |
Suffix the mesh name alone and Godot imports ordinary visible geometry with no collision and no warning. This Actor writes it onto both, so the file does not depend on which of the two an importer reads.
Delivered files then went back through that importer on three assets — 1, 3
and 49 instances, up to 769 placed hulls. Every collision node came in
as a ConvexPolygonShape3D, zero null shapes, one visible mesh per render
node, collision AABB enclosing the mesh AABB, centre offset at most
0.22 % of the span.
A scene is not one triangle soup
Hand a whole GLB to a decomposer as one mesh and a chair and a table in the same file get hulls spanning the gap between them. The player collides with empty air halfway across the room, and the hull count looks fine.
Every mesh node is decomposed on its own, in local space, and nodes sharing geometry are decomposed once and placed with each instance transform: a 49-piece chess set is 15 decompositions, not 49, and 769 placed hulls in the file. Outputs are named after the node, not the mesh datablock, and duplicate names are suffixed and counted rather than left for a glTF writer to resolve by quietly overwriting one of them.
Auto Convex Collision is free, in the editor, one mesh at a time
Unreal ships the same algorithm behind a dialog, and several Blender addons wrap it too. For a hero prop that is the right tool. None of them batch — an editor session, a dialog and an artist, per static mesh.
This one is an endpoint. A rejection is a SUCCEEDED run carrying
status: rejected and a reason code — UNRECOGNISED_FORMAT,
FBX_IMPORT_FAILED, UNSUITABLE_NO_HULLS — because a failed run hands an
agent a stack trace instead of the report, and the report is the product.
Everything past those ships a ZIP and a verdict: PASS at 2 % overshoot p95
and 2 % of the surface outside collision or better, PASS_WITH_WARNINGS to
6 % overshoot, REVIEW_FIDELITY above it. Gate the ingest on PASS, queue
the rest for a human. Nobody opens a viewer to find out which is which.
PASS_WITH_WARNINGS is decided on overshoot alone, so 1 % overshoot with
5 % of the surface outside collision reads as a warning rather than a
review. Gate looser than PASS and you should read
pctSurfaceOutsideCollision yourself.
Enclosing hulls by default, because tunnelling is a bug report
V-HACD shrink-wraps its hulls onto the source surface by default, which
leaves part of that surface fractionally outside the collision. Measured on
three reference assets: 28.6 %, 18.3 % and 4.4 % of sampled surface points
fell outside, and every one of them dropped to exactly 0 % with
shrink-wrap off, for about 0.8 percentage points of extra overshoot. Across
the seven-asset fidelity set, enclosing reads 0 % outside on 7 of 7.
A hull 0.8 % too fat is invisible to the player. A surface the player can
pass through is a bug report. hugging is opt-in, for assets where the
silhouette matters more than containment.
One degenerate hull sinks the whole asset, so none ship
MuJoCo refuses a coplanar hull and fails the entire model when it meets
one — not that hull, the model. V-HACD emits them routinely. So every hull
is rebuilt through qhull first, which guarantees convexity and outward
winding, then rejected if it has fewer than four vertices, no volume at the
object's own scale, or vertices all in one plane. Counts and reasons ship in
perObject[].rejectReasons, so an asset that lost half its hulls tells you
instead of shipping quietly.
The rebuild is not cosmetic: taking V-HACD's own face winding at face value gave hulls whose volumes summed to 4 % of the source, because trimesh computes a signed volume and inconsistent winding collapses it.
UCX collision naming is verified to the FBX bytes, and no further
Unreal binds collision to a render mesh by name: UCX_<RenderMesh>_NN. The
FBX goes out through headless Blender, which re-imports the file it just
wrote and re-measures it — names surviving a round trip prove the convention
held and say nothing about the geometry:
ABeautifulGame, 49 instances → collision_ucx.fbxUCX_ names in / out all preserved, 0 lostscale ratio 1.0position drift 0.0 % of the diagonalvertices 984 306 → 984 306
FBX carries its own unit scale, and a mismatch between writer and reader is the classic way a collision mesh arrives perfectly named and 100× too large.
The names come from the node, not the geometry datablock. Blender writes its
mesh-DATA name into that second slot, so an object called SM_Crate whose
mesh data is called Crate.001 leaves a naive exporter as
UCX_Crate_001_00, which binds to nothing: Unreal looks for
UCX_SM_Crate_00. A build guard found that one, where a cube named Widget
arrived through the FBX input leg as Cube.
Unreal's own importer has never been run against these files. There is
no UE5 install behind this Actor. Naming and geometry are conformant and
measured up to the bytes; what UE5 does with them is not measured here,
unrealObj ships verified: false, and the unreal field of every report
says so. Import one asset by hand before you batch five hundred.
Unity has no convex decomposition, and a 255-triangle ceiling
MeshCollider with convex = true gives you exactly one hull for the whole
mesh. Multi-hull collision means compound colliders you assemble yourself, a
third-party tool, or hand authoring — and Unity's convex mesh colliders are
capped at 255 triangles each.
hulls.glb carries one node per convex hull, transforms applied, no render
meshes and no naming magic: wire them to MeshColliders in a short editor
script. Triangles per hull are checked against that limit per object and
reported as unityPhysXCompliant, so you find out before the cooker does.
Reported, not enforced: nothing is simplified on your behalf and the verdict
ignores it. Unity was never launched against these files; the structure and
the transforms are what is verified.
Input
| Field | Type | Default | |
|---|---|---|---|
modelUrl | string | — | Public URL to a self-contained .glb, .gltf or binary .fbx |
preset | string | prop | Hulls per object: prop 16, architecture 8, organic 32, tight 64 |
hullFit | string | enclosing | enclosing contains the surface; hugging is tighter and lets thin bodies tunnel |
outputFormats | array | godotGlb, hullsGlb, unrealFbx | Add unrealObj for the same UCX naming in ASCII OBJ |
The hull budget is per object, not per file: a 15-geometry scene on prop
can reach 240 unique hulls before instancing multiplies the placements, and
nothing caps the scene total. Meshes over 100 000 triangles are decimated
first, reported as preDecimatedTo, so hulls on very dense geometry fit a
proxy of it. unrealObj is off by default because it says exactly what the
FBX says: on the chess set, 62 MB of ASCII OBJ beside 81 MB of FBX.
modelUrl is the only required field. The measurement rig and the diagnostic
mode that produced the numbers on this page are still in the image, but they
need an environment variable the owner sets, so an empty input returns
NO_MODEL_URL instead of quietly spending two minutes on a 20-asset
benchmark.
preset changes the hull budget, not the bill. On the four assets measured
both ways, tight — 64 hulls at 400 000 voxel resolution against prop's 16
at 200 000 — cost between 1.3× and 1.5× the decomposition time, never the 4×
the parameters suggest. V-HACD's cost is bound by voxelisation, and raising
the hull count mostly loads the merge stage. The slowest single mesh measured
on tight took 4.0 s.
What comes back
A ZIP under OUTPUT in the run's key-value store — always a ZIP, with
outputKind saying so — holding whichever of collision_godot.glb,
hulls.glb, collision_ucx.fbx and collision_ucx.obj you asked for. The
dataset record carries the verdict, the deliveredFileCheck block with its
in-memory cross-check, the per-format sub-reports including the FBX
round-trip numbers, and a perObject array: source triangles, instances,
hulls kept, hulls rejected and why, per-hull triangle maxima.
Rigged or animated sources. The three files that embed a render mesh
re-export it through trimesh: geometry and basic materials, no skinning, no
morph targets, no animation, no hierarchy. Treat them as collision plus a
static proxy, not as a replacement for your source. hulls.glb carries no
render mesh and sidesteps it.
Billing
Two events. One collision-completed per run that delivers a collision set,
and one mesh-decomposed per unique mesh that went through V-HACD.
The second one exists because the first one alone would misprice this. Cost
scales with unique meshes, and nothing caps that number: a 200-object scene
is 200 decompositions and a proportionally larger download. Charging per
unique mesh also states the instancing behaviour where you can check it —
geometry shared across placements is decomposed once and charged once, so a
49-piece chess set built from 15 unique meshes bills 15, not 49. The count is
in chargedEvents on every record, next to the uniqueGeometries it came
from.
The budget check runs before decomposition, not after. If a run's
maxTotalChargeUsd cannot cover the scene, it stops and tells you how many
unique meshes the budget does cover, rather than spending two minutes of
compute and then discovering it cannot bill.
The three business rejections deliver nothing and charge nothing — no base
event, no per-mesh events. A downgraded verdict is not a rejection:
PASS_WITH_WARNINGS, REVIEW_FIDELITY and REVIEW_EXPORT all ship the ZIP
and all charge in full, because the verdict is the product.
Verified against
The Docker build refuses to finish unless seven checks pass in its log. A
unit box must decompose back to a hull volume of 1.0 ± 0.05 — the canary
that caught a real bug, where handing V-HACD its faces as a plain (N,3)
array returned 64 collinear zero-volume hulls for a cube. A ray fired down a
torus hole must miss, which is what proves the concavity survived.
MuJoCo must load the hulls as collision geoms and answer raycasts — at build
time, on reference shapes, not on your asset. A 12-instance grid must
round-trip with an exact placed-hull count, and both FBX directions must
survive Blender with names, scale and vertex count intact. An operator
rename in a future Blender breaks the build here, not a run on your side.
Eight deliberately malformed inputs go through the same pipeline: open box, flat plane, interpenetrating boxes, zero-area faces, thin sliver, 50 km from the origin, sub-millimetre, coincident duplicates. Zero crashes, and honest numbers on all eight.
The verification has also been wrong once, on record. It used to bound its
cost by keeping the first 400 hulls and dropping the rest; on the chess set
that discarded 369 of 769 and reported 14.2 % of the surface outside
collision on a delivery that measured 0.0 % uncapped. REVIEW_FIDELITY
stamped on good work. The rule since: the point count adapts, the hull set
never shrinks.
Speed
WaterBottle 4 500 tris, 1 object 2.0 sLantern 5 400 tris, 3 objects, 48 hulls 3.1 sToyCar 108 000 tris, 3 objects, 48 hulls 6.3 s 14.5 MBABeautifulGame 574 000 tris, 15 geometries, 49 instances 126.5 s 94 MB
Deployed runs at the default 4096 MB, all on prop. Compute scales with
unique geometries; the download scales with placements. Instancing saves the
decomposition, not the bytes — every output flattens instances into
per-instance copies, which is where the chess set's 94 MB comes from.
Limits worth knowing
A download that fails — 404, DNS, TLS, or longer than 180 s — fails the run rather than rejecting it: no dataset record, no reason code. That is the one place the rejections-succeed contract does not hold.
Node names are rewritten: sanitised to [A-Za-z0-9_-], truncated to 48
characters, suffixed per instance. UCX binding is self-consistent inside the
delivered file, but pairing your original mesh with only the collision from
here can leave the two out of step.
Neither shipped metric detects a hull that has filled an interior cavity;
that needs the raycast method, which runs in diagnostics, not on your asset.
A .gltf with an external .bin sniffs correctly and then loads with
missing buffers, and ASCII FBX is refused with FBX_IMPORT_FAILED — only
self-contained files work. Godot verification is against 4.7.1 and no other
version.
Only Khronos sample assets and synthetic stress cases have been through this. No studio production asset has.