"My model has no collision even though it loads fine"
- Model renders correctly;
Model.Loadsucceeds;IsErroris false. - Players walk straight through it.
- Traces / raycasts against the mesh miss every time.
- Adding a
ModelCollidercomponent changes nothing — still no shapes.
A vmdl with only a RenderMeshFile compiles clean but has zero collision. A ModelCollider on that model creates no shapes at all.
"Model loads clean / IsError false" proves render, never collision.
For per-poly collision you need a PhysicsShapeList whose child is a PhysicsMeshFile (concave, static-safe) with the same filename + import_scale as the RenderMeshFile. PhysicsHullFile exists too (convex) — wrong for tapered / shelved shapes.
Detect hollow models in code before trusting a per-poly branch:
int physParts = model.Physics?.Parts?.Count ?? 0;
if (physParts == 0)
{
// fall back to BoxCollider from bounds — do not attach ModelCollider
}On disk: a compiled .vmdl_c whose PHYS block is a ~323-byte stub is hollow; real mesh physics runs tens of KB (parse the Source 2 block directory at offset 8).
Author physics in the vmdl (same mesh path + scale as render):
{
_class = "PhysicsShapeList"
children =
[
{
_class = "PhysicsMeshFile"
filename = "models/your-project/foo.obj"
import_scale = 39.37
surface_prop = "default"
collision_tags = "solid"
},
]
}Prefer hand-sized BoxColliders over ModelCollider for decorative props — per-poly collision on clutter is wasted cost and traces get noisy. Compute the box from model bounds at generation time.
For AI-delivered assets that ship render-only, always add a bounds-derived BoxCollider (or bake a physics node into a forked vmdl). See ai-generated-models.
Render and physics are separate ModelDoc graphs that happen to share a mesh file. The resource compiler will happily produce a drawable model with an empty physics part list. ModelCollider only instantiates whatever physics parts already exist on the compiled model — it does not invent a hull from the render mesh at runtime.