Reference · Updated 2552.08.07.08.17
Collision Meshes for Unreal Engine
How UE binds collision to geometry, what it demands of that geometry, and how Level 1's main floor collision is generated. Written 2026-07-31 while building main-floor.mb collision
How UE binds collision to geometry, what it demands of that geometry, and how Level 1's main floor collision is generated. Written 2026-07-31 while building main-floor.mb collision.
Companion footguns: Docs/Footguns.md → Maya / Collision Export.
Sourcing. Sections 1–5 are quoted from Epic's own documentation (linked at the bottom). Section 5a
onward is measured in this project.
⚠ Before authoring collision for anything, read §5a. For Level 1's main floor the authored hulls
came out 2.6× heavier than the render mesh they approximate, and complex collision would have been
exactly as accurate. Measure first.
#Quick answers
| Question | Answer | Detail |
|---|---|---|
| How thick must a collision hull be? | 100 cm for floors, 50 cm for walls/cover, never under 20 cm | §0 |
| What do I name it? | UCX_<RenderMeshName>_00, matching the mesh node inside the FBX | §1 |
| Can one FBX hold several meshes with collision? | No — only the first mesh's collision imports | §3 |
| Do I even need authored collision? | Often not. Measure the render mesh first | §5a |
| Which import settings? | Import Collisions ON, auto-generate OFF | §4 |
#0. How thick must a collision hull be?
The rule:
thickness ≥ 2 × (fastest speed ÷ lowest frame rate you'll tolerate)
For SystemLink: the dash covers 3.25 m in 0.2 s = 1625 cm/s. At 30 fps that is 54 cm in a single frame, so ≥ 110 cm.
| Geometry | Minimum | Why |
|---|---|---|
| Floors — anything walkable or landable | 100 cm | Covers the dash at 30 fps under the 2× rule, plus spawn offsets and network correction snaps |
| Walls, cover, ramps | 50 cm | You sweep into these and stop; you rarely start a frame inside one |
| Absolute floor for any hull | 20 cm | Below this you rely entirely on sweeps never being skipped |
Why the factor of 2 — it is depenetration, not tunnelling. Penetrate a slab of thickness T by depth d and the solver's exits are d upward or T − d downward; it takes the shorter one. So it ejects you downward the moment d exceeds T/2 — the floor spits you out underneath itself. Half the thickness has to survive the worst single-frame penetration, not the whole thickness.
Three mechanisms, only one of which cares about thickness:
- Swept queries — thickness is irrelevant. Character movement sweeps the capsule along its path and
reports the first blocking hit, so it catches a surface of any thickness, including zero. This is the dominant path for walking, dashing and ProjectileMovementComponent.
- Depenetration — the real failure mode. Applies when a capsule starts a frame already
overlapping: teleports, spawns, network corrections, skipped sweeps. This is what the rule above protects against.
- Simulated rigid bodies without CCD — classic discrete-step tunnelling. Not a factor for static
level geometry.
Thickness is free. A box is 12 triangles whether it is 5 cm or 5 m deep — no cost in hull count, memory or query time. This is only ever a correctness knob, never a budget one, so never be stingy.
Better habit for floors: don't think in thickness at all. Extend every floor hull down to one shared base plane beneath the level. The question disappears, and so do the gaps between separately-thickened stacked hulls where a player can wedge.
What thickness cannot save you from: a long fall reaches speeds that beat any sane slab. That is what Kill Z is for — do not chase it with thicker collision.
Chaos margin, for reference (UE 5.7 Chaos/PBDRigidsEvolutionGBF.h): CollisionMarginFraction = 0.05 (5% of a shape's smallest extent), CollisionMarginMax = 10.0 cm, CollisionCullDistance = 3.0 cm. The core shape is shrunk by the margin and it is added back at detection time — "a shape of the same size but with rounded corners". Because it is a fraction of size, a thin hull gets a proportionally tiny margin rather than degenerating. These are solver settings; the character sweep path is separate, so do not treat them as governing player movement.
#1. Collision binds BY NAME — nothing else
The importer associates a collision mesh with a render mesh purely by name. There is no flag, no parent-child relationship, no layer.
| Prefix | Type | Example |
|---|---|---|
UBX_ | Box | UBX_Tree_01 |
USP_ | Sphere | USP_Tree_01 |
UCP_ | Capsule | UCP_Tree_01 |
UCX_ | Convex | UCX_Tree_01 |
- The part after the prefix must exactly match the render mesh's node name inside the FBX — not the
UE asset name, not the file name. Rename the render mesh later and every hull silently detaches.
- Multiple hulls on one mesh take numeric suffixes:
UCX_Tree_01_00,UCX_Tree_01_01, …
- Collision must be exported in the same FBX as its render mesh. On import UE finds it, strips it
out of the render geometry, and converts it into the collision model.
#2. What each primitive type demands
UBX_box — "A Box must be created using a regular rectangular 3D object. You cannot move the
vertices around or deform it in any way to make it something other than a rectangular prism, or else it will not work."
USP_sphere — 8 segments is plenty; it is converted into a true sphere. Don't move vertices.
UCP_capsule — a cylinder capped with hemispheres, 8 segments is fine. Don't move vertices.
UCX_convex — "A Convex object can be any completely closed convex 3D shape." UE does not
decompose a concave shape for you: "Breaking up a non-convex mesh into convex primitives is a complex operation, and can give unpredictable results."
Boxes and spheres do not work on non-uniformly scaled Static Meshes. Epic's own recommendation: "In general you probably want to create UCX primitives." This project uses UCX_ for everything, including hulls that happen to be perfect boxes — one code path, no mixed-prefix risk.
A non-planar face is not strictly convex either. Fan it into triangles. Chaos will build a hull
from the point cloud regardless, but then the hull is not the shape you authored.
#3. The limitation that shapes the whole pipeline
**"Currently, when importing multiple meshes with custom collision in a single file, only the first
mesh's collision will be imported."**
One FBX holding 21 render meshes, each with correct UCX hulls, yields one mesh with collision and 20 silently collisionless. Nothing warns you. Three ways out:
- Combine into a single render mesh and give it many hulls — what Level 1's main floor does.
- One FBX per render mesh.
- Merge in the DCC before export.
#4. Import settings
- Interchange → Assets → Collision → Import Collisions must be ON.
- Auto-generate collision OFF, or the importer's own guess can replace authored hulls.
- Verify after import: the static mesh editor's collision count should match the hull count exported.
#5. UE's own collision tools (when you don't author it)
From the Static Mesh Editor's Collision menu:
- Sphere / Capsule / Box simplified collision — good "where having precise collision does not matter".
- K-DOP generators — 10-DOP (box, 4 edges beveled), 18-DOP (all edges beveled), 26-DOP (all edges
and corners beveled).
- Auto Convex Collision — convex decomposition with Hull Count, Max Hull Verts, Hull Precision.
"The higher these values, the more precise your collision mesh will be, but also the more complex."
Author collision by hand when the shape is gameplay-critical — floors you walk, ramps you climb, cover you hide behind. Auto convex decomposition is unpredictable on large level geometry and gives no control over where a seam lands.
Simple vs complex. Simple collision is the primitives and convex hulls described here — cheap, and what character movement and most gameplay sweeps use. Complex collision is the render mesh's own triangles, exact by definition. Use Complex Collision As Simple on a static mesh makes the triangle mesh serve simple queries too, so a capsule collides with the art directly and no hulls are needed.
The standard warning against that setting is that it means colliding against the entire mesh — ruinous at production poly counts, with many objects or many characters. That warning is about scale, and does not transfer to light geometry. See §5a: for a 436-triangle blockout it is the wrong reason to reject it.
#5a. Do you actually need authored collision? (read this first)
Author hulls when the render mesh is too heavy to collide against, or when the art cannot supply the solidity gameplay needs. Not by reflex. Measure before building anything:
| Level 1 main floor | |
|---|---|
| Render mesh | 210 faces → 436 triangles |
| Authored collision | 95 hulls → 1140 triangles |
| Ratio | collision is 2.6× the mesh it approximates |
On this mesh, Use Complex Collision As Simple would have been exactly as accurate and cheaper: no 3.2 cm level merge, no one-cell overhang at diagonal edges, none of the holes that had to be hunted down, no pipeline to re-run, and no risk of collision drifting from the art.
The one thing complex collision could not have fixed here: the source is an open shell, and the ground plane is literally zero-thickness (polySurfaceShape2 — 9 faces, every vertex at y = 0.0). Complex collision on a zero-thickness sheet is a floor with nothing beneath it. Any penetration — a dash covering 3.25 m in 0.2 s, a spawn a few centimetres low, a client correction — puts a player through it with only the Kill Z failsafe to catch them. The authored hulls make that floor a 332 cm solid, a property the art does not have and complex collision cannot invent. The 21 pieces also carry open borders at their seams, which a capsule can catch on; solids have no borders.
So the hulls here were not needed for shape — they were needed for solidity. Had the source been closed solids with real thickness, complex-as-simple would have been the better engineering call.
Probably the better fix is upstream: give the floor real thickness and close it in Maya, then use complex collision and retire this pipeline. That addresses the root cause instead of working around it, and finished art will be solid geometry anyway.
Settle it empirically before relying on either — import SM_MainFloor.fbx twice, once with the hulls and once with Use Complex Collision As Simple; dash at the ground plane, spawn on it, and walk the plate seams and pit edge in both. If nothing tunnels through the paper floor, complex wins on every other axis and the hulls are dead weight.
#6. Rules for good hulls
- Closed. Every edge shared by exactly two faces. An open shell is not a solid.
- Convex. Every vertex on or behind every face plane.
- Non-overlapping. Overlapping convex bodies interpenetrate and jitter in Chaos. Community reports
are consistent that many overlapping convex shapes degrade performance badly.
- As few as accuracy allows. Aim for the lowest hull and vertex count that still holds the shape.
- No holes. A gap in floor collision is a fall-through, and it is invisible until someone falls.
#7. Case study — Level1/scenes/main-floor.mb
#What the source actually is
Surveyed headless with mayapy; Y-up, centimetres, one group main_floor:
- 21
polySurfacepieces, 210 faces, all at identity transform, spanning 65.8 m × 57.0 m.
- Every piece is an open shell, not a solid — top face plus a skirt, no underside. Collision had to
be new geometry; duplicating and renaming the shells would have imported nothing.
- Three flat levels:
y = 3.19(58 faces, the plates) ·y = 0.0(9 faces, the 2490 m² ground
plane) · y = −276.65 (19 faces, the basement floor). The ground plane has a hole cut for the pit.
- Six sloped faces: four real ramps at 16.3° rising the full 276 cm from basement to ground, and
two 5.6 cm lips that are not ramps at all.
#The method: a column stack, not bounding boxes
- Classify every face by its Newell normal — flat, ramp, or lip.
- Cut a grid at every vertex X and Z coordinate.
- For each cell, collect the surfaces covering it; keep the top one, then walk down skipping anything
within LEVEL_MERGE_TOLERANCE of the surface above it.
- Extrude each kept surface down to the next surface beneath it in that same cell — that is what
produces the pit's vertical walls for free — or to a global floor when nothing is below.
- Merge cells into maximal rectangles per (top, bottom) pair.
- Ramps become sloped convex prisms; lips fold into the flat level at their top.
Boxes are disjoint by construction because they come from one shared cell grid. Only wedges could overlap them, so ramp footprints are cut out of the grid entirely.
#Result
hulls=95 (91 boxes + 4 ramp wedges) triangles=1140
overlaps=0 coverage=1479/1479 points VALIDATION PASSED
#The deliberate 3.2 cm simplification
The plates sit 3.2 cm proud of the ground plane. Expressing that step costs 282 hulls instead of 91 — 3.5× the collision to represent a difference no player can feel. LEVEL_MERGE_TOLERANCE (5.0 cm) in the builder is the single edit that buys the exact-shape version.
#8. What bit us, and the symptom to recognise
MItMeshPolygon.getNormal()hard-crashes Maya on this scene. Process gone, no traceback, a
[Recovered] file in %TEMP%. The scene carries invalid components that kill the polygon normal iterator; MItMeshEdge walks are the same family. Derive normals with Newell's method from MFnMesh.getPoints() + getVertices(). This is why the earlier attempt fell back to bounding boxes and lost the shape — it is the data, not the script.
- A grid tested at cell CENTRES silently drops cells that a diagonal edge only partly covers. That
is a hole in the floor. It cost 6 of 1479 sampled points here, all at diagonal edges of the ground plane, and nothing but a coverage test would have found it. Include a cell the surface merely touches: a one-cell overhang at a diagonal is invisible, a hole is not.
- Small "slopes" are lips, not ramps. Two 5.6 cm faces with diagonal plan edges produced wedges that
overlapped the boxes beside them, because an axis-aligned grid cannot cut around a diagonal. Anything rising less than LIP_RISE_MAX folds into the flat level instead.
- Trust the validator over the viewport. Every one of the above passed a look-check and failed a
scripted check.
#9. The scripts
All three run headless — & "C:\Program Files\Autodesk\Maya2026\bin\mayapy.exe" <script>.
| Script | Does |
|---|---|
Tools/maya_build_main_floor_collision.py | Generates the hulls into the scene. Idempotent — clears and rebuilds. Keeps one pristine copy at main-floor.pre-collision.mb. |
Tools/maya_validate_main_floor_collision.py | Naming contiguity, closed, convex, pairwise non-overlap (SAT), and a coverage sweep over sampled walkable points. Reports where coverage fails. |
Tools/maya_export_main_floor.py | Combines duplicates of the 21 pieces into one render mesh SM_MainFloor, exports Z-up FBX with the hulls, re-imports to verify the names survived. Never modifies or saves the authored scene. |
Output: C:\3D-DEV\HaloProject\Maya\Level1\FBX\SM_MainFloor.fbx — SM_MainFloor + UCX_SM_MainFloor_00..94.
**SourceArt and the Maya tree are gitignored — the scripts are the only version history the art
has.** Edit the generator and re-run it; never hand-mutate a scene you cannot recover.
Close the file in the Maya GUI before running the builder. A GUI session holding an older copy
will overwrite the generated collision the moment it saves.
#10. Not done
- The §5a question is open and should be settled first: hulls vs
Use Complex Collision As Simple.
The answer decides whether this pipeline is kept or retired.
- Nothing is PIE-proven. The FBX has not been imported into UE.
- Only
main-floor.mbhas collision. The rest of Level 1 —layerFortMain,layerMainWallsUE,
layerMainTop, layerBasement — lives in other scenes with none.
Tools/maya_create_main_floor_collision.pyis the superseded bounding-box version. Delete it or it
will eventually be run by mistake.
- The generator is written against this scene's conventions (Y-up, one source group, up-facing floors).
Reusing it for walls or props needs the classification step revisited — a wall has no up-facing surface.
#Sources
- FBX Static Mesh Pipeline in Unreal Engine — naming contract, per-type rules, the multi-mesh limitation
- Setting Up Collisions With Static Meshes — K-DOP, Auto Convex Collision, simplified primitives