Skip to content

Commit b3cb42e

Browse files
feat(water): add flowmap parallax (#1636)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent ecf23ed commit b3cb42e

File tree

2 files changed

+245
-29
lines changed

2 files changed

+245
-29
lines changed

features/Water Effects/Shaders/WaterEffects/WaterParallax.hlsli

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,126 @@ namespace WaterEffects
8686

8787
return parallaxOffsetTS.xy * parallaxAmount;
8888
}
89+
90+
#if defined(FLOWMAP)
91+
float GetFlowmapHeight(PS_INPUT input, float2 uvShift, float multiplier, float offset, float mipLevel)
92+
{
93+
FlowmapData flowData = GetFlowmapDataUV(input, uvShift);
94+
float2 baseUV = offset + (flowData.flowVector - float2(multiplier * ((0.001 * ReflectionColor.w) * flowData.color.w), 0));
95+
return FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, baseUV, mipLevel).w;
96+
}
97+
98+
float GetFlowmapBlendedHeight(PS_INPUT input, float2 normalMul, float2 uvShift, float mipLevel)
99+
{
100+
float height0 = GetFlowmapHeight(input, uvShift, 9.92, 0, mipLevel);
101+
float height1 = GetFlowmapHeight(input, float2(0, uvShift.y), 10.64, 0.27, mipLevel);
102+
float height2 = GetFlowmapHeight(input, 0.0.xx, 8, 0, mipLevel);
103+
float height3 = GetFlowmapHeight(input, float2(uvShift.x, 0), 8.48, 0.62, mipLevel);
104+
105+
float blendedHeight =
106+
normalMul.y * (normalMul.x * height2 + (1 - normalMul.x) * height3) +
107+
(1 - normalMul.y) * (normalMul.x * height1 + (1 - normalMul.x) * height0);
108+
109+
return blendedHeight;
110+
}
111+
112+
float GetFlowmapParallaxAmount(PS_INPUT input, float2 flowmapDims, float3 viewDirection)
113+
{
114+
float viewDotUp = -viewDirection.z;
115+
116+
if (viewDotUp < 0.05)
117+
return 0.0;
118+
119+
float2 parallaxDir = viewDirection.xy / -viewDirection.z;
120+
parallaxDir.y = -parallaxDir.y;
121+
122+
float parallaxScale = 0.008 * saturate(viewDotUp * 2.0);
123+
parallaxDir *= parallaxScale;
124+
125+
float2 uvShiftPx = 1 / (128 * flowmapDims);
126+
127+
int numSteps = (int)lerp(32.0, 8.0, viewDotUp);
128+
float stepSize = rcp((float)numSteps);
129+
130+
float currBound = 0.0;
131+
float currHeight = 1.0;
132+
float prevHeight = 1.0;
133+
134+
[loop] for (int i = 0; i < numSteps && currHeight > currBound; i++)
135+
{
136+
prevHeight = currHeight;
137+
currBound += stepSize;
138+
139+
PS_INPUT offsetInput = input;
140+
offsetInput.TexCoord3.xy = input.TexCoord3.xy + currBound * parallaxDir;
141+
142+
float2 cellBlend = 0.5 + -(-0.5 + abs(frac(offsetInput.TexCoord2.zw * (64 * flowmapDims)) * 2 - 1));
143+
currHeight = 1.0 - GetFlowmapBlendedHeight(offsetInput, cellBlend, uvShiftPx, 0);
144+
}
145+
146+
float prevBound = currBound - stepSize;
147+
float delta2 = prevBound - prevHeight;
148+
float delta1 = currBound - currHeight;
149+
float denominator = delta2 - delta1;
150+
151+
return denominator != 0.0 ? (currBound * delta2 - prevBound * delta1) / denominator : currBound;
152+
}
153+
154+
float GetFlowmapParallaxHeight(PS_INPUT input, float2 currentOffset, float3 normalScalesRcp, float mipLevel)
155+
{
156+
float height = Normals01Tex.SampleLevel(Normals01Sampler, input.TexCoord1.xy + currentOffset * normalScalesRcp.x, mipLevel).w;
157+
height *= NormalsAmplitude.x;
158+
return 1.0 - height;
159+
}
160+
161+
float2 GetFlowmapParallaxUVOffset(PS_INPUT input, float3 viewDirection, float3 normalScalesRcp)
162+
{
163+
float2 parallaxOffsetTS = viewDirection.xy / -viewDirection.z;
164+
parallaxOffsetTS *= 80.0;
165+
166+
float2 textureDims;
167+
Normals01Tex.GetDimensions(textureDims.x, textureDims.y);
168+
#if defined(VR)
169+
textureDims /= 16.0;
170+
#else
171+
textureDims /= 8.0;
172+
#endif
173+
float2 texCoordsPerSize = input.TexCoord1.xy * textureDims;
174+
float2 dxSize = ddx(texCoordsPerSize);
175+
float2 dySize = ddy(texCoordsPerSize);
176+
float2 dTexCoords = dxSize * dxSize + dySize * dySize;
177+
float minTexCoordDelta = max(dTexCoords.x, dTexCoords.y);
178+
float mipLevel = max(0.5 * log2(minTexCoordDelta), 0);
179+
#if defined(VR)
180+
mipLevel += 4;
181+
#else
182+
mipLevel += 3;
183+
#endif
184+
185+
float stepSize = rcp(16.0);
186+
float currBound = 0.0;
187+
float currHeight = 1.0;
188+
float prevHeight = 1.0;
189+
190+
[loop] while (currHeight > currBound)
191+
{
192+
prevHeight = currHeight;
193+
currBound += stepSize;
194+
currHeight = GetFlowmapParallaxHeight(input, currBound * parallaxOffsetTS.xy, normalScalesRcp, mipLevel);
195+
}
196+
197+
float prevBound = currBound - stepSize;
198+
float delta2 = prevBound - prevHeight;
199+
float delta1 = currBound - currHeight;
200+
float denominator = delta2 - delta1;
201+
float parallaxAmount = (currBound * delta2 - prevBound * delta1) / denominator;
202+
203+
return parallaxOffsetTS.xy * parallaxAmount;
204+
}
205+
206+
float2 GetFlowmapParallaxOffset(PS_INPUT input, float2 flowmapDimensions, float3 viewDirection, float3 normalScalesRcp)
207+
{
208+
return GetFlowmapParallaxUVOffset(input, viewDirection, normalScalesRcp);
209+
}
210+
#endif
89211
}

package/Shaders/Water.hlsl

Lines changed: 123 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ struct VS_OUTPUT
8888
float4 TexCoord3 : TEXCOORD3;
8989
# endif
9090
# if defined(FLOWMAP)
91-
nointerpolation float TexCoord4 : TEXCOORD4;
91+
nointerpolation float2 TexCoord4 : TEXCOORD4;
9292
# endif
9393
# if NUM_SPECULAR_LIGHTS == 0
9494
float4 MPosition : TEXCOORD5;
@@ -453,7 +453,7 @@ struct FlowmapData
453453
FlowmapData GetFlowmapDataTextureSpace(PS_INPUT input, float2 uvShift)
454454
{
455455
FlowmapData data;
456-
data.color = FlowMapTex.Sample(FlowMapSampler, input.TexCoord2.zw + uvShift);
456+
data.color = FlowMapTex.SampleLevel(FlowMapSampler, input.TexCoord2.zw + uvShift, 0);
457457
data.flowVector = (64 * input.TexCoord3.xy) * sqrt(1.01 - data.color.z);
458458
// NOTE: flowVector is NOT transformed yet - this is the raw vector before rotation matrix
459459
return data;
@@ -486,23 +486,90 @@ FlowmapData GetFlowmapDataUV(PS_INPUT input, float2 uvShift)
486486
data.flowVector = mul(transpose(flowRotationMatrix), data.flowVector);
487487
return data;
488488
}
489+
// ----------------------------------------------------------------
490+
// Flowmap Parallax Functions
491+
// ----------------------------------------------------------------
489492

490493
/**
491-
* Generates flowmap-based normal perturbation for water surface
494+
* Samples height from flowmap texture using the same 4-sample blend as flowmap normals
495+
* This ensures height transitions match the normal transitions exactly
492496
*
493-
* @param input Pixel shader input containing texture coordinates and world position
494-
* @param uvShift UV offset for flowmap sampling (used for animation phases)
495-
* @param multiplier Intensity multiplier for the flow effect
496-
* @param offset Base UV offset for the normal texture sampling
497-
* @return float3 Normal perturbation (XY=normal offset, Z=flow strength mask)
498-
*
499-
* @details This function uses flowmap data to:
500-
* - Calculate flow-displaced UV coordinates for normal texture sampling
501-
* - Apply flow-based animation to water normal textures
502-
* - Return both the normal perturbation and flow strength information
503-
*
504-
* @note The returned Z component contains the original flowmap strength value
505-
* which can be used for blending between flow and non-flow normals
497+
* @param input PS_INPUT for flowmap coordinate access
498+
* @param normalMul The blend weights from the flowmap system (same as used for normals)
499+
* @param uvShift The UV shift value (1 / (128 * flowmapDimensions))
500+
* @param mipLevel Mip level for texture sampling
501+
*/
502+
float GetFlowmapHeightBlended(PS_INPUT input, float2 normalMul, float2 uvShift, float mipLevel)
503+
{
504+
// Sample height using the EXACT same UV computation as GetFlowmapNormal
505+
// This ensures the height blending matches the normal blending perfectly
506+
507+
// Sample 0: uvShift, multiplier=9.92, offset=0
508+
FlowmapData flowData0 = GetFlowmapDataUV(input, uvShift);
509+
float2 uv0 = 0 + (flowData0.flowVector - float2(9.92 * ((0.001 * ReflectionColor.w) * flowData0.color.w), 0));
510+
float height0 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv0, mipLevel).w;
511+
512+
// Sample 1: float2(0, uvShift.y), multiplier=10.64, offset=0.27
513+
FlowmapData flowData1 = GetFlowmapDataUV(input, float2(0, uvShift.y));
514+
float2 uv1 = 0.27 + (flowData1.flowVector - float2(10.64 * ((0.001 * ReflectionColor.w) * flowData1.color.w), 0));
515+
float height1 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv1, mipLevel).w;
516+
517+
// Sample 2: 0.0.xx, multiplier=8, offset=0
518+
FlowmapData flowData2 = GetFlowmapDataUV(input, 0.0.xx);
519+
float2 uv2 = 0 + (flowData2.flowVector - float2(8 * ((0.001 * ReflectionColor.w) * flowData2.color.w), 0));
520+
float height2 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv2, mipLevel).w;
521+
522+
// Sample 3: float2(uvShift.x, 0), multiplier=8.48, offset=0.62
523+
FlowmapData flowData3 = GetFlowmapDataUV(input, float2(uvShift.x, 0));
524+
float2 uv3 = 0.62 + (flowData3.flowVector - float2(8.48 * ((0.001 * ReflectionColor.w) * flowData3.color.w), 0));
525+
float height3 = FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, uv3, mipLevel).w;
526+
527+
// Use the EXACT same blending formula as flowmap normals
528+
float blendedHeight =
529+
normalMul.y * (normalMul.x * height2 + (1 - normalMul.x) * height3) +
530+
(1 - normalMul.y) * (normalMul.x * height1 + (1 - normalMul.x) * height0);
531+
532+
return blendedHeight;
533+
}
534+
535+
// Keep this for compatibility - just forwards to the proper function
536+
float GetFlowmapHeightBarycentric(PS_INPUT input, float2 flowmapDimensions, float2 baseUV, float mipLevel)
537+
{
538+
// This is now unused - we use GetFlowmapHeightBlended directly
539+
return FlowMapNormalsTex.SampleLevel(FlowMapNormalsSampler, baseUV, mipLevel).w;
540+
}
541+
542+
/**
543+
* Computes mip level for flowmap texture sampling
544+
*/
545+
float GetFlowmapMipLevel(float2 flowmapUV)
546+
{
547+
float2 textureDims;
548+
FlowMapNormalsTex.GetDimensions(textureDims.x, textureDims.y);
549+
550+
#if defined(VR)
551+
textureDims /= 16.0;
552+
#else
553+
textureDims /= 8.0;
554+
#endif
555+
556+
float2 texCoordsPerSize = flowmapUV * textureDims;
557+
float2 dxSize = ddx(texCoordsPerSize);
558+
float2 dySize = ddy(texCoordsPerSize);
559+
float2 dTexCoords = dxSize * dxSize + dySize * dySize;
560+
float minTexCoordDelta = max(dTexCoords.x, dTexCoords.y);
561+
return max(0.5 * log2(minTexCoordDelta), 0);
562+
}
563+
564+
/**
565+
* Samples height from flowmap texture (riverflow.dds alpha channel)
566+
* Uses the same UV calculation as GetFlowmapNormal for consistency
567+
*/
568+
569+
570+
/**
571+
* Generates flowmap-based normal (no parallax - flowmap normals are not parallax-shifted)
572+
* Uses mip clamping to preserve detail at distance and prevent over-blurring
506573
*/
507574
float3 GetFlowmapNormal(PS_INPUT input, float2 uvShift, float multiplier, float offset)
508575
{
@@ -588,14 +655,34 @@ WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float norma
588655
# endif
589656

590657
# if defined(FLOWMAP)
591-
float2 normalMul =
592-
0.5 + -(-0.5 + abs(frac(input.TexCoord2.zw * (64 * input.TexCoord4)) * 2 - 1));
593-
float uvShift = 1 / (128 * input.TexCoord4);
658+
# if defined(UNIFIED_WATER)
659+
float2 flowmapDimensions = input.TexCoord4.xy;
660+
# else
661+
float2 flowmapDimensions = input.TexCoord4.xx;
662+
# endif
663+
float2 uvShift = 1 / (128 * flowmapDimensions);
664+
665+
// Compute flowmap parallax and create parallaxed input for normal sampling
666+
PS_INPUT flowmapInput = input;
667+
float2 flowmapParallaxOffset = float2(0, 0);
668+
# if defined(WATER_PARALLAX) && !defined(LOD)
669+
float parallaxAmount = WaterEffects::GetFlowmapParallaxAmount(input, flowmapDimensions, viewDirection);
670+
float2 parallaxDir = viewDirection.xy / -viewDirection.z;
671+
parallaxDir.y = -parallaxDir.y;
672+
float viewDotUp = -viewDirection.z;
673+
parallaxDir *= 0.008 * saturate(viewDotUp * 2.0);
674+
flowmapInput.TexCoord3.xy = input.TexCoord3.xy + parallaxAmount * parallaxDir;
675+
flowmapParallaxOffset = WaterEffects::GetFlowmapParallaxOffset(input, flowmapDimensions, viewDirection, normalScalesRcp);
676+
# endif
594677

595-
float3 flowmapNormal0 = GetFlowmapNormal(input, uvShift.xx, 9.92, 0);
596-
float3 flowmapNormal1 = GetFlowmapNormal(input, float2(0, uvShift), 10.64, 0.27);
597-
float3 flowmapNormal2 = GetFlowmapNormal(input, 0.0.xx, 8, 0);
598-
float3 flowmapNormal3 = GetFlowmapNormal(input, float2(uvShift, 0), 8.48, 0.62);
678+
// Calculate cell blend weights using parallaxed input
679+
float2 normalMul = 0.5 + -(-0.5 + abs(frac(flowmapInput.TexCoord2.zw * (64 * flowmapDimensions)) * 2 - 1));
680+
681+
// Sample flowmap normals with parallax applied
682+
float3 flowmapNormal0 = GetFlowmapNormal(flowmapInput, uvShift, 9.92, 0);
683+
float3 flowmapNormal1 = GetFlowmapNormal(flowmapInput, float2(0, uvShift.y), 10.64, 0.27);
684+
float3 flowmapNormal2 = GetFlowmapNormal(flowmapInput, 0.0.xx, 8, 0);
685+
float3 flowmapNormal3 = GetFlowmapNormal(flowmapInput, float2(uvShift.x, 0), 8.48, 0.62);
599686

600687
float2 flowmapNormalWeighted =
601688
normalMul.y * (normalMul.x * flowmapNormal2.xy + (1 - normalMul.x) * flowmapNormal3.xy) +
@@ -608,14 +695,21 @@ WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float norma
608695
0);
609696
flowmapNormal.z =
610697
sqrt(1 - flowmapNormal.x * flowmapNormal.x - flowmapNormal.y * flowmapNormal.y);
611-
# endif
612-
698+
float2 baseNormalUv = input.TexCoord1.xy;
613699
# if defined(WATER_PARALLAX)
614-
float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy + parallaxOffset.xy * normalScalesRcp.x, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2);
615-
# else
616-
float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2);
700+
// Use flowmap-derived parallax offset for base normals
701+
baseNormalUv += flowmapParallaxOffset.xy * normalScalesRcp.x;
617702
# endif
618-
703+
float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, baseNormalUv, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2);
704+
# endif // End of FLOWMAP block
705+
706+
# if !defined(FLOWMAP)
707+
# if defined(WATER_PARALLAX)
708+
float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy + parallaxOffset.xy * normalScalesRcp.x, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2);
709+
# else
710+
float3 normals1 = Normals01Tex.SampleBias(Normals01Sampler, input.TexCoord1.xy, SharedData::MipBias).xyz * 2.0 + float3(-1, -1, -2);
711+
# endif
712+
# endif // End of !FLOWMAP block
619713
# if defined(FLOWMAP) && !defined(BLEND_NORMALS)
620714
# ifdef DISABLE_FLOWMAP_NORMALS
621715
// FLOWMAP NORMALS DISABLED: Using only base normals (flow system still active for ripples/splashes)

0 commit comments

Comments
 (0)