204 lines
7.2 KiB
Markdown
204 lines
7.2 KiB
Markdown
# 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
|
|
|
|
```gdshader
|
|
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.
|