Birdoggydog's Builds

Giving decayed dungeon squares a worn picture in Godot

A new generator pass draws grime, rust and chipped paint over the house wall set, and a one-texel-per-square map tells the shader which squares wear it.

A decayed square in a dungeon now looks decayed. The house painted set has a worn picture for its wall, upper wall, floor and ceiling, and each square shows the new or the worn one by its state. The worn picture meets the new one with no step.

41 seconds: a new square beside a decayed one along a wall, the decayed and new ceiling, the hall’s hazard band worn and new, then the same hall with no theme and its floor.

Why did decay not show on painted walls?

The test that laid every wall kind in every theme showed that new and worn painted walls were identical. I said yes to worn pictures for cyberpunk and house first. A cyberpunk floor has no painted set, theme_of() hands it the house set, so one set of worn pictures covers both. The other three themes are a separate work item.

Four pairs of gray-blue metal panels and brown flagstones, each pair a clean picture followed by one with rust streaks, dark soot patches and missing paint.

The house set at double size, each picture new then worn: wall, upper wall, floor, ceiling.

How is the worn picture drawn?

It is not a separate picture. The kit pass style_paint.wear() takes the new picture’s level array (one palette index per texel, 64 texels per meter) and draws decay over it, so every seam and tile size matches. Levels 1 to 3 are paint, 4 and 5 are the hazard band, and the pass adds two rust levels, 6 and 7. All its noise wraps, so the result tiles. The stages run in this order:

  1. Grime. Two noise scales are blended, 0.75 at about half a meter and 0.25 at about 12 cm. In the top 35% of the noise (50% on the ceiling), the lighter body value drops to the darker one and a lit edge drops to the body. In the top 30% of those patches, one texel in five goes to the shadow value as soot.
  2. Rust at the seams. A noise gate lets rust run in broken lines along the row under each horizontal seam, and beside each vertical seam. One column in twelve also gets a stain running down from a horizontal seam, up to a third of a panel.
  3. Chips. About 3.5% of the paint is replaced with rust, with the texel below each chip turned to a darker rust as a lip.
  4. Scrapes. Twelve horizontal runs of 6 to 20 texels of bare metal, between 0.6 and 1.4 m up, where carts and shoulders would rub.
  5. The band. A third of the hazard band’s paint is lost to the plate under it, mostly at its foot, with rust in the worst of it. Under it, the plate is rusted through.
  6. On the floor, rust blotches and dark cracks that random-walk across the flags.

The wall and upper pictures are 192 by 256 and the floor and ceiling 192 by 192, written by tools/style_trial_paint.py next to the new ones as T_painted_<picture>_worn_D.png.

How does a square choose its picture?

PaintedSurface.material_for() takes worn and wear arguments, and the material cache key gains @worn or @wear<id>. has_worn(theme) is true only when all four worn files exist, so a theme without them behaves as before. The worn textures go into the shader as side_worn, top_worn and upper_worn.

Pieces laid one MultiMesh per member and state already know their state, so a decayed MultiMesh just gets the worn material (wear_all is 1.0). The faces the importer merges across squares, the slopes and the edging strips cannot do that, so they read a map. PaintedWear.mark() makes an R8 image with one texel per square of the plan, 255 where the square is decayed, and keeps it on the plan. In the fragment shader, the face’s world position is pushed 0.3 m out along its normal, so a wall reads the square it bounds and not the one behind it. It is divided by the 3 m square size and fetched with texelFetch from a nearest-filtered sampler. That decides which of the two pictures each texture lookup uses.

Which squares are decayed follows the rule the doors already use: the interior’s state where squares are laid, otherwise the flat floor’s hash against a room’s worn share times one minus the dungeon’s upkeep, and every square when a state is forced. If no square is decayed the map is null and the faces are untouched. The edging strip uses the worn wall picture on decayed squares so its band wears too, and turning painted off and on keeps the worn picture.

A per-state material for each merged face would not work, because one merged face crosses squares of both states.

tests/theme_walls now demands the worn picture on decayed squares: 328 checks, 0 failed.

I think it looks good, and if I get sick of it I’ll tone it down later.