Moving brush entities

A door, a lift or a secret wall is brushwork that moves. While it stands where it was baked, a lightmap lights it best: the shadow of the frame, the lamp across the hall. The moment it moves, that lightmap is a picture of somewhere it no longer is. So a brush entity hands its faces over to the light probes as it starts to move, and - if it comes back exactly where it was - hands them back.

1. Is it in the bake?

By default a brush entity's brushwork is not lightmapped: the bake treats it as something that is somewhere else as often as not, and does not let it cast shadows it will not be there to cast. A door or a lift should stay that way, and be lit by the probes from the start.

Something that is flush with the world nearly all the time - a secret door in a panelled wall - wants the lightmap while it is shut. Say so on the type, and every brush of every such entity is baked:

HammeriteEntityTypeRegistry.register_brush(&"secret_door", {
    &"classname": &"SecretDoor",
    &"bake_static": true,
})

An author can say the same of one brush, with Bake into the world (lighting/bake_static) in its Lighting properties. get_lighting().is_baked_static() answers which it is.

2. Hand over as it starts moving

Every HammeriteBrushEntity* base class carries the handover. Call hand_over_to_probes() as the movement starts, so the quarter-second blend from lightmap to probes happens under the motion rather than as a pop once it has moved:

class_name SecretDoor
extends HammeriteBrushEntityStaticBody3D

const SLIDE: Vector3 = Vector3(0, 0, 64)

func open() -> void:
    hand_over_to_probes()
    create_tween().tween_property(self, ^"position", SLIDE, 1.5)

func set_editor_active(_active: bool) -> void:
    pass

func apply_entity_property(_prop_name: StringName, _value: Variant) -> void:
    pass

The node of a brush entity stands at the map's origin with its brushwork in map space, so moving the node by SLIDE moves the brushwork by SLIDE.

From then on, each frame, the entity finds the probes around the middle of its brushwork - again as it moves far enough - and writes them into its faces' materials, following every lamp that is switched. Nothing needs driving: the base class's lighting runs itself while there is anything to blend.

Call it for an entity that is not baked too. It has no lightmap to leave, but probes light it far better than the flat ambient it otherwise wears.

3. Hand back only when it is flush again

func close() -> void:
    var slide := create_tween()
    slide.tween_property(self, ^"position", Vector3.ZERO, 1.5)
    slide.finished.connect(hand_back_to_lightmap)

hand_back_to_lightmap() blends back to the bake, and the entity stops costing anything per frame. Only call it when the brushwork is back exactly where it was baked: a door left ajar is not where its lightmap says it is, and should stay on the probes.

4. In the editor

The handover belongs to the running world: put it in the movement, which runs while the world is simulating, and reset_simulation() puts the entity back where the map built it - flush, so hand back there too:

func reset_simulation() -> void:
    position = Vector3.ZERO
    hand_back_to_lightmap()

5. Driving it yourself

get_lighting() returns the entity's HammeriteBrushEntityLighting, made the first time it is asked for. get_blend() says how far over it is - 0.0 all lightmap, 1.0 all probes. After a rebake replaces the brushwork's meshes, the lighting finds the new materials itself.

See also

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