Probe lighting
A lightmap lights a surface where it was baked. Anything that moves - a guard, a crate being carried, a door on its hinge - is somewhere else by the next frame, and needs the bake's light at the place it has got to. That is what light probes are: points through the level's air, each carrying the light that reaches it, baked in the same pass as the lightmaps and from the same lights.
1. What the bake makes
HammeriteLightmapper.bake() places the probes before it lights
anything, then lights them:
- surface probes just off every wall, on the room's side;
- volume probes on a lattice through the air, clear of the walls.
They are saved with the map as its probe_set, in map space. A
probe carries the bake's light in two parts:
- the light that can never change - static lights, and their bounce;
- up to four slots of switchable light, each the share of one
HammeriteBakedLight(a switchable lamp, or a group of them) at full strength.
What is drawn multiplies each slot by its light's current modulation, so a lamp switched off goes dark on everything probe-lit as it does on the walls.
The lattice spacing is HammeriteBakeSettings.probe_cell_size
(64 units by default): smaller follows light more closely, at more probes to bake and store.
2. Asking the probes
Two calls on HammeriteMap light a thing of your own:
var probes := map.query_light_probes(position_in_map)
map.update_probe_lit_uniforms(material, probes)
query_light_probes()finds the nearest few probes that can actually see the point through air - never one in the next room, however near it is through the wall.update_probe_lit_uniforms()writes them, and the lights in their slots, into aShaderMaterialwhose shader includes Hammerite's probe sampling. Each thing needs a material of its own.
Finding probes costs a lookup and a few line-of-sight tests; writing them costs little. So find again only when the thing has moved some way, and write every frame - a flickering lamp is not followed otherwise:
const REGATHER_DISTANCE: float = 32.0
var _probes: Array = []
var _gathered_at: Vector3 = Vector3.INF
func _process(_delta: float) -> void:
var here: Vector3 = map_3d.to_local(global_position)
if here.distance_to(_gathered_at) > REGATHER_DISTANCE:
_probes = map_3d.map.query_light_probes(here)
_gathered_at = here
map_3d.map.update_probe_lit_uniforms(_material, _probes)
A bake or a variant switch replaces the probes. Gather again on the map's light_probes_changed and
light_probes_baked signals:
map.light_probes_changed.connect(func() -> void: _gathered_at = Vector3.INF)
map.light_probes_baked.connect(func() -> void: _gathered_at = Vector3.INF)
3. The shader
The shader side is an include. probe_lit_albedo() is the lit colour of a surface, with the
exposure and the light curve applied, so the surface is drawn unshaded:
shader_type spatial;
render_mode unshaded;
#include "res://addons/hammerite-core/resources/materials/probe_lit.gdshaderinc"
uniform vec3 albedo : source_color = vec3(1.0);
void fragment() {
vec3 world_position = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 world_normal = (INV_VIEW_MATRIX * vec4(NORMAL, 0.0)).xyz;
ALBEDO = probe_lit_albedo(albedo, world_position, world_normal);
}
Experimental. The include's functions and uniforms are not yet among the shader contracts, and may change in any release. The probes are in map space and the shader samples at a world position, so this holds for a
HammeriteMap3Dstanding at the origin.
4. What else reads the probes
HammeriteMap.light_level_at()reads the same probes with the same weighting, for a game asking how lit a point is: see Sampling the light level.HammeriteEyeAdaptation3D, under the camera, reads the level where the player stands and where they look, and sets the exposure every probe-lit and lightmapped surface is drawn with (hammerite_exposure, inHammeriteShaderGlobals).- A brush entity that moves hands itself over from its lightmap to the probes: see Moving brush entities.
A placed model (prop_model) is not probe-lit: it stands still, and the bake gives it a lightmap of
its own.
5. The darkest a probe-lit thing gets
Where no probe can see, a probe-lit thing would be black. The shader global hammerite_ambient_light
is a floor added before the light curve; nothing in Hammerite sets it, so the game decides how dark is
too dark to play in:
RenderingServer.global_shader_parameter_set(HammeriteShaderGlobals.AMBIENT_LIGHT, Color(0.02, 0.02, 0.03))
See also
- Baking from a script - baking the lightmaps and probes in a pipeline.
HammeriteLightProbeSet,HammeriteBakedLight,HammeriteLightCurve.