1010 lines
42 KiB
HLSL
1010 lines
42 KiB
HLSL
#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
|