# Collision mesh generator: Godot, Unreal, Unity (`ernestx/collision-mesh-generator`) Actor

Convex collision hulls from GLB or FBX. Godot -convcolonly, Unreal UCX FBX, Unity hulls GLB. Measures how much surface still passes through.

- **URL**: https://apify.com/ernestx/collision-mesh-generator.md
- **Developed by:** [Jean-Baptiste Couche](https://apify.com/ernestx) (community)
- **Categories:** AI, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $50.00 / 1,000 collision set delivereds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js/docs.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python/docs.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## 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:

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

# Actor input Schema

## `modelUrl` (type: `string`):

Publicly reachable URL to a self-contained .glb.
## `preset` (type: `string`):

prop: balanced, the default for scatter and set dressing. architecture: few large hulls for walls and floors. organic: more hulls to follow curvature. tight: maximum fidelity, highest cost.
## `hullFit` (type: `string`):

Enclosing: hulls fully contain the surface, so nothing can pass through them. Measured on reference assets, this puts 0% of the surface outside collision, at a cost of roughly 0.8 percentage points of extra overshoot. Hugging: hulls sit on the surface for a tighter silhouette, but 4-29% of the surface ends up fractionally outside the collider, which thin or fast-moving objects can tunnel through. Use enclosing for gameplay collision, hugging only when the silhouette matters more than containment.
## `outputFormats` (type: `array`):

Which files to build. Every format carries the same hulls, so pick only what your pipeline reads. The Unreal OBJ and the Unreal FBX are two spellings of the same UCX convention, and both embed the render meshes, so shipping both roughly doubles the download for nothing. Leave this empty to get the default set: Godot GLB, hulls GLB and Unreal FBX.

## Actor input object example

```json
{
  "modelUrl": "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/WaterBottle/glTF-Binary/WaterBottle.glb",
  "preset": "prop",
  "hullFit": "enclosing",
  "outputFormats": [
    "godotGlb",
    "hullsGlb",
    "unrealFbx"
  ]
}
````

# Actor output Schema

## `collisionFiles` (type: `string`):

A ZIP under the key OUTPUT holding the formats you asked for: collision\_godot.glb (render mesh plus -convcolonly nodes), hulls.glb (one node per hull, for Unity MeshColliders and custom pipelines), collision\_ucx.fbx and collision\_ucx.obj (UCX\_<RenderMesh>\_NN). Every format carries the same hulls. Check outputFormats in the dataset record for what was built.

## `fidelityReport` (type: `string`):

One dataset item per run: hull counts per object, overshoot and surface-outside percentages measured by reloading the file that ships, the in-memory cross-check that catches a lying exporter, Unity PhysX 255-triangle compliance per object, the FBX round-trip comparison, and what each engine claim is and is not based on.

## `runInConsole` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "modelUrl": "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/WaterBottle/glTF-Binary/WaterBottle.glb"
};

// Run the Actor and wait for it to finish
const run = await client.actor("ernestx/collision-mesh-generator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "modelUrl": "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/WaterBottle/glTF-Binary/WaterBottle.glb" }

# Run the Actor and wait for it to finish
run = client.actor("ernestx/collision-mesh-generator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "modelUrl": "https://raw.githubusercontent.com/KhronosGroup/glTF-Sample-Assets/main/Models/WaterBottle/glTF-Binary/WaterBottle.glb"
}' |
apify call ernestx/collision-mesh-generator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=ernestx/collision-mesh-generator",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Collision mesh generator: Godot, Unreal, Unity",
        "description": "Convex collision hulls from GLB or FBX. Godot -convcolonly, Unreal UCX FBX, Unity hulls GLB. Measures how much surface still passes through.",
        "version": "0.1",
        "x-build-id": "bvZiXGrtXluF0KIOs"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/ernestx~collision-mesh-generator/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-ernestx-collision-mesh-generator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/ernestx~collision-mesh-generator/runs": {
            "post": {
                "operationId": "runs-sync-ernestx-collision-mesh-generator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/ernestx~collision-mesh-generator/run-sync": {
            "post": {
                "operationId": "run-sync-ernestx-collision-mesh-generator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "modelUrl"
                ],
                "properties": {
                    "modelUrl": {
                        "title": "Source model URL (GLB)",
                        "type": "string",
                        "description": "Publicly reachable URL to a self-contained .glb."
                    },
                    "preset": {
                        "title": "Hull budget preset",
                        "enum": [
                            "prop",
                            "architecture",
                            "organic",
                            "tight"
                        ],
                        "type": "string",
                        "description": "prop: balanced, the default for scatter and set dressing. architecture: few large hulls for walls and floors. organic: more hulls to follow curvature. tight: maximum fidelity, highest cost.",
                        "default": "prop"
                    },
                    "hullFit": {
                        "title": "Hull fit",
                        "enum": [
                            "enclosing",
                            "hugging"
                        ],
                        "type": "string",
                        "description": "Enclosing: hulls fully contain the surface, so nothing can pass through them. Measured on reference assets, this puts 0% of the surface outside collision, at a cost of roughly 0.8 percentage points of extra overshoot. Hugging: hulls sit on the surface for a tighter silhouette, but 4-29% of the surface ends up fractionally outside the collider, which thin or fast-moving objects can tunnel through. Use enclosing for gameplay collision, hugging only when the silhouette matters more than containment.",
                        "default": "enclosing"
                    },
                    "outputFormats": {
                        "title": "Output formats",
                        "type": "array",
                        "description": "Which files to build. Every format carries the same hulls, so pick only what your pipeline reads. The Unreal OBJ and the Unreal FBX are two spellings of the same UCX convention, and both embed the render meshes, so shipping both roughly doubles the download for nothing. Leave this empty to get the default set: Godot GLB, hulls GLB and Unreal FBX.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "godotGlb",
                                "hullsGlb",
                                "unrealFbx",
                                "unrealObj"
                            ],
                            "enumTitles": [
                                "Godot GLB - render mesh plus -convcolonly collision nodes",
                                "Hulls GLB - hull geometry only, for Unity and custom pipelines",
                                "Unreal FBX - UCX_ naming, verified through Blender",
                                "Unreal OBJ - UCX_ naming, ASCII, large"
                            ]
                        },
                        "default": [
                            "godotGlb",
                            "hullsGlb",
                            "unrealFbx"
                        ]
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
