HammerForge Texture and Materials¶
Last updated: October 22, 2026
This document describes the per-face material system, UV editing tools, and surface paint workflow.
Overview¶
HammerForge supports per-face materials on DraftBrushes. Each face stores: - A material index (from a shared palette). - UV projection settings or explicit UVs. - Optional paint layers with per-pixel weights.
Surface paint is separate from floor paint. Floor paint is a grid-based system that generates geometry. Surface paint is a per-face splat system that updates DraftBrush previews and optional face-material baking.
Data Model (FaceData)¶
Face data lives on each DraftBrush in faces and is serialized into .hflevel.
material_idx: Index intoMaterialManager.materials, or-1for no material. A slot that is neither is refused on assignment, andValidate Levelreports and resets one that is already on a face.uv_projection: Projection enum (planar X/Y/Z, box, cylindrical, face). An integer outside the enum is refused on assignment and falls back to Box UV on load, as does a record that names none, so a file cannot carry a projection no control can show.uv_scale,uv_offset,uv_rotation: Projection transform. Transform order: rotate → scale → offset (matches Valve 220 convention). A non-finite value is refused, as is a scale component of zero, which collapses the face onto one texel. A negative scale is allowed and mirrors the texture. The rotation is stored wrapped into[-pi, pi).custom_uvs: Optional explicit UVs (per vertex).uv_format_version: Serialization version (3 = current). Old data is auto-migrated on load: version 0 used a different transform order, version 1 projected in the brush's space, and version 2 ran a wall's V up the wall.paint_layers: Array of paint layers.uv_group,uv_group_justify: The linked surface the face is in, 0 for none, and the Justify mode last used on it. Written only for a linked face.smoothing_group: Which faces shade as one smooth surface, 0 for flat. Faces in the same group that meet at a corner share the average of their normals there, across brushes and materials, on every bake path. New round brushes and the curved faces of arches and domes start in group 1; a face from an older file loads flat.
Paint layer fields:
- texture: Texture2D for the layer.
- weight_image: Image storing weights (R channel).
- blend_mode: Overlay, Multiply, Add.
- opacity: Layer opacity.
Getting Materials¶
Materials are Godot resources stored as .tres or .material files.
Using Prototype Textures (Quick Start)¶
For rapid prototyping without creating custom materials: 1. Open the Paint tab in the dock. 2. Click Refresh Prototypes under Material Library in the Materials section. 3. 150 built-in SVG textures are added to the palette (15 patterns x 10 colors). 4. Browse textures visually in the Material Browser thumbnail grid — filter by pattern, color, or search text. 5. Click a thumbnail to select it, then click Assign to Selected Faces (or right-click → "Apply to Selected Faces"). 6. Alternatively, press T to use the Texture Picker — click any face to sample its material.
For full details, see Prototype Textures.
Creating Custom Materials¶
- In the FileSystem dock, right-click a folder (e.g.
materials/). - Choose
New Resource->StandardMaterial3D(orShaderMaterial). - Save it as a
.tresfile (example:materials/test_mat.tres).
You can then click Add in the Paint tab → Materials → Material Library and pick that resource. Or drag the file from the FileSystem dock onto a brush face: it joins the palette and paints that face, or the whole face selection when the face is picked, in one undo step.
Materials Palette & Visual Browser¶
The Paint tab → Materials section hosts a visual material browser (HFMaterialBrowser) that replaces the old text-only list.
Visual Browser¶

The browser displays a scrollable thumbnail grid of 64px cells, as many to a row as the dock is wide. Each cell shows the actual texture preview with a short label. Features: - Search bar: live text filtering by material name. - Pattern filter: dropdown with 15 patterns + "All". - Color swatches: 10 clickable color buttons + "All" to filter by color. - View toggle: Prototypes (only built-in), Palette (all loaded), Favorites (starred only). - Hover preview: hovering a thumbnail temporarily applies that material to selected faces in the viewport. - Right-click context menu: Apply to Selected Faces, Apply to Whole Brush, Apply + Re-project (Box UV), Set as Active Material, Toggle Favorite, Copy Name. - Count: "X of Y materials" in the Materials header, with filter feedback.
Workflow¶
- Open Material Library and click
Addto load a material resource, orRefresh Prototypesto load all 150 built-in textures. - Browse the thumbnail grid — use filters and search to narrow down.
- Click a thumbnail to select it as the current material.
- Enable
Face Select Modeand click faces in the viewport (Select tool). - Click
Assign to Selected Faces(or right-click the thumbnail → "Apply to Selected Faces").
Texture Picker (Eyedropper)¶
Press T to activate the texture picker. Click any face in the viewport to sample its assigned material — the browser selection updates to match. Useful for "what material is this?" queries and quick material matching.
The picker also keeps the face's whole look: its material, projection, scale, offset and rotation. Alt+T (Apply Picked Look, also a button in the UV Editor) gives that look to every selected face as one undo step, clearing a hand-dragged UV layout on each so the copied settings show. Shift+T (Apply Last Texture) gives the material alone.
Favorites and Recent¶
Right-click any thumbnail and choose "Toggle Favorite". Switch the view toggle to "Favorites" to see only starred materials. Favorites are kept by resource path in user://hammerforge_prefs.json, so they survive an editor restart. The first nine carry their number in the Favorites view, and Alt+1 to Alt+9 apply them to the selected faces, or to the selected brushes when no face is selected, as one undo step.
A Recent row above the grid holds the last eight materials applied by any route, newest first, kept beside the favorites. A favorite whose file is gone shows as "!" with its old path in the tooltip, and an empty palette slot shows as "!" where it stands in the Palette view.
Notes:
- Per-face materials override the DraftBrush material for preview and face-material bake.
- The palette is saved in .hflevel.
- Library persistence: the palette can be saved to / loaded from a JSON library file via MaterialManager.save_library() / load_library(). This preserves the material list across projects. Missing or unloadable materials are preserved as null placeholder slots to keep palette indices stable.
- Unused materials: there is no usage tracker. The one that existed counted material_override, which HammerForge only uses for the draft preview tint, rather than FaceData.material_idx, which is what the Paint tab writes and the bake reads, so it reported the material every face was painted with as unused. It was removed rather than left as a correct-looking implementation of the wrong model.
UV Editor¶
The Paint tab → Face Tools → UV Editor section shows a simple per-face UV editor.
Workflow:
1. Select a face (Face Select Mode).
2. Use drag handles to move UV points. With UV Snap on, the default, a dragged point lands on the nearest multiple of the snap step; hold Ctrl (Cmd on macOS) to place it freely.
3. Click Reset Projected UVs to regenerate UVs from the projection mode.
Notes:
- A face is drawn from its projection until one of its points is dragged, which gives it custom UVs; showing it does not. A typed Scale, Offset or Rotate moves custom UVs as it moves the projection.
- UV edits trigger preview rebuilds on the owning DraftBrush.
- The canvas draws the face's albedo texture under the outline, one tile per UV unit, and the snap grid over it.
- x2, /2, Flip U, Flip V, Rotate 90, Fit U, Fit V and Smoothing act on every selected face as one undo step. Smoothing sets how the faces shade, not their UVs: see Smoothing Groups in the User Guide.
- Align to Face and Align to World put every selected face on the Face projection or on Box UV, as one undo step.
- Wrap carries the selected face's texture over an edge onto each face clicked next, one undo step a click (HFUVWrap.solve()).
- Link, Unlink and Select Group make, break and select linked surfaces, faces sharing one mapping that edits keep aligned.
- Texture Gizmo shows a gizmo in the viewport on the picked faces: the middle square slides the texture, an axis end stretches it, the ring turns it. Each drag is one undo step on every picked face.
Surface Paint (3D)¶
Surface paint uses the Paint tab → Surface Paint section with target set to Surface.
Workflow:
1. Enable Paint Mode.
2. Paint tab → Surface Paint section: set Paint Target = Surface.
3. Select a layer and pick a texture.
4. Paint on faces in the viewport.
Controls:
- Radius (UV) scales the brush relative to the face UV space.
- Strength controls weight accumulation.
- Layer selects which weight image is painted.
Notes:
- Weights are stored per face as images (default 256x256).
- A face takes at most 8 paint layers. They are composited into one image at every texel on each preview rebuild and again at bake, so past a handful each extra layer costs preview time, save size and load time without looking different.
- The painted composite replaces the material's albedo texture and nothing else. The face keeps its normal map, roughness and metallic maps, emission, UV scale and offset, and cull mode, in the preview and in the bake.
- A face whose material is a ShaderMaterial cannot be painted over: there is no way to composite an albedo image into a shader, so the shader is kept, the paint is not drawn, and a warning says so. The material atlas treats a ShaderMaterial the same way.
- Surface paint updates the DraftBrush preview immediately.
- Surface paint does not modify floor paint layers.
- If paint affects floors, set Paint Target = Surface in the Paint tab → Surface Paint section.
Bake Integration¶
A bake option Use Face Materials switches the bake pipeline to per-face materials.
Behavior: - Each face is grouped by its assigned material. - With no cut brushes in the level, faces are baked directly from face data and no CSG runs. - One or more cut brushes moves the whole bake onto the CSG path, because independent face triangulation has no boolean stage. Per-face materials still reach the baked mesh: a textured brush enters the boolean as a mesh with one surface per material and comes out with them intact. - Every round brush enters the boolean as its own faces, textured or not, so a cut does not change its segments or turn its ring (#1596). The boolean works out its own normals, so the smoothing groups are put back on what it made afterwards. - The interior a cut exposes takes the texturing of the brush that cut it, face by face: the boolean gives a carved face the material of the face that carved it. Texture the cutter to texture the reveal. An untextured cutter leaves it bare, and a mirrored cutter stays on its primitive, so painting one changes nothing.
When to use: - Leave it on. Cutting a window, a doorway or a vent no longer costs the level its texturing. - Turn it off only to bake the whole level on one material per brush, which is faster and is what the CSG path did for everything before.
Serialization (.hflevel)¶
.hflevel saves include:
- Materials palette.
- Per-brush face data (materials, UVs, paint layers, smoothing groups and linked surfaces).
- Floor paint layers (unchanged).
Paint weights are stored as embedded PNG bytes (base64) per layer.
Projection Space¶
The planar projections read a vertex's position in the level, not in its
brush. Two brushes the same size in different places therefore get different
UVs, and a wall built from several brushes reads as one surface with the texture
running across the seams. uv_offset is an offset from the world grid, which is
what the .map formats already mean by it -- hf_map_valve220.gd writes world
texture axes.
Cylindrical is the exception and stays in the brush's own space. Its angle is measured about the brush's axis, so taking it in world space would spin the texture as the brush moved.
The Face projection reads positions in the level too, along axes that come off
the face: U is up x normal, level along the face, and V runs down its slope,
both in its plane (FaceData.face_axes_for()). A floor or a ceiling has no
level direction and reads Planar Y's axes.
Projection Axes¶
The planar axes are qbsp's base axes turned Y-up (#907). A floor or ceiling reads
(x, z). A Z wall reads (x, -y) and an X wall reads (-z, -y), so V runs down
every wall and Cylindrical measures V from the top: Godot reads V = 0 as the top
row of an image, and the texture comes out the right way up. These are the axes
a Classic Quake reader takes from the plane, so a Classic Quake export opens
looking as it does in the viewport. FaceData.projection_axes() is the one
definition, and the move compensation, the turn compensation and the Valve 220
export all read it.
Before #907 a wall's V ran up the wall and an X wall's U along +Z, which drew
every wall upside down. A face somebody had laid on by then (aligned, painted or
given UVs of its own) carries legacy_wall_axes and keeps those axes, so it
draws, paints and exports exactly as it did. An old scene marks its faces the
first time it opens, which LevelRoot.face_axes_version records; an older
.hflevel face is marked as it is read. A face on the defaults takes the new
axes. Re-project UVs and Apply + Re-project clear the mark.
UV Transform Order¶
The UV transform is applied as: rotate → scale → offset (matching Valve 220 convention). This means uv_rotation rotates the raw projected UV around the origin, then uv_scale is applied, then uv_offset shifts the result. This order ensures that offset values are stable regardless of rotation.
Serialized face data includes uv_format_version, currently 3, and legacy_wall_axes. On load, older data is auto-migrated.
Version 0 used a different order (scale+offset → rotate):
- Uniform scale (sx == sy): offset is rotated to match the new semantics.
- Non-uniform scale with rotation: UVs are baked into custom_uvs using the old transform, then parametric transforms are cleared.
Version 1 projected from the brush's own vertices rather than from the level, so an offset measured from the brush. A face below version 2 folds its brush's placement back into uv_offset the first time the brush tells it where that is, which cannot happen in from_dict() -- a face does not know where its brush is until the brush says so. Only an offset somebody set is folded in; a face still on zero was never positioned by hand and takes the new projection. A rotated brush cannot be corrected by an offset, because world projection is a different map there rather than the same one shifted, so those faces are baked into custom_uvs the way version 0's non-uniform scale is.
Version 2 ran a wall's V up the wall. A face below version 3 that was laid on by hand is marked legacy_wall_axes as it is read, and keeps those axes.
Carve and UV Preservation¶
When a brush is carved (boolean subtracted), clipped, hollowed or intersected, each face of a piece is a piece of one face of the original, and the cut keeps track of which (origins). The piece face takes that face's whole look through FaceData.copy_appearance_from(): material, projection, scale, offset and rotation, so the texture stays where it was on every surviving face. Its paint and sculpt are carried by HFConvexClip.carry_surface_detail(). A new face made where the cut went through takes the look of the original face nearest its direction. A piece of a linked face stays in its linked surface, and after a Clip or a Hollow a group last fitted is fitted again.
Known Limitations¶
- Face selection supports marquee/box selection across multiple brushes (added in the Improved Selection & Multi-Select wave). Lasso selection is not supported.
- A level with cut brushes bakes through CSG rather than through face triangulation. The per-face materials survive it; the material atlas does not run there.
- The interior face a cut exposes wears the cutting brush's own texturing.
- Surface paint is per-face and does not share weights across faces.
- Preview materials are rebuilt per face and can be heavy on very large brush counts.
- Hover preview applies to the first surface override material slot only; multi-material faces may not preview accurately.
See Also¶
- Prototype Textures -- 150 built-in SVG textures for greyboxing.
Suggested Testing¶
- Click Refresh Prototypes and confirm 150 materials appear in the browser grid with thumbnails.
- Click it a second time and confirm the palette stays at 150. A pattern/colour already in the palette is skipped, so the button is a refresh rather than a second helping.
- Remove a material that is not the last one and confirm brushes using a later slot keep the material they had. Faces that used the removed slot fall back to unset.
- Use pattern filter, color swatches, and search bar to narrow the grid. Verify counts in the status label.
- Switch between Prototypes / Palette / Favorites views.
- Right-click a thumbnail → Toggle Favorite. Switch to Favorites view and confirm it appears.
- Hover a thumbnail with faces selected and confirm temporary material preview in viewport.
- Press T (Texture Picker) and click a face — confirm the browser selection updates.
- Right-click → Apply to Selected Faces and confirm assignment.
- Right-click → Apply to Whole Brush and confirm all faces change.
- Right-click → Copy Name and confirm clipboard contents.
- Assign different materials to multiple faces and verify preview rebuild.
- Edit UVs and confirm the preview updates.
- Paint two layers and confirm blend behavior.
- Save and load
.hfleveland verify face data is restored. - Toggle
Use Face Materialsand compare bake output. - Apply a rotated texture (non-zero
uv_rotation), save, reload, and confirm texture alignment is preserved. - Carve a textured brush and confirm slice pieces retain the original texture alignment.
- Use Tile justify on a non-square face and confirm the texture tiles along the longer axis.