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 a ShaderMaterial whose 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 HammeriteMap3D standing at the origin.

4. What else reads the probes

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

Hammerite 1.0 · built from a6dd634, 2026-10-10