#ifndef WATER_INCLUDED #define WATER_INCLUDED // Animated water: the exact noise pattern doesn't matter, use the cheaper hash #define NOISES_FAST_HASH #include "Assets/_Content/Shaders/Include/Noises.hlsl" // ------------------------------------------------------------ // WATER - ENKI BILAL STYLE (painted / pencil-sketch water) // ------------------------------------------------------------ // ============================================================ // TUNING // ============================================================ #define WATER_FLOW_DIR float2(0.55, 0.20) #define WATER_FLOW_SPEED 0.008 #define WATER_WARP_STRENGTH 2.0 #define WATER_MORPH_SCALE 0.07 #define WATER_MORPH_STRENGTH 1.6 #define WATER_MORPH_SPEED 0.10 #define WATER_BLOTCH_SCALE_A 0.095 #define WATER_BLOTCH_SCALE_B 0.140 #define WATER_BLOTCH_SCALE_C 0.190 // Swell / surface motion (vertical vertex displacement) #define WATER_WAVE_AMPLITUDE 0.1 // swell height, world units (rough scale) #define WATER_WAVE_SPEED 0.45 #define WATER_RIPPLE_AMPLITUDE 0.6 // mid-scale ripples on top of the swell #define WATER_RIPPLE_SPEED 1.2 #define WATER_WAVELET_AMPLITUDE 0.35 // dense small wavelets everywhere #define WATER_WAVELET_SPEED 2.4 #define WATER_LINE_WIDTH_MAIN 1.3 #define WATER_LINE_WIDTH_SECOND 1.1 #define WATER_LINE_WIDTH_HORIZON 2.8 // extra thick -> more stable, less shimmer // Pencil / painted texture #define WATER_CRAYON_SCALE 2.2 #define WATER_CRAYON_JITTER 0.10 // distorts blotch edges (scribble look) #define WATER_WASH_SCALE 0.85 // large-scale "mixing paint" color variation // Detail layers fade out beyond this LOD value (0 = horizon, 1 = near camera) #define WATER_LOD_THRESHOLD 0.08 // Always-on fine grain (NOT gated by LOD -- meant to stay visible even // at the horizon, where the surface otherwise reads as a flat wash). // Cheap value noise (see WaterCheapValueNoise below), two octaves at // different scales to avoid an obviously repeating pattern. #define WATER_FINE_GRAIN_SCALE_A 6.0 #define WATER_FINE_GRAIN_SCALE_B 23.0 #define WATER_FINE_GRAIN_WEIGHT_A 0.6 #define WATER_FINE_GRAIN_WEIGHT_B 0.4 #define WATER_FINE_GRAIN_STRENGTH 0.03 // "Vat" effect: the water level rises with distance from the camera, // like standing at the bottom of a giant tub instead of an open // ocean. Purely geometric (vertex-only), meant to feel subtly wrong // rather than obviously stylized -- keep WATER_BOWL_STRENGTH modest // unless you want it to read as an overt visual gag. #define WATER_BOWL_STRENGTH 2 // max vertical rise at the horizon, world units #define WATER_BOWL_RADIUS 60.0 // distance over which the rise ramps up #define WATER_BOWL_POWER 1.7 // >1 = stays flat near the boat, rises more sharply toward the edge // ============================================================ // UTILS // ============================================================ float WaterPosterize(float v, float steps) { return floor(saturate(v) * steps) / steps; } float2 WaterRotate(float2 p, float angle) { float s = sin(angle); float c = cos(angle); return float2(p.x * c - p.y * s, p.x * s + p.y * c); } // Thin line centered on coord == round(coord) float WaterStroke(float coord, float thickness, float softness) { float d = abs(frac(coord) - 0.5); return saturate(1.0 - smoothstep(thickness, thickness + softness, d)); } float WaterHash(float n) { return frac(sin(n) * 43758.5453123); } // Cheap value noise: 4 hash lookups + bilinear interpolation, no FBM // octaves. Much cheaper than FBMNoise3D -- good enough for fine grain // where you don't need the extra octave detail, just something to // break up flat color. float WaterCheapValueNoise(float2 uv) { float2 i = floor(uv); float2 f = frac(uv); float a = WaterHash(i.x + i.y * 57.0); float b = WaterHash(i.x + 1.0 + i.y * 57.0); float c = WaterHash(i.x + (i.y + 1.0) * 57.0); float d = WaterHash(i.x + 1.0 + (i.y + 1.0) * 57.0); f = f * f * (3.0 - 2.0 * f); // smoothstep interpolation return lerp(lerp(a, b, f.x), lerp(c, d, f.x), f.y); } float2 WaterDomainWarp(float2 uv, float scale, float strength, float speed, float seed) { float t = _Time.y; float wx = FBMNoise3D(float3(uv * scale + float2(t * speed, 0.0), seed), 1.0, 2, 2.0, 0.5) - 0.5; float wy = FBMNoise3D(float3(uv * scale + float2(0.0, t * speed * 0.8), seed + 30.0), 1.0, 2, 2.0, 0.5) - 0.5; return uv + float2(wx, wy) * strength; } // ============================================================ // 1) FLOW FIELD (color blotches) // ============================================================ // Distortion shared by flowA/flowB/flowC (same scale/strength/speed for // all three) so it only gets evaluated once per vertex/pixel instead of // once per field. float2 WaterSharedFlowWarp(float2 uv) { float2 morphed = WaterDomainWarp(uv, WATER_MORPH_SCALE, WATER_MORPH_STRENGTH, WATER_MORPH_SPEED, 5.0); float warpX = FBMNoise3D(float3(uv * 0.05, 40.0), 1.0, 2, 2.0, 0.5) - 0.5; float warpY = FBMNoise3D(float3(uv * 0.05, 80.0), 1.0, 2, 2.0, 0.5) - 0.5; return morphed + float2(warpX, warpY) * WATER_WARP_STRENGTH; } // offset decorrelates flowA/flowB/flowC from each other even though // they now share the same warped UV (cheap: no extra noise taps). float WaterFlowField(float2 warpedUV, float2 offset, float scale, float2 dir, float speedMul, float seed) { float t = _Time.y; float2 flowUV = warpedUV + offset + dir * (t * WATER_FLOW_SPEED * speedMul); return FBMNoise3D(float3(flowUV * scale, seed), 1.0, 3, 2.0, 0.55); } // ============================================================ // 2) THIN OUTLINES // ============================================================ float WaterThinContour(float field, float level, float pixelWidth) { float fw = fwidth(field) * pixelWidth; fw = max(fw, 1e-5); return 1.0 - smoothstep(0.0, fw, abs(field - level)); } float WaterCurrentOutlines(float2 uv, float flowA, float flowB, float flowC) { float outlines = 0.0; outlines += WaterThinContour(flowA, 0.42, WATER_LINE_WIDTH_MAIN); outlines += WaterThinContour(flowA, 0.58, WATER_LINE_WIDTH_MAIN); outlines += WaterThinContour(flowB, 0.50, WATER_LINE_WIDTH_MAIN * 0.85); outlines += WaterThinContour(flowC, 0.46, WATER_LINE_WIDTH_MAIN * 0.65); outlines = saturate(outlines); // Break the lines into irregular hand-drawn segments instead of a // continuous, perfectly-vector contour. float cellA = floor(uv.x * 2.4) + floor(uv.y * 2.4) * 57.0; float cellB = floor(uv.x * 2.4 + 0.5) + floor(uv.y * 2.4 + 0.5) * 61.0; float breakup = max(WaterHash(cellA), WaterHash(cellB) * 0.6); outlines *= saturate(breakup * 1.6); return outlines; } float WaterSecondaryInkTaches(float2 uv) { float2 morphed = WaterDomainWarp(uv, 0.09, 1.2, 0.07, 130.0); float2 flowUV = morphed + WATER_FLOW_DIR * (_Time.y * WATER_FLOW_SPEED * 0.5); float field = FBMNoise3D(float3(flowUV * 0.16, 99.0), 1.0, 2, 2.0, 0.55); float outlines = 0.0; outlines += WaterThinContour(field, 0.38, WATER_LINE_WIDTH_SECOND); outlines += WaterThinContour(field, 0.62, WATER_LINE_WIDTH_SECOND); return saturate(outlines); } // ============================================================ // 3) HORIZON: fewer lines, faster, more stable // ============================================================ float WaterHorizonField(float2 uv, float rowFrequency, float speed, float seed) { float t = _Time.y; float2 suv = uv * float2(rowFrequency * 0.05, rowFrequency); suv.y += t * speed; float wobble = FBMNoise3D(float3(uv * 0.02, seed + 5.0), 1.0, 2, 2.0, 0.5) - 0.5; suv.x += wobble * 1.4; return FBMNoise3D(float3(suv, seed), 1.0, 3, 2.0, 0.55); } // distFactor: 0 = near the camera, 1 = far toward the horizon float WaterHorizonLines(float2 uv, float3 positionWS, out float distFactor) { float camDist = distance(positionWS, _WorldSpaceCameraPos); distFactor = saturate(camDist / 55.0); // Low row frequency (wide spacing) avoids the "railway track" // strobing effect that high-frequency lines produce in perspective. float rowFrequency = 0.55; float widthNoise = FBMNoise3D(float3(uv * 0.02, 90.0), 1.0, 2, 2.0, 0.5); float baseWidth = WATER_LINE_WIDTH_HORIZON * lerp(0.7, 1.3, widthNoise); // Progressive thickness: already thick near the player (by design), // slightly thicker/denser still toward the horizon. float width = baseWidth * lerp(3.2, 4.6, distFactor); float lines = WaterThinContour(WaterHorizonField(uv, rowFrequency, 0.18, 20.0), 0.48, width); return saturate(lines); } // ============================================================ // 4) PENCIL / PAINT: edge jitter + color variation // ============================================================ // Distorts the tone value before quantization -> irregular blotch // edges (scribble look) instead of perfectly smooth noise curves. float WaterCrayonJitter(float2 uv, float seed) { float n1 = FBMNoise3D(float3(uv * WATER_CRAYON_SCALE, seed), 1.0, 2, 2.0, 0.55); float n2 = FBMNoise3D(float3(uv * WATER_CRAYON_SCALE * 2.3, seed + 7.0), 1.0, 2, 2.0, 0.5); return (n1 - 0.5) * 0.7 + (n2 - 0.5) * 0.3; } // Large non-posterized color patch: simulates pigment bleeding/mixing // in some areas, layered on top of the sharp-edged blotches. float3 WaterColorWash(float2 uv, float3 warmTint, float3 coolTint) { float2 morphed = WaterDomainWarp(uv, 0.04, 3.0, 0.05, 250.0); float wash = FBMNoise3D(float3(morphed * WATER_WASH_SCALE * 0.06, 300.0), 1.0, 3, 2.0, 0.55); return lerp(coolTint, warmTint, wash); } // Per-channel noise -> fine hue variation rather than plain brightness // variation. Sells the "reacting pigments" look instead of a flat, // uniform color. float3 WaterColorNoise(float2 uv, float scale, float seed) { float r = FBMNoise3D(float3(uv * scale, seed), 1.0, 2, 2.0, 0.55); float g = FBMNoise3D(float3(uv * scale, seed + 11.0), 1.0, 2, 2.0, 0.55); float b = FBMNoise3D(float3(uv * scale, seed + 23.0), 1.0, 2, 2.0, 0.55); return float3(r, g, b) - 0.5; } // ============================================================ // 5) OIL SLICK: multi-tone gradient (iridescent/grimy) instead of a // plain white-blue highlight. Simulates "petrol on water": black // -> algae -> iridescent violet -> rust -> dirty ochre. // ============================================================ float3 WaterOilGradient(float t) { float3 c0 = float3(0.006, 0.014, 0.018); // near-black oily base float3 c1 = float3(0.020, 0.130, 0.115); // dark algae green float3 c2 = float3(0.130, 0.070, 0.190); // violet / iridescent oil sheen float3 c3 = float3(0.360, 0.170, 0.060); // rust / oxidation float3 c4 = float3(0.430, 0.400, 0.260); // dirty ochre, near-highlight float3 col = lerp(c0, c1, smoothstep(0.0, 0.30, t)); col = lerp(col, c2, smoothstep(0.24, 0.52, t)); col = lerp(col, c3, smoothstep(0.48, 0.74, t)); col = lerp(col, c4, smoothstep(0.72, 1.00, t)); return col; } // Moon reflection "road": view/light half-vector highlight, same // approach as a real moonlit reflection (soft, wave-driven, no hard // geometric edges). Two symmetric lobes -- one toward the moon // direction, one toward the exact opposite -- so it reads on both // sides of the boat instead of only where the surface happens to // face the light. MoonRotationDegrees is meant to be exposed as a // Shader Graph input and matched to your actual moon/light direction. #define WATER_MOONBAND_ELEVATION 0.5 // 0..1, treat as a rough vertical component before normalizing #define WATER_MOONBAND_SHARPNESS 4.5 // higher = tighter, more contained road #define WATER_MOONBAND_STEPS 3.0 #define WATER_MOONBAND_TINT float3(0.85, 0.66, 0.40) float WaterMoonBandMask(float2 uv, float3 positionWS, float angleDeg, float2 grad, out float core, out float banded) { float angle = radians(angleDeg); float3 lightDir = normalize(float3(cos(angle), WATER_MOONBAND_ELEVATION, sin(angle))); float3 viewDir = normalize(_WorldSpaceCameraPos - positionWS); // Approximate surface normal from the same wave gradient used for // the hachures, so the road picks up the actual swell/wavelet // relief instead of a perfectly flat mirror. float3 normalApprox = normalize(float3(-grad.x, 1.0, -grad.y)); // Two lobes: one facing the moon, one facing directly away from // it -- keeps the specular softness/quality but reads on both // sides of the boat instead of only one. float3 halfVecFront = normalize(viewDir + lightDir); float3 halfVecBack = normalize(viewDir - lightDir); float alignFront = dot(normalApprox, halfVecFront); float alignBack = dot(normalApprox, halfVecBack); float alignment = saturate(max(alignFront, alignBack)); // Irregular boundary: warp the alignment test with coarse noise // so the road's edge isn't a perfectly smooth curve. float lobes = FBMNoise3D(float3(uv * float2(0.08, 0.08), 950.0), 1.0, 2, 2.0, 0.55) - 0.5; alignment = saturate(alignment + lobes * 0.12); // Wide and soft far away, tapering tighter as it comes toward the // camera -- a real moon road spreads out near the horizon. float camDist = distance(positionWS, _WorldSpaceCameraPos); float distFactor = saturate(camDist / 55.0); // 0 near camera, 1 at the horizon float sharpness = lerp(WATER_MOONBAND_SHARPNESS * 3.0, WATER_MOONBAND_SHARPNESS * 0.18, distFactor); float body = pow(alignment, sharpness); core = pow(alignment, sharpness * 2.5); // Discrete painted steps instead of a smooth glow -- reads like // engraved/drawn highlight bands rather than a photographic bloom. banded = WaterPosterize(saturate(body * 1.6), WATER_MOONBAND_STEPS); return saturate(body); } // Scattered dark patches, randomly placed across the whole surface // (independent of the main path's position). Same organic-edge idea // as the path, but random blobs instead of a directed channel -- // reads like isolated deep/oily pockets rather than a single feature. #define WATER_SCATTERED_DARK_LOW 0.48 #define WATER_SCATTERED_DARK_HIGH 0.62 #define WATER_SCATTERED_DARK_OPACITY 0.65 #define WATER_AMBIENT_LIGHT_STRENGTH 0.16 // moon-facing side #define WATER_AMBIENT_DARK_STRENGTH 0.90 // away side, brutal now -- meant to force the player to want to turn toward the moon #define WATER_AMBIENT_DARK_CURVE 0.22 // lower = darkens faster even close to the moon-facing side #define WATER_AMBIENT_CRUSH_STRENGTH 0.55 // second, harder pass pushing the far side toward pure black float WaterScatteredDarkPatches(float2 uv) { float field = FBMNoise3D(float3(uv * 0.11, 970.0), 1.0, 2, 2.0, 0.55); float edgeJitter = FBMNoise3D(float3(uv * 0.5, 975.0), 1.0, 1, 2.0, 0.5) - 0.5; return smoothstep(WATER_SCATTERED_DARK_LOW, WATER_SCATTERED_DARK_HIGH, field + edgeJitter * 0.14); } // Rust/grime streaks: elongated blotches, dirtier-looking than the // regular water blotches. float WaterGrimeStreaks(float2 uv) { float2 stretched = uv * float2(1.0, 3.2); // stretch -> vertical/diagonal streaks float2 morphed = WaterDomainWarp(stretched, 0.05, 1.0, 0.03, 310.0); float field = FBMNoise3D(float3(morphed * 0.05, 400.0), 1.0, 2, 2.0, 0.55); return smoothstep(0.58, 0.90, field); } // ============================================================ // 5) SWELL: visible vertical displacement, otherwise the plane // looks frozen. // ============================================================ // Wavelet field alone (extracted so it can be reused by the // cross-hatch layer below). float WaterWaveletField(float2 uv) { float t = _Time.y; float waveletA = FBMNoise3D(float3(uv * 0.9 + float2(t * WATER_WAVELET_SPEED * 0.20, -t * WATER_WAVELET_SPEED * 0.13), 520.0), 1.0, 2, 2.0, 0.5) - 0.5; float waveletB = FBMNoise3D(float3(uv * 1.6 + float2(-t * WATER_WAVELET_SPEED * 0.17, t * WATER_WAVELET_SPEED * 0.24), 525.0), 1.0, 2, 2.0, 0.5) - 0.5; return waveletA * 0.6 + waveletB * 0.4; } float WaterWaveDisplacement(float2 uv) { float t = _Time.y; // Large, slow, organic swell (global bas-relief that "breathes") float swell = FBMNoise3D(float3(uv * 0.02 + WATER_FLOW_DIR * t * WATER_WAVE_SPEED * 0.06, 500.0), 1.0, 3, 2.0, 0.5) - 0.5; // Second swell layer, different direction/speed -> avoids a // single repeating pattern float swell2 = FBMNoise3D(float3(uv * 0.035 - WATER_FLOW_DIR.yx * t * WATER_WAVE_SPEED * 0.045, 505.0), 1.0, 3, 2.0, 0.5) - 0.5; // Faster small-scale ripples layered on top, for lively detail float ripple = FBMNoise3D(float3(uv * 0.25 + float2(t * WATER_RIPPLE_SPEED * 0.15, t * WATER_RIPPLE_SPEED * 0.11), 510.0), 1.0, 3, 2.0, 0.5) - 0.5; // Dense small wavelets everywhere, to break up the "flat plane" // look even up close float wavelets = WaterWaveletField(uv); return swell * WATER_WAVE_AMPLITUDE + swell2 * WATER_WAVE_AMPLITUDE * 0.6 + ripple * WATER_RIPPLE_AMPLITUDE + wavelets * WATER_WAVELET_AMPLITUDE; } // Cheap stand-in for WaterWaveDisplacement, used for hatching, glint // and chiaroscuro. Only the large swell and the fine wavelets // contribute: the wavelets are what give this an actually-measurable // local slope (the raw swell alone varies far too slowly to produce // any visible gradient within a normal camera frame). float WaterWaveHachureHeight(float2 uv) { float t = _Time.y; float swell = FBMNoise3D(float3(uv * 0.02 + WATER_FLOW_DIR * t * WATER_WAVE_SPEED * 0.06, 500.0), 1.0, 1, 2.0, 0.5) - 0.5; float wavelets = WaterWaveletField(uv); return swell * WATER_WAVE_AMPLITUDE + wavelets * WATER_WAVELET_AMPLITUDE; } // Local slope via forward differences (center + x + y taps). Used to // orient and densify the hatching based on the swell relief, like a // classic engraving: steeper slope -> denser hatching. Also reused // for the moon glint and the chiaroscuro shading below. float2 WaterWaveGradient(float2 uv, float eps) { float h0 = WaterWaveHachureHeight(uv); float hR = WaterWaveHachureHeight(uv + float2(eps, 0.0)); float hU = WaterWaveHachureHeight(uv + float2(0.0, eps)); return float2((hR - h0) / eps, (hU - h0) / eps); } // Hatching that follows the swell relief: lines run along the swell's // contour lines, denser/cross-hatched on steep slopes -> reads as // "engraved" volume instead of a stamped-on pattern. float WaterWaveHachures(float2 uv, float2 grad) { float slope = saturate(length(grad) * 1.4); // Lines run along the slope (perpendicular to the gradient) float angle = atan2(grad.y, grad.x) + 1.5707963; float2 rotated = WaterRotate(uv, -angle); float freq = 5.5; float thickness = lerp(0.10, 0.26, slope); float lineMain = WaterStroke(rotated.y * freq, thickness, thickness * 0.6); // Cross-hatching only where the slope is steep (like a denser // engraved shadow area) float crossAmount = smoothstep(0.35, 0.75, slope); float lineCross = WaterStroke(rotated.x * freq * 1.35, thickness * 0.75, thickness * 0.45) * crossAmount; float mask = smoothstep(0.06, 0.30, slope); return saturate((lineMain + lineCross) * mask); } // ============================================================ // 6) PENCIL HATCHING REACTING TO THE WAVELETS // Not a grid: short, broken strokes (like pencil marks), waved by // noise, oriented by the local relief -> reads as "hand-drawn" // rather than a technical pattern. Density reacts to the wavelets. // ============================================================ #define WATER_HATCH_SCALE 48.0 // small, fine strokes #define WATER_HATCH_THICKNESS 0.028 #define WATER_HATCH_JITTER 2.2 // waviness along the stroke #define WATER_HATCH_DASH_FREQ 2.6 // stroke break-up frequency #define WATER_HATCH_OPACITY 0.45 // A stroke along "along", positioned by "across", broken into short // segments with pseudo-random length/visibility -> pencil stroke // rather than a continuous line. float WaterHatchStroke(float alongCoord, float acrossCoord, float thickness, float dashFreq, float seed) { float lineMask = WaterStroke(acrossCoord, thickness, thickness * 0.9); float cell = floor(alongCoord * dashFreq + seed); float rnd = WaterHash(cell); float localFrac = frac(alongCoord * dashFreq + seed); float visible = step(0.28, rnd); // some segments are empty -> discontinuous strokes float strokeLen = lerp(0.30, 0.90, frac(rnd * 7.0)); // variable stroke length float coverage = step(localFrac, strokeLen); return lineMask * visible * coverage; } float WaterWaveletHachures(float2 uv) { float wave = WaterWaveletField(uv); // Local wavelet slope via forward differences, reusing `wave` as // the center tap. float eps = 0.10; float wR = WaterWaveletField(uv + float2(eps, 0.0)); float wU = WaterWaveletField(uv + float2(0.0, eps)); float2 grad = float2((wR - wave) / eps, (wU - wave) / eps); float slope = length(grad); float angle = atan2(grad.y, grad.x); // Waviness that distorts the stroke along its length, avoiding // any "ruled/perfect" look float jitterA = FBMNoise3D(float3(uv * 2.4, 610.0), 1.0, 2, 2.0, 0.5) - 0.5; float jitterB = FBMNoise3D(float3(uv * 2.4, 615.0), 1.0, 2, 2.0, 0.5) - 0.5; // Two hatch directions aligned with the local relief, but NOT at // exactly 90 degrees -> more natural, sketch-like float2 rotA = WaterRotate(uv, -angle); float2 rotB = WaterRotate(uv, -angle + 1.15); float coordA = rotA.y * WATER_HATCH_SCALE + jitterA * WATER_HATCH_JITTER; float coordB = rotB.y * WATER_HATCH_SCALE * 1.1 + jitterB * WATER_HATCH_JITTER; float strokeA = WaterHatchStroke(rotA.x, coordA, WATER_HATCH_THICKNESS, WATER_HATCH_DASH_FREQ, 700.0); // The 2nd (crossing) direction only appears in more pronounced // areas -> densifies the shadow instead of a uniform grid float crossAmount = smoothstep(0.28, 0.60, slope); float strokeB = WaterHatchStroke(rotB.x, coordB, WATER_HATCH_THICKNESS * 0.85, WATER_HATCH_DASH_FREQ * 1.35, 750.0) * crossAmount; float hatch = saturate(strokeA + strokeB); // Visibility driven by the wavelets: the more "carved/bulging" or // sloped an area is, the more the hatching stands out float amount = saturate(abs(wave) * 1.6 + slope * 2.2); amount = smoothstep(0.15, 0.60, amount); return hatch * amount; } // ============================================================ // INK STAINS: dark, organic bleeding patches around corpse positions // ============================================================ // Positions are fed in from C# as world-space XZ + a per-corpse seed // (packed into a float4 array), via Shader.SetGlobalVectorArray / // Shader.SetGlobalInt. Declaring them here as plain global HLSL // variables (not Shader Graph-exposed properties) means they still // compile fine into the final shader even though this file is // consumed through a Custom Function node -- Shader Graph doesn't // strip globals referenced inside the function it calls. #define WATER_MAX_INK_STAINS 32 float4 _InkStainData[WATER_MAX_INK_STAINS]; // xy = world XZ position, z = seed, w = corpse rotation (radians) int _InkStainCount; // The puddle itself: several overlapping rounded lobes blended // together (metaball-style), offset along the corpse's body axis -- // reads as an organic pooled shape with bulges and curves, like a // real spilled liquid, instead of a clean ellipse or circle. #define WATER_INK_BODY_ASPECT 2.0 // how elongated the lobe placement is along the body axis #define WATER_INK_LOBE_COUNT 5 #define WATER_INK_LOBE_SPREAD 0.38 // how far lobe centers scatter from the main center #define WATER_INK_LOBE_RADIUS_MIN 0.25 #define WATER_INK_LOBE_RADIUS_MAX 0.46 #define WATER_INK_LOBE_SMOOTH_K 0.16 // higher = lobes melt together more smoothly #define WATER_INK_CORE_EDGE_NOISE 0.05 // light roughness on top of the lobed silhouette // Many thin, independent threads escaping the puddle, biased toward // the water's own flow direction with a wide spread -- not a perfect // radial fan, and not glued to a single heading either. #define WATER_INK_THREAD_COUNT 12 #define WATER_INK_THREAD_MIN_DIST 0.25 #define WATER_INK_THREAD_MAX_DIST 1.4 #define WATER_INK_THREAD_MIN_LEN 0.35 #define WATER_INK_THREAD_MAX_LEN 1.3 #define WATER_INK_THREAD_WOBBLE 0.35 // slow, broad curve #define WATER_INK_THREAD_WOBBLE2 0.16 // fast, tight zigzag layered on top #define WATER_INK_THREAD_THICKNESS 0.04 #define WATER_INK_THREAD_DIR_SPREAD 2.4 // radians of spread around the flow direction // A handful of small, fully disconnected round droplets scattered // further out -- matches the little isolated dots you get around a // real spilled puddle. #define WATER_INK_DROPLET_COUNT 6 #define WATER_INK_DROPLET_SPREAD 1.8 #define WATER_INK_DROPLET_RADIUS_MIN 0.03 #define WATER_INK_DROPLET_RADIUS_MAX 0.08 #define WATER_INK_MAX_OPACITY 0.75 #define WATER_INK_ANIM_SPEED 0.03 #define WATER_INK_ANIM_STRENGTH 0.05 float WaterInkMaxExtent() { float lobeExtent = WATER_INK_LOBE_SPREAD * WATER_INK_BODY_ASPECT + WATER_INK_LOBE_RADIUS_MAX; float threadExtent = WATER_INK_THREAD_MAX_DIST + WATER_INK_THREAD_MAX_LEN; float dropletExtent = WATER_INK_DROPLET_SPREAD + WATER_INK_DROPLET_RADIUS_MAX; return max(max(lobeExtent, threadExtent), dropletExtent) + 0.2; } // Polynomial smooth-min -- blends two distance fields together with // a soft, rounded transition instead of a hard union. This is what // makes the lobes melt into each other rather than just overlapping // as separate circles. float SmoothMinInk(float a, float b, float k) { float h = saturate(0.5 + 0.5 * (b - a) / max(k, 1e-4)); return lerp(b, a, h) - k * h * (1.0 - h); } // Puddle signed distance field: union of several overlapping round // lobes, scattered along the body axis so the overall silhouette // bulges and curves organically, like the blood-puddle reference -- // not a clean ellipse. float WaterInkCoreSDF(float2 localUV, float corpseRotation, float seed) { float ca = cos(-corpseRotation); float sa = sin(-corpseRotation); float d = 1e5; for (int i = 0; i < WATER_INK_LOBE_COUNT; i++) { float lobeSeed = seed + i * 17.3; float alongBody = (WaterHash(lobeSeed) - 0.5) * 2.0 * WATER_INK_LOBE_SPREAD * WATER_INK_BODY_ASPECT; float acrossBody = (WaterHash(lobeSeed + 3.1) - 0.5) * WATER_INK_LOBE_SPREAD * 0.6; float lobeRadius = lerp(WATER_INK_LOBE_RADIUS_MIN, WATER_INK_LOBE_RADIUS_MAX, WaterHash(lobeSeed + 6.7)); float2 lobeOffsetLocal = float2(alongBody, acrossBody); float2 lobeOffsetWorld = float2( lobeOffsetLocal.x * ca + lobeOffsetLocal.y * sa, -lobeOffsetLocal.x * sa + lobeOffsetLocal.y * ca ); float lobeDist = length(localUV - lobeOffsetWorld) - lobeRadius; d = SmoothMinInk(d, lobeDist, WATER_INK_LOBE_SMOOTH_K); } return d; } float WaterInkCore(float2 localUV, float corpseRotation, float seed) { float sdf = WaterInkCoreSDF(localUV, corpseRotation, seed); // Cheap reject before the extra edge-roughness noise call float margin = WATER_INK_CORE_EDGE_NOISE + 0.05; if (sdf < -margin) return 1.0; if (sdf > margin) return 0.0; float edgeNoise = FBMNoise3D(float3(localUV * 3.0, seed + 55.0), 1.0, 2, 2.0, 0.55) - 0.5; float adjustedSDF = sdf + edgeNoise * WATER_INK_CORE_EDGE_NOISE; return 1.0 - smoothstep(-0.02, 0.02, adjustedSDF); } // A single independent thread, with two layers of wobble stacked -- // a slow broad curve plus a fast tight zigzag on top, so it reads as // properly meandering rather than a single smooth arc. float WaterInkThread(float2 uv, float2 origin, float angle, float length, float seed) { float2 dir = float2(cos(angle), sin(angle)); float2 perp = float2(-dir.y, dir.x); float2 local = uv - origin; float s = dot(local, dir); if (s < 0.0 || s > length) return 0.0; float d = dot(local, perp); float maxPossibleD = WATER_INK_THREAD_THICKNESS + WATER_INK_THREAD_WOBBLE + WATER_INK_THREAD_WOBBLE2 + 0.02; if (abs(d) > maxPossibleD) return 0.0; float wobble1 = (FBMNoise3D(float3(s * 1.1, seed, 0.0), 1.0, 1, 2.0, 0.5) - 0.5) * WATER_INK_THREAD_WOBBLE; float wobble2 = sin(s * 9.0 + seed * 5.0) * WATER_INK_THREAD_WOBBLE2; float dist = abs(d - wobble1 - wobble2); float lineAlpha = 1.0 - smoothstep(0.0, WATER_INK_THREAD_THICKNESS, dist); float fadeIn = smoothstep(0.0, length * 0.15, s); float fadeOut = 1.0 - smoothstep(length * 0.8, length, s); return lineAlpha * fadeIn * fadeOut; } // Many threads, biased toward the water's flow direction (both where // they start and which way they head) but with enough spread that // they don't look like a perfect fan. float WaterInkThreads(float2 uv, float2 stainCenter, float seed, float corpseRotation) { float result = 0.0; float baseAngle = corpseRotation; for (int k = 0; k < WATER_INK_THREAD_COUNT; k++) { float originAngle = baseAngle + (WaterHash(seed + k * 7.31) - 0.5) * WATER_INK_THREAD_DIR_SPREAD * 1.3; float originDist = lerp(WATER_INK_THREAD_MIN_DIST, WATER_INK_THREAD_MAX_DIST, WaterHash(seed + k * 13.7)); float2 threadOrigin = stainCenter + float2(cos(originAngle), sin(originAngle)) * originDist; float threadAngle = baseAngle + (WaterHash(seed + k * 19.1 + 4.0) - 0.5) * WATER_INK_THREAD_DIR_SPREAD; float threadLength = lerp(WATER_INK_THREAD_MIN_LEN, WATER_INK_THREAD_MAX_LEN, WaterHash(seed + k * 23.3 + 8.0)); float thread = WaterInkThread(uv, threadOrigin, threadAngle, threadLength, seed + k * 3.7); result = max(result, thread); if (result > 0.999) break; } return result; } // Small disconnected round droplets, scattered further out, also // biased toward the flow direction. float WaterInkDroplets(float2 uv, float2 stainCenter, float seed, float corpseRotation) { float result = 0.0; float baseAngle = corpseRotation; for (int k = 0; k < WATER_INK_DROPLET_COUNT; k++) { float dropletAngle = baseAngle + (WaterHash(seed + k * 31.7 + 50.0) - 0.5) * 2.6; float dropletDist = lerp(0.3, WATER_INK_DROPLET_SPREAD, WaterHash(seed + k * 41.1 + 60.0)); float2 dropletCenter = stainCenter + float2(cos(dropletAngle), sin(dropletAngle)) * dropletDist; float dropletRadius = lerp(WATER_INK_DROPLET_RADIUS_MIN, WATER_INK_DROPLET_RADIUS_MAX, WaterHash(seed + k * 51.3 + 70.0)); float dist = length(uv - dropletCenter) - dropletRadius; if (dist > 0.02) continue; // cheap reject, no need for smoothstep math float droplet = 1.0 - smoothstep(-0.015, 0.015, dist); result = max(result, droplet); if (result > 0.999) break; } return result; } // Alpha contribution from a single corpse's stain at this world point float WaterInkStainAt(float2 uv, float2 stainCenter, float seed, float corpseRotation) { float2 localUV = uv - stainCenter; float t = _Time.y * WATER_INK_ANIM_SPEED; float wx = FBMNoise3D(float3(localUV * 0.6 + float2(t, 0.0), seed), 1.0, 1, 2.0, 0.5) - 0.5; float wy = FBMNoise3D(float3(localUV * 0.6 + float2(0.0, t * 0.8), seed + 30.0), 1.0, 1, 2.0, 0.5) - 0.5; localUV += float2(wx, wy) * WATER_INK_ANIM_STRENGTH; float core = WaterInkCore(localUV, corpseRotation, seed); float threads = WaterInkThreads(uv, stainCenter, seed, corpseRotation); float droplets = WaterInkDroplets(uv, stainCenter, seed, corpseRotation); return max(max(core, threads), droplets) * WATER_INK_MAX_OPACITY; } // Loops over every active corpse and returns the strongest stain // influence at this point. Cheap squared-distance rejection happens // BEFORE any noise is evaluated. float WaterInkStains(float2 uv) { float result = 0.0; float maxExtent = WaterInkMaxExtent(); float maxExtentSq = maxExtent * maxExtent; for (int i = 0; i < WATER_MAX_INK_STAINS; i++) { if (i >= _InkStainCount) break; float4 data = _InkStainData[i]; float2 delta = uv - data.xy; float distSq = dot(delta, delta); if (distSq > maxExtentSq) continue; result = max(result, WaterInkStainAt(uv, data.xy, data.z, data.w)); if (result >= WATER_INK_MAX_OPACITY - 0.001) break; } return saturate(result); } // ============================================================ // VERTEX // ============================================================ void WaterVertex_float ( float3 PositionWS, float3 NormalWS, out float3 Position, out float Height ) { float2 uv = PositionWS.xz; float2 warpedUV = WaterSharedFlowWarp(uv); float flowA = WaterFlowField(warpedUV, 0.0, WATER_BLOTCH_SCALE_A, WATER_FLOW_DIR, 1.0, 1.0); float flowB = WaterFlowField(warpedUV, 20.3, WATER_BLOTCH_SCALE_B, WATER_FLOW_DIR * 0.7, 1.4, 2.0); float h = 0.34; h += flowA * 0.14; h += flowB * 0.06; h = saturate(h); h = WaterPosterize(h, 20.0); Position = PositionWS; Position.y += h * 0.0035 - 0.00175; Position.y += WaterWaveDisplacement(uv); // "Vat" effect: water level rises with distance from the camera, // closing in like the walls of a giant tub around the boat. float distFromCamera = length(PositionWS.xz - _WorldSpaceCameraPos.xz); float bowlT = saturate(distFromCamera / WATER_BOWL_RADIUS); Position.y += pow(bowlT, WATER_BOWL_POWER) * WATER_BOWL_STRENGTH; Height = h; } // ============================================================ // COLOR // ============================================================ void WaterColorNoFoam_float ( float3 PositionWS, float3 NormalWS, float MoonRotationDegrees, out float3 Color, out float Height ) { float2 uv = PositionWS.xz; float2 warpedUV = WaterSharedFlowWarp(uv); float flowA = WaterFlowField(warpedUV, 0.0, WATER_BLOTCH_SCALE_A, WATER_FLOW_DIR, 1.0, 1.0); float flowB = WaterFlowField(warpedUV, 20.3, WATER_BLOTCH_SCALE_B, WATER_FLOW_DIR * 0.7, 1.4, 2.0); float flowC = WaterFlowField(warpedUV, -11.7, WATER_BLOTCH_SCALE_C, WATER_FLOW_DIR * 1.3, 0.8, 3.0); float currentOutlines = WaterCurrentOutlines(uv, flowA, flowB, flowC); // Wave gradient computed early -- shared by the hachures, the // chiaroscuro and the moon reflection below. float waveHeight = WaterWaveHachureHeight(uv); float2 waveGrad = WaterWaveGradient(uv, 0.35); // Cheap distance-only check (no noise) so we can skip the moon // band's noise-driven computation entirely when it's guaranteed // to be invisible near the camera, instead of computing it and // multiplying by ~0 later. float camDistCheap = distance(PositionWS, _WorldSpaceCameraPos); float distFactorCheap = saturate(camDistCheap / 55.0); float moonDistanceFade = smoothstep(0.15, 0.30, distFactorCheap); // Moon reflection band float moonCore = 0.0, moonBanded = 0.0, moonBody = 0.0; if (moonDistanceFade > 0.001) { moonBody = WaterMoonBandMask(uv, PositionWS, MoonRotationDegrees, waveGrad, moonCore, moonBanded); } // distFactor also drives the LOD gate below (0 near camera, 1 at horizon) float horizonDistFactor; float horizonLines = WaterHorizonLines(uv, PositionWS, horizonDistFactor); float lod = 1.0 - horizonDistFactor; float pale = smoothstep(0.42, 0.80, flowA); float dark = smoothstep(0.42, 0.80, 1.0 - flowB); float mid = smoothstep(0.35, 0.80, flowC); float cold = smoothstep(0.25, 0.70, FBMNoise3D(float3(uv * 0.10, 8.0), 1.0, 2, 2.0, 0.5)); float h = 0.34 + flowA * 0.14 + flowB * 0.06 - dark * 0.08; h = saturate(h); h = WaterPosterize(h, 20.0); Height = h; // --- base palette: oily/grimy base, more tonal range than plain blue --- float3 colInk = float3(0.0, 0.0, 0.0); float3 colDeep = float3(0.006, 0.014, 0.020); float3 colRust = float3(0.340, 0.160, 0.055); float3 colAlgae = float3(0.030, 0.150, 0.120); float3 colViolet = float3(0.140, 0.075, 0.200); float3 colGrime = float3(0.180, 0.160, 0.110); float3 colGlint = float3(0.470, 0.430, 0.330); // Tone quantization with "pencil-jittered" edges (jitter applied // before posterize) float tone = 0.34 + flowA * 0.42 + pale * 0.36 - dark * 0.44; tone += WaterCrayonJitter(uv, 40.0) * WATER_CRAYON_JITTER; tone = saturate(tone); tone = WaterPosterize(tone, 4.0); // fewer steps -> bigger, more decisive masses // Oil-gradient parameter: combines several fields so each blotch // leans toward a different hue instead of a flat blue wash. float oilT = saturate(tone * 0.55 + pale * 0.30 - dark * 0.20 + (flowC - 0.5) * 0.55); float3 baseCol = WaterOilGradient(oilT); baseCol = lerp(baseCol, colAlgae, mid * 0.30); baseCol = lerp(baseCol, colViolet, cold * 0.28); baseCol = lerp(baseCol, colDeep, dark * 0.55); // --- extra dedicated blotch layer (smaller, more numerous) to // reinforce coverage across the whole surface --- float extraDark = smoothstep(0.55, 0.85, flowC); float extraPale = smoothstep(0.55, 0.85, 1.0 - flowC); baseCol = lerp(baseCol, colDeep, extraDark * 0.48); baseCol = lerp(baseCol, colGrime, extraPale * 0.28); // --- interaction between blotches: where several fields overlap, // an emergent tint appears (like two pigments mixing where they meet) --- float overlapWarm = saturate(pale * (1.0 - dark) * mid * 3.0); float overlapCool = saturate(dark * cold * 3.0); baseCol = lerp(baseCol, colRust, overlapWarm * 0.26); baseCol = lerp(baseCol, colViolet, overlapCool * 0.26); // --- ambient light/dark wash tied to the moon's direction: the // general side of the water facing the moon reads a touch // lighter/cooler, the opposite side a touch darker. Broad and // subtle, independent of the tight specular road above. --- float2 moonDirXZ = float2(cos(radians(MoonRotationDegrees)), sin(radians(MoonRotationDegrees))); float2 toCamXZ = uv - _WorldSpaceCameraPos.xz; float moonFacing = dot(normalize(toCamXZ + 1e-4), moonDirXZ) * 0.5 + 0.5; // 0 = away, 1 = toward moon // Curve pushes mid-tones darker faster (not just the extreme away // side), while staying capped below the path's own darkness. float awayAmount = pow(saturate(1.0 - moonFacing), WATER_AMBIENT_DARK_CURVE); baseCol = lerp(baseCol, colDeep, awayAmount * WATER_AMBIENT_DARK_STRENGTH); baseCol = lerp(baseCol, colInk, awayAmount * awayAmount * awayAmount * WATER_AMBIENT_CRUSH_STRENGTH); baseCol = lerp(baseCol, colGlint, moonFacing * WATER_AMBIENT_LIGHT_STRENGTH); // chiaroscuro: real value hierarchy tied to the local wave relief, // troughs pushed toward near-black, crests only lifted slightly float trough = smoothstep(0.05, -0.30, waveHeight); baseCol = lerp(baseCol, colDeep, trough * 0.55); float crestLift = smoothstep(-0.05, 0.30, waveHeight); baseCol = lerp(baseCol, colGlint, crestLift * 0.08); // --- always-on fine grain: cheap value noise, NOT gated by LOD, // so the horizon keeps some texture instead of reading as a flat // wash. Two octaves at different scales avoid an obvious tiling // pattern. Deliberately subtle -- this breaks flatness, it isn't // meant to fight the hachures/outlines for attention. --- float fineGrain = WaterCheapValueNoise(uv * WATER_FINE_GRAIN_SCALE_A) * WATER_FINE_GRAIN_WEIGHT_A + WaterCheapValueNoise(uv * WATER_FINE_GRAIN_SCALE_B) * WATER_FINE_GRAIN_WEIGHT_B; baseCol += (fineGrain - 0.5) * WATER_FINE_GRAIN_STRENGTH; // --- fine detail layers, faded out toward the horizon --- if (lod > WATER_LOD_THRESHOLD) { float secondaryOutlines = WaterSecondaryInkTaches(uv); baseCol = lerp(baseCol, colInk, secondaryOutlines * 0.85 * lod); float grime = WaterGrimeStreaks(uv); baseCol = lerp(baseCol, colRust, grime * 0.45 * lod); float3 wash = WaterColorWash(uv, colRust, colViolet); baseCol = lerp(baseCol, wash, 0.30 * lod); float3 colorNoise = WaterColorNoise(uv, 0.55, 70.0); baseCol += colorNoise * 0.10 * lod; float waveletHatch = WaterWaveletHachures(uv); baseCol = lerp(baseCol, colInk, waveletHatch * WATER_HATCH_OPACITY * lod); // Fine pencil grain float grainA = FBMNoise3D(float3(uv * 0.65, 50.0), 1.0, 2, 2.0, 0.50); float grainB = FBMNoise3D(float3(uv * 1.60, 55.0), 1.0, 2, 2.0, 0.50); float grainC = FBMNoise3D(float3(uv * WATER_CRAYON_SCALE, 61.0), 1.0, 2, 2.0, 0.5); baseCol += (grainA - 0.5) * 0.022 * lod; baseCol += (grainB - 0.5) * 0.012 * lod; baseCol += (grainC - 0.5) * 0.018 * lod; } // --- ink: horizon darker/denser at a distance, more discreet near the boat --- baseCol = lerp(baseCol, colInk, currentOutlines * 0.95); baseCol = lerp(baseCol, colInk, horizonLines * lerp(0.55, 0.95, horizonDistFactor)); // --- hatching that follows the swell relief (engraving look) --- float waveHachures = WaterWaveHachures(uv, waveGrad); baseCol = lerp(baseCol, colInk, waveHachures * 0.55); // Scattered dark patches across the water float scatteredDark = WaterScatteredDarkPatches(uv); baseCol = lerp(baseCol, colDeep, scatteredDark * WATER_SCATTERED_DARK_OPACITY); baseCol = lerp(baseCol, colInk, scatteredDark * scatteredDark * 0.35); // solid core where the patch is strongest // --- moon reflection band: painted in with a few discrete steps // (not a smooth glow), textured by the hachures so it still reads // as drawn rather than a photographic highlight. Keep the boat's // immediate surroundings clear -- the band only fades in starting // some distance out (moonDistanceFade). --- float moonTexture = 0.55 + 0.45 * (1.0 - waveHachures); baseCol = lerp(baseCol, WATER_MOONBAND_TINT, moonBanded * 0.20 * moonTexture * moonDistanceFade); baseCol = lerp(baseCol, WATER_MOONBAND_TINT * 1.1, moonCore * 0.14 * moonTexture * moonDistanceFade); baseCol = lerp(baseCol, colInk, moonBody * waveHachures * 0.4 * moonDistanceFade); // keep the ink linework visible inside the band // --- ink stains bleeding from corpses -- applied last, on top of // everything else, so it reads as a fresh dark stain sitting over // the painted water rather than getting washed out by the other layers float inkStain = WaterInkStains(uv); baseCol = lerp(baseCol, colInk, inkStain); Color = saturate(baseCol); } // Legacy compatibility for the old node name. void Water_float ( float3 PositionWS, float3 NormalWS, out float3 Position, out float Height ) { WaterVertex_float(PositionWS, NormalWS, Position, Height); } #endif