#version 330 in vec2 fragWorldPos; out vec4 finalColor; uniform float time; #define TRAIL_POINTS 12 uniform vec2 trailPoints[TRAIL_POINTS]; uniform float trailAges[TRAIL_POINTS]; uniform int trailCount; uniform float wakeIntensity; uniform vec4 foamColor = vec4(0.96, 0.99, 1.0, 1.0); uniform float wakeHalfWidthStart = 2.0; uniform float wakeHalfWidthEnd = 7.0; uniform float foamNoiseScale = 0.5; uniform float foamAnimSpeed = 2.4; float hash(vec2 p) { p = fract(p * vec2(123.34, 456.21)); p += dot(p, p + 45.32); return fract(p.x * p.y); } float valueNoise(vec2 p) { vec2 i = floor(p); vec2 f = fract(p); float a = hash(i); float b = hash(i + vec2(1.0, 0.0)); float c = hash(i + vec2(0.0, 1.0)); float d = hash(i + vec2(1.0, 1.0)); vec2 u = f * f * (3.0 - 2.0 * f); return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; } float segmentDistance(vec2 p, vec2 a, vec2 b, out float segT) { vec2 ab = b - a; float lenSq = dot(ab, ab); segT = lenSq > 0.0001 ? clamp(dot(p - a, ab) / lenSq, 0.0, 1.0) : 0.0; vec2 closest = a + ab * segT; return length(p - closest); } void main() { if (trailCount < 2) discard; float bestDist = 1e9; float bestAge = 0.0; for (int i = 0; i < TRAIL_POINTS - 1; i++) { if (i >= trailCount - 1) break; float segT; float d = segmentDistance(fragWorldPos, trailPoints[i], trailPoints[i + 1], segT); if (d < bestDist) { bestDist = d; bestAge = mix(trailAges[i], trailAges[i + 1], segT); } } float halfWidth = mix(wakeHalfWidthStart, wakeHalfWidthEnd, bestAge); if (bestDist > halfWidth) discard; float edgeFade = 1.0 - smoothstep(halfWidth * 0.55, halfWidth, bestDist); float ageFade = 1.0 - bestAge; float envelope = edgeFade * ageFade; vec2 pixelized = floor(fragWorldPos); float n = valueNoise(pixelized * foamNoiseScale + vec2(time * foamAnimSpeed, 0.0)); float churn = mix(0.55, 1.0, n); float speedScale = mix(0.65, 1.0, wakeIntensity); float mask = envelope * churn * speedScale; finalColor = vec4(foamColor.rgb, foamColor.a * mask); }