Fading out the walls between you and the camera

In a top-down action game, the camera sits above and behind the player. That means environment geometry (walls, pillars, columns) regularly ends up between the camera and the character. If you don't handle this, the player just disappears behind a wall and dies to enemies they can't see. Not great.

Most game engines have built-in solutions for this. RealityKit doesn't. So I built one.

The approach

Every frame, cast rays from the camera to the player. If anything with an OccludableComponent is in the way, fade it down to 30% opacity. When it's no longer blocking, fade it back up. Smooth interpolation both ways so nothing pops in or out.

The component itself is minimal:

public struct OccludableComponent: Component, Codable {
    public var currentOpacity: Float = 1.0
    public var occludedOpacity: Float = 0.3
    public var isOccluding: Bool = false
    public var fadeSpeed: Float = 4.0
}

You drop this on any entity, either in Reality Composer Pro or directly in code, that might block the camera. Walls, pillars, whatever. The system handles the rest at runtime.

Why three rays?

One ray from the camera to the player's feet isn't enough. Something hanging from the wall might block the upper half of the player while the feet are still visible, or something short on the floor might only block the player’s lower half. I cast three rays: one to the player's feet, one to mid-height, and one to the top.

let bounds = player.visualBounds(relativeTo: player)
let playerHeight = max(bounds.max.y - bounds.min.y, 1.0)

let rayTargets = [
    playerPos,                                    // feet
    playerPos + SIMD3<Float>(0, playerCenter, 0), // mid
    playerPos + SIMD3<Float>(0, playerHeight, 0), // head
]

The player's height comes from visualBounds so it adapts automatically if the character model changes. If any of the three rays hits an occludable entity, that entity fades.

Smooth fading

Instant opacity changes look jarring. The system interpolates toward the target opacity each frame using a configurable fade speed:

let targetOpacity = component.isOccluding ? component.occludedOpacity : 1.0
let diff = targetOpacity - component.currentOpacity
let change = component.fadeSpeed * deltaTime

if abs(diff) < change {
    component.currentOpacity = targetOpacity
} else {
    component.currentOpacity += (diff > 0 ? change : -change)
}
entity.components.set(OpacityComponent(opacity: component.currentOpacity))

At a fade speed of 4.0, a full fade takes about 0.25 seconds. Fast enough to not feel sluggish, slow enough to look smooth.

Hierarchy traversal

RealityKit raycasts hit the deepest entity in the hierarchy. If you have a wall entity with a child mesh, the raycast hits the mesh, not the wall. The system walks up the parent chain to find the nearest ancestor with an OccludableComponent. It also skips the player entity and all its descendants so you don't accidentally fade the character you're trying to protect.

Platform note

This whole system checks whether or not there is a follow camera. On Vision Pro, the user controls the camera with their head. There's no fixed camera to occlude from, and fading real geometry in a spatial context would feel wrong. The system only runs on iOS, iPadOS, and macOS where there's a traditional follow camera.

The nice thing about opt-in components

Because occlusion is driven by a component you place on an entity, level design controls which geometry participates. Floors never need to fade. Decorative objects might not either. You tag what matters and the system ignores everything else. No collision layer tricks, no naming conventions. Just a component.

Next
Next

How I made enemies attack while chasing you