Created
August 1, 2025 05:43
-
-
Save wangbinyq/abdc35af15881708df4c493c0d3c6757 to your computer and use it in GitHub Desktop.
Revisions
-
wangbinyq created this gist
Aug 1, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,373 @@ using UnityEngine; using UnityEngine.Rendering; using WaveHarmonic.Crest.Internal; namespace WaveHarmonic.Crest { /// <summary> /// Approximates the interaction between an arbitrary SDF shape and the water. /// </summary> /// <remarks> /// Uses a 3D texture containing SDF data to define the interaction shape. /// </remarks> [AddComponentMenu(Constants.k_MenuPrefixInputs + "SDF Water Interaction")] [@HelpURL("Manual/Waves.html#adding-interaction-forces")] public sealed partial class SDFWaterInteraction : ManagedBehaviour<WaterRenderer> { [SerializeField, HideInInspector] #pragma warning disable 414 int _Version = 0; #pragma warning restore 414 [Header("SDF Settings")] [Tooltip("3D texture containing the SDF data. Expected to be in world space units.")] [GenerateAPI] [SerializeField] Texture3D _SDFTexture; [Tooltip("Compute shader used for SDF water interaction calculations.")] [GenerateAPI] [SerializeField] ComputeShader _ComputeShader; [Tooltip("Scale factor applied to the SDF. Allows scaling the interaction without regenerating the texture.")] [@Range(0.1f, 10f)] [GenerateAPI] [SerializeField] float _SDFScale = 1f; [Tooltip("Bounds of the SDF interaction in world space. Should encompass the area where the SDF has meaningful values.")] [GenerateAPI] [SerializeField] Bounds _InteractionBounds = new Bounds(Vector3.zero, Vector3.one * 2f); [Header("Interaction Parameters")] [Tooltip("Intensity of the forces.\n\nCan be set negative to invert.")] [@Range(-40f, 40f)] [GenerateAPI] [SerializeField] float _Weight = 1f; [Tooltip("Intensity of the forces from vertical motion.\n\nScales ripples generated from vertical movement.")] [@Range(0f, 2f)] [GenerateAPI] [SerializeField] float _WeightVerticalMultiplier = 0.5f; [Tooltip("Model parameter that can be used to modify the shape of the interaction.\n\nScales the effect of forces inside the SDF shape.")] [@Range(0f, 10f)] [GenerateAPI] [SerializeField] float _InnerShapeMultiplier = 1.55f; [Tooltip("Model parameter that controls the transition zone for inner shape effects.\n\nDefines the normalized distance from the surface where inner effects apply.")] [@Range(0f, 1f)] [GenerateAPI] [SerializeField] float _InnerShapeOffset = 0.109f; [Tooltip("Offset in direction of motion to help ripples appear in front of object.\n\nThere is some latency between applying a force to the wave simulation and the resulting waves appearing.")] [@Range(0f, 2f)] [GenerateAPI] [SerializeField] float _VelocityOffset = 0.04f; [Tooltip("How much to correct the position for horizontal wave displacement.\n\nCompensates for wave displacement to prevent interaction drift.")] [@Range(0f, 1f)] [GenerateAPI] [SerializeField] float _CompensateForWaveMotion = 0.45f; [Tooltip("Whether to improve visibility in larger LODs.\n\nBoosts output for better visibility at distance.")] [GenerateAPI] [SerializeField] bool _BoostLargeWaves = false; [Header("Limits")] [Tooltip("Teleport speed (km/h).\n\nIf the calculated speed is larger than this amount, the object is deemed to have teleported and the computed velocity is discarded.")] [GenerateAPI] [SerializeField] float _TeleportSpeed = 500f; [Tooltip("Outputs a warning to the console on teleport.")] [GenerateAPI] [SerializeField] bool _WarnOnTeleport = false; [Tooltip("Maximum speed clamp (km/h).\n\nUseful for controlling/limiting wake.")] [GenerateAPI] [SerializeField] float _MaximumSpeed = 100f; [Tooltip("Outputs a warning to the console on speed clamp.")] [GenerateAPI] [SerializeField] bool _WarnOnSpeedClamp = false; #pragma warning disable 414 [Header("Debug")] [Tooltip("Draws debug lines at each substep position. Editor only.")] [SerializeField] bool _DebugSubsteps = false; [Tooltip("Draw the interaction bounds as a wireframe gizmo.")] [SerializeField] bool _DrawBounds = true; #pragma warning restore 414 static class ShaderIDs { public static readonly int s_Velocity = Shader.PropertyToID("_Crest_Velocity"); public static readonly int s_Weight = Shader.PropertyToID("_Crest_Weight"); public static readonly int s_SDFTexture = Shader.PropertyToID("_Crest_SDFTexture"); public static readonly int s_SDFScale = Shader.PropertyToID("_Crest_SDFScale"); public static readonly int s_SDFBoundsMin = Shader.PropertyToID("_Crest_SDFBoundsMin"); public static readonly int s_SDFBoundsSize = Shader.PropertyToID("_Crest_SDFBoundsSize"); public static readonly int s_InnerShapeOffset = Shader.PropertyToID("_Crest_InnerShapeOffset"); public static readonly int s_InnerShapeMultiplier = Shader.PropertyToID("_Crest_InnerShapeMultiplier"); public static readonly int s_LargeWaveMultiplier = Shader.PropertyToID("_Crest_LargeWaveMultiplier"); } Vector3 _Velocity; Vector3 _VelocityClamped; Vector3 _PreviousPosition; Vector3 _RelativeVelocity; Vector3 _Displacement; float _WeightThisFrame; readonly SampleCollisionHelper _SampleHeightHelper = new(); readonly SampleFlowHelper _SampleFlowHelper = new(); ComputeShader ComputeShader => _ComputeShader; Rect Rect { get { var bounds = GetWorldBounds(); var size = new Vector2(bounds.size.x, bounds.size.z); var center = new Vector2(bounds.center.x, bounds.center.z); return new Rect(center - size * 0.5f, size); } } Bounds GetWorldBounds() { var worldBounds = _InteractionBounds; worldBounds.center = transform.TransformPoint(_InteractionBounds.center); worldBounds.size = Vector3.Scale(_InteractionBounds.size, transform.lossyScale) * _SDFScale; return worldBounds; } bool IsValidConfiguration() { if (_SDFTexture == null) { Debug.LogError("Crest: SDF Texture is not assigned.", this); return false; } if (_ComputeShader == null) { Debug.LogError("Crest: Compute Shader is not assigned.", this); return false; } return true; } private protected override System.Action<WaterRenderer> OnUpdateMethod => OnUpdate; void OnUpdate(WaterRenderer water) { if (!IsValidConfiguration()) return; var bounds = GetWorldBounds(); var sampleRadius = Mathf.Max(bounds.size.x, bounds.size.z) * 0.5f; _SampleHeightHelper.SampleDisplacement(transform.position, out _Displacement, minimumLength: sampleRadius); LateUpdateComputeVel(water); // Velocity relative to water _RelativeVelocity = _VelocityClamped; { _SampleFlowHelper.Sample(transform.position, out var surfaceFlow, minimumLength: sampleRadius); _RelativeVelocity -= new Vector3(surfaceFlow.x, 0, surfaceFlow.y); _RelativeVelocity.y *= _WeightVerticalMultiplier; } // Use weight from user with a multiplier to make interactions look plausible _WeightThisFrame = 3.75f * _Weight; var waterHeight = _Displacement.y + water.SeaLevel; LateUpdateSDFWeight(waterHeight, ref _WeightThisFrame); // Weighting with this value helps keep ripples consistent for different gravity values var gravityMul = Mathf.Sqrt(water._DynamicWavesLod.Settings._GravityMultiplier) / 5f; _WeightThisFrame *= gravityMul; _PreviousPosition = transform.position; } void LateUpdateComputeVel(WaterRenderer water) { // Compute velocity using finite difference _Velocity = (transform.position - _PreviousPosition) / water.DeltaTime; if (water.DeltaTime < 0.0001f) { _Velocity = Vector3.zero; } var speedKmh = _Velocity.magnitude * 3.6f; if (speedKmh > _TeleportSpeed) { // teleport detected _Velocity *= 0f; if (_WarnOnTeleport) { Debug.LogWarning("Crest: Teleport detected (speed = " + speedKmh.ToString() + "), velocity discarded.", this); } speedKmh = _Velocity.magnitude * 3.6f; } if (speedKmh > _MaximumSpeed) { // limit speed to max _VelocityClamped = _Velocity * _MaximumSpeed / speedKmh; if (_WarnOnSpeedClamp) { Debug.LogWarning("Crest: Speed (" + speedKmh.ToString() + ") exceeded max limited, clamped.", this); } } else { _VelocityClamped = _Velocity; } } void LateUpdateSDFWeight(float waterHeight, ref float weight) { var bounds = GetWorldBounds(); var centerDepthInWater = waterHeight - transform.position.y; var effectiveRadius = bounds.size.y * 0.5f; if (centerDepthInWater >= 0f) { // Center in water - exponential fall off of interaction influence as object gets deeper var prop = centerDepthInWater / effectiveRadius; prop *= 0.5f; weight *= Mathf.Exp(-prop * prop); } else { // Center out of water - ramp off with square root var height = -centerDepthInWater; var heightProp = 1f - Mathf.Clamp01(height / effectiveRadius); weight *= Mathf.Sqrt(heightProp); } } private protected override void Initialize() { base.Initialize(); if (!IsValidConfiguration()) return; _Input ??= new(this); ILodInput.Attach(_Input, DynamicWavesLod.s_Inputs); _PreviousPosition = transform.position; } private protected override void OnDisable() { base.OnDisable(); ILodInput.Detach(_Input, DynamicWavesLod.s_Inputs); } void Draw(Lod simulation, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1f, int slices = -1) { if (!IsValidConfiguration()) return; var waves = simulation as DynamicWavesLod; var timeBeforeCurrentTime = waves.TimeLeftToSimulate; var wrapper = new PropertyWrapperCompute(buffer, ComputeShader, 0); #if UNITY_EDITOR // Draw debug lines at each substep position if (_DebugSubsteps) { var col = 0.7f * (Time.frameCount % 2 == 1 ? Color.green : Color.red); var pos = transform.position - _Velocity * (timeBeforeCurrentTime - _VelocityOffset); var right = Vector3.Cross(Vector3.up, _Velocity.normalized); Debug.DrawLine(pos - right + transform.up, pos + right + transform.up, col, 0.5f); } #endif // Reconstruct the position of this input at the current substep time var offset = _Velocity * (timeBeforeCurrentTime - _VelocityOffset); var displacement = _Displacement.XNZ() * _CompensateForWaveMotion; wrapper.SetVector(Crest.ShaderIDs.s_Position, transform.position - offset - displacement); var bounds = GetWorldBounds(); wrapper.SetFloat(ShaderIDs.s_Weight, _WeightThisFrame); wrapper.SetFloat(ShaderIDs.s_SDFScale, _SDFScale); wrapper.SetVector(ShaderIDs.s_SDFBoundsMin, bounds.min); wrapper.SetVector(ShaderIDs.s_SDFBoundsSize, bounds.size); wrapper.SetFloat(ShaderIDs.s_InnerShapeOffset, _InnerShapeOffset); wrapper.SetFloat(ShaderIDs.s_InnerShapeMultiplier, _InnerShapeMultiplier); wrapper.SetFloat(ShaderIDs.s_LargeWaveMultiplier, _BoostLargeWaves ? 2f : 1f); wrapper.SetVector(ShaderIDs.s_Velocity, _RelativeVelocity); wrapper.SetTexture(ShaderIDs.s_SDFTexture, _SDFTexture); wrapper.SetTexture(Crest.ShaderIDs.s_Target, target); var threads = simulation.Resolution / Lod.k_ThreadGroupSize; wrapper.Dispatch(threads, threads, slices); } #if UNITY_EDITOR void OnDrawGizmosSelected() { if (_DrawBounds) { Gizmos.color = Color.cyan; Gizmos.matrix = transform.localToWorldMatrix; Gizmos.DrawWireCube(_InteractionBounds.center, _InteractionBounds.size * _SDFScale); } } #endif } partial class SDFWaterInteraction { Input _Input; sealed class Input : ILodInput { readonly SDFWaterInteraction _Input; public Input(SDFWaterInteraction input) => _Input = input; public bool Enabled => _Input.enabled; public bool IsCompute => true; public int Queue => 0; public int Pass => -1; public Rect Rect => _Input.Rect; public MonoBehaviour Component => _Input; public float Filter(WaterRenderer water, int slice) => 1f; public void Draw(Lod lod, CommandBuffer buffer, RenderTargetIdentifier target, int pass = -1, float weight = 1, int slice = -1) => _Input.Draw(lod, buffer, target, pass, weight, slice); } } } This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,163 @@ #pragma kernel CrestExecute #include "HLSLSupport.cginc" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Macros.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Constants.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Globals.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Helpers.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/InputsDriven.hlsl" #include "Packages/com.waveharmonic.crest/Runtime/Shaders/Library/Cascade.hlsl" RWTexture2DArray<float2> _Crest_Target; Texture3D<float> _Crest_SDFTexture; SamplerState sampler_Crest_SDFTexture; CBUFFER_START(CrestPerWaterInput) float3 _Crest_Position; float3 _Crest_Velocity; float _Crest_SimDeltaTime; float _Crest_Weight; float _Crest_SDFScale; float3 _Crest_SDFBoundsMin; float3 _Crest_SDFBoundsSize; float _Crest_InnerShapeOffset; float _Crest_InnerShapeMultiplier; float _Crest_LargeWaveMultiplier; CBUFFER_END m_CrestNameSpace // Resolution-aware interaction falloff function float InteractionFalloff(float a, float x) { float ax = a * x; float ax2 = ax * ax; float ax4 = ax2 * ax2; return ax / (1.0 + ax2 * ax4); } // Sample SDF from 3D texture and calculate normal void SampleSDFAndNormal(float3 worldPos, out float sdf, out float2 normal2D) { // Convert world position to texture coordinates float3 localPos = (worldPos - _Crest_SDFBoundsMin) / _Crest_SDFBoundsSize; // Check if we're outside the texture bounds if (any(localPos < 0.0) || any(localPos > 1.0)) { normal2D = float2(1.0, 0.0); sdf = length(worldPos.xz - _Crest_Position.xz) * _Crest_SDFScale; // Return large positive distance return; } // Sample the SDF value sdf = _Crest_SDFTexture.SampleLevel(sampler_Crest_SDFTexture, localPos, 0).r; sdf *= _Crest_SDFScale; // Calculate gradient using central differences for normal calculation uint width, height, depth; _Crest_SDFTexture.GetDimensions(width, height, depth); float3 texelSize = 1.0 / float3(width, height, depth); float3 gradient; gradient.x = _Crest_SDFTexture.SampleLevel(sampler_Crest_SDFTexture, localPos + float3(texelSize.x, 0, 0), 0).r - _Crest_SDFTexture.SampleLevel(sampler_Crest_SDFTexture, localPos - float3(texelSize.x, 0, 0), 0).r; gradient.y = _Crest_SDFTexture.SampleLevel(sampler_Crest_SDFTexture, localPos + float3(0, texelSize.y, 0), 0).r - _Crest_SDFTexture.SampleLevel(sampler_Crest_SDFTexture, localPos - float3(0, texelSize.y, 0), 0).r; gradient.z = _Crest_SDFTexture.SampleLevel(sampler_Crest_SDFTexture, localPos + float3(0, 0, texelSize.z), 0).r - _Crest_SDFTexture.SampleLevel(sampler_Crest_SDFTexture, localPos - float3(0, 0, texelSize.z), 0).r; gradient /= (2.0 * texelSize * _Crest_SDFScale); // Project gradient to 2D (XZ plane) and normalize for water surface interaction float2 gradientXZ = gradient.xz; float gradientLength = length(gradientXZ); normal2D = gradientLength > 0.001 ? gradientXZ / gradientLength : float2(1.0, 0.0); } void Execute(uint3 id) { const Cascade cascade = Cascade::MakeDynamicWaves(id.z); // Early exit based on scale - use a representative size similar to sphere radius float effectiveRadius = max(_Crest_SDFBoundsSize.x, max(_Crest_SDFBoundsSize.y, _Crest_SDFBoundsSize.z)) * 0.5; if (_Crest_LargeWaveMultiplier * effectiveRadius < cascade._Texel) { return; } float2 positionXZ = cascade.IDToWorld(id.xy); float2 offsetXZ = positionXZ - _Crest_Position.xz; // Quick bounds check for culling - use distance-based culling like sphere float3 boundsCenter = _Crest_SDFBoundsMin + _Crest_SDFBoundsSize * 0.5; float2 boundsCenterXZ = boundsCenter.xz; float boundsRadius = length(_Crest_SDFBoundsSize.xz) * 0.5; // Culling based on distance, similar to sphere (with 4x radius buffer) if (length(positionXZ - boundsCenterXZ) > boundsRadius * 4.0) { return; } // Feather at edges of LOD to reduce streaking half weight = _Crest_Weight * FeatherWeightFromUV(cascade.WorldToUV(positionXZ).xy, 0.1); // Check we are within bounds if (weight <= 0.0) { return; } float minimumWavelength = Cascade::Make(id.z)._MaximumWavelength * 0.5; // Sample SDF at the water surface level float3 worldPos = float3(positionXZ.x, _Crest_Position.y, positionXZ.y); float sdf; float2 sdfNormal; SampleSDFAndNormal(worldPos, sdf, sdfNormal); // Push in same direction as velocity inside SDF shape, and opposite direction outside float verticalForce = 0.0; { verticalForce = -_Crest_Velocity.y; // Range / radius of interaction force const float a = 1.67 / minimumWavelength; verticalForce *= InteractionFalloff(a, sdf); } // Push water up in direction of motion, pull down behind float horizontalForce = 0.0; if (sdf > 0.0 || sdf < -effectiveRadius * _Crest_InnerShapeOffset) { // Range / radius of interaction force const float a = 1.43 / minimumWavelength; // Invert within SDF shape, to balance / negate forces applied outside of shape float forceSign = sign(sdf); horizontalForce = forceSign * dot(sdfNormal, _Crest_Velocity.xz) * InteractionFalloff(a, abs(sdf)); // If inside SDF shape, add an additional weight if (sdf < 0.0) { horizontalForce *= _Crest_InnerShapeMultiplier; } } // Add to velocity (y-channel) to accelerate water. Magic number was the default // value for _Strength which has been removed. float acceleration = weight * (verticalForce + horizontalForce) * 0.2; // Helps interaction to work at different scales acceleration /= minimumWavelength; _Crest_Target[id] = float2(_Crest_Target[id].x, _Crest_Target[id].y + acceleration * _Crest_SimDeltaTime); } m_CrestNameSpaceEnd m_CrestInputKernelDefault(Execute)