How to adapt Dynamic Lights to the size of a Volume
We can scale the intensity and attenuation values for lights relative to a root entity.
The Problem:
If you use Dynamic Lights like PointLight or Spotlight inside a volume, you may notice that things don’t look right. When the user resizes the volume, the lighting in the scene changes. The issue is that the dynamic light settings don’t scale with the rest of the scene. For example, if a user scales the volume down to a small size, light has a shorter distance to travel for a given intensity.
Solution
We can adjust the intensity and attenuationRadius values based on the scale of a root entity. We need to scale the intensity by scaler². This keeps the apparent brightness (power) consistent as the distance it has to travel changes. We can also adjust the attenuation radius so the light has the same relative reach.
Note: I’m using this technique to scale my content when the volume resizes.
// Assumes a uniform scale for the volume
let scaler = volumeRootEntity.transform.scale.x
light.intensity = 1500 * scaler * scaler
light.attenuationRadius = 6 * scalerVideo Demo
Scalable Light Component & System
We can use RealityKit ECS to create a component and system that will adapt our lights. We can add this new component to any entity with a SpotLight or PointLight component. The values we enter will override any values from the original light component.
guard let lightSource = subject.findEntity(named: "PointLight") else { return }
let slc = ScalableLightComponent(
baseIntensity: 1200,
baseRadius: 10,
scaleSourceName: "VolumeRoot", // the name of a top-level entity that will be scaled with the volume.
referenceScale: 1.0,
epsilon: 0.02,
nonUniformMode: .average
)
lightSource.components.set(slc)ScalableLightComponent.swift
import RealityKit
struct ScalableLightComponent: Component, Codable, Hashable {
/// Base intensity at referenceScale = 1.0
/// Will by multiplied by × scale²
var baseIntensity: Float = 1500
/// Base radius at referenceScale = 1.0
/// Will by multiplied by × scale²
var baseRadius: Float = 6
/// Name of the entity whose (uniform) scale we watch (e.g. your volume root)
var scaleSourceName: String
/// Reference scale at which base* values were authored
var referenceScale: Float = 1.0
/// Minimum relative change in scale before recalculating light values.
/// This avoids constant updates from tiny scale fluctuations (floating-point jitter or micro resize events).
/// For example, epsilon = 0.02 means the light only updates when the scale changes more than ~2% compared to the last applied value.
var epsilon: Float = 0.02
/// How to collapse non-uniform scale into one value
enum NonUniformMode: String, Codable { case average, max }
var nonUniformMode: NonUniformMode = .average
// cache the last used scale
var lastAppliedScale: Float = .nan
}ScalableLightSystem.swift
import RealityKit
struct ScalableLightSystem: System {
static let query = EntityQuery(where: .has(ScalableLightComponent.self))
init(scene: Scene) {}
func update(context: SceneUpdateContext) {
for entity in context.entities(matching: Self.query, updatingSystemWhen: .rendering) {
guard var comp = entity.components[ScalableLightComponent.self] else { continue }
guard let source = context.scene.findEntity(named: comp.scaleSourceName) else { continue }
// derive a single scale 's' from the source
let t = source.transform.scale
let sRaw: Float = switch comp.nonUniformMode {
case .average: (t.x + t.y + t.z) / 3
case .max: max(t.x, max(t.y, t.z))
}
// normalize against the reference authoring scale
let s = max(0.0001, sRaw / max(0.0001, comp.referenceScale))
// threshold check
if !comp.lastAppliedScale.isNaN {
let delta = abs(s - comp.lastAppliedScale)
if delta <= (abs(s) * comp.epsilon) { continue }
}
// Apply to Point/Spot lights if present
var touched = false
if var point = entity.components[PointLightComponent.self] {
point.intensity = comp.baseIntensity * s * s
point.attenuationRadius = comp.baseRadius * s
// other properties remain as-authored
entity.components.set(point); touched = true
}
if var spot = entity.components[SpotLightComponent.self] {
spot.intensity = comp.baseIntensity * s * s
spot.attenuationRadius = comp.baseRadius * s
// other properties remain as-authored
entity.components.set(spot); touched = true
}
if touched {
comp.lastAppliedScale = s
entity.components.set(comp)
}
}
}
}Download the Xcode project from this repo. Look for the project named ScaledLightSystem
Support our work so we can continue to bring you new examples and articles.

Follow Step Into Vision