Lighting that lives in a .tscn can only ship with the build. Lighting that is
a dictionary can come out of a map file, a content pack downloaded at run time,
a server’s configuration or a level editor — and a game that reads one reads all
four.
var doc := DotLightDocument.from_dictionary(manifest.get("lighting", {}))
DotLightRig.apply(world_root, doc, DotLightProfile.web())
Requires dot-core.
This is not dot-fx, on purpose
dot-fx is built on the rule that an effect is
safe to drop — it is pooled, budgeted and distance-culled, and nothing breaks
when one never plays. Lighting is neither of those things: it is not an event,
and a world that quietly drops it is a world nobody can see.
Why a document
It matters most for converted worlds. A map authored in another engine describes its own lighting — where the sun is, what colour it is, how far the fog reaches, which sky is behind it — and an importer that reads the geometry and leaves that behind produces a world that is geometrically perfect and looks like nothing in particular. Every map ends up under whatever one sun the game happened to hardcode.
Absent is not zero
Every field is optional, and a document that says nothing about the sun is
different from one that says the sun is black. The first keeps whatever default
the consumer holds; the second means black. has_sun and has_ambient exist so
a renderer can tell those apart, because they are different instructions and a
world lit by the wrong one looks broken in a way nothing reports.
A document is the world; a profile is the machine
DotLightDocument says what the world is, and is the same on every machine
that loads it. DotLightProfile says what this machine will draw of it.
Folding the two together is how a world ends up looking different because
somebody turned their settings down — which, for a world whose surfaces are read
for speed, is not a cosmetic difference.
high() |
web() |
low() |
|
|---|---|---|---|
| tone mapping | yes | yes | yes |
| glow | yes | yes | no |
| fog | yes | yes | yes |
| sun shadows | yes | no | no |
The browser drops the expensive half and not the cheap one. A shadow pass scales with how much geometry is in frame, which on a large level is all of it; glow is a fixed cost in screen pixels that a phone-class GPU handles. Dropping glow first is the mistake the profiles exist to prevent.
Tone mapping survives every tier, including the lowest. Godot’s default is
LINEAR, which is not a tone map — it clips. A filmic shoulder is why a bright
sky and a dark wall can both be readable in one frame instead of one being white
and the other black, and it is a curve applied to a pixel that was going to be
written anyway.
What it can change, and what it cannot
A world drawn from a baked lightmap has its lighting in its textures: its materials are unshaded, and a directional light added beside them lights nothing they own.
So the sun here lights the characters, the props and anything else with an ordinary material. What changes how the baked world itself looks is the post-processing — the tone map, the fog, the glow and the colour behind everything. That is not so much a limitation as the division of labour a baked world already has, and those four are most of the difference between a faithful conversion and one that feels like the engine it came from.
Three things that are easy to get wrong
- A brightness is not an energy. Worlds from the older BSP-family formats
write a light’s compiled intensity alongside its colour, and it ranges from 20
to 600 across eight ordinary maps. Handing that to
Light3D.light_energygives a sun six hundred times too bright on one map and twenty times on the next. It is carried unconverted in the document and curved against a reference by the rig, because what to do with 600 is a renderer’s decision. - A yaw in one handedness is not a yaw in the other. Converting pitch and
yaw separately between a Z-up left-handed world and Godot’s Y-up right-handed
one produces a sun that rises in the north, and nothing about the result looks
wrong enough to question.
DotLightRig.sun_rotationbuilds the direction in the source convention and swaps axes once, on the vector. - A sun straight overhead has no
looking_at. It is parallel to the default up vector, and a basis cannot be built from two parallel vectors. Worlds with an exactly vertical sun are ordinary, not a corner case, and the suite asserts the result is a number rather than aNaN.
The glow threshold is under 1.0 on purpose
A world lit by a baked 8-bit lightmap has nothing above white in it anywhere, so an HDR glow threshold finds nothing to bloom at, on any map, for ever. Lowering it is what lets the brightest surfaces — the strip lights and signage a level is signposted with — read as light rather than as paint. Raise it above 1.0 for a world whose materials genuinely emit.
No autoloads
A lighting rig belongs to a world. A process running a server and a client, or holding two worlds while one loads over the other, needs two of them — and a global would make that the one arrangement this family cannot have.
