Light Balls
This note documents the first light-ball design and implementation.
Summary
light-ball is a new scene object kind.
- It owns its own physics body.
- It owns its own editable light-ball config.
- It attempts to create one runtime point light when present in a live scene.
- The runtime point light is transient and is not persisted into
scene.lights. - Persistence works like
physics-ball: the scene panel record is the source of truth.
This keeps the feature local to the scene-object system and avoids entangling graph/world light persistence with physics-owned light behavior.
Why Not Persist The Point Light?
We explored two models:
- Persist the linked point light in
scene.lights.point. - Persist the light settings inside the light ball itself and recreate the runtime point light on load.
The second model was chosen.
Reasons:
- It avoids feature-disable/re-enable overwrite problems. If light balls are disabled for a while and the user edits normal point lights directly, re-enabling light balls should not silently overwrite those point lights from stale state.
- It keeps ownership clear: normal point lights are persisted by the world lighting state, while light-ball point lights are derived runtime state.
- It matches the existing scene-object persistence model better.
physics-ballpersists as a scene panel;light-ballnow does the same.
Trade-off:
- Runtime point lights created by light balls are not visible through persisted world light state alone.
- The first version accepts that trade-off because light balls are not exposed through graph yet.
Source Of Truth
There are two separate sources of truth:
scene.lights- persisted lighting state for normal world lights
scene.panels[kind=light-ball]- persisted state for light-ball-owned light config and physics config
At runtime, a light ball materializes a point light into app.lights.
The runtime point light is derived state:
- created when the light ball is added/restored
- may remain absent when all runtime point-light slots are already in use
- removed when the light ball is removed/dropped
- retried automatically during sync when capacity later becomes available
- recreated automatically if the live light system is replaced
Runtime Lifecycle
Creation
Creation is routed through Scene.add-light-ball.
Creation no longer preflights point-light capacity.
The light ball scene object is always created.
If runtime point-light capacity is already exhausted:
- the light ball still exists
- it simply starts in an unlit state
- it keeps retrying runtime light allocation during sync
This keeps persistence simple and makes it possible to support future runtime light-selection strategies, such as activating only the nearest light balls to the camera.
Live Scene Registration
When the element is added to the scene:
- it attempts to allocate one transient runtime point light in
app.lights - it registers itself as a scene object for per-frame sync and physics lifecycle
- it registers a right-click context menu with
EditandRemove
Sync
The light ball sync path does two things:
- sync physics body -> scene layout
- sync scene layout center -> runtime point light position
Runtime light allocation is also retried during sync when the ball is currently unlit.
Only transform is synchronized every frame once a runtime light exists.
Light parameters are not rewritten every frame.
Why:
- per-frame transform sync is required because physics is the source of truth for spatial state
- per-frame full light-field stomping would add unnecessary work
- if a user edits the runtime point light elsewhere, those non-transform edits are allowed to survive in this simplified version
Removal
Manual removal through the context menu is ordered strictly:
- remove runtime point light
- remove scene object
If no runtime point light is currently assigned, removal just removes the scene object.
Scene unload/drop also removes the runtime point light in the element drop path.
Persistence
light-ball persistence includes:
- transform captured by the scene panel record
- point-light parameters
- physics tuning parameters
- render color
- radius and size
It restores through light-ball.restore.
The restore path delegates to Scene.add-light-ball.
Restore does not fail when the world contains more light balls than available runtime point-light slots. Extra light balls restore normally and remain unlit until a slot becomes available.
Why Runtime Lights Must Be Transient
Scene.capture-state persists app.lights:get-state().
If light-ball-created runtime point lights were serialized like normal lights:
- they would be saved into
scene.lights - restore would reapply them as normal persisted point lights
- restoring the panels would then create a second copy through
light-ball
That would duplicate lights on restore.
To avoid that, the runtime point light created by light-ball is marked transient inside LightSystem.
Transient lights:
- participate in the live renderer path
- count against runtime capacity
- are excluded from serialized light state
This keeps world save output clean without teaching the graph or persisted light state about light-ball ownership.
Why Graph Is Not Involved
The current feature deliberately does not introduce graph coupling.
light-ballis not a graph node type- normal point-light graph views stay unchanged
- no “controlled by …” logic is added to graph light editing
This means:
- graph continues to expose persisted world lights
- light-ball runtime lights remain a scene-object concern
That is intentional simplification for the first version.
Visual Path
The original raw-sphere.fnl path was not used directly for the final runtime visual because it allocates and rewrites triangle data per instance.
light-ball instead uses a shared instanced sphere visual built from the same sphere algorithm:
- sphere geometry is generated once per style/resolution
- instances use
InstancedColorMeshBatch - shared mesh lifetime is handled through
SharedInstancedMeshCache
This matches the performance model already used by soccer-ball-visual.fnl while keeping the light-ball mesh visually simple.
Editor
Editing is done through a HUD float dialog opened from the light-ball context menu.
The dialog contains two tabs:
Light- point-light parameters
Physics- the same tuning surface as the soccer ball path
Edits apply only when the user presses Apply.
Light Tab
Applying light changes:
- updates the persisted light-ball config
- updates the live runtime point light immediately
Physics Tab
Applying physics changes:
- updates the persisted light-ball config
- rebuilds the live rigid body immediately
- keeps the object centered at the same physics/layout position
Radius changes are live and rebuild both physics assumptions and rendered size.
Defaults
Light-ball defaults intentionally do not use the canonical point-light defaults.
The initial runtime light parameters are copied from the customized point light in the first home world:
- ambient
[5, 5, 4] - diffuse
[100, 70, 0] - specular
[50, 50, 0] - specular power
5 - constant
3 - linear
0.001 - quadratic
0.00132
Physics defaults match the soccer ball path except:
- radius is half-sized
- mass is lower
Known Trade-Offs
- Because graph is intentionally uninvolved, light-ball-owned runtime point lights are not represented as persisted world point lights.
- Because only transform is synchronized every frame, edits to runtime light parameters from elsewhere are not blocked and may persist until the light ball is reconfigured or recreated.
- Because runtime lighting is now best-effort, some light balls may currently be unlit when the scene contains more candidate emitters than the live point-light budget allows.
- The first version chooses simpler ownership and persistence boundaries over unified light editing semantics.
Future Directions
Potential later improvements:
- expose light balls through graph as first-class scene objects
- surface read-only runtime light inspection in graph
- add generic runtime light-controller concepts if more systems begin to own live lights
- separate reusable physics-sphere object logic from both
ballandlight-ballif more sphere-like scene objects appear
