Quick answer: You are comparing AABBs that live in different coordinate systems — one in local space, one in world space. Multiply each local AABB by its node’s global_transform, normalize with .abs(), then compare. The boxes will start matching reality.

You write what should be a simple overlap test: get the AABB of two MeshInstance3Ds and ask whether they intersect. The boxes obviously overlap on screen, but aabb_a.intersects(aabb_b) returns false. You print both AABBs and the numbers look reasonable. The bug is real and the fix is one line, but you have to know which line.

The Symptom

Two common patterns produce a wrong intersection result. The first is when one AABB is in local space and the other is in world space — this happens implicitly when one of your nodes has a non-identity transform. The second is when an AABB has been constructed with a negative size component, usually because of a Vector3 subtraction or a transform with negative scale.

You will see things like:

What Causes This

Cause 1: Local vs. global space mismatch.

VisualInstance3D.get_aabb() returns the AABB in local coordinates — relative to the node’s own origin, before any parent transforms are applied. A child of a node positioned at (100, 0, 0) with a local AABB at (-1, -1, -1) to (1, 1, 1) is actually at (99, -1, -1) to (101, 1, 1) in the world. If you compare the local AABBs of two nodes, you are comparing them as if both started at the world origin.

Cause 2: Negative size from inverted scale.

If you build an AABB by subtracting two corners (AABB(corner_a, corner_b - corner_a)) and corner_b happens to be on the wrong side of corner_a, the size component contains negative values. intersects() does not normalize internally, so the resulting box has zero or negative volume and never overlaps anything.

Cause 3: Stale AABB from before a child was added.

If you cache an AABB and then add a child mesh to the node, the cached AABB is no longer accurate. get_aabb() on a parent that aggregates children only sees what existed at the moment of the call.

The Fix

Step 1: Always transform local AABBs to global before comparing.

func get_global_aabb(node: VisualInstance3D) -> AABB:
    return node.global_transform * node.get_aabb()

func _check_overlap(a: VisualInstance3D, b: VisualInstance3D) -> bool:
    var aabb_a = get_global_aabb(a).abs()
    var aabb_b = get_global_aabb(b).abs()
    return aabb_a.intersects(aabb_b)

The global_transform * local_aabb operator is overloaded in Godot 4 and produces the smallest AABB that contains the rotated, scaled, translated original. Note that this is conservative — the result is the bounding box of the bounding box, so two rotated objects may report overlap even when their actual geometry does not.

Step 2: Always call .abs().

AABB.abs() takes any AABB with negative size components and returns an equivalent AABB with non-negative size and the position pointing to the lowest corner. It is cheap and idempotent. Add it as a habit any time you build an AABB from arbitrary corners.

Step 3: Visualize what you are comparing.

When the math is opaque, draw it. Spawn a temporary wireframe MeshInstance3D for each AABB you are testing and put it in the scene at the AABB’s position with the AABB’s size. The bug becomes obvious in five seconds.

func debug_draw_aabb(aabb: AABB, color: Color = Color.RED) -> void:
    var mesh = BoxMesh.new()
    mesh.size = aabb.size
    var mat = StandardMaterial3D.new()
    mat.albedo_color = color
    mat.flags_unshaded = true
    mat.flags_transparent = true
    mat.albedo_color.a = 0.3
    mesh.material = mat
    var mi = MeshInstance3D.new()
    mi.mesh = mesh
    mi.global_position = aabb.position + aabb.size * 0.5
    add_child(mi)

The Conservative Bound Trap

Even with the right transforms, an AABB is by definition axis-aligned. A long thin object rotated 45 degrees has a bounding box twice the size of the object itself. Two such objects can report overlap from their AABBs while their actual geometry is meters apart. If you need exact collision detection, do not use AABBs as the final check — use them as a broad-phase filter and follow up with a real shape query (PhysicsDirectSpaceState3D.intersect_shape or a SAT test).

Verifying the Fix

Place two MeshInstance3Ds in your scene. Move one of them so it is clearly inside the other. Print both global AABBs and call intersects:

print("A: ", get_global_aabb($A))
print("B: ", get_global_aabb($B))
print("intersects: ", get_global_aabb($A).intersects(get_global_aabb($B)))

If the printed positions are far from where the meshes appear in the editor, you forgot to apply the global transform. If the positions look right but intersect is still false, check the size for any negative axis.

“An AABB without a coordinate system is just six numbers. The numbers are useless until you commit to a frame of reference and convert everything into it.”

Related Issues

For broader collision debugging, see Godot Area3D overlap detection not working. For raycasts that miss obvious targets, check Godot raycast not detecting collisions. If your collision is fine but the visual mesh is offset, see Godot collision layer mask not working.

Use the editor’s “Show AABB” gizmo on a MeshInstance3D to confirm the local AABB matches what you expect before doing math on it.