A code sample for LÖVR that calculates a lightmap, while maintaining app responsiveness. The lightmap includes direct light and shadow, indirect bounce lighting, and ambient occlusion. The calculation is programmed in Lua, and can be run in a LÖVR project without additional external dependencies.
The project uses a simple, limited data representation. A type called GeoFace
is implemented in geoface.lua
, alongside a hand-written sample scene. A
GeoFace
defines colored rectangles.
To maintain responsiveness, the lightmap is calculated using worker threads. A
scheduler thread is implemented in scheduler.lua
, which manages the whole
process, and acts as an intermediary between the main thread and the worker
threads.
The worker threads are implemented in worker.lua
. They wait for jobs to come
in on a queue, and invoke tasks implemented in the tasks/
folder accordingly.
There are three tasks used to implement different lightmap passes: direct, bounce, and ambient occlusion.
The direct lighting pass calculates light contribution from a point light source, attenuating based off of distance and facing, with shadows based on occluding geometry. A configurable quality setting allows multiple samples to be taken per sample from slightly offset positions, to anti-alias the shadow.
Bounce lighting allows surfaces to receive light reflected off other surfaces. The bounced lighting accounts for surface color. Rays are cast randomly, and a denoiser cleans the image at the end.
An ambient occlusion pass paints artificial darkness into corners and crevices, enhancing the sense of definition. Rays are cast out a limited distance, and the surface is darkened based on how many rays hit another surface, and how close those hits are to the surface.
Normal bilinear texture filtering produces a noticeable blocky pattern that stands out for the large texel sizes of a lightmap. A shader-based bicubic texture sampler smooths out shadow edges. Combined with the anti-aliasing from the direct lighting pass, shadows have minimal artifacting.