Collision mesh generator: Godot, Unreal, Unity avatar

Collision mesh generator: Godot, Unreal, Unity

Pricing

from $50.00 / 1,000 collision set delivereds

Go to Apify Store
Collision mesh generator: Godot, Unreal, Unity

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

Jean-Baptiste Couche

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

3 days ago

Last modified

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 runs
shipped 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:

suffixCollisionShape3Dshape typevisible meshes
-col16ConcavePolygonShape3D17
-convcol16ConvexPolygonShape3D17
-colonly16ConcavePolygonShape3D1
-convcolonly16ConvexPolygonShape3D1

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 onCollision shapesVisible meshes
the node name161
the mesh name017
both161

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.fbx
UCX_ names in / out all preserved, 0 lost
scale ratio 1.0
position drift 0.0 % of the diagonal
vertices 984 306984 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

FieldTypeDefault
modelUrlstringPublic URL to a self-contained .glb, .gltf or binary .fbx
presetstringpropHulls per object: prop 16, architecture 8, organic 32, tight 64
hullFitstringenclosingenclosing contains the surface; hugging is tighter and lets thin bodies tunnel
outputFormatsarraygodotGlb, hullsGlb, unrealFbxAdd 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 s
Lantern 5 400 tris, 3 objects, 48 hulls 3.1 s
ToyCar 108 000 tris, 3 objects, 48 hulls 6.3 s 14.5 MB
ABeautifulGame 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.