// Shared helpers, prepended to every shader by the loader (after #version). float hash13(vec3 p) { p = fract(p * 0.1031); p += dot(p, p.zyx + 31.32); return fract((p.x + p.y) * p.z); } // Smooth value noise in 0..1 float valueNoise(vec3 p) { vec3 i = floor(p); vec3 f = fract(p); vec3 u = f * f * (3.0 - 2.0 * f); return mix(mix(mix(hash13(i), hash13(i + vec3(1.0, 0.0, 0.0)), u.x), mix(hash13(i + vec3(0.0, 1.0, 0.0)), hash13(i + vec3(1.0, 1.0, 0.0)), u.x), u.y), mix(mix(hash13(i + vec3(0.0, 0.0, 1.0)), hash13(i + vec3(1.0, 0.0, 1.0)), u.x), mix(hash13(i + vec3(0.0, 1.0, 1.0)), hash13(i + vec3(1.0, 1.0, 1.0)), u.x), u.y), u.z); } // Three octaves of noise in 0..1 float fbm3(vec3 p) { float sum = 0.0, amp = 0.5; for (int i = 0; i < 3; i++) { sum += amp * valueNoise(p); p = p * 2.03 + 17.1; amp *= 0.5; } return sum / 0.875; } // Hand-drawn wobble: world-space displacement, identical in scene and shadow // pass so shadows bend with the geometry. t is the stepped animation clock. const float WOBBLE_AMPLITUDE = 0.07; const float WOBBLE_SCALE = 0.45; const float WOBBLE_SPEED = 0.8; vec3 wobble(vec3 p, float t) { vec3 q = p * WOBBLE_SCALE + vec3(t * WOBBLE_SPEED); return (vec3(valueNoise(q), valueNoise(q + 19.7), valueNoise(q + 43.1)) - 0.5) * 2.0 * WOBBLE_AMPLITUDE; } // ---------- Painterly lighting (shared by scene and figure shaders) ---------- // 1 = lit, 0 = shadowed. lightSpacePos is already projected. // 3x3 PCF, each tap blended between its four texels (bilinear): the edge runs // smoothly between texels instead of in stair steps. float shadowVisibility(sampler2D map, vec4 lightSpacePos, vec3 n, vec3 l) { vec3 p = lightSpacePos.xyz / lightSpacePos.w * 0.5 + 0.5; if (p.x <= 0.0 || p.x >= 1.0 || p.y <= 0.0 || p.y >= 1.0 || p.z >= 1.0) return 1.0; float bias = 0.0003 + 0.0010 * (1.0 - clamp(dot(n, l), 0.0, 1.0)); vec2 size = vec2(textureSize(map, 0)); vec2 uv = p.xy * size - 0.5; vec2 cell = floor(uv), f = uv - cell; float lit[16]; // compare results on the 4x4 texels round the point for (int y = 0; y < 4; ++y) for (int x = 0; x < 4; ++x) lit[y * 4 + x] = (p.z - bias > texture(map, (cell + vec2(x - 1, y - 1) + 0.5) / size).r) ? 0.0 : 1.0; float sum = 0.0; for (int y = 0; y < 3; ++y) for (int x = 0; x < 3; ++x) { float a = mix(lit[y * 4 + x], lit[y * 4 + x + 1], f.x); float b = mix(lit[(y + 1) * 4 + x], lit[(y + 1) * 4 + x + 1], f.x); sum += mix(a, b, f.y); } return sum / 9.0; } // Blotchy paint patches and brush streaks. Returns the patch value in .w. vec4 paintAlbedo(vec3 base, vec3 worldPos, float t) { float patches = fbm3(worldPos * 0.9 + vec3(0.0, t * 0.06, 0.0)); /* drifting slowly */ float streaks = valueNoise(vec3(worldPos.x * 0.7, worldPos.y * 5.0, worldPos.z * 0.7) + t * 0.2); vec3 albedo = base * (0.82 + 0.36 * patches); albedo += (vec3(streaks, patches, 1.0 - streaks) - 0.5) * 0.08; return vec4(albedo, patches); } // Three flat tones with hand-painted, wobbly borders, plus sky bounce. // wrap > 0 lets light reach around the form (soft faces on figures). // The wobble only shows along borders: at shadow edges, and across curved // figures. A flat wall or roof lit at the turn between two tones keeps one // tone (the noise used to boil over the whole face like drifting clouds). vec3 paintLight(vec3 albedo, vec3 n, vec3 l, float visible, vec3 worldPos, float t, vec3 lightColor, vec3 shadowColor, vec3 skyTop, float wrap) { float wobbly = fbm3(worldPos * 1.7 + t * 0.5) - 0.5; float edge = clamp(visible + wobbly * 0.6 * smoothstep(0.0, 0.25, visible * (1.0 - visible) * 4.0), 0.0, 1.0); float light = max((dot(n, l) + wrap) / (1.0 + wrap), 0.0) * edge; float jitter = wrap > 0.0 ? wobbly * 0.3 * smoothstep(0.0, 0.05, light) : 0.0; float tone = 0.7 * smoothstep(0.08, 0.14, light + jitter) + 0.3 * smoothstep(0.55, 0.61, light + jitter); vec3 color = albedo * mix(shadowColor, lightColor, tone); return color + albedo * skyTop * 0.18 * (n.y * 0.5 + 0.5); } // Warm light of a campfire: xyz = where, w = how bright (0 = no fire). uniform vec4 fireLight; const vec3 FIRE_COLOR = vec3(1.0, 0.55, 0.22); vec3 fireGlow(vec3 albedo, vec3 n, vec3 worldPos) { if (fireLight.w <= 0.0) return vec3(0.0); vec3 to = fireLight.xyz - worldPos; float d = length(to); float face = max(dot(n, to / max(d, 1e-3)), 0.0) * 0.75 + 0.25; return albedo * FIRE_COLOR * fireLight.w * face / (1.0 + d * d * 0.15); } // Distance melts into the horizon color. vec3 applyFog(vec3 color, float distance, vec3 horizon) { float fog = 1.0 - exp(-distance * 0.018); return mix(color, horizon, fog * 0.85); }