HammeriteFace

Core: the map. Inherits Resource

One bounding plane of a HammeriteBrush, and what its surface looks like.

The plane's normal points OUT of its own brush - for an air brush, into the surrounding rock, so a room's floor faces down. A face is textured by name (texture_name), carries its UV transform and bake, and a sparse properties bag for anything an addon hangs on the surface. Its corners are worked out by its brush; a face with no brush answers from the last corners it was given, or none. A face loaded from a map has only its texture_name until something asks for its texture, so is_sky() and texture_span() resolve it then - after the game has pointed HammeriteTextureLibrary at its textures. The _no_emit setters change a value without the signal its own setter emits, for a caller changing many faces that remeshes their brush once; set_plane_no_emit() still marks the brush's corners stale, since its corners are worked out from the planes.

Properties

TypeNameDefault
Vector3centroid
booldrawn
Vector2idrawn_in
HammeriteBakedLightmaplightmap
floatlightmap_scale16.0
PlaneplanePlane(0, 1, 0, 0)
Dictionaryproperties{}
Texture2Dtexture
StringNametexture_name&""
Vector2[]uv2_coordinates[]
Transform2Duv_transformTransform2D(1, 0, 0, 1, 0, 0)
Vector3[]vertices

Methods

ReturnsMethod
voidapply_uv_anchor(anchor: HammeriteFaceUvAnchor, moved: Transform3D)
Vector2calculate_uv(point: Vector3)
PackedVector2Arraycalculate_uvs(vertices: Vector3[])
HammeriteFaceUvAnchorcapture_uv_anchor()
HammeriteFacecarved(ancestor: HammeriteFace, cut_plane: Plane, named: StringName, resolved: Texture2D, corners: Vector3[]) static
voidclear_property(prop_name: StringName)
boolcontains_point(point: Vector3)
voidcopy_texture_no_emit(other: HammeriteFace)
HammeriteFacefrom_face(other_face: HammeriteFace) static
HammeriteFacefrom_plane(p: Plane, tex: Texture2D) static
float[]get_barycentric_weights(p: Vector3, a: Vector3, b: Vector3, c: Vector3) static
Vector3get_centroid()
Texture2Dget_emission_texture()
PackedVector3Arrayget_light_colors()
Vector4get_light_modulation()
PackedVector4Arrayget_light_placements()
Vector2i[]get_looks_log() static
intget_looks_log_epoch() static
ShaderMaterialget_material()
floatget_minimum_distance_to_face(other_face: HammeriteFace)
floatget_minimum_distance_to_point(point: Vector3)
Variantget_property(prop_name: StringName, default = null)
Vector3[]get_vertices()
boolhas_point_on_plane(point: Vector3)
boolhas_property(prop_name: StringName)
voidinvalidate()
boolis_connected_to(other_face: HammeriteFace, epsilon: float = 0.01)
boolis_exact(face: HammeriteFace)
boolis_parallel_to(other_face: HammeriteFace, max_angle_deg: float = 5.0)
boolis_sky()
boolis_uv_locked(editor_default: bool)
Vector2[]project_vertices(vertices: Vector3[])
Vector2projected_uv(point: Vector3)
voidset_lightmap_no_emit(new_lightmap: HammeriteBakedLightmap)
voidset_lightmap_scale_no_emit(new_lightmap_scale: float)
voidset_looks_log_epoch(value: int) static
voidset_material(mat: ShaderMaterial)
voidset_plane_distance(dist: float)
voidset_plane_no_emit(new_plane: Plane)
voidset_property(prop_name: StringName, value)
voidset_texture_named_no_emit(named: StringName, resolved: Texture2D)
voidset_texture_no_emit(new_texture: Texture2D)
voidset_uv_transform_no_emit(new_transform: Transform2D)
floattexels_per_unit() static
Vector2texture_span()
voidupdate_light_parameters(light_index: int)
Vector2iuv_axes()
StringNameuv_lock_property() static

Signals

attribute_changed

signal attribute_changed()

Emitted when the face's look changes - texture, lightmap, lightmap scale, UV transform. The owning brush answers by remeshing. Not emitted by the _no_emit setters or set_property().

geometry_changed

signal geometry_changed()

Emitted when plane changes through its setter. The owning brush answers by recompiling.

Property descriptions

centroid

var centroid: Vector3

The centroid cache beside vertices; Vector3.INF until first computed. Prefer get_centroid().

Experimental: The brush's cache, not part of the supported API.

drawn

var drawn: bool

Whether a view has drawn this face, and so wants its look changing logged in get_looks_log().

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

drawn_in

var drawn_in: Vector2i

The room (HammeriteCell.key()) a view drew this face in.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

lightmap

var lightmap: HammeriteBakedLightmap

This face's region of the baked lightmap atlas, or null before a bake. Setting it emits attribute_changed.

lightmap_scale

var lightmap_scale: float = 16.0

World units per lightmap texel: larger is coarser. 16 by default; the packer never goes finer than 4. Takes effect at the next bake.

plane

var plane: Plane = Plane(0, 1, 0, 0)

The plane of the face, in map space, normal pointing out of the brush. Emits geometry_changed when set to a plane not approximately equal to the current one.

properties

var properties: Dictionary = {}

Authored values hung on this surface by addons, keyed by namespaced StringName - portal/name - so two addons cannot collide. The brush-side equivalent (HammeriteBrush.properties) has a schema registry behind it, because a brush has a TIER: a schema can say "rooms only" and the inspector can show exactly the sections that apply. A face has no such discriminator, and no generic face inspector to feed, so faces carry the storage without the declaration machinery - whoever writes a key is also the one who knows its default, and passes it to get_property(). Give faces a registry when something needs to enumerate face properties it did not itself declare. Sparse, like the brush bag: only what was authored is here, so an untouched face serializes nothing. The first user is portal naming - a doorway is an aperture between two air brushes, and the coplanar face it is punched from is the only authored thing that survives a recompile, so it is where a portal's identity lives.

texture

var texture: Texture2D

The texture itself, resolved from texture_name the first time it is asked for. Deliberately NOT exported: what a map stores is the name, and a resource reference stored beside it would be a second answer to the same question - the one that goes stale when a texture moves. Assigning a texture still records its name, so texturing a face with a resource in hand is exactly as good as naming it. A texture built at runtime has no name to record, so it lives for as long as the face is in memory and no longer. Give it one with HammeriteTextureLibrary.add_texture() if it should outlive that.

texture_name

var texture_name: StringName = &""

What this face is textured with, by name (#37) - "gbl_stone_cobble_01", resolved through HammeriteTextureLibrary. This is what a map records: a name survives a texture moving, being re-imported, or being added after the map was made, and a map missing one shows the author which wall is wrong instead of failing to load.

uv2_coordinates

var uv2_coordinates: Vector2[] = []

Lightmap UVs, one per corner in get_vertices() order, written by the bake. Used only when the count matches the corners.

uv_transform

var uv_transform: Transform2D = Transform2D(1, 0, 0, 1, 0, 0)

Applied after the world-axis projection (projected_uv()) to give the face's texture UVs: the author's shift, scale and rotation. Setting it emits attribute_changed.

vertices

var vertices: Vector3[]

The corner cache the owning brush fills (map space). Read corners through get_vertices() instead: this may be stale or empty. Worked out from the planes, so never saved.

Experimental: The brush's cache, not part of the supported API.

Method descriptions

apply_uv_anchor()

func apply_uv_anchor(anchor: HammeriteFaceUvAnchor, moved: Transform3D) -> void

Put the texture back where anchor saw it, given this face has since been moved by moved - the same transform that was applied to the geometry. Call it AFTER the plane has been updated: the projection depends on which world axes the face now runs along, and the whole point is to solve against the new ones. Rotation far enough to change those axes is where this stops being exact - the projection is discontinuous there, so no transform carries the texture across it. The face keeps the closest fit the three points allow and the seam lands where it lands.

calculate_uv()

func calculate_uv(point: Vector3) -> Vector2

The texture UV at point on this face, as calculate_uvs() gives a corner's.

calculate_uvs()

func calculate_uvs(vertices: Vector3[]) -> PackedVector2Array

Texture UVs for vertices (map space): projected_uv() then uv_transform.

capture_uv_anchor()

func capture_uv_anchor() -> HammeriteFaceUvAnchor

What the texture looks like right now, for apply_uv_anchor() to put back. Sampled from the plane rather than the vertices: a move can change how many corners a face has - a clip, an extrude - and three points derived from the plane are three points however that goes.

carved()

static func carved(ancestor: HammeriteFace, cut_plane: Plane, named: StringName, resolved: Texture2D, corners: Vector3[]) -> HammeriteFace

A side of a carved fragment: ancestor's surface as from_face() copies it, or a cut on cut_plane when there is no ancestor; wearing named, already resolved to resolved; with its corners known. What the setters would leave on a face nothing listens to yet, assigned without them - a compile makes one of these for every side of every fragment, and going through the setters twice over was half of what making a fragment cost.

clear_property()

func clear_property(prop_name: StringName) -> void

Drop this face's authored value for prop_name.

contains_point()

func contains_point(point: Vector3) -> bool

Check if this face's polygon contains the given point. Assumes the point is coplanar with the face plane.

copy_texture_no_emit()

func copy_texture_no_emit(other: HammeriteFace) -> void

Take other's texture as from_face() does - its name, and whatever it had already resolved - without announcing it. For a face being built, which nothing draws yet: carving restores the textures of every fragment it makes, and each announcement made the fragment re-intersect all of its planes, once per face (#111).

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

from_face()

static func from_face(other_face: HammeriteFace) -> HammeriteFace

A new face with other_face's plane, texture, lightmap scale, lightmap, UV transform and a deep copy of its properties. Not attached to any brush, and with no corners cached.

from_plane()

static func from_plane(p: Plane, tex: Texture2D) -> HammeriteFace

A new face on p wearing tex (which may be null), attached to no brush.

get_barycentric_weights()

static func get_barycentric_weights(p: Vector3, a: Vector3, b: Vector3, c: Vector3) -> float[]

The barycentric weights [u, v, w] of p in triangle a b c, or all zero for a degenerate triangle.

get_centroid()

func get_centroid() -> Vector3

The centre of this face's polygon in map space, worked out from the planes each call; with no brush, the middle of the corners it was last given, or the origin with none.

get_emission_texture()

func get_emission_texture() -> Texture2D

The emission texture paired with this face's colour texture, or null when it has none. Paired by naming convention - see HammeriteTextureLibrary.emission_for(), which a decal that opted into the bake asks the same way. The emission texel is radiance the surface emits by itself, so it lights the surface and bounces onto its neighbours WITHOUT any light entity reaching it. It always contributes STATIC lighting: an emissive texel is a property of the surface, not of a light that could be switched or modulated, so it never claims one of the four dynamic light slots. Resolved lazily and cached; the cache is dropped whenever the colour texture changes.

get_light_colors()

func get_light_colors() -> PackedVector3Array

The colours of the up-to-four dynamic light slots from lightmap, or white for each when there is no lightmap.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

get_light_modulation()

func get_light_modulation() -> Vector4

The current modulation of the four light slots from lightmap, or all ones when there is none.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

get_light_placements()

func get_light_placements() -> PackedVector4Array

Per-slot light placement data from lightmap, or zero for each when there is no lightmap.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

get_looks_log()

static func get_looks_log() -> Vector2i[]

The rooms a face drawn in one (drawn_in) has changed its look in - its plane, texture, lightmap, lightmap scale, UV transform or UV2 - however it was changed, said or not, in the order they changed: what lets a view that keeps the rooms it drew look again at only those, rather than at every face of the level to be sure. Plain keys, so nothing here outlives the faces; emptied when it grows long, which get_looks_log_epoch() says, and a view that sees it moved looks at every room again.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

get_looks_log_epoch()

static func get_looks_log_epoch() -> int

Bumped each time get_looks_log() is emptied for growing too long; a view that sees it move must look at every room again.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

get_material()

func get_material() -> ShaderMaterial

What this face is currently drawn with, or null before anything has built one.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

get_minimum_distance_to_face()

func get_minimum_distance_to_face(other_face: HammeriteFace) -> float

The smallest corner-to-corner distance between this face and other_face.

get_minimum_distance_to_point()

func get_minimum_distance_to_point(point: Vector3) -> float

The distance from point to this face's nearest CORNER (not its nearest point), or INF with no corners.

get_property()

func get_property(prop_name: StringName, default = null) -> Variant

The authored value of surface property prop_name, or default when this face was never given one. Faces have no schema registry, so the caller supplies the default - see properties.

get_vertices()

func get_vertices() -> Vector3[]

This face's polygon corners in map space, wound about the outward normal - the brush's cached array, not a copy. With no brush, the corners it was last given, or none.

has_point_on_plane()

func has_point_on_plane(point: Vector3) -> bool

Whether point lies on this face's plane, within 0.001 units. Not bounded by the polygon.

has_property()

func has_property(prop_name: StringName) -> bool

True when this face carries an authored value for prop_name.

invalidate()

func invalidate() -> void

Mark the owning brush's cached corners stale (HammeriteBrush.invalidate()). Does nothing without a brush.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

is_connected_to()

func is_connected_to(other_face: HammeriteFace, epsilon: float = 0.01) -> bool

Check if this face shares an edge with another face. Two faces are connected if they share at least one edge (two vertices). We project edges onto each face's plane and check for 2D segment overlap.

is_exact()

func is_exact(face: HammeriteFace) -> bool

Whether face's polygon has exactly the same corners, compared with exact float equality and ignoring order.

is_parallel_to()

func is_parallel_to(other_face: HammeriteFace, max_angle_deg: float = 5.0) -> bool

Whether this face's normal is within max_angle_deg of other_face's. Compares only which way they face: two parallel walls a metre apart are parallel.

is_sky()

func is_sky() -> bool

True when this face is a SKY face: a hole to the outside rather than a lit surface (cf. Quake's SURF_SKY and Source's toolsskybox). Identified by texture name, matching the convention already in use for alpha masks, which a leading "!" marks. A sky face is one whose texture name begins with "sky_" (case-insensitively). The trailing underscore matters: a bare "sky" prefix would also claim "skyline_bricks.png", turning an ordinary wall into a hole in the world. A sky face is an APERTURE. It is not baked (nothing to light), it does not occlude (it is a hole, not solid), and it is what makes the cell behind it exterior.

is_uv_locked()

func is_uv_locked(editor_default: bool) -> bool

Whether this face's texture should follow its geometry, falling back to editor_default when the face has no opinion.

project_vertices()

func project_vertices(vertices: Vector3[]) -> Vector2[]

vertices in 2D, with the world axis this face's normal is most aligned with dropped - an axis-aligned projection, not one onto the face plane, so lengths are not preserved on a slope. For the shape of the face - its area, what lies inside it - where which way round the two axes come does not matter. Its texture runs along uv_axes() instead, which orders them so the texture stands upright on a wall. The bake lays out the lightmap with this one, so the two stay apart.

projected_uv()

func projected_uv(point: Vector3) -> Vector2

Where point falls on the texture BEFORE this face's uv_transform is applied. The projection alone: the face's world position dropped onto the two axes least aligned with its normal, in texture widths. Separating it from the transform is what lets a UV editor draw the face as a shape over a fixed texture and move it about - the shape is this, and everything the author does is the transform. An untextured face is legitimate - one freshly cut, or built by a tool or a test - and asking a null texture for its size used to crash here, once per face per remesh, which buried whatever else the log was trying to say. The nominal size texture_span() gives one is meaningless but survivable, which is the right way round.

set_lightmap_no_emit()

func set_lightmap_no_emit(new_lightmap: HammeriteBakedLightmap) -> void

Set lightmap and update the current material's lightmap uniforms, without emitting.

set_lightmap_scale_no_emit()

func set_lightmap_scale_no_emit(new_lightmap_scale: float) -> void

Set lightmap_scale without emitting.

set_looks_log_epoch()

static func set_looks_log_epoch(value: int) -> void

Bumped each time get_looks_log() is emptied for growing too long; a view that sees it move must look at every room again.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

set_material()

func set_material(mat: ShaderMaterial) -> void

Record mat as what this face is drawn with, so baked-light changes are pushed into it. Called by HammeriteBrush.build_face_material(); pass null to stop listening.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

set_plane_distance()

func set_plane_distance(dist: float) -> void

Move the plane along its normal to distance dist from the origin, through plane.

set_plane_no_emit()

func set_plane_no_emit(new_plane: Plane) -> void

Set plane without emitting. Still marks the brush's corners stale.

set_property()

func set_property(prop_name: StringName, value) -> void

Author surface property prop_name. Deliberately does NOT emit attribute_changed: that signal means "this face needs remeshing", and a property changes no pixel. Naming a doorway must not rebuild the world.

set_texture_named_no_emit()

func set_texture_named_no_emit(named: StringName, resolved: Texture2D) -> void

Take named as this face's texture, already resolved to resolved, without announcing it.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

set_texture_no_emit()

func set_texture_no_emit(new_texture: Texture2D) -> void

Setting texture without the attribute_changed, so a caller retexturing many faces remeshes their brush once.

set_uv_transform_no_emit()

func set_uv_transform_no_emit(new_transform: Transform2D) -> void

Set uv_transform without emitting.

texels_per_unit()

static func texels_per_unit() -> float

Texels of a surface texture to one world unit, and therefore the scale every face's UVs are calculated at. A texel is a quarter of a unit. Four, which is the convention this toolkit inherits from Hammer's texture scale of 0.25, and it is what makes textures and the grid agree: a 256x256 texture covers 64 units, one square of the default 64-unit grid, so a wall tiles without anyone measuring anything. Changing it re-scales every UV in every map, so it is a constant rather than a setting.

texture_span()

func texture_span() -> Vector2

How many world units one repeat of this face's texture covers: its size in texels over texels_per_unit(). The one place a texture's size becomes a distance, so the projection a face is drawn with and the one it is baked with cannot disagree about it.

update_light_parameters()

func update_light_parameters(light_index: int) -> void

Push slot light_index's colour and modulation into this face's material. Connected to the lightmap's light_parameter_changed; does nothing without a material.

Experimental: Bookkeeping of the views that draw it, not part of the supported API.

uv_axes()

func uv_axes() -> Vector2i

The two world axes this face's texture runs along, chosen by which one its normal is most aligned with - the wall is textured across the two directions it actually faces.

uv_lock_property()

static func uv_lock_property() -> StringName

Whether this face's texture is carried when its geometry moves (#51). Absent means "whatever the editor is set to", which is why every reader passes the global default in rather than this having one of its own. Set on a face, it pins that face either way - a wall whose texture must not move while everything around it does, or the reverse.

Hammerite 2.0 · built from 6d57ab2d, 2026-10-17