diff --git a/features/Wetness Effects/Shaders/WetnessEffects/WetnessEffects.hlsli b/features/Wetness Effects/Shaders/WetnessEffects/WetnessEffects.hlsli index 4c2c2cd17c..a1590134e4 100644 --- a/features/Wetness Effects/Shaders/WetnessEffects/WetnessEffects.hlsli +++ b/features/Wetness Effects/Shaders/WetnessEffects/WetnessEffects.hlsli @@ -54,8 +54,11 @@ namespace WetnessEffects } // xyz - ripple normal, w - splotches - float4 GetRainDrops(float3 worldPos, float t, float3 normal) + float4 GetRainDrops(float3 worldPos, float t, float3 normal, float rippleStrengthModifier = 1.0, float2 flowOffset = float2(0.0, 0.0)) { + // Apply flow offset to world position for flow-aware ripple positioning + worldPos.xy += flowOffset; + const static float uintToFloat = rcp(4294967295.0); const float rippleBreadthRcp = rcp(SharedData::wetnessEffectsSettings.RippleBreadth); @@ -106,13 +109,18 @@ namespace WetnessEffects float distSqr = dot(vec2Centre, vec2Centre); float rippleT = residual * SharedData::wetnessEffectsSettings.RippleLifetimeRcp; if (rippleT < 1.) { - float ripple_r = lerp(0., SharedData::wetnessEffectsSettings.RippleRadius, rippleT); + // vary ripple size using high-quality random hash (preserves full entropy) + uint sizeHash = Random::iqint3(hash.xy); + float sizeRandom = float(sizeHash) * uintToFloat; + float sizeVariation = lerp(0.7, 1.3, sizeRandom); + + float ripple_r = lerp(0.f, SharedData::wetnessEffectsSettings.RippleRadius * sizeVariation, rippleT); float ripple_inner_radius = ripple_r - SharedData::wetnessEffectsSettings.RippleBreadth; float band_lerp = (sqrt(distSqr) - ripple_inner_radius) * rippleBreadthRcp; if (band_lerp > 0. && band_lerp < 1.) { float deriv = (band_lerp < .5 ? SmoothstepDeriv(band_lerp * 2.) : -SmoothstepDeriv(2. - band_lerp * 2.)) * - lerp(SharedData::wetnessEffectsSettings.RippleStrength, 0, rippleT * rippleT); + lerp(SharedData::wetnessEffectsSettings.RippleStrength * rippleStrengthModifier, 0, rippleT * rippleT); float3 grad = float3(normalize(vec2Centre), -deriv); float3 bitangent = float3(-grad.y, grad.x, 0); @@ -137,7 +145,7 @@ namespace WetnessEffects float3 R = reflect(-V, N); float NoV = saturate(dot(N, V)); -#if defined(DYNAMIC_CUBEMAPS) +#if defined(DYNAMIC_CUBEMAPS) && !defined(WATER) # if defined(DEFERRED) float level = roughness * 7.0; float3 specularIrradiance = 1.0; @@ -168,4 +176,120 @@ namespace WetnessEffects { return LightingFuncGGX_OPT3(N, V, L, roughness, 0.02) * lightColor; } + +// Debug visualization functions for DEBUG_WETNESS_EFFECTS +#ifdef DEBUG_WETNESS_EFFECTS + /** + * Calculates ripple and splash effect intensities from water ripple info + * + * @param rippleInfo float4 containing scaled ripple normal (xyz) and splash intensity (w) + * Note: xyz = normalized ripple normal * intensity multiplier + * @param rippleMultiplier Multiplier for ripple effect intensity + * @param splashMultiplier Multiplier for splash effect intensity + * @return float2 where x=ripple effect, y=splash effect + */ + float2 GetDebugEffectIntensities(float4 rippleInfo, float rippleMultiplier, float splashMultiplier) + { + // rippleInfo.xyz is a scaled normal vector (normalized normal * intensity) + // length() gives us the intensity/magnitude of the ripple effect + float rippleEffect = saturate(length(rippleInfo.xyz) * rippleMultiplier); + float splashEffect = saturate(rippleInfo.w * splashMultiplier); + return float2(rippleEffect, splashEffect); + } + + /** + * Generates debug color visualization for wetness effects + * + * @param effectIntensities float2 from GetDebugEffectIntensities() + * @param rippleColor Color to use for ripple visualization + * @param splashColor Color to use for splash visualization + * @param baseColor Base color to start with (default black) + * @param brightnessMultiplier Multiplier for effect brightness + * @return float3 Debug color, or (0,0,0) if no effects are active + */ + float3 GetDebugWetnessColor(float2 effectIntensities, float3 rippleColor, float3 splashColor, float3 baseColor = float3(0, 0, 0), float brightnessMultiplier = 1.0) + { + float rippleEffect = effectIntensities.x; + float splashEffect = effectIntensities.y; + + if (rippleEffect > 0.01 || splashEffect > 0.01) { + float3 debugColor = baseColor; + if (rippleEffect > 0.01) { + debugColor += rippleColor * rippleEffect * brightnessMultiplier; + } + if (splashEffect > 0.01) { + debugColor += splashColor * splashEffect * brightnessMultiplier; + } + return saturate(debugColor); + } + return float3(0, 0, 0); // No debug override + } + + /** + * Convenience function for standard water debug colors + */ + float3 GetDebugWetnessColorStandard(float4 rippleInfo, float rippleMultiplier, float splashMultiplier) + { + float2 effects = GetDebugEffectIntensities(rippleInfo, rippleMultiplier, splashMultiplier); + float3 rippleColor = float3(1.0, 0.0, 1.0); // BRIGHT MAGENTA + float3 splashColor = float3(0.0, 1.0, 0.0); // BRIGHT GREEN + return GetDebugWetnessColor(effects, rippleColor, splashColor); + } + + /** + * Convenience function for specular debug colors (extra bright) + */ + float3 GetDebugWetnessColorSpecular(float4 rippleInfo, float rippleMultiplier, float splashMultiplier) + { + float2 effects = GetDebugEffectIntensities(rippleInfo, rippleMultiplier, splashMultiplier); + float3 rippleColor = float3(1.0, 0.0, 1.0); // BRIGHT MAGENTA + float3 splashColor = float3(0.0, 1.0, 0.0); // BRIGHT GREEN + return GetDebugWetnessColor(effects, rippleColor, splashColor, float3(0, 0, 0), 1.5); // Extra bright + } + + /** + * Convenience function for underwater debug colors (darker) + */ + float3 GetDebugWetnessColorUnderwater(float4 rippleInfo, float rippleMultiplier, float splashMultiplier) + { + float2 effects = GetDebugEffectIntensities(rippleInfo, rippleMultiplier, splashMultiplier); + float3 rippleColor = float3(0.7, 0.0, 0.7); // DARK MAGENTA + float3 splashColor = float3(0.0, 0.7, 0.0); // DARK GREEN + return GetDebugWetnessColor(effects, rippleColor, splashColor, float3(0, 0, 0.2)); // Dark blue base + } +#endif + + /** + * Calculates flow-aware ripple positioning with proper timing synchronization + * + * @param worldFlowVector Flow vector in world coordinate space + * @param flowStrength Flow strength (0-1) from flowmap alpha channel + * @param reflectionTimingScale Timing scale factor (typically 0.001 * ReflectionColor.w) + * @param avgFlowmapMultiplier Average multiplier from flowmap normal calculations + * @param uvToWorldScale Scale factor converting UV coordinates to world positioning (typically 1/8) + * @return float2 Flow offset to apply to ripple positioning + * + * @details This function synchronizes ripple movement timing with flowmap normal animations + * by using the same mathematical relationship and dual-phase smoothstep timing. + * The timing creates natural flow-based ripple movement that matches the water surface animation. + */ + float2 GetFlowAwareRippleOffset(float2 worldFlowVector, float flowStrength, float reflectionTimingScale, float avgFlowmapMultiplier = 9.26, float uvToWorldScale = 0.125) + { + // Calculate flow timing scale matching flowmap normal timing + // Mathematical relationship: avgMultiplier × uvToWorldScale gives base flow scaling + // uvToWorldScale (1/8) relates to the 64× texture coordinate scaling: 64 × (1/8) = 8 + float baseFlowMultiplier = avgFlowmapMultiplier * uvToWorldScale; // ≈ 1.16 + float flowTimeScale = baseFlowMultiplier * reflectionTimingScale; // Match flowmap timing + + // Calculate base flow offset (strength-modulated) + float2 flowOffset = worldFlowVector * flowTimeScale * flowStrength; + + // Apply dual-phase smoothstep timing for natural flow animation + // This creates the essential dual-phase animation pattern used in flowmap blending + float smoothTime = smoothstep(0.0, 1.0, frac(flowTimeScale)); + smoothTime = 0.15 + 0.85 * smoothTime; // Range: 0.15→1.0→0.15 (avoids complete stops) + + return flowOffset * smoothTime; + } + } diff --git a/package/Shaders/Common/SharedData.hlsli b/package/Shaders/Common/SharedData.hlsli index 61bca68556..a38b2cabc5 100644 --- a/package/Shaders/Common/SharedData.hlsli +++ b/package/Shaders/Common/SharedData.hlsli @@ -99,22 +99,23 @@ namespace SharedData bool EnableSplashes; bool EnableRipples; + uint EnableVanillaRipples; + float RaindropFxRange; + float RaindropGridSizeRcp; float RaindropIntervalRcp; - float RaindropChance; float SplashesLifetime; + float SplashesStrength; float SplashesMinRadius; - float SplashesMaxRadius; float RippleStrength; + float RippleRadius; float RippleBreadth; - float RippleLifetimeRcp; - - float3 pad0; + float pad0; }; struct SkylightingSettings diff --git a/package/Shaders/Water.hlsl b/package/Shaders/Water.hlsl index ad1c8ebd18..9ab2a99c89 100644 --- a/package/Shaders/Water.hlsl +++ b/package/Shaders/Water.hlsl @@ -420,20 +420,141 @@ float CalculateDepthMultFromUV(float2 uv, float depth, uint eyeIndex = 0) # if defined(SIMPLE) || defined(UNDERWATER) || defined(LOD) || defined(SPECULAR) # if defined(FLOWMAP) -float3 GetFlowmapNormal(PS_INPUT input, float2 uvShift, float multiplier, float offset, uint eyeIndex) + +/** + * Structure containing complete flowmap information + */ +struct FlowmapData +{ + float4 color; // Raw flowmap color (R=flow_x, G=flow_y, B=flow_strength, A=flow_mask) + float2 flowVector; // Flow vector (coordinate space depends on source function) +}; + +/** + * Gets raw flowmap data before UV-space coordinate transformation + * + * @param input Pixel shader input containing texture coordinates + * @param uvShift UV offset for sampling the flowmap texture + * @return FlowmapData with raw components: + * - color: Raw flowmap texture sample (RG=rotation, B=strength, A=mask) + * - flowVector: Base flow vector before any coordinate transformation + * Ready for direct application of rotation matrix for world positioning + * + * @details This function provides flowmap data in its original coordinate space, suitable + * for world-space positioning effects (like ripple movement). The flowVector has + * NOT been transformed for UV-space normal sampling - that transformation is only + * applied in GetFlowmapDataUV() which uses transpose for UV coordinate perturbation. + * + * Use this function when you need to apply the rotation matrix directly for + * world-space effects without needing to reverse any existing transformations. + * + * @see GetFlowmapDataUV() for UV-space normal sampling (applies transpose transformation) + */ +FlowmapData GetFlowmapDataTextureSpace(PS_INPUT input, float2 uvShift) { - float4 flowmapColor = FlowMapTex.Sample(FlowMapSampler, input.TexCoord2.zw + uvShift); - float2 flowVector = (64 * input.TexCoord3.xy) * sqrt(1.01 - flowmapColor.z); - float2 flowSinCos = flowmapColor.xy * 2 - 1; + FlowmapData data; + data.color = FlowMapTex.Sample(FlowMapSampler, input.TexCoord2.zw + uvShift); + data.flowVector = (64 * input.TexCoord3.xy) * sqrt(1.01 - data.color.z); + // NOTE: flowVector is NOT transformed yet - this is the raw vector before rotation matrix + return data; +} +/** + * Samples flowmap texture and calculates UV-space flow data for texture sampling + * + * @param input Pixel shader input containing texture coordinates and world position data + * @param uvShift UV offset for sampling the flowmap texture (used for animation/variation) + * @return FlowmapData Complete flowmap information with UV-space flow vector + * + * @details This function: + * - Samples the flowmap texture at the specified UV coordinates + * - Decodes flow direction from RG channels (remapped from [0,1] to [-1,1]) + * - Calculates flow strength using the blue channel with sqrt falloff + * - Applies transpose rotation matrix to transform flow direction to UV space + * - Scales flow vector by world position and strength factors + * + * @note Flowmap format: + * - Red channel: Flow direction X component (0.5 = no flow, 0/1 = negative/positive flow) + * - Green channel: Flow direction Y component (0.5 = no flow, 0/1 = negative/positive flow) + * - Blue channel: Flow strength (0 = no flow, 1 = maximum flow) + * - Alpha channel: Flow mask/intensity multiplier + */ +FlowmapData GetFlowmapDataUV(PS_INPUT input, float2 uvShift) +{ + FlowmapData data = GetFlowmapDataTextureSpace(input, uvShift); + float2 flowSinCos = data.color.xy * 2 - 1; float2x2 flowRotationMatrix = float2x2(flowSinCos.x, flowSinCos.y, -flowSinCos.y, flowSinCos.x); - float2 rotatedFlowVector = mul(transpose(flowRotationMatrix), flowVector); - float2 uv = offset + (rotatedFlowVector - float2(multiplier * ((0.001 * ReflectionColor.w) * flowmapColor.w), 0)); - return float3(FlowMapNormalsTex.SampleBias(FlowMapNormalsSampler, uv, SharedData::MipBias).xy, flowmapColor.z); + data.flowVector = mul(transpose(flowRotationMatrix), data.flowVector); + return data; +} + +/** + * Generates flowmap-based normal perturbation for water surface + * + * @param input Pixel shader input containing texture coordinates and world position + * @param uvShift UV offset for flowmap sampling (used for animation phases) + * @param multiplier Intensity multiplier for the flow effect + * @param offset Base UV offset for the normal texture sampling + * @return float3 Normal perturbation (XY=normal offset, Z=flow strength mask) + * + * @details This function uses flowmap data to: + * - Calculate flow-displaced UV coordinates for normal texture sampling + * - Apply flow-based animation to water normal textures + * - Return both the normal perturbation and flow strength information + * + * @note The returned Z component contains the original flowmap strength value + * which can be used for blending between flow and non-flow normals + */ +float3 GetFlowmapNormal(PS_INPUT input, float2 uvShift, float multiplier, float offset) +{ + FlowmapData flowData = GetFlowmapDataUV(input, uvShift); + float2 uv = offset + (flowData.flowVector - float2(multiplier * ((0.001 * ReflectionColor.w) * flowData.color.w), 0)); + return float3(FlowMapNormalsTex.SampleBias(FlowMapNormalsSampler, uv, SharedData::MipBias).xy, flowData.color.z); +} + +/** + * Gets flowmap data with world-space flow vector for positioning effects + * + * @param input Pixel shader input containing texture coordinates + * @param uvShift UV offset for flowmap sampling (used for animation phases) + * @return FlowmapData Complete flowmap information with world-space flow vector + * + * @details This function: + * - Samples raw flowmap data (before UV-space transformations) + * - Decodes flow direction from flowmap RG channels + * - Applies component-wise directional transformation + * - Returns complete flowmap data with world-space flow vector + * + * @note Use this for effects that need to move with water current (ripples, debris, foam, etc.) + * For UV-space normal sampling, use GetFlowmapDataUV() instead + */ +FlowmapData GetFlowmapDataWorldSpace(PS_INPUT input, float2 uvShift) +{ + FlowmapData data = GetFlowmapDataTextureSpace(input, uvShift); + float2 flowDirection = -(data.color.xy * 2 - 1); // Decode direction with 180° correction + data.flowVector = data.flowVector * flowDirection; // Transform to world space + return data; +} + +/** + * Converts existing texture-space flowmap data to world-space (avoids duplicate sampling) + * + * @param textureSpaceData FlowmapData from GetFlowmapDataTextureSpace() + * @return FlowmapData Complete flowmap data with world-space flow vector + * + * @note Use this overload when you already have texture-space flowmap data to avoid duplicate texture sampling + */ +FlowmapData GetFlowmapDataWorldSpace(FlowmapData textureSpaceData) +{ + FlowmapData data = textureSpaceData; + float2 flowDirection = -(data.color.xy * 2 - 1); // Decode direction with 180° correction + data.flowVector = data.flowVector * flowDirection; // Transform to world space + return data; } # endif -# if (defined(FLOWMAP) && !defined(BLEND_NORMALS)) || defined(LOD) +# if defined(LOD) # undef WATER_EFFECTS +# undef WETNESS_EFFECTS # endif # if defined(WATER_EFFECTS) && !defined(VC) @@ -445,8 +566,21 @@ float3 GetFlowmapNormal(PS_INPUT input, float2 uvShift, float multiplier, float # include "DynamicCubemaps/DynamicCubemaps.hlsli" # endif -float3 GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFactor, float3 viewDirection, float depth, uint eyeIndex) +# if defined(WETNESS_EFFECTS) +# include "WetnessEffects/WetnessEffects.hlsli" +# endif + +// Structure to return both normal and ripple/splash color information +struct WaterNormalData { + float3 normal; + float4 rippleInfo; // xyz = scaled ripple normal (normalized normal * intensity), w = splash effect intensity +}; + +WaterNormalData GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFactor, float3 viewDirection, float depth, uint eyeIndex) +{ + WaterNormalData result; + result.rippleInfo = float4(0, 0, 0, 0); float3 normalScalesRcp = rcp(input.NormalsScale.xyz); # if defined(WATER_PARALLAX) @@ -458,10 +592,10 @@ float3 GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFa 0.5 + -(-0.5 + abs(frac(input.TexCoord2.zw * (64 * input.TexCoord4)) * 2 - 1)); float uvShift = 1 / (128 * input.TexCoord4); - float3 flowmapNormal0 = GetFlowmapNormal(input, uvShift.xx, 9.92, 0, eyeIndex); - float3 flowmapNormal1 = GetFlowmapNormal(input, float2(0, uvShift), 10.64, 0.27, eyeIndex); - float3 flowmapNormal2 = GetFlowmapNormal(input, 0.0.xx, 8, 0, eyeIndex); - float3 flowmapNormal3 = GetFlowmapNormal(input, float2(uvShift, 0), 8.48, 0.62, eyeIndex); + float3 flowmapNormal0 = GetFlowmapNormal(input, uvShift.xx, 9.92, 0); + float3 flowmapNormal1 = GetFlowmapNormal(input, float2(0, uvShift), 10.64, 0.27); + float3 flowmapNormal2 = GetFlowmapNormal(input, 0.0.xx, 8, 0); + float3 flowmapNormal3 = GetFlowmapNormal(input, float2(uvShift, 0), 8.48, 0.62); float2 flowmapNormalWeighted = normalMul.y * (normalMul.x * flowmapNormal2.xy + (1 - normalMul.x) * flowmapNormal3.xy) + @@ -483,8 +617,13 @@ float3 GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFa # endif # if defined(FLOWMAP) && !defined(BLEND_NORMALS) - float3 finalNormal = - normalize(lerp(normals1 + float3(0, 0, 1), flowmapNormal, distanceFactor)); +# ifdef DISABLE_FLOWMAP_NORMALS + // FLOWMAP NORMALS DISABLED: Using only base normals (flow system still active for ripples/splashes) + float3 finalNormal = normalize(normals1 + float3(0, 0, 1)); +# else + // FLOWMAP NORMALS ENABLED: Blending flow-based normals with base normals + float3 finalNormal = normalize(lerp(normals1 + float3(0, 0, 1), flowmapNormal, distanceFactor)); +# endif # elif !defined(LOD) # if defined(WATER_PARALLAX) @@ -525,7 +664,69 @@ float3 GetWaterNormal(PS_INPUT input, float distanceFactor, float normalsDepthFa finalNormal = lerp(displacement, finalNormal, displacement.z); # endif - return finalNormal; +# if defined(WETNESS_EFFECTS) + // Wetness Effects Debug System: + // DEBUG_WETNESS_EFFECTS Color Legend: + // - BRIGHT MAGENTA: Ripples, BRIGHT GREEN: Splashes, CYAN: Both effects + const bool inWorld = (Permutation::ExtraShaderDescriptor & Permutation::ExtraFlags::InWorld); +# if defined(SKYLIGHTING) +# if defined(VR) + float3 positionMSSkylight = input.WPosition.xyz + FrameBuffer::CameraPosAdjust[eyeIndex].xyz - FrameBuffer::CameraPosAdjust[0].xyz; +# else + float3 positionMSSkylight = input.WPosition.xyz; +# endif + sh2 skylightingSH = Skylighting::sample(SharedData::skylightingSettings, Skylighting::SkylightingProbeArray, Skylighting::stbn_vec3_2Dx1D_128x128x64, input.HPosition.xy, positionMSSkylight, float3(0, 0, 1)); + float skylighting = SphericalHarmonics::Unproject(skylightingSH, float3(0, 0, 1)); + + float wetnessOcclusion = inWorld ? pow(saturate(skylighting), 2) : 0; +# else + float wetnessOcclusion = inWorld; +# endif + + float4 raindropInfo = float4(0, 0, 1, 0); + float maxRainDropDistance = SharedData::wetnessEffectsSettings.RaindropFxRange * SharedData::wetnessEffectsSettings.RaindropFxRange * 3; + float rainDropDistance = dot(input.WPosition, input.WPosition); + float distanceFadeout = saturate((1 - saturate(rainDropDistance / maxRainDropDistance)) * 3); + if (finalNormal.z > 0 && SharedData::wetnessEffectsSettings.Raining > 0.0f && SharedData::wetnessEffectsSettings.EnableRaindropFx && + (rainDropDistance < maxRainDropDistance) && wetnessOcclusion > 0.05) { + float rippleStrengthModifier = (wetnessOcclusion * wetnessOcclusion) * distanceFadeout; + float3 rippleWPosition = input.WPosition.xyz + finalNormal * 16; +# if defined(WATER_PARALLAX) + rippleWPosition.xy += parallaxOffset; +# endif +# if defined(FLOWMAP) + // Flow-following ripple enhancement: Makes raindrops follow water current + FlowmapData worldFlowData = GetFlowmapDataWorldSpace(input, float2(0, 0)); + + // Calculate flow-aware ripple offset using centralized timing logic + // Parameters: avgFlowmapMultiplier=9.26 (average of GetWaterNormal flowmap normal multipliers: 9.92, 10.64, 8, 8.48) + // uvToWorldScale=0.125 (1/8 - relates to 64× texture coordinate scaling factor) + float2 flowOffset = WetnessEffects::GetFlowAwareRippleOffset( + worldFlowData.flowVector, + worldFlowData.color.w, // Flow strength from flowmap alpha + 0.001 * ReflectionColor.w, // Reflection timing scale (matches GetFlowmapNormal) + 9.26, // Average flowmap normal multiplier + 0.125 // UV-to-world scale factor (1/8) + ); + + rippleWPosition.xy += flowOffset; +# endif + raindropInfo = WetnessEffects::GetRainDrops(rippleWPosition + FrameBuffer::CameraPosAdjust[eyeIndex].xyz, SharedData::wetnessEffectsSettings.Time, finalNormal, rippleStrengthModifier); + + // Calculate ripple and splash color intensities + float rippleIntensity = length(raindropInfo.xy) * rippleStrengthModifier; + float splashIntensity = raindropInfo.w * distanceFadeout; + + // Store ripple and splash information for color effects + result.rippleInfo.xyz = raindropInfo.xyz * rippleIntensity; + result.rippleInfo.w = splashIntensity; + } + float3 rippleNormal = normalize(raindropInfo.xyz); + finalNormal = WetnessEffects::ReorientNormal(rippleNormal, finalNormal); +# endif + + result.normal = finalNormal; + return result; } float3 GetWaterSpecularColor(PS_INPUT input, float3 normal, float3 viewDirection, @@ -835,11 +1036,11 @@ PS_OUTPUT main(PS_INPUT input) # else float4 depthControl = DepthControl * (distanceMul - 1) + 1; # endif - float3 viewPosition = mul(FrameBuffer::CameraView[eyeIndex], float4(input.WPosition.xyz, 1)).xyz; float2 screenUV = FrameBuffer::ViewToUV(viewPosition, true, eyeIndex); - float3 normal = GetWaterNormal(input, distanceFactor, depthControl.z, viewDirection, depth, eyeIndex); + WaterNormalData waterData = GetWaterNormal(input, distanceFactor, depthControl.z, viewDirection, depth, eyeIndex); + float3 normal = waterData.normal; float fresnel = GetFresnelValue(normal, viewDirection); @@ -857,6 +1058,13 @@ PS_OUTPUT main(PS_INPUT input) } finalColor *= fresnel; +# if defined(WETNESS_EFFECTS) && defined(DEBUG_WETNESS_EFFECTS) + // DEBUG MODE: Override specular color with debug visualization + float3 debugColor = WetnessEffects::GetDebugWetnessColorSpecular(waterData.rippleInfo, 2.5, 4.0); + if (any(debugColor)) { + finalColor = debugColor; + } +# endif isSpecular = true; # else @@ -914,6 +1122,14 @@ PS_OUTPUT main(PS_INPUT input) # if defined(UNDERWATER) float3 finalSpecularColor = lerp(ShallowColor.xyz, specularColor, 0.5); float3 finalColor = saturate(1 - input.WPosition.w * 0.002) * ((1 - fresnel) * (diffuseColor - finalSpecularColor)) + finalSpecularColor; + // Add ripple and splash color effects for underwater +# if defined(WETNESS_EFFECTS) && defined(DEBUG_WETNESS_EFFECTS) + // DEBUG MODE: Override water color with debug visualization (darker for underwater) + float3 debugColor = WetnessEffects::GetDebugWetnessColorUnderwater(waterData.rippleInfo, 1.5, 2.0); + if (any(debugColor)) { + finalColor = debugColor; + } +# endif # else float3 sunColor = GetSunColor(normal, viewDirection); @@ -925,6 +1141,14 @@ PS_OUTPUT main(PS_INPUT input) float specularFraction = lerp(1, fresnel * diffuseOutput.refractionMul, distanceFactor); float3 finalColorPreFog = lerp(diffuseColor, specularColor, specularFraction) + sunColor * depthControl.w; float3 finalColor = lerp(finalColorPreFog, input.FogParam.xyz * PosAdjust[eyeIndex].w, input.FogParam.w); +# if defined(WETNESS_EFFECTS) && defined(DEBUG_WETNESS_EFFECTS) + // DEBUG MODE: Override water color with debug visualization + float3 debugColor = WetnessEffects::GetDebugWetnessColorStandard(waterData.rippleInfo, 2.0, 3.0); + if (any(debugColor)) { + finalColor = debugColor; + } +# endif + # else float specularFraction = lerp(1, fresnel, distanceFactor); float3 finalColorPreFog = lerp(diffuseOutput.refractionDiffuseColor, specularColor, specularFraction) + sunColor * depthControl.w; @@ -937,6 +1161,13 @@ PS_OUTPUT main(PS_INPUT input) refractionColor = lerp(refractionColor, fogColor, fogFactor); float3 finalColor = lerp(refractionColor, finalColorPreFog, diffuseOutput.refractionMul); +# if defined(WETNESS_EFFECTS) && defined(DEBUG_WETNESS_EFFECTS) + // DEBUG MODE: Override water color with debug visualization + float3 debugColor = WetnessEffects::GetDebugWetnessColorStandard(waterData.rippleInfo, 2.0, 3.0); + if (any(debugColor)) { + finalColor = debugColor; + } +# endif # endif # endif diff --git a/src/Features/WetnessEffects.cpp b/src/Features/WetnessEffects.cpp index 535f405730..e9eaef32a7 100644 --- a/src/Features/WetnessEffects.cpp +++ b/src/Features/WetnessEffects.cpp @@ -16,6 +16,8 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( EnableRaindropFx, EnableSplashes, EnableRipples, + EnableVanillaRipples, + RaindropFxRange, RaindropGridSize, RaindropInterval, RaindropChance, @@ -28,10 +30,78 @@ NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT( RippleBreadth, RippleLifetime) +// Ripples code borrowed from po3 SplashesofStorms +// https://github.com/powerof3/SplashesOfStorms/blob/master/src/Hooks.cpp under MIT License +namespace Ripples +{ + // Cache settings to avoid repeated singleton access + static bool s_isEnabled = false; + static bool s_vanillaRipplesEnabled = false; + + struct ToggleWaterSplashes + { + static void thunk(RE::TESWaterSystem* a_waterSystem, bool a_enabled, float a_fadeAmount) + { + // Apply our logic only if wetness effects are enabled + if (s_isEnabled) { + a_enabled = a_enabled && s_vanillaRipplesEnabled; + } + for (auto& waterObject : a_waterSystem->waterObjects) { + if (waterObject) { + if (const auto& rippleObject = waterObject->waterRippleObject; rippleObject) { + rippleObject->SetAppCulled(!a_enabled); + } + } + } + + func(a_waterSystem, a_enabled, a_fadeAmount); + } + static inline REL::Relocation func; + }; + + WetnessEffects* UpdateSettings() + { + const auto WetnessEffects = WetnessEffects::GetSingleton(); + if (WetnessEffects) { + s_isEnabled = WetnessEffects->settings.EnableWetnessEffects; + s_vanillaRipplesEnabled = WetnessEffects->settings.EnableVanillaRipples; + logger::debug("[{}] UpdateSettings: EnableWetnessEffects={}, EnableVanillaRipples={}", + WetnessEffects->GetName(), s_isEnabled, s_vanillaRipplesEnabled); + } else { + logger::debug("[WetnessEffects] UpdateSettings: WetnessEffects singleton not found"); + } + return WetnessEffects; + } + + void Install() + { + const auto WetnessEffects = UpdateSettings(); // Initialize cached values + if (!WetnessEffects) + return; + REL::Relocation target{ RELOCATION_ID(25638, 26179), REL::VariantOffset(0x238, 0x223, 0x238) }; + stl::write_thunk_call(target.address()); + logger::info("[{}] Installed ripple hooks", WetnessEffects->GetName()); + } +} + +void WetnessEffects::PostPostLoad() +{ + splashesOfStormsLoaded = static_cast(GetModuleHandle(L"po3_SplashesOfStorms.dll")); + if (splashesOfStormsLoaded) { + logger::info("[{}] Splashes of Storms detected, compatibility enabled", GetName()); + return; + } + + // Only hook if SoS is not loaded + Ripples::Install(); +} + void WetnessEffects::DrawSettings() { if (ImGui::TreeNodeEx("Wetness Effects", ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Checkbox("Enable Wetness", (bool*)&settings.EnableWetnessEffects); + if (ImGui::Checkbox("Enable Wetness", (bool*)&settings.EnableWetnessEffects)) { + Ripples::UpdateSettings(); // Update cache when settings change + } if (auto _tt = Util::HoverTooltipWrapper()) { ImGui::Text("Enables a wetness effect near water and when it is raining."); } @@ -57,6 +127,21 @@ void WetnessEffects::DrawSettings() if (auto _tt = Util::HoverTooltipWrapper()) ImGui::Text("Enables circular ripples on puddles, and to a less extent other wet surfaces"); + ImGui::BeginDisabled(splashesOfStormsLoaded); + std::string checkboxLabel = splashesOfStormsLoaded ? + "Enable Vanilla Ripples - Controlled by Splashes of Storms" : + "Enable Vanilla Ripples"; + + if (ImGui::Checkbox(checkboxLabel.c_str(), (bool*)&settings.EnableVanillaRipples)) { + Ripples::UpdateSettings(); // Update cache when settings change + } + if (auto _tt = Util::HoverTooltipWrapper()) { + ImGui::Text( + "Enables default ripples (e.g., Ripples01).\n" + "Disabling may not take effect until the next weather change.\n"); + } + ImGui::EndDisabled(); + ImGui::SliderFloat("Effect Range", &settings.RaindropFxRange, 1e2f, 2e3f, "%.0f game unit(s)"); if (ImGui::TreeNodeEx("Raindrops")) { ImGui::BulletText( "At every interval, a raindrop is placed within each grid cell.\n" @@ -274,6 +359,7 @@ void WetnessEffects::Prepass() void WetnessEffects::LoadSettings(json& o_json) { settings = o_json; + Ripples::UpdateSettings(); // Sync cached values after loading } void WetnessEffects::SaveSettings(json& o_json) @@ -284,4 +370,21 @@ void WetnessEffects::SaveSettings(json& o_json) void WetnessEffects::RestoreDefaultSettings() { settings = {}; + Ripples::UpdateSettings(); // Sync cached values after restoring defaults +} + +void WetnessEffects::DrawUnloadedUI() +{ + ImGui::TextColored(ImVec4(1.0f, 0.3f, 0.3f, 1.0f), "This feature is not installed!"); + + ImGui::Spacing(); + ImGui::TextWrapped( + "Wetness Effects adds a collection of realistic wetness and weather effects to Skyrim.\n"); + ImGui::Spacing(); + ImGui::TextWrapped("Key features:"); + ImGui::BulletText("Rain Wetness"); + ImGui::BulletText("Puddles"); + ImGui::BulletText("Raindrop Effects (Splashes and Ripples)"); + ImGui::BulletText("Shore Wetness"); + ImGui::Spacing(); } \ No newline at end of file diff --git a/src/Features/WetnessEffects.h b/src/Features/WetnessEffects.h index e64ffd4290..127b68908d 100644 --- a/src/Features/WetnessEffects.h +++ b/src/Features/WetnessEffects.h @@ -49,6 +49,8 @@ struct WetnessEffects : Feature uint EnableRaindropFx = true; uint EnableSplashes = true; uint EnableRipples = true; + uint EnableVanillaRipples = false; + float RaindropFxRange = 1000.f; float RaindropGridSize = 4.f; float RaindropInterval = .5f; float RaindropChance = .3f; @@ -59,7 +61,7 @@ struct WetnessEffects : Feature float RippleStrength = 1.f; float RippleRadius = 1.f; float RippleBreadth = .5f; - float RippleLifetime = .15f; + float RippleLifetime = .5f; }; struct alignas(16) PerFrame @@ -70,7 +72,7 @@ struct WetnessEffects : Feature float Wetness; float PuddleWetness; Settings settings; - uint pad0[3]; + uint pad0; }; Settings settings; @@ -78,6 +80,7 @@ struct WetnessEffects : Feature PerFrame GetCommonBufferData(); virtual void Prepass() override; + virtual void PostPostLoad() override; virtual void DrawSettings() override; @@ -87,4 +90,8 @@ struct WetnessEffects : Feature virtual void RestoreDefaultSettings() override; virtual bool SupportsVR() override { return true; }; + virtual void DrawUnloadedUI() override; + +private: + bool splashesOfStormsLoaded = false; };