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
| Type | Name | Default |
|---|---|---|
Plane | plane | |
StringName | texture_name | &"" |
Texture2D | texture | |
HammeriteBakedLightmap | lightmap | |
float | lightmap_scale | |
Transform2D | uv_transform | |
Vector2[] | uv2_coordinates | [] |
Vector3[] | vertices | [] |
Vector3 | centroid | Vector3(inf, inf, inf) |
Dictionary | properties | {} |
Vector2i[] | looks_log | [] |
int | looks_log_epoch | 0 |
bool | drawn | false |
Vector2i | drawn_in | Vector2i(0, 0) |
Methods
| Returns | Method |
|---|---|
HammeriteFace | from_face(other_face: HammeriteFace) static |
HammeriteFace | carved(ancestor: HammeriteFace, cut_plane: Plane, named: StringName, resolved: Texture2D, corners: Vector3[]) static |
HammeriteFace | from_plane(p: Plane, tex: Texture2D) static |
void | invalidate() |
void | set_material(mat: ShaderMaterial) |
ShaderMaterial | get_material() |
PackedVector3Array | get_light_colors() |
PackedVector4Array | get_light_placements() |
Vector4 | get_light_modulation() |
void | update_light_parameters(light_index: int) |
Vector3 | get_centroid() |
Vector3[] | get_vertices() |
float | get_minimum_distance_to_point(point: Vector3) |
float | get_minimum_distance_to_face(other_face: HammeriteFace) |
Texture2D | get_emission_texture() |
bool | is_sky() |
bool | has_point_on_plane(point: Vector3) |
bool | contains_point(point: Vector3) |
bool | is_exact(face: HammeriteFace) |
Plane | get_plane() |
void | set_plane(new_plane: Plane) |
void | set_plane_no_emit(new_plane: Plane) |
Texture2D | get_texture() |
void | set_texture(new_texture: Texture2D) |
void | copy_texture_no_emit(other: HammeriteFace) |
void | set_texture_named_no_emit(named: StringName, resolved: Texture2D) |
void | set_texture_no_emit(new_texture: Texture2D) |
HammeriteBakedLightmap | get_lightmap() |
void | set_lightmap(new_lightmap: HammeriteBakedLightmap) |
void | set_lightmap_no_emit(new_lightmap: HammeriteBakedLightmap) |
float | get_lightmap_scale() |
void | set_lightmap_scale(new_lightmap_scale: float) |
void | set_lightmap_scale_no_emit(new_lightmap_scale: float) |
void | set_plane_distance(dist: float) |
Transform2D | get_uv_transform() |
void | set_uv_transform(new_transform: Transform2D) |
void | set_uv_transform_no_emit(new_transform: Transform2D) |
bool | is_uv_locked(editor_default: bool) |
HammeriteFace.UvAnchor | capture_uv_anchor() |
void | apply_uv_anchor(anchor: HammeriteFace.UvAnchor, moved: Transform3D) |
Variant | get_property(prop_name: StringName, default = null) |
bool | has_property(prop_name: StringName) |
void | set_property(prop_name: StringName, value) |
void | clear_property(prop_name: StringName) |
HammeriteBrush | get_brush() |
void | set_brush(brush: HammeriteBrush) |
PackedVector2Array | calculate_uvs(vertices: Vector3[]) |
Vector2 | projected_uv(point: Vector3) |
Vector2 | texture_span() |
Vector2i | uv_axes() |
Vector2 | calculate_uv(point: Vector3) |
float[] | get_barycentric_weights(p: Vector3, a: Vector3, b: Vector3, c: Vector3) static |
Vector2[] | project_vertices(vertices: Vector3[]) |
bool | is_connected_to(other_face: HammeriteFace, epsilon: float = 0.01) |
bool | is_parallel_to(other_face: HammeriteFace, max_angle_deg: float = 5.0) |
Signals
geometry_changed
signal geometry_changed()
Emitted when plane changes through its setter. The owning brush answers by recompiling.
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().
Constants
TEXELS_PER_UNIT
const TEXELS_PER_UNIT = 4.0
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.
UV_LOCK_PROPERTY
const UV_LOCK_PROPERTY = &"uv_lock"
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.
Property descriptions
plane
var plane: Plane
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.
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.
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.
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
World units per lightmap texel: larger is coarser. 16 by default; the packer never goes finer than 4. Takes effect at the next bake.
uv_transform
var uv_transform: Transform2D
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.
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.
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.
centroid
var centroid: Vector3 = Vector3(inf, inf, inf)
The centroid cache beside vertices; Vector3.INF until first computed. Prefer get_centroid().
Experimental: The brush's cache, not part of the supported API.
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.
looks_log
var 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 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.
looks_log_epoch
var looks_log_epoch: int = 0
Bumped each time 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.
drawn
var drawn: bool = false
Whether a view has drawn this face, and so wants its look changing logged in looks_log.
Experimental: Bookkeeping of the views that draw it, not part of the supported API.
drawn_in
var drawn_in: Vector2i = Vector2i(0, 0)
The room (HammeriteCell.key()) a view drew this face in.
Experimental: Bookkeeping of the views that draw it, not part of the supported API.
Method descriptions
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.
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.
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.
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.
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.
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_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_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_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.
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.
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_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.
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_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_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.
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.
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.
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.
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.
get_plane()
func get_plane() -> Plane
Getter for plane.
set_plane()
func set_plane(new_plane: Plane) -> void
Setter for plane: invalidates the brush and emits geometry_changed unless the plane is approximately unchanged.
set_plane_no_emit()
func set_plane_no_emit(new_plane: Plane) -> void
Set plane without emitting. Still marks the brush's corners stale.
get_texture()
func get_texture() -> Texture2D
Getter for texture: resolves texture_name through HammeriteTextureLibrary on first call. Null when unnamed or when the library has no such texture.
set_texture()
func set_texture(new_texture: Texture2D) -> void
Setter for texture: records its library name too (a runtime texture with none keeps the old name) and emits attribute_changed.
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.
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
set_texture() without the attribute_changed, so a caller retexturing many faces remeshes their brush once.
get_lightmap()
func get_lightmap() -> HammeriteBakedLightmap
Getter for lightmap.
set_lightmap()
func set_lightmap(new_lightmap: HammeriteBakedLightmap) -> void
Setter for lightmap: updates the current material, invalidates the brush and emits attribute_changed.
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.
get_lightmap_scale()
func get_lightmap_scale() -> float
Getter for lightmap_scale.
set_lightmap_scale()
func set_lightmap_scale(new_lightmap_scale: float) -> void
Setter for lightmap_scale; emits attribute_changed when it changes.
set_lightmap_scale_no_emit()
func set_lightmap_scale_no_emit(new_lightmap_scale: float) -> void
Set lightmap_scale without emitting.
set_plane_distance()
func set_plane_distance(dist: float) -> void
Move the plane along its normal to distance dist from the origin, through set_plane().
get_uv_transform()
func get_uv_transform() -> Transform2D
Getter for uv_transform.
set_uv_transform()
func set_uv_transform(new_transform: Transform2D) -> void
Setter for uv_transform; emits attribute_changed when it changes.
set_uv_transform_no_emit()
func set_uv_transform_no_emit(new_transform: Transform2D) -> void
Set uv_transform without emitting.
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.
capture_uv_anchor()
func capture_uv_anchor() -> HammeriteFace.UvAnchor
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.
apply_uv_anchor()
func apply_uv_anchor(anchor: HammeriteFace.UvAnchor, 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.
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.
has_property()
func has_property(prop_name: StringName) -> bool
True when this face carries an authored value for prop_name.
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.
clear_property()
func clear_property(prop_name: StringName) -> void
Drop this face's authored value for prop_name.
get_brush()
func get_brush() -> HammeriteBrush
The brush this face bounds, or null for a loose face (or once the brush is freed; held weakly).
set_brush()
func set_brush(brush: HammeriteBrush) -> void
Point this face at brush. Assigning HammeriteBrush.faces does it; this does not add the face to the brush.
calculate_uvs()
func calculate_uvs(vertices: Vector3[]) -> PackedVector2Array
Texture UVs for vertices (map space): projected_uv() then uv_transform.
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.
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.
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.
calculate_uv()
func calculate_uv(point: Vector3) -> Vector2
The texture UV at point on this face, as calculate_uvs() gives a corner's.
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.
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.
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_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.
class UvAnchor
Three points on this face and where they land on the texture, taken before the geometry moves.
Three is exactly enough to pin an affine transform, which is what uv_transform is - so putting a texture back is solving for the transform that lands the same three points on the same three places, whatever the geometry did in between. That covers translation and scale exactly, and rotation for as long as the projection axes hold.
Properties
| Type | Name | Default |
|---|---|---|
Vector3[] | points | [] |
Vector2[] | uvs | [] |
Methods
| Returns | Method |
|---|---|
bool | is_valid() |
Property descriptions
points
var points: Vector3[] = []
Three points on the face, in map space, before the move.
uvs
var uvs: Vector2[] = []
Where each of points landed on the texture, HammeriteFace.uv_transform included.
Method descriptions
is_valid()
func is_valid() -> bool
Whether it holds the three points and three UVs HammeriteFace.apply_uv_anchor() needs.