trench/docs/weathered_wall_shader.md
saarsena@gmail.com c2bb3893a9 initial commit
2026-04-22 10:19:57 -04:00

7.2 KiB

Weathered Wall Fragment Shader

Setup

  1. Create a new ShaderMaterial in Godot
  2. Assign this shader code
  3. Set your base wall texture in the base_texture slot
  4. Apply the material to your FuncGodot map geometry

Every surface using this material will look different because the noise samples from world position, not UV space. Two walls with the same texture and UVs get completely different weathering patterns because they're at different world coordinates.


Shader Code

shader_type spatial;

// Base texture
uniform sampler2D base_texture : source_color, filter_nearest, repeat_enable;

// === WEATHERING CONTROLS ===
// Gradient (rising damp)
uniform float gradient_strength : hint_range(0.0, 1.0) = 0.6;
uniform float gradient_exponent : hint_range(1.0, 5.0) = 3.0;
uniform float gradient_noise_warp : hint_range(0.0, 1.0) = 0.4;

// Hue shifting
uniform float shadow_cool_shift : hint_range(0.0, 0.15) = 0.06;
uniform float highlight_warm_shift : hint_range(0.0, 0.15) = 0.04;

// Surface grime (noise overlay)
uniform float grime_strength : hint_range(0.0, 1.0) = 0.3;
uniform float grime_scale : hint_range(0.5, 10.0) = 3.0;

// Noise scale for large weathering patterns
uniform float weather_scale : hint_range(0.1, 5.0) = 1.2;

// Stain blotches
uniform float stain_strength : hint_range(0.0, 1.0) = 0.35;
uniform float stain_scale : hint_range(0.5, 5.0) = 2.0;

// Overall intensity multiplier
uniform float weathering_intensity : hint_range(0.0, 2.0) = 1.0;


// ============================================================
// Noise functions (no external texture needed)
// ============================================================

vec2 hash22(vec2 p) {
    p = vec2(dot(p, vec2(127.1, 311.7)),
             dot(p, vec2(269.5, 183.3)));
    return -1.0 + 2.0 * fract(sin(p) * 43758.5453123);
}

float noise(vec2 p) {
    vec2 i = floor(p);
    vec2 f = fract(p);
    vec2 u = f * f * (3.0 - 2.0 * f);

    return mix(mix(dot(hash22(i + vec2(0.0, 0.0)), f - vec2(0.0, 0.0)),
                   dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0)), u.x),
               mix(dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0)),
                   dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0)), u.x), u.y);
}

float fbm(vec2 p, int octaves) {
    float value = 0.0;
    float amplitude = 0.5;
    float frequency = 1.0;
    for (int i = 0; i < octaves; i++) {
        value += amplitude * noise(p * frequency);
        frequency *= 2.0;
        amplitude *= 0.5;
    }
    return value;
}


// ============================================================
// RGB <-> HSV conversion
// ============================================================

vec3 rgb2hsv(vec3 c) {
    vec4 K = vec4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    vec4 p = mix(vec4(c.bg, K.wz), vec4(c.gb, K.xy), step(c.b, c.g));
    vec4 q = mix(vec4(p.xyw, c.r), vec4(c.r, p.yzx), step(p.x, c.r));
    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return vec3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);
}

vec3 hsv2rgb(vec3 c) {
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}


// ============================================================
// Main fragment
// ============================================================

void fragment() {
    // Sample base texture
    vec4 tex = texture(base_texture, UV);
    vec3 color = tex.rgb;

    // World position for noise - makes each wall unique
    vec3 world_pos = (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).xyz;
    vec2 wp = world_pos.xz;

    // UV.y for vertical gradient (0 = top, 1 = bottom)
    float v_pos = UV.y;

    // ---- COARSE WEATHERING NOISE ----
    float weather_noise = fbm(wp * weather_scale, 3) * 0.5 + 0.5;

    // ---- FINE SURFACE GRIME ----
    float grime_noise = fbm(wp * grime_scale + vec2(50.0, 50.0), 3);

    // ---- STAIN BLOTCHES ----
    float stain_noise = fbm(wp * stain_scale + vec2(100.0, 200.0), 4);
    stain_noise = smoothstep(0.15, 0.45, stain_noise);

    // Convert to HSV
    vec3 hsv = rgb2hsv(color);

    // ---- GRADIENT: RISING DAMP ----
    float gradient = pow(v_pos, gradient_exponent);
    float warped_gradient = gradient * mix(1.0, weather_noise * 1.6, gradient_noise_warp);
    warped_gradient = clamp(warped_gradient, 0.0, 1.0);

    float grad_effect = warped_gradient * gradient_strength * weathering_intensity;
    hsv.z -= grad_effect * 0.35;
    hsv.x += grad_effect * shadow_cool_shift;
    hsv.y += grad_effect * 0.12;

    // ---- HUE SHIFTING ----
    float luminance = hsv.z;
    float cool_warm = (luminance - 0.5) * 2.0;
    hsv.x += mix(shadow_cool_shift, -highlight_warm_shift, cool_warm * 0.5 + 0.5)
             * weathering_intensity;

    // ---- SURFACE GRIME ----
    float grime_effect = grime_noise * grime_strength * weathering_intensity * 0.08;
    hsv.z -= grime_effect;
    hsv.y += abs(grime_effect) * 0.5;

    // ---- STAIN BLOTCHES ----
    float stain_effect = stain_noise * stain_strength * weathering_intensity;
    hsv.z -= stain_effect * 0.1;
    hsv.x += stain_effect * 0.02;

    // Clamp HSV
    hsv.x = fract(hsv.x);
    hsv.y = clamp(hsv.y, 0.0, 1.0);
    hsv.z = clamp(hsv.z, 0.0, 1.0);

    // Back to RGB
    color = hsv2rgb(hsv);

    ALBEDO = color;
}

What Each Pass Does

Rising damp gradient: Runs on UV.y (top to bottom of the face) with the cubic exponent keeping the top clean. Coarse FBM noise warps the damp line so it's not perfectly horizontal — moisture follows porosity, not a ruler.

Hue shifting: Automatic based on luminance. Darker areas shift cool (blue-green), brighter areas shift warm (yellow). Same principle as palette ramps but continuous.

Surface grime: Fine-scale FBM that slightly varies value across brick faces. Every brick reads slightly different.

Stain blotches: Thresholded FBM that creates irregular dark patches, different on every wall.


Uniform Reference

Uniform Default What it does
gradient_strength 0.6 How dark the bottom gets
gradient_exponent 3.0 Curve shape (higher = top stays cleaner longer)
gradient_noise_warp 0.4 How much noise distorts the damp line
shadow_cool_shift 0.06 Blue shift in dark areas
highlight_warm_shift 0.04 Yellow shift in bright areas
grime_strength 0.3 Fine surface dirt intensity
grime_scale 3.0 Size of grime noise pattern
weather_scale 1.2 Size of large weather blotches
stain_strength 0.35 Dark stain patch intensity
stain_scale 2.0 Size of stain blotch pattern
weathering_intensity 1.0 Master dial — 0 = clean, 2 = ancient

Notes

  • UV.y assumption: The gradient assumes UVs run top-to-bottom on wall faces. If FuncGodot generates UVs differently, swap v_pos = UV.y to v_pos = 1.0 - (world_pos.y / wall_height) or similar.
  • Performance: The FBM loops are the most expensive part. Reduce octave counts if needed. On your Radeon 890M this should be fine.
  • Combining with Python script: You can use the Python script for baking variant textures for your material browser in TrenchBroom (visual reference while editing), then apply this shader at runtime for the actual in-game look. Best of both worlds.