Skip to content

Light Portals

Light portals let selected scene-channel geometry act as a proxy source for real-world window light. This is intended for spatial twin scenes where windows, doors, or openings should let ambient real-world light appear to enter the virtual model.

The feature uses the existing area-light shader path. Each active portal surface becomes a temporary two-sided area light derived from that entity's transform and local bounds. The portal plane is inferred from the two largest local bounding-box axes, so XY, XZ, and YZ window meshes can all be used as portal surfaces. The renderer does not create persistent light entities.

Basic Setup

Define a project-specific channel for windows:

extension SceneChannel {
    static let windowGeometry = SceneChannel.userCustom(index: 1)
}

Assign entities to the channel directly:

setEntitySceneChannels(entityId: windowEntity, channels: .windowGeometry)

Or map exported names by prefix:

registerSceneChannelPrefix("WIN_", channels: .windowGeometry)

Enable the channel as a light portal:

setSceneChannel(
    .windowGeometry,
    .lightPortal(.enabled(
        intensity: 1.0,
        range: 6.0,
        useRealWorldTint: true,
        maxActivePortals: 8,
        activationDistance: 15.0
    ))
)

Disable it again:

setSceneChannel(.windowGeometry, .lightPortal(.disabled))

Parameters

Parameter Meaning
intensity Base area-light intensity for each portal. Non-finite values fall back to 1.0; negative values clamp to 0.0.
range Maximum portal area-light influence distance in scene units, measured from the closest point on the portal rectangle. Portal contribution fades smoothly over the last 25% of the range.
useRealWorldTint When real-world XR lighting is active and valid, scales portal intensity by the current XR probe intensity and real-world contribution factor. If XR lighting is unavailable while the runtime mode requests real-world lighting, the portal emits at 0.0 instead of using the configured base intensity.
maxActivePortals Maximum active portal proxy lights for the channel. Nearby portals are selected first within each portal-enabled channel.
activationDistance Maximum camera distance from the portal rectangle for a portal to become active.

If an entity belongs to multiple portal-enabled channels, the engine combines the channel settings using the most permissive intensity, range, and activation distance. useRealWorldTint is enabled if any channel requests it. Active portal limits are enforced per portal-enabled channel.

Real-World Tint

useRealWorldTint: true applies the XR probe's normalized intensity scale and the engine's real-world lighting contribution multiplier:

portal intensity = portal intensity * xr intensity scale * real-world contribution

If no valid XR probe is available while XR real-world lighting is enabled, real-world-tinted portals emit no light. When XR lighting is valid, changing setRendering(.environment(.realWorldLightingContribution(...))) affects portal strength immediately.

Light portals do not enable XR lighting by themselves. Configure XR real-world probe lighting separately with setRendering(.environment(.lightingMode(.realWorldEstimate))). See XR Lighting for probe setup, provider lifecycle, diagnostics, and passthrough behavior.

Diagnostics

Discovery diagnostics report which scene entities are eligible portal surfaces:

let candidates = discoverSceneLightPortalCandidates()
let discovery = getLightPortalDiscoveryDiagnostics()
print(candidates)
print(discovery)

Resolution diagnostics report which portals become active proxy lights:

let proxies = resolveSceneLightPortalProxyLightsForActiveCamera()
let resolution = getLightPortalResolutionDiagnostics()
let performance = getLightPortalPerformanceDiagnostics()
let render = getLightPortalRenderDiagnostics()
print(proxies)
print(resolution)
print(performance)
print(render)

Important fields:

Field Meaning
scannedRenderableEntityCount Renderable entities scanned for portal eligibility.
candidateCount Renderable, visible entities on portal-enabled channels.
skippedHiddenCount Entities skipped because their scene-channel render mode is hidden.
skippedInvisibleRenderComponentCount Entities skipped because their render component is invisible.
skippedDisabledPortalCount Renderable entities whose channels are not portal-enabled.
skippedInvalidGeometryCount Portal-channel entities skipped because their local bounds are non-finite, too small, or not thin enough to infer a portal plane.
discoveredCandidateCount Portal candidates considered for proxy-light resolution.
activePortalCount Portal proxy lights emitted after distance filtering and active-count capping.
skippedByActivationDistanceCount Candidates outside activationDistance.
maxActivePortals Active portal cap used for the current resolved list.
lastDiscoveryDurationMs Time spent scanning renderable entities and building portal candidates during the latest discovery pass.
lastResolutionDurationMs Time spent distance-filtering, sorting, and selecting active portal proxy lights during the latest resolution pass.
lastScannedRenderableEntityCount Renderable entity count from the latest discovery pass, useful for spotting broad scans in large scenes.
lastResolvedProxyLightCount Active proxy-light count from the latest resolution pass.
environmentIntensityScale Final XR/environment multiplier used for real-world-tinted portal proxy lights.
xrIntensityScale XR probe intensity scale before the user contribution multiplier.
environmentTintColor RGB tint applied to real-world-tinted portal proxy lights.
realWorldLightingContribution User contribution multiplier from the rendering settings API.
maxEffectivePortalIntensity Brightest portal intensity emitted into the area-light buffer for the latest frame.

Performance

The feature is designed to be bounded:

  • When no scene channel has light portals enabled, the renderer takes a fast path and skips portal discovery entirely.
  • When at least one portal channel is enabled, discovery scans renderable entities with transform and render components.
  • Resolution filters by distance to the portal rectangle and sorts candidates by nearest first.
  • Portal discovery and resolution are cached per render frame and reused by the engine's render passes.
  • Only maxActivePortals are emitted as proxy area lights.
  • Authored area lights keep priority; portal lights only fill remaining maxAreaLights capacity.
  • Portal lights fade down near their own source plane to avoid making the window frame itself look like an emissive object.

For large spatial twins, keep the portal channel narrow. Assign only actual window/opening surfaces to the portal channel, not full walls or entire room shells.

Collecting Performance Data

Light portal diagnostics follow the same category-log pattern described in Profiler. Enable the .lightPortal category once, and the engine logs a throttled diagnostics summary automatically from the frame monitor path. No code is needed inside your app or game update() function.

setLogger(.category(.lightPortal, true))
// Reproduce the issue. The engine logs light portal diagnostics about once per second.
setLogger(.category(.lightPortal, false))

The automatic log emits the latest discovery, resolution, performance, and render snapshots. The snapshots are updated by the normal portal discovery/resolution/render paths; the one-second interval only controls log emission.

For a one-shot snapshot:

setLogger(.category(.lightPortal, true))
LightPortalSystem.shared.logDiagnosticsNow()
setLogger(.category(.lightPortal, false))

For scale checks, focus on:

Field Why it matters
lastScannedRenderableEntityCount How broad the portal discovery scan is.
lastDiscoveryDurationMs CPU time spent finding portal candidates.
lastResolutionDurationMs CPU time spent selecting active proxy lights.
activePortalCount Number of portal candidates selected after distance and cap filtering.
portalAreaLightCount Number of portal proxy lights uploaded into the area-light path.

Recommended starting values:

setSceneChannel(
    .windowGeometry,
    .lightPortal(.enabled(
        intensity: 0.5,
        range: 4.0,
        useRealWorldTint: true,
        maxActivePortals: 4,
        activationDistance: 10.0
    ))
)

Increase maxActivePortals only if the visual benefit is clear on device.

Limitations

Light portals are an approximation. They do not ray trace sunlight through openings, clip light to the portal shape, or compute physical bounce lighting. They emit proxy area lights from the selected window surfaces.

Portal brightness follows accepted XR probe updates, not every rendered frame. If the real room lights change, use XR lighting diagnostics to confirm that acceptedProbeUpdateCount, latestProbeTimestamp, and latestIntensityScale changed before judging the visual result.

The feature does not make windows transparent by itself. Use scene-channel render modes separately if a window or wall should be hidden, wireframed, or rendered as a passthrough ghost.

Portal proxy lights do not create persistent ECS light entities, so they are not serialized as scene-authored lights. Configure them through scene channels at runtime.