Brush materials and shaders

Every wall, floor and ceiling is drawn with Hammerite's brush shader: the face's texture, lit by its lightmap, through the exposure and light curve the rest of the world uses. A game changes how its world looks without taking any of that on, from least to most work:

To change Use
a surface that glows, is see-through, or is a window on the sky the texture's name
the walls themselves - normal maps, detail textures, a glow of their own a wall shader on Hammerite's surface contract
what water looks like a water shader on Hammerite's water contract
a room full of something else - lava, tar, fog a fill with a material of your own
the sky through the windows a sky shader
a shader of your own that sits in the same light the shader globals

1. What a texture name says

A face stores a texture name, and HammeriteTextureLibrary resolves it. Three things about a surface are said by the name and the files beside it, so an author never sets them twice:

  • wall_01_emission.png beside wall_01.png makes every face of wall_01 glow where the emission texture is bright. The glow is radiance the surface gives off by itself: it is drawn, and it lights the bake, bouncing onto the walls around it. It is static light - it cannot be switched off. HammeriteBakeSettings.emission_scale sets how bright fully white glows.
  • !grate - a leading ! marks an alpha mask: the texture's alpha cuts holes, in what is drawn and in what the bake lets through.
  • sky_night - a leading sky_ makes a face a hole to the outside, drawn as the sky.

Textures a game makes itself - a mod's PNGs - come from a source of its own, asked before the search paths:

HammeriteTextureLibrary.add_source(
    func(texture_name: StringName) -> Texture2D: return _mod_textures.get(texture_name),
    func() -> Array: return _mod_textures.keys())

2. Walls

A game draws its walls with a shader of its own by writing one function, brush_surface(), between two includes. The includes do everything the bake and the editor need - the lightmap, caustics, the probes a moving brush entity hands over to, a torch in somebody's hand, the editor's views - and hand the function a HammeriteSurface, s, to change:

shader_type spatial;
render_mode depth_draw_always, cull_back, ambient_light_disabled, specular_disabled;
#include "res://addons/hammerite-core/resources/materials/hammerite_surface.gdshaderinc"

uniform sampler2D normal_texture : hint_normal;
uniform float bumpiness = 1.0;

void brush_surface(inout HammeriteSurface s) {
    s.normal = surface_normal_from_map(s, texture(normal_texture, s.uv).rgb, bumpiness);
}

#include "res://addons/hammerite-core/resources/materials/hammerite_surface_main.gdshaderinc"
HammeriteBrush.surface_material = preload("res://materials/our_walls.tres")

s starts as Hammerite's own surface, and the function changes only what it wants to:

albedo the colour the light falls on - the face's texture to begin with
normal which way the surface faces, in world space - the face's own to begin with
glow light it gives off by itself, added before the exposure; drawn, not baked

and reads where it is: uv (the face's texture coordinates, so color_texture and anything of yours tiles with the texture), uv2, position, face_normal, view_dir, time, and u_axis and v_axis, the ways the texture runs across the face, which a normal map is written against. surface_normal_from_map() turns a texel of a tangent-space normal map into a world-space normal.

A detail texture is a few lines more:

uniform sampler2D detail_texture : source_color, repeat_enable;

void brush_surface(inout HammeriteSurface s) {
    s.albedo *= texture(detail_texture, s.uv * 8.0).rgb * 2.0;
}

What a normal map responds to. A lightmap holds how much light reaches a texel, not where it came from. So a normal turned away from the face's plane is shaded against the light that does know a direction:

  • each switchable light, whose place the shader is told - a lamp the player can put out shades the bumps it falls across;
  • carried lights, a torch in somebody's hand;
  • the probes, on brushwork that moves.

The static layer and the bounce stay as baked, flat across the texel. A level lit mostly by lamps that never switch shows its normal maps under torchlight, and as a switchable lamp is dimmed or lit.

Each face is drawn with a copy of the material. A uniform Hammerite's own shader is given - the caustic and noise textures - that your material leaves unset is taken from Hammerite's, so a wall under a pool still ripples. Set the material once, before a map is built; null puts Hammerite's own back. Alpha-masked faces, the sky, the surfaces of fills and placed models (prop_model) are drawn as before. What the bake bounces is the face's texture, not your albedo: a detail texture that changes a wall's colour does not change the colour it throws on its neighbours.

3. Water

Hammerite's water shows how clear and how disturbed it is, and nothing else. A game draws its own by writing one function, water_surface(), between two includes. The includes do the plumbing - what is under the surface, the bake's light on it, which side it is seen from - and hand it a HammeriteWater, w, to fill in:

shader_type spatial;
render_mode blend_mix, depth_draw_never, cull_disabled, ambient_light_disabled, specular_disabled;
#include "res://addons/hammerite-core/resources/materials/hammerite_water.gdshaderinc"

void water_surface(inout HammeriteWater w) {
    float through = water_transmittance(water_travelled(w), w.clarity);
    vec3 below = water_below(w, water_slope(w, w.uv) * 0.002);
    w.colour = mix(water_lit(w, w.tint), below, through);
}

#include "res://addons/hammerite-core/resources/materials/hammerite_water_main.gdshaderinc"
HammeriteBrushFillRegistry.set_material(HammeriteBrushFillRegistry.WATER, preload("res://materials/our_water.tres"))

w.clarity, w.tint and w.disturbance are what the author set on the pool's brush. The helpers measure and the function decides: water_travelled is how much water the view crosses, water_below the scene behind the surface, water_lit the surface lit by the bake, water_slope the ripple, and water_transmittance and water_fresnel the usual optics. The whole list is the water contract, which is supported: a shader written against it keeps working.

Register before the map is built. A built map redraws its filled rooms when a fill's material changes.

4. A fill of your own

A fill is what an air brush is full of instead of air. Core ships water; a game adds its own - and the material its surface is drawn with:

HammeriteBrushFillRegistry.register_fill(&"lava", "Lava", &"", Color.ORANGE_RED,
    preload("res://materials/lava.tres"))

The room is still a room: light, sound and agents pass through it, and a node of the game's (classname, here none) can be put over each one to burn whoever falls in.

Each pool's surface gets its own copy of the material, with the face's texture as color_texture. A property declared for the fill reaches the copy as a uniform of the same name, / as _ - so an author can make one pool hotter than another:

var lava := HammeriteBrushPropertySchema.new(&"lava", "Lava")
lava.fill = &"lava"
lava.add(&"glow", "Glow", 0.5).set_range(0.0, 1.0, 0.05)
HammeriteBrushPropertyRegistry.register(lava)
shader_type spatial;
render_mode unshaded;

global uniform float hammerite_exposure;
uniform sampler2D color_texture : source_color;
uniform float lava_glow = 0.5;

void fragment() {
    vec2 flow = UV + vec2(TIME * 0.02, 0.0);
    ALBEDO = texture(color_texture, flow).rgb * (1.0 + 4.0 * lava_glow) * hammerite_exposure;
}

Lava lights itself, so it is drawn unshaded and needs nothing of the bake. For it to light the room around it too, give its texture an emission partner - lava_emission.png - and the bake will.

5. The sky

Through every sky_ face, Hammerite draws a gradient from the horizon to the zenith, with the sun or moon in it. A game draws its own with a shader_type sky material, one for every map:

HammeriteSkybox.dome_material = preload("res://materials/our_sky.tres")

Every uniform it declares under one of the skybox's names is filled in: horizon_color, zenith_color, body_direction, body_color, body_energy, body_is_moon, light_energy. And a property a game declares on the sky reaches it the same way, clouds/cover as clouds_cover:

var clouds := HammeriteSkyPropertySchema.new(&"clouds", "Clouds")
clouds.add(&"cover", "Cover", 0.45).set_range(0.0, 1.0, 0.01)
HammeriteSkyPropertyRegistry.register(clouds)
shader_type sky;

uniform vec3 horizon_color : source_color;
uniform vec3 zenith_color : source_color;
uniform float clouds_cover = 0.45;

void sky() {
    vec3 gradient = mix(horizon_color, zenith_color, clamp(EYEDIR.y, 0.0, 1.0));
    COLOR = mix(gradient, vec3(0.6), clouds_cover * smoothstep(0.0, 0.3, EYEDIR.y));
}

What the sky lights the world with is its gradient and its sun, in the bake. A cloudier sky that should also be darker is a greyer gradient, not just a shader that draws more cloud.

6. Matching the world

A shader of your own - a prop's, a particle's, the lava's - sits in a world drawn through an eye that adapts. Read the same values, and it brightens and darkens with everything else. The names are HammeriteShaderGlobals, and they are supported:

  • hammerite_exposure - what the eye has adapted to. Multiply anything self-lit by it.
  • hammerite_ambient_light - the floor of darkness the game set.
  • the hammerite_light_curve* five - how the world shapes its light for the eye.

Enabling the core plugin declares them in the project settings. A shader reading one that is not declared does not compile.

Something that moves, and should be lit by the bake rather than glow, is lit by the light probes; see Probe lighting.

Contracts and promises

The three shader contracts - walls, water and the sky - are supported, as the classes are: a shader written against what API stability lists keeps working. The rest of what the includes declare, and Hammerite's own shaders (brush.gdshader, brush_common.gdshaderinc, the probe and light-curve includes), are the renderer's and may change in any release.

See also

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