HammeriteLightCurve

Core: lighting. Inherits Resource

What a measured light level is worth to the eye, as a gain over the level itself.

Exposure is one number for the whole picture. It can carry a room up or down, and it cannot make the dark half of a room darker than the lit half - so a bake that is physically honest is a bake with nowhere to hide in it: a candle really does put a readable amount of light on every wall of a small room, and read linearly that is a room with no shadow in it at all. This is the curve over the reading. gain is sampled at level / lit_surface_level and multiplies the light there, so a flat 1.0 is the linear picture unchanged and a dip at the left-hand end is what makes shadow shadow. It is a gain rather than a transfer for two reasons: the identity is a shape anybody can recognise at a glance, and nothing above lit_surface_level is clamped - a sunlit courtyard keeps its range instead of being flattened onto the curve's end. Two orderings that are the whole point of it: - It shapes the LIGHT, before a surface's own colour is applied. A black rug in a lit room is a lit black rug, not a patch of shadow, and what the picture calls dark is where the light is. - It is applied BEFORE the exposure. The eye then adapts to a world already shaped, rather than the shaping sliding about underneath as the eye moves - and since adaptation is one scalar for the frame it can lift a dark room without ever putting its contrast back. What it is handed is the light on a SURFACE - the irradiance a shader assembles for one texel, ambient and dynamic and static together - and not the volume reading HammeriteMap.light_level_at() answers with. The two are the same units and nothing like the same scale: measured in one room of a game built on it, the probe at chest height read 1.76 while nine surface texels in ten were under 0.07. So lit_surface_level cannot be copied from a game's stealth tuning, and the way to find it is to look - the editor's lightmap visualisation draws this exact quantity with nothing else in it. Hammerite measures and this shapes the measurement for the eye; what counts as dark enough to hide in is still the game's, over HammeriteMap.light_level_at(), and nothing here touches that reading. The picture never gets to decide what can be seen. The lift is the player's half: a brightness setting that lets them see into shadow without the shadow stopping being one. Exposure cannot do that - a gain on the whole picture puts lit walls into white long before a dark corner is readable. The lift is a power below 1 over the level under lit_surface_level, applied after the curve: black stays black, a lit surface stays as lit, the order of everything between is kept, and the most it can do is lift_exponent. What it adds can be drawn in lift_tint, so a lifted shadow reads as shadow by its colour when it no longer does by how dark it is.

Properties

TypeNameDefault
Stringnote""
Curvegainnull
floatlit_surface_level0.3
intresolution256
floatlift_exponent0.5
Colorlift_tintColor(1, 1, 1, 0)
floatlift0.0

Methods

ReturnsMethod
floatgain_at(light_level: float)
floatshaped(light_level: float)
floatlifted(fraction: float)
ImageTexturelut()
voidpublish()
HammeriteLightCurvepublished() static
voidpublish_none() static

Constants

UNIFORM_LUT

const UNIFORM_LUT = &"hammerite_light_curve"

The global shader uniform the lookup texture (lut()) is published to. This and the other UNIFORM_* globals are what the shipped shaders read the curve through; enabling the plugin declares any the project has not (see HammeriteShaderGlobals).

UNIFORM_LIT_LEVEL

const UNIFORM_LIT_LEVEL = &"hammerite_light_curve_lit_surface_level"

Where lit_surface_level is published.

UNIFORM_ENABLED

const UNIFORM_ENABLED = &"hammerite_light_curve_enabled"

Where whether the curve or the lift applies at all is published.

UNIFORM_LIFT

const UNIFORM_LIFT = &"hammerite_light_curve_lift"

Where the strength of the lift, from lift and lift_exponent, is published.

UNIFORM_LIFT_TINT

const UNIFORM_LIFT_TINT = &"hammerite_light_curve_lift_tint"

Where lift_tint is published.

MIN_LIT_LEVEL

const MIN_LIT_LEVEL = 0.0001

Keeps the division that normalises a level away from zero.

Property descriptions

note

var note: String = ""

Why it is the way it is, for whoever tunes it. Never seen by a player. A field rather than a comment in the file: a .tres comment is dropped the moment Godot rewrites the resource, and what belongs here is where a number was MEASURED, which is the one thing about a tuning file nobody can work out again by looking at it.

gain

var gain: Curve = null

What each light level is multiplied by, sampled across 0..1 - flat 1.0 for the linear picture. Null means no opinion, which publishes as the curve being off rather than as a curve of zeroes: an unset resource must never be able to black a level out.

lit_surface_level

var lit_surface_level: float = 0.3

The SURFACE light level the curve's right-hand edge stands for. Anything brighter takes that same end value, so the curve shapes the range a room is lit in and leaves the sun alone. Not a number to guess at, and not one to borrow from a stealth system - see the note above about which quantity this is. Measure it: turn on the lightmap visualisation, which draws the light with no surface colour and no exposure in it, and read where a directly lit floor sits.

resolution

var resolution: int = 256

How many texels the shader's lookup gets across the range - finer than the curve editor can be drawn in, and the whole of what it costs is one row of floats.

lift_exponent

var lift_exponent: float = 0.5

The power the light below lit_surface_level is raised to at full lift. 0.5 takes a shadow at 2% of a lit surface to 14% and one at 10% to 32%.

lift_tint

var lift_tint: Color = Color(1, 1, 1, 0)

The colour the lift's added light is drawn in, as a hue - its brightness is ignored. Alpha is how much of the added light takes it rather than the light's own colour: 0 lifts shadow in the colour it already was.

lift

var lift: float = 0.0

How far the player has asked to see into shadow, 0..1. Not exported: it is a setting, not tuning, and a value saved into the resource would be every player's.

Method descriptions

gain_at()

func gain_at(light_level: float) -> float

What light_level is multiplied by. 1.0 where there is no curve to ask.

shaped()

func shaped(light_level: float) -> float

What light_level is worth once the curve and the lift have had it, in the same units it came in. The luminance of what the shader draws: the lift's tint moves its hue and not its level.

lifted()

func lifted(fraction: float) -> float

fraction of a lit surface's light, as the lift draws it. Anything at or past 1 is left alone.

lut()

func lut() -> ImageTexture

The curve as the shader reads it: one row of single-channel texels across 0..1. Each texel holds what the gain there takes AWAY (1 - gain) rather than the gain itself, so that zero means "leave the light alone". A sampler global nobody has published reads black - measured, not assumed - and an addon whose host forgot to declare this one would otherwise have multiplied every surface in the game by nothing.

publish()

func publish() -> void

Hand this curve to every shader that reads light, and keep doing so as it is edited. There is one published curve, since what it fills are global uniforms: publishing this replaces whichever was, and that one's edits no longer reach the picture.

published()

static func published() -> HammeriteLightCurve

The curve publish() last handed to the shaders, or null after publish_none().

publish_none()

static func publish_none() -> void

Put the linear picture back. What a project with no curve to publish leaves behind it.

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