// Crest Water System // Copyright © 2024 Wave Harmonic. All rights reserved. #pragma kernel CrestInitialize #pragma kernel CrestInitializeGroundHeight #pragma kernel CrestAdvect #pragma kernel CrestUpdateHeight #pragma kernel CrestHeightOvershootReduction #pragma kernel CrestUpdateVelocity #pragma kernel CrestBlurHeight #pragma kernel CrestBlur #pragma kernel CrestMaskEdge #pragma kernel CrestExpandEdge #pragma kernel CrestFinalizeHeight #pragma kernel CrestInjectShape #pragma kernel CrestInjectLevel #pragma kernel CrestInjectFoam #pragma kernel CrestInjectFlow #pragma kernel CrestBakeLevel #pragma kernel CrestBakeFoam #pragma kernel CrestBakeFlow #include "HLSLSupport.cginc" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Macros.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Globals.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/InputsDriven.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Helpers.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Cascade.hlsl" #define FLT_MIN 1.175494351e-38 SamplerState linear_clamp_sampler; Texture2D _Crest_LevelSource; Texture2D _Crest_HeightSource; RWTexture2D _Crest_HeightTarget; Texture2D _Crest_VelocityXSource; RWTexture2D _Crest_VelocityXTarget; Texture2D _Crest_VelocityYSource; RWTexture2D _Crest_VelocityYTarget; Texture2D _Crest_GroundHeightSource; RWTexture2D _Crest_GroundHeightTarget; Texture2D _Crest_MaskSource; RWTexture2D _Crest_MaskTarget; // Injector Targets. RWTexture2DArray _Crest_FoamTarget; RWTexture2DArray _Crest_FlowTarget; RWTexture2DArray _Crest_ShapeTarget; RWTexture2DArray _Crest_LevelTarget; // Bake Targets. RWTexture2D _Crest_FoamBakeTarget; RWTexture2D _Crest_FlowBakeTarget; RWTexture2D _Crest_LevelBakeTarget; // Depth Probe. Texture2D _Crest_DepthProbe; CBUFFER_START(CrestBuffer) float _Crest_SimDeltaTime; float _Crest_SimulationDeltaTime; float _Crest_DomainWidth; float3 _Crest_DomainOrigin; uint _Crest_Resolution; float _Crest_DrainWaterAtBoundaries; float _Crest_Evaporation; float _Crest_Friction; float _Crest_MaximumVelocity; float _Crest_AddAdditionalWater; float _Crest_TexelSize; bool _Crest_MacCormackAdvection; bool _Crest_MacCormackAdvectionForHeight; bool _Crest_UpwindHeight; bool _Crest_DepthLimiter; bool _Crest_InjectLevel; float _Crest_OvershootReductionStrength; float _Crest_ShallowMinimumDepth; float _Crest_ShallowMaximumDepth; float _Crest_BlendPushUpStrength; float _Crest_InjectionStrength; float _Crest_InjectionMaximum; float _Crest_Weight; bool _Crest_SampleFromDepthProbe; bool _Crest_SampleFromLevelInputs; float _Crest_DepthProbeHeightOffset; float _Crest_DepthProbeResolution; float _Crest_MarginWidth; CBUFFER_END m_CrestNameSpace float2 IdToWorldXZ_H(uint2 id) { // H grid is centered return ((id + 0.5) / _Crest_Resolution - 0.5) * _Crest_DomainWidth + _Crest_DomainOrigin.xz; } float2 IdToWorldXZ_GroundHeight(uint2 id) { // H and GroundHeight are aligned return IdToWorldXZ_H(id); } float2 IdToWorldXZ_Vx(uint2 id) { // Vx grid is offset right half a texel return ((id + float2(1.0, 0.5)) / _Crest_Resolution - 0.5) * _Crest_DomainWidth + _Crest_DomainOrigin.xz; } float2 IdToWorldXZ_Vy(uint2 id) { // Vy grid is offset forward half a texel return ((id + float2(0.5, 1.0)) / _Crest_Resolution - 0.5) * _Crest_DomainWidth + _Crest_DomainOrigin.xz; } float2 WorldXZToUv_H(float2 worldXZ) { // H grid is centered return (worldXZ - _Crest_DomainOrigin.xz) / _Crest_DomainWidth + 0.5; } float2 WorldXZToUv_GroundHeight(float2 worldXZ) { // Aligned to H return WorldXZToUv_H(worldXZ); } float2 WorldXZToUv_Vx(float2 worldXZ) { // Vx grid is offset right half a texel return (worldXZ - _Crest_DomainOrigin.xz) / _Crest_DomainWidth + 0.5 - float2(0.5 / _Crest_Resolution, 0.0); } float2 WorldXZToUv_Vy(float2 worldXZ) { // Vy grid is offset forward half a texel return (worldXZ - _Crest_DomainOrigin.xz) / _Crest_DomainWidth + 0.5 - float2(0.0, 0.5 / _Crest_Resolution); } float SampleWaterLevel(float2 worldXZ) { return _Crest_LevelSource.SampleLevel(linear_clamp_sampler, WorldXZToUv_H(worldXZ), 0.0).x; } float SampleH(float2 worldXZ) { return _Crest_HeightSource.SampleLevel(linear_clamp_sampler, WorldXZToUv_H(worldXZ), 0.0).x; } float SampleGroundHeight(float2 worldXZ) { return _Crest_GroundHeightSource.SampleLevel(linear_clamp_sampler, WorldXZToUv_GroundHeight(worldXZ), 0.0).x; } float SampleVx0(float2 worldXZ) { return _Crest_VelocityXSource.SampleLevel(linear_clamp_sampler, WorldXZToUv_Vx(worldXZ), 0.0).x; } float SampleVy0(float2 worldXZ) { return _Crest_VelocityYSource.SampleLevel(linear_clamp_sampler, WorldXZToUv_Vy(worldXZ), 0.0).x; } // // Initialize // void Initialize(const uint3 i_ID) { const uint2 id = i_ID.xy; const float2 worldXZ = IdToWorldXZ_H(id); uint slice0, slice1; float alpha; PosToSliceIndices(worldXZ, 0.0, g_Crest_LodCount - 1.0, g_Crest_WaterScale, slice0, slice1, alpha); float h = g_Crest_WaterCenter.y; const float weight0 = (1.0 - alpha) * g_Crest_CascadeData[slice0].y; const float weight1 = (1.0 - weight0) * g_Crest_CascadeData[slice1].y; if (_Crest_InjectLevel) { if (_Crest_SampleFromLevelInputs) { h += SampleWaterLevel(worldXZ); } else { h += weight0 * Cascade::MakeLevel(slice0).SampleLevel(worldXZ) + weight1 * Cascade::MakeLevel(slice1).SampleLevel(worldXZ); } } h -= _Crest_DomainOrigin.y; h = max(0.0, h - SampleGroundHeight(worldXZ)); h += _Crest_AddAdditionalWater; _Crest_HeightTarget[id] = h; _Crest_VelocityXTarget[id] = 0.0; _Crest_VelocityYTarget[id] = 0.0; } // Called first. void InitializeGroundHeight(const uint3 i_ID) { const uint2 id = i_ID.xy; const float2 worldXZ = IdToWorldXZ_GroundHeight(id); uint slice0, slice1; float alpha; PosToSliceIndices(worldXZ, 0.0, g_Crest_LodCount - 1.0, g_Crest_WaterScale, slice0, slice1, alpha); const float weight0 = (1.0 - alpha) * g_Crest_CascadeData[slice0].y; const float weight1 = (1.0 - weight0) * g_Crest_CascadeData[slice1].y; float g; if (_Crest_SampleFromDepthProbe) { // Sample from the Depth Probe directly otherwise we bake in LOD losses. g = _Crest_DepthProbe.SampleLevel(linear_clamp_sampler, id.xy / (float)_Crest_Resolution, 0.0).x; g += _Crest_DepthProbeHeightOffset; } else { g = weight0 * Cascade::MakeDepth(slice0).SampleSceneHeight(worldXZ) + weight1 * Cascade::MakeDepth(slice1).SampleSceneHeight(worldXZ); } // LOD losses for height should be acceptable providing height is flat (which it should be). // Bake water level into ground by offsetting ground with water level. if (!_Crest_InjectLevel) { g -= weight0 * Cascade::MakeLevel(slice0).SampleLevel(worldXZ) + weight1 * Cascade::MakeLevel(slice1).SampleLevel(worldXZ); } // Mask is directly based off depth at the moment. float depth = g_Crest_WaterCenter.y - g; _Crest_MaskTarget[id] = 1.0 - saturate((depth - _Crest_ShallowMinimumDepth) / _Crest_ShallowMaximumDepth); g = max(0.0, g - _Crest_DomainOrigin.y); _Crest_GroundHeightTarget[id] = g; } // // Advect // void Advect(const uint3 id) { // Vx if (id.z == 0) { const float2 worldXZ = IdToWorldXZ_Vx(id.xy); // Advect forwards to predict advected position const float2 worldXZAdvected_prediction = worldXZ - _Crest_SimulationDeltaTime * float2(SampleVx0(worldXZ), SampleVy0(worldXZ)); float2 worldXZAdvected = worldXZAdvected_prediction; const float vxSemiLagrangian = SampleVx0(worldXZAdvected); float vxAdvected = vxSemiLagrangian; if (_Crest_MacCormackAdvection) { // Update prediction by advecting backwards to obtain a starting position and using the difference as an error term // Advect forwards to predict original position const float2 worldXZ_prediction = worldXZAdvected_prediction + _Crest_SimulationDeltaTime * float2(vxSemiLagrangian, SampleVy0(worldXZAdvected_prediction)); // Remove half of prediction error to yield average of both worldXZAdvected -= (worldXZ_prediction - worldXZ) / 2.0; float vxAdvectedNew = SampleVx0(worldXZAdvected); const float4 vxsUsedForFirstSLStep = _Crest_VelocityXSource.Gather(linear_clamp_sampler, WorldXZToUv_Vx(worldXZ)); const float vxMin = min(min(vxsUsedForFirstSLStep[0], vxsUsedForFirstSLStep[1]), min(vxsUsedForFirstSLStep[2], vxsUsedForFirstSLStep[3])); const float vxMax = max(max(vxsUsedForFirstSLStep[0], vxsUsedForFirstSLStep[1]), max(vxsUsedForFirstSLStep[2], vxsUsedForFirstSLStep[3])); if (vxAdvectedNew >= vxMin && vxAdvectedNew <= vxMax) { vxAdvected = vxAdvectedNew; } } // Abs to silence 3571 which for some reason cannot disable with pragma even // HLSLSupport can do exactly that. const float h_vx = SampleH(worldXZAdvected); const float friction = _Crest_Friction * _Crest_SimulationDeltaTime / max(0.001f, pow(abs(h_vx), 1.333333f)); vxAdvected -= abs(vxAdvected) * vxAdvected * friction; vxAdvected = clamp(vxAdvected, -_Crest_MaximumVelocity, _Crest_MaximumVelocity); _Crest_VelocityXTarget[id.xy] = vxAdvected; } // Vy if (id.z == 1) { const float2 worldXZ = IdToWorldXZ_Vy(id.xy); // Advect forwards to predict advected position const float2 worldXZAdvected_prediction = worldXZ - _Crest_SimulationDeltaTime * float2(SampleVx0(worldXZ), SampleVy0(worldXZ)); float2 worldXZAdvected = worldXZAdvected_prediction; const float vySemiLagrangian = SampleVy0(worldXZAdvected); float vyAdvected = vySemiLagrangian; if (_Crest_MacCormackAdvection) { // Update prediction by advecting backwards to obtain a starting position and using the difference as an error term // Advect forwards to predict original position const float2 worldXZ_prediction = worldXZAdvected_prediction + _Crest_SimulationDeltaTime * float2(SampleVx0(worldXZAdvected_prediction), vySemiLagrangian); // Remove half of prediction error to yield average of both worldXZAdvected -= (worldXZ_prediction - worldXZ) / 2.0; float vyAdvectedNew = SampleVy0(worldXZAdvected); const float4 vysUsedForFirstSLStep = _Crest_VelocityYSource.Gather(linear_clamp_sampler, WorldXZToUv_Vy(worldXZ)); const float vyMin = min(min(vysUsedForFirstSLStep[0], vysUsedForFirstSLStep[1]), min(vysUsedForFirstSLStep[2], vysUsedForFirstSLStep[3])); const float vyMax = max(max(vysUsedForFirstSLStep[0], vysUsedForFirstSLStep[1]), max(vysUsedForFirstSLStep[2], vysUsedForFirstSLStep[3])); if (vyAdvectedNew >= vyMin && vyAdvectedNew <= vyMax) { vyAdvected = vyAdvectedNew; } } // Abs to silence 3571 which for some reason cannot disable with pragma even // HLSLSupport can do exactly that. const float h_vy = SampleH(worldXZAdvected); const float friction = _Crest_Friction * _Crest_SimulationDeltaTime / max(0.001f, pow(abs(h_vy), 1.333333f)); vyAdvected -= abs(vyAdvected) * vyAdvected * friction; vyAdvected = clamp(vyAdvected, -_Crest_MaximumVelocity, _Crest_MaximumVelocity); _Crest_VelocityYTarget[id.xy] = vyAdvected; } // H if (id.z == 2) { const float2 worldXZ = IdToWorldXZ_H(id.xy); // Advect forwards to predict advected position const float2 worldXZAdvected_prediction = worldXZ - _Crest_SimulationDeltaTime * float2(SampleVx0(worldXZ), SampleVy0(worldXZ)); float2 worldXZAdvected = worldXZAdvected_prediction; if (_Crest_MacCormackAdvectionForHeight) { // Update prediction by advecting backwards to obtain a starting position and using the difference as an error term // Advect forwards to predict original position const float2 worldXZ_prediction = worldXZAdvected_prediction + _Crest_SimulationDeltaTime * float2(SampleVx0(worldXZAdvected_prediction), SampleVy0(worldXZAdvected_prediction)); // Remove half of prediction error to yield average of both worldXZAdvected -= (worldXZ_prediction - worldXZ) / 2.0; } _Crest_HeightTarget[id.xy] = SampleH(worldXZAdvected); } } // // Update Height // float HeightAdjustment(uint2 id) { const float gravity = 9.81f; float hn = _Crest_HeightSource[id + uint2(0, 1)]; float hs = _Crest_HeightSource[id - uint2(0, 1)]; float he = _Crest_HeightSource[id + uint2(1, 0)]; float hw = _Crest_HeightSource[id - uint2(1, 0)]; float h_avg = 0.25 * (hn + hs + he + hw); float beta = 2.0; float minThickness = beta * _Crest_TexelSize / (gravity * _Crest_SimulationDeltaTime); return max(0.0, h_avg - minThickness); } void UpdateHeight(const uint3 i_ID) { const int2 id = i_ID.xy; // Fix height at border to prevent instability. if (i_ID.x == 0 || i_ID.y == 0 || i_ID.x == _Crest_Resolution - 1 || i_ID.y == _Crest_Resolution - 1) { _Crest_HeightTarget[id] = _Crest_HeightSource[id]; return; } float h = _Crest_HeightSource[id]; const uint x = id.x; const uint y = id.y; const float2 worldXZ = IdToWorldXZ_H(id); float pump = 0.0; if (h > 0.01) { // Drain water out at boundary of domain. { const float2 offset = abs(id / float(_Crest_Resolution) - 0.5); pump += _Crest_DrainWaterAtBoundaries * smoothstep(0.4, 0.5, max(offset.x, offset.y)); } // Blend waves into simulation. Pumps water in. { uint slice0, slice1; float alpha; PosToSliceIndices(worldXZ, 0.0, g_Crest_LodCount - 1.0, g_Crest_WaterScale, slice0, slice1, alpha); const float weight0 = (1.0 - alpha) * g_Crest_CascadeData[slice0].y; const float weight1 = (1.0 - weight0) * g_Crest_CascadeData[slice1].y; float waterY = g_Crest_WaterCenter.y; // If injecting to height then ignore waves. if (_Crest_InjectLevel) { if (_Crest_SampleFromLevelInputs) { waterY += SampleWaterLevel(worldXZ); } else { waterY += weight0 * Cascade::MakeLevel(slice0).SampleLevel(worldXZ) + weight1 * Cascade::MakeLevel(slice1).SampleLevel(worldXZ); } } else { waterY += weight0 * Cascade::MakeAnimatedWaves(slice0).SampleAnimatedWaves(worldXZ).y + weight1 * Cascade::MakeAnimatedWaves(slice1).SampleAnimatedWaves(worldXZ).y; } float simulationY = h + _Crest_DomainOrigin.y + _Crest_GroundHeightSource[id]; if (waterY > simulationY) { // This is tricksy. Don't apply forces from waves in deep, but also don't apply in very shallow. // The latter will mean that the SWS is held at sea level instead of being allowed to drop below. // I tried asserting disp.y > somevalue but that led to a whole different set of issues. Therefore // settle for a y=4x(1-x) weight that chooses the mid point of the mask blend. Seems to work. float maskWeight = 1.0; if (!_Crest_InjectLevel) { maskWeight = _Crest_MaskSource[id]; maskWeight *= 4.0 * (1.0 - maskWeight); } pump += _Crest_BlendPushUpStrength * maskWeight * (waterY - simulationY); } } } // Update height based on total flow in minus total flow out. // Prevents wave reflections at domain boundary. float drain = 30.0; float vxp = (x == uint(_Crest_Resolution - 1)) ? drain : _Crest_VelocityXSource[id]; float vyp = (y == uint(_Crest_Resolution - 1)) ? drain : _Crest_VelocityYSource[id]; float vxm = (x == 0) ? -drain : _Crest_VelocityXSource[uint2(x - 1, y)]; float vym = (y == 0) ? -drain : _Crest_VelocityYSource[uint2(x, y - 1)]; // FIXME: Causes jitters which triggers foam. float hvel; if (_Crest_UpwindHeight) { // Get forward or backward diffs respectively. float h = _Crest_HeightSource[id]; float2 hx = vxp <= 0.0 ? float2(h, _Crest_HeightSource[id + uint2(1, 0)]) : float2(_Crest_HeightSource[id - uint2(1, 0)], h); float2 hy = vyp <= 0.0 ? float2(h, _Crest_HeightSource[id + uint2(0, 1)]) : float2(_Crest_HeightSource[id - uint2(0, 1)], h); if (_Crest_DepthLimiter) { const float h_adj = HeightAdjustment(id); hx -= h_adj; hy -= h_adj; } hvel = -(vxp * hx[1] - vxm * hx[0] + vyp * hy[1] - vym * hy[0]) / _Crest_TexelSize; } else { const float dx = (vxp - vxm) / _Crest_TexelSize; const float dy = (vyp - vym) / _Crest_TexelSize; const float divergence = dx + dy; if (_Crest_DepthLimiter) { hvel = -(h - HeightAdjustment(id)) * divergence; } else { hvel = -h * divergence; } } h += _Crest_SimulationDeltaTime * hvel + pump - _Crest_Evaporation; h = max(h, 0.0); // // Dry - force to zero. // if (h < 0.001) h = 0.0; _Crest_HeightTarget[id] = h; } // // Height Overshoot Reduction // void HeightOvershootReduction(const uint3 i_ID) { const uint2 id = i_ID.xy; float h = _Crest_HeightSource[id]; if (any(id == 0) || any(id == _Crest_Resolution - 1)) { // Don't do this at boundary _Crest_HeightTarget[id] = h; return; } float y = _Crest_HeightSource[id] + _Crest_GroundHeightSource[id]; float hn = _Crest_HeightSource[id + uint2(1, 0)]; float hs = _Crest_HeightSource[id - uint2(1, 0)]; float he = _Crest_HeightSource[id + uint2(1, 0)]; float hw = _Crest_HeightSource[id - uint2(1, 0)]; float yn = hn + _Crest_GroundHeightSource[id + uint2(1, 0)]; float ys = hs + _Crest_GroundHeightSource[id - uint2(1, 0)]; float ye = he + _Crest_GroundHeightSource[id + uint2(1, 0)]; float yw = hw + _Crest_GroundHeightSource[id - uint2(1, 0)]; float lambdaEdge = 2.0 * _Crest_TexelSize; // X axis if ((y - yw) > lambdaEdge && y > ye) { h += _Crest_OvershootReductionStrength * (max(0.0, 0.5 * (h + he)) - h); } if ((y - ye) > lambdaEdge && y > yw) { h += _Crest_OvershootReductionStrength * (max(0.0, 0.5 * (h + hw)) - h); } // Z axis if ((y - ys) > lambdaEdge && y > yn) { h += _Crest_OvershootReductionStrength * (max(0.0, 0.5 * (h + hn)) - h); } if ((y - yn) > lambdaEdge && y > ys) { h += _Crest_OvershootReductionStrength * (max(0.0, 0.5 * (h + hs)) - h); } _Crest_HeightTarget[id] = h; } // // Update Velocity // void UpdateVelocity(const uint3 i_ID) { const int2 id = i_ID.xy; // Clear velocity at border to prevent water leaking. if (i_ID.x == 0 || i_ID.y == 0 || i_ID.x == _Crest_Resolution - 1 || i_ID.y == _Crest_Resolution - 1) { _Crest_VelocityXTarget[id] = 0; _Crest_VelocityYTarget[id] = 0; return; } // Height before vel const float2 worldXZ_H0 = IdToWorldXZ_H(id); const float g0 = _Crest_GroundHeightSource[id]; const float h0 = _Crest_HeightSource[id]; const uint x = id.x; const uint y = id.y; const uint xp = min(x + 1, _Crest_Resolution - 1); const uint yp = min(y + 1, _Crest_Resolution - 1); const float gravity = 9.81f; // Vx { float vx = _Crest_VelocityXTarget[id]; const float2 worldXZ_H1 = IdToWorldXZ_H(uint2(xp, y)); const float g1 = _Crest_GroundHeightSource[uint2(xp, y)]; const float h1 = _Crest_HeightSource[uint2(xp, y)]; const float hdiff = (h1 + g1) - (h0 + g0); //if (fabs(hdiff) > maxDiff) hdiff = sign(hdiff) * maxDiff; float accel = -gravity * hdiff / _Crest_TexelSize; vx += _Crest_SimulationDeltaTime * accel; // Kill velocity if totally dry if (h0 < 0.001 && h1 < 0.001) { vx = 0.0; } else if (accel < 0 && h1 < 0.001) { // Don't produce acceleration from water flowing in from a dry cell vx = 0.0; } else if (accel > 0 && h0 < 0.001) { // Don't produce acceleration from water flowing in from a dry cell vx = 0.0; } _Crest_VelocityXTarget[id] = vx; if (h0 < 0.001 && h1 < 0.001) _Crest_VelocityXTarget[id] = 0.0; } // Vy { float vy = _Crest_VelocityYTarget[id]; const float2 worldXZ_H1 = IdToWorldXZ_H(uint2(x, yp)); const float g1 = _Crest_GroundHeightSource[uint2(x, yp)]; const float h1 = _Crest_HeightSource[uint2(x, yp)]; const float hdiff = (h1 + g1) - (h0 + g0); //if (fabs(hdiff) > maxDiff) hdiff = sign(hdiff) * maxDiff; float accel = -gravity * hdiff / _Crest_TexelSize; vy += _Crest_SimulationDeltaTime * accel; // Kill velocity if totally dry if (h0 < 0.001 && h1 < 0.001) { vy = 0.0; } else if (accel < 0 && h1 < 0.001) { // Don't produce acceleration from water flowing in from a dry cell vy = 0.0; } else if (accel > 0 && h0 < 0.001) { // Don't produce acceleration from water flowing in from a dry cell vy = 0.0; } _Crest_VelocityYTarget[id] = vy; } } // // Blur // void Blur(const uint3 i_ID) { const int2 id = i_ID.xy; const int resolution = _Crest_Resolution; float result = 0.0; const int radius = 1; float sampleCount = 0.0; for (int y = -radius; y <= radius; y++) { for (int x = -radius; x <= radius; x++) { int2 idx = id + int2(x, y); // Skip pixel if outside. Clamping coordinates produced bad values at edge. if (any(idx < 0) || any(idx >= resolution)) continue; result += _Crest_MaskSource[idx]; sampleCount += 1.0; } } _Crest_MaskTarget[id] = result / sampleCount; } void BlurHeight(const uint3 i_ID) { const int2 id = i_ID.xy; float result = 0.0; float twt = 0.0; const int rad = 1; float hcenter = _Crest_HeightSource[id]; if (hcenter < 0.001) { _Crest_HeightTarget[id] = 0.0; return; } for (int y = -rad; y <= rad; y++) { for (int x = -rad; x <= rad; x++) { int2 idx; // Need to cast otherwise load will wrap around at edges. idx.x = clamp(id.x + x, 0, (int)_Crest_Resolution - 1); idx.y = clamp(id.y + y, 0, (int)_Crest_Resolution - 1); float h = _Crest_HeightSource[idx]; if (h >= 0.001) { result += h + _Crest_GroundHeightSource[idx]; twt += 1.0; } } } if (twt > 0.0) { float blurredY = result / twt; _Crest_HeightTarget[id] = max(blurredY - _Crest_GroundHeightSource[id], 0.0); } else { _Crest_HeightTarget[id] = hcenter; } } void AddAboveGroundHeight(const uint2 i_Coordinates, const float i_Ground, inout float io_Height, inout float io_Samples) { float h = _Crest_HeightSource[i_Coordinates]; if (h > FLT_MIN) { // Make sure we do not push water through ground. if (h < i_Ground) { io_Height += h; io_Samples += 1.0; } } } void MaskOut(const uint2 i_Coordinates, const float i_HeightBelowGround, inout bool i_Mask) { const float h = _Crest_HeightSource[i_Coordinates]; if (h > 0.0) { const float g = _Crest_GroundHeightSource[i_Coordinates]; i_Mask = i_Mask || (i_HeightBelowGround < (h + g)); } } void MaskEdge(const uint3 i_ID) { const uint2 id = i_ID.xy; float h = _Crest_HeightSource[id]; // Mask above ground height so we do not process it in expand edge. bool mask = h > 0.0; // Add ground height to water height to get world height of surface. h += _Crest_GroundHeightSource[id]; if (!mask) { const int3 dd = int3(1, -1, 0); MaskOut(id + dd.xx, h, mask); MaskOut(id + dd.xy, h, mask); MaskOut(id + dd.xz, h, mask); MaskOut(id + dd.yx, h, mask); MaskOut(id + dd.yy, h, mask); MaskOut(id + dd.yz, h, mask); MaskOut(id + dd.zx, h, mask); MaskOut(id + dd.zy, h, mask); } if (mask) { // Move to world space. h += _Crest_DomainOrigin.y; // Make relative to sea level. h -= g_Crest_WaterCenter.y; } else { h = FLT_MIN; } _Crest_HeightTarget[id] = h; } void ExpandEdge(const uint3 i_ID) { const uint2 id = i_ID.xy; const int3 dd = int3(1, -1, 0); float h = _Crest_HeightSource[id]; if (h > FLT_MIN) { _Crest_HeightTarget[id] = h; return; } const float g = _Crest_GroundHeightSource[id] + _Crest_DomainOrigin.y - g_Crest_WaterCenter.y; // Add margin inside geometry to prevent rise or drops at intersections. float samples = 0.0; h = 0.0; AddAboveGroundHeight(id + dd.xx, g, h, samples); AddAboveGroundHeight(id + dd.xy, g, h, samples); AddAboveGroundHeight(id + dd.xz, g, h, samples); AddAboveGroundHeight(id + dd.yx, g, h, samples); AddAboveGroundHeight(id + dd.yy, g, h, samples); AddAboveGroundHeight(id + dd.yz, g, h, samples); AddAboveGroundHeight(id + dd.zx, g, h, samples); AddAboveGroundHeight(id + dd.zy, g, h, samples); // Found a height. if (samples > 0.0) { // Includes ground, sea level and origin. h /= samples; } _Crest_HeightTarget[id] = h; } void FinalizeHeight(const uint3 i_ID) { const uint2 id = i_ID.xy; float h = _Crest_HeightSource[id]; // Clip doesn't work if ground is below sea level - it will make the water pop back up to sea level/anim waves. // Same for multiplying down alpha blend weight. This does something reasonable - yanks down water when almost dry.. h -= 0.1 * (1.0 - saturate(h / 0.02)); // Add ground height to water height to get world height of surface. h += _Crest_GroundHeightSource[id]; // Move to world space. h += _Crest_DomainOrigin.y; // Make relative to sea level. h -= g_Crest_WaterCenter.y; _Crest_HeightTarget[id] = h; } // // Injectors // float WaterHeight(float2 i_UV, float i_Current = 0.0) { float h = _Crest_HeightSource.SampleLevel(LODData_linear_clamp_sampler, i_UV, 0.0).x; // Reject any samples below ground. if (h < 0.001) { // Return the current height to not trigger the curve test. h = i_Current; } else { // Add ground height to water height to get height of surface h += _Crest_GroundHeightSource.SampleLevel(LODData_linear_clamp_sampler, i_UV, 0.0).x; } // Silences: shader warning use of potentially uninitialized variable. return h; } float ComputeAlpha(const float2 i_UV) { // Domain mask. const float2 offset = abs(i_UV - 0.5); float alpha = smoothstep(0.5, 0.45, max(offset.x, offset.y)); if (alpha >= 0.0) { // Simulation mask. alpha *= _Crest_MaskSource.SampleLevel(LODData_linear_clamp_sampler, i_UV, 0.0); } return alpha; } float ComputeAlphaFromMargin(const float2 i_Position) { float alpha = 1.0; float2 position = abs(i_Position - _Crest_DomainOrigin.xz); if (max(position.x, position.y) > (_Crest_DomainWidth.x * 0.5 - _Crest_MarginWidth)) alpha = 0.0; return alpha; } void InjectShape(const uint3 i_ID) { const float2 position = Cascade::MakeAnimatedWaves(i_ID.z).IDToWorld(i_ID.xy); const float2 uv = (position - _Crest_DomainOrigin.xz) / _Crest_DomainWidth + 0.5; // Over scan to ensure signal continued off the edges which helps at low LODs. if (any(uv != saturate(uv))) return; float alpha = _Crest_Weight * ComputeAlpha(uv); if (alpha <= 0.0) return; float h = _Crest_HeightSource.SampleLevel(LODData_linear_clamp_sampler, uv, 0.0).x; // Power up alpha to bring anim waves further in towards shore. alpha = pow(alpha, 2.0); float4 result = _Crest_ShapeTarget[i_ID]; // Alpha blend. result.xyz *= 1.0 - alpha; result.xyz += float3(0, h * alpha, 0); _Crest_ShapeTarget[i_ID] = result; } void InjectLevel(const uint3 i_ID) { const float2 position = Cascade::MakeLevel(i_ID.z).IDToWorld(i_ID.xy); const float2 uv = (position - _Crest_DomainOrigin.xz) / _Crest_DomainWidth + 0.5; // Over scan to ensure signal continued off the edges which helps at low LODs. if (any(uv != saturate(uv))) return; float alpha = _Crest_Weight * ComputeAlphaFromMargin(position); if (alpha <= 0.0) return; float h = _Crest_HeightSource.SampleLevel(LODData_linear_clamp_sampler, uv, 0.0).x; float result = _Crest_Weight * _Crest_LevelTarget[i_ID]; // Height is a source of water and should be the minimum height. // The simulation will still ripple over the top which is acceptable. result = max(result, h); _Crest_LevelTarget[i_ID] = result; } void InjectFoam(const uint3 i_ID) { const float2 position = Cascade::MakeFoam(i_ID.z).IDToWorld(i_ID.xy); const float2 uv = (position - _Crest_DomainOrigin.xz) / _Crest_DomainWidth + 0.5; // Over scan to ensure signal continued off the edges which helps at low LODs. if (any(uv != saturate(uv))) return; float alpha = _Crest_Weight * (_Crest_InjectLevel ? ComputeAlphaFromMargin(position) : ComputeAlpha(uv)); if (alpha <= 0.0) return; // Use approximation of max curvature as foam term. Seems to grab leading wave edge alright. const float2 dx = float2(1.0 / _Crest_Resolution, 0.0); const float h = WaterHeight(uv); if (h <= 0.0) return; const float h_xm = WaterHeight(uv - dx.xy, h); const float h_xp = WaterHeight(uv + dx.xy, h); const float h_zm = WaterHeight(uv - dx.yx, h); const float h_zp = WaterHeight(uv + dx.yx, h); const float curvature = max(abs(h_xp + h_xm - 2.0 * h), abs(h_zp + h_zm - 2.0 * h)) / (2.0 * dx.x * _Crest_DomainWidth); float foam = 10.0 * curvature; // Apply velocity to flow to prevent foam at edges of geometry. const float vx = _Crest_VelocityXSource.SampleLevel(LODData_linear_clamp_sampler, uv, 0.0); const float vy = _Crest_VelocityYSource.SampleLevel(LODData_linear_clamp_sampler, uv, 0.0); foam *= length(float2(vx, vy)); foam *= alpha; // Integrate. foam *= _Crest_SimDeltaTime; foam *= _Crest_InjectionStrength; _Crest_FoamTarget[i_ID] += foam; } void InjectFlow(const uint3 i_ID) { const Cascade cascade = Cascade::MakeFlow(i_ID.z); const float2 position = cascade.IDToWorld(i_ID.xy); const float2 uv = (position - _Crest_DomainOrigin.xz) / _Crest_DomainWidth + 0.5; // Over scan to ensure signal continued off the edges which helps at low LODs. if (any(uv != saturate(uv))) return; float alpha = _Crest_Weight * (_Crest_InjectLevel ? ComputeAlphaFromMargin(position) : ComputeAlpha(uv)); if (alpha <= 0.0) return; float mip; { const float3 uv = cascade.IDToUV(i_ID.xy); const float2 dd = float2(cascade._OneOverResolution * 8.0, 0); const float2 dx = cascade.UVToWorld(uv + dd.xyy) - position; const float2 dy = cascade.UVToWorld(uv + dd.yxy) - position; // Calculate the rate of change in x and y directions. const float2 rateOfChange = float2(length(dx.xy), length(dy.xy)); mip = log2(max(rateOfChange.x, rateOfChange.y)); } // These should use mip maps. Without this bad pops ensue from aliasing x combine-pass-flow. const float vx = _Crest_VelocityXSource.SampleLevel(LODData_linear_clamp_sampler, uv, mip); const float vy = _Crest_VelocityYSource.SampleLevel(LODData_linear_clamp_sampler, uv, mip); float2 result = _Crest_FlowTarget[i_ID]; // Alpha blend. result *= 1.0 - alpha; result += float2(vx, vy) * _Crest_InjectionStrength * alpha; _Crest_FlowTarget[i_ID] = result; } // // Baking // // Copy texture. Here because script code path simpler if same for all outputs // rather than using CopyTexture. void BakeLevel(const uint3 i_ID) { const uint2 id = i_ID.xy; _Crest_LevelBakeTarget[id] = _Crest_HeightSource[id]; } void BakeFoam(const uint3 i_ID) { const uint2 id = i_ID.xy; const float2 uv = id / (float)_Crest_Resolution; // Over scan to ensure signal continued off the edges which helps at low LODs. if (any(uv != saturate(uv))) return; float alpha = ComputeAlphaFromMargin(IdToWorldXZ_H(id)); if (alpha <= 0.0) return; // Cannot load texture as curvature is not picked up. Must need interpolation. const float2 dx = float2(1.0 / _Crest_Resolution, 0.0); const float h = WaterHeight(uv); if (h <= 0.0) { _Crest_FoamBakeTarget[id] = 0.0; return; } const float h_xm = WaterHeight(uv - dx.xy, h); const float h_xp = WaterHeight(uv + dx.xy, h); const float h_zm = WaterHeight(uv - dx.yx, h); const float h_zp = WaterHeight(uv + dx.yx, h); const float curvature = max(abs(h_xp + h_xm - 2.0 * h), abs(h_zp + h_zm - 2.0 * h)) / (2.0 * dx.x * _Crest_DomainWidth); float foam = 10.0 * curvature; foam *= length(float2(_Crest_VelocityXSource[id], _Crest_VelocityYSource[id])); foam *= _Crest_InjectionStrength; _Crest_FoamBakeTarget[id] = foam; } void BakeFlow(const uint3 i_ID) { const uint2 id = i_ID.xy; float alpha = ComputeAlphaFromMargin(IdToWorldXZ_H(id)); if (alpha <= 0.0) return; _Crest_FlowBakeTarget[id.xy] = float2(_Crest_VelocityXSource[id], _Crest_VelocityYSource[id]) * _Crest_InjectionStrength; } m_CrestNameSpaceEnd m_CrestKernelDefault(Initialize); m_CrestKernelDefault(InitializeGroundHeight); m_CrestKernelDefault(Advect); m_CrestKernelDefault(UpdateHeight); m_CrestKernelDefault(HeightOvershootReduction); m_CrestKernelDefault(UpdateVelocity); m_CrestKernelDefault(BlurHeight); m_CrestKernelDefault(Blur); m_CrestKernelDefault(MaskEdge); m_CrestKernelDefault(ExpandEdge); m_CrestKernelDefault(FinalizeHeight); m_CrestInputKernelDefault(InjectShape); m_CrestInputKernelDefault(InjectLevel); m_CrestInputKernelDefault(InjectFoam); m_CrestInputKernelDefault(InjectFlow); m_CrestKernelDefault(BakeLevel); m_CrestKernelDefault(BakeFoam); m_CrestKernelDefault(BakeFlow);