// Crest Water System // Copyright © 2024 Wave Harmonic. All rights reserved. using UnityEngine; using WaveHarmonic.Crest.Internal; using WaveHarmonic.Crest.Utility; namespace WaveHarmonic.Crest { /// /// Different physics models for /// [@GenerateDoc] public enum FloatingObjectModel { /// [Tooltip("A simple model which aligns the object with the wave normal.")] AlignNormal, /// [Tooltip("A more advanced model which samples water at the probes positions.")] Probes, } /// /// Probes for the model. /// [System.Serializable] public struct FloatingObjectProbe { /// /// How much this probe affects the outcome (not a physical weight). /// [SerializeField] public float _Weight; /// /// The position of the probe. /// [SerializeField] public Vector3 _Position; } /// /// Physics including buoyancy and drag. /// [@HelpURL("Manual/FloatingObjects.html#physics")] [AddComponentMenu(Constants.k_MenuPrefixPhysics + "Floating Object")] public sealed partial class FloatingObject : ManagedBehaviour { [SerializeField, HideInInspector] #pragma warning disable 414 int _Version = 0; #pragma warning restore 414 [Tooltip("The rigid body to affect.\n\nIt will automatically get the sibling rigid body if not set.")] [@GenerateAPI] [@DecoratedField, SerializeField] Rigidbody _RigidBody; [Tooltip("The model to use for buoyancy.\n\nAlign Normal is simple and only uses a few queries whilst Probes is more advanced and uses a few queries per probe. Cannot be changed at runtime after Start.")] [@GenerateAPI] [@DecoratedField, SerializeField] FloatingObjectModel _Model = FloatingObjectModel.AlignNormal; [Tooltip(ICollisionProvider.k_LayerTooltip)] [@GenerateAPI] [@DecoratedField, SerializeField] CollisionLayer _Layer = CollisionLayer.AfterAnimatedWaves; [Header("Buoyancy")] [@Label("Force Strength")] [Tooltip("Strength of buoyancy force.\n\nFor probes, roughly a mass to force ratio of 100 to 1 to keep the center of mass near the surface. For Align Normal, default value is for a default sphere with a default rigidbody.")] [@GenerateAPI] [@DecoratedField, SerializeField] float _BuoyancyForceStrength = 10f; [@Label("Torque Strength")] [Tooltip("Strength of torque applied to match boat orientation to water normal.")] [@Predicated(nameof(_Model), inverted: true, nameof(FloatingObjectModel.AlignNormal), hide: true)] [@GenerateAPI] [@DecoratedField, SerializeField] float _BuoyancyTorqueStrength = 8f; [@Label("Maximum Force")] [Tooltip("Clamps the buoyancy force to this value.\n\nUseful for handling fully submerged objects.")] [@GenerateAPI] [@DecoratedField, SerializeField] float _MaximumBuoyancyForce = 100f; [@Label("Height Offset")] [Tooltip("Height offset from transform center to bottom of boat (if any).\n\nDefault value is for a default sphere. Having this value be an accurate measurement from center to bottom is not necessary.")] [@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal), hide: true)] [@GenerateAPI] [@DecoratedField, SerializeField] float _CenterToBottomOffset = -1f; [Tooltip("Approximate hydrodynamics of 'surfing' down waves.")] [@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal))] [@Range(0, 1)] [@GenerateAPI] [SerializeField] float _AccelerateDownhill; [UnityEngine.Space(10)] [Tooltip("Query points for buoyancy.\n\nOnly applicable to Probes model.")] [@GenerateAPI] [SerializeField] internal FloatingObjectProbe[] _Probes = new FloatingObjectProbe[] { }; [Header("Drag")] [Tooltip("Drag when in water.\n\nAdditive to the drag declared on the rigid body.")] [@GenerateAPI] [@DecoratedField, SerializeField] Vector3 _Drag = new(2f, 3f, 1f); [Tooltip("Angular drag when in water.\n\nAdditive to the angular drag declared on the rigid body.")] [@GenerateAPI] [@DecoratedField, SerializeField] float _AngularDrag = 0.2f; [Tooltip("Vertical offset for where drag force should be applied.")] [@GenerateAPI] [@DecoratedField, SerializeField] float _ForceHeightOffset; [Header("Wave Response")] [Tooltip("Width of object for physics purposes.\n\nThe larger this value, the more filtered/smooth the wave response will be. If larger wavelengths cannot be filtered, increase the LOD Levels")] [@GenerateAPI] [@DecoratedField, SerializeField] float _ObjectWidth = 3f; [Tooltip("Computes a separate normal based on boat length to get more accurate orientations.\n\nRequires the cost of an extra collision sample.")] [@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal), hide: true)] [@GenerateAPI] [@DecoratedField, SerializeField] bool _UseObjectLength; [Tooltip("Length dimension of boat.\n\nOnly used if Use Boat Length is enabled.")] [@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.AlignNormal), hide: true)] [@Predicated(nameof(_UseObjectLength))] [@GenerateAPI] [@DecoratedField, SerializeField] float _ObjectLength = 3f; // Debug [UnityEngine.Space(10)] [@DecoratedField, SerializeField] DebugFields _Debug = new(); [System.Serializable] sealed class DebugFields { [Tooltip("Draw queries for each force point as gizmos.")] [@DecoratedField, SerializeField] internal bool _DrawQueries = false; } internal const string k_FixedUpdateMarker = "Crest.FloatingObject.FixedUpdate"; static readonly Unity.Profiling.ProfilerMarker s_FixedUpdateMarker = new(k_FixedUpdateMarker); /// /// Is any part of this object in water. /// public bool InWater { get; private set; } readonly SampleCollisionHelper _SampleHeightHelper = new(); readonly SampleFlowHelper _SampleFlowHelper = new(); Vector3[] _QueryPoints; Vector3[] _QueryResultDisplacements; Vector3[] _QueryResultVelocities; Vector3[] _QueryResultNormal; internal FloatingObjectProbe[] _Probe = new FloatingObjectProbe[] { new() { _Weight = 1f } }; const float k_WaterDensity = 1000; float _TotalWeight; bool Advanced => _Model == FloatingObjectModel.Probes; private protected override void OnStart() { base.OnStart(); if (_RigidBody == null) TryGetComponent(out _RigidBody); var points = Advanced ? _Probes : _Probe; // Advanced needs an extra spot for the center. var length = Advanced ? points.Length + 1 : points.Length; _QueryPoints = new Vector3[length]; _QueryResultDisplacements = new Vector3[length]; _QueryResultVelocities = new Vector3[length]; if (!Advanced) _QueryResultNormal = new Vector3[length]; } private protected override System.Action OnFixedUpdateMethod => OnFixedUpdate; void OnFixedUpdate(WaterRenderer water) { s_FixedUpdateMarker.Begin(this); var points = Advanced ? _Probes : _Probe; // Queries { var collisions = water.AnimatedWavesLod.Provider; _TotalWeight = 0; // Update query points. for (var i = 0; i < points.Length; i++) { var point = points[i]; _TotalWeight += point._Weight; _QueryPoints[i] = transform.TransformPoint(point._Position + new Vector3(0, _RigidBody.centerOfMass.y, 0)); } _QueryPoints[^1] = transform.position + new Vector3(0, _RigidBody.centerOfMass.y, 0); collisions.Query(GetHashCode(), _ObjectWidth, _QueryPoints, _QueryResultDisplacements, _QueryResultNormal, _QueryResultVelocities, _Layer); if (Advanced && _Debug._DrawQueries) { for (var i = 0; i < points.Length; i++) { var query = _QueryPoints[i]; query.y = water.SeaLevel + _QueryResultDisplacements[i].y; DebugUtility.DrawCross(Debug.DrawLine, query, 1f, Color.magenta); } } } // We could filter the surface velocity as the minimum of the last 2 frames. There // is a hard case where a wavelength is turned on/off which generates single frame // velocity spikes - because the surface legitimately moves very fast. var surfaceVelocity = _QueryResultVelocities[^1]; _SampleFlowHelper.Sample(transform.position, out var surfaceFlow, minimumLength: _ObjectWidth); surfaceVelocity += new Vector3(surfaceFlow.x, 0, surfaceFlow.y); if (_Debug._DrawQueries) { Debug.DrawLine(transform.position + 5f * Vector3.up, transform.position + 5f * Vector3.up + surfaceVelocity, new(1, 1, 1, 0.6f)); } // Buoyancy if (Advanced) { var archimedesForceMagnitude = k_WaterDensity * Mathf.Abs(Physics.gravity.y); InWater = false; for (var i = 0; i < points.Length; i++) { var height = water.SeaLevel + _QueryResultDisplacements[i].y; var difference = height - _QueryPoints[i].y; if (difference > 0) { InWater = true; if (_TotalWeight > 0f) { var force = _BuoyancyForceStrength * points[i]._Weight * archimedesForceMagnitude * difference * Vector3.up / _TotalWeight; if (_MaximumBuoyancyForce < Mathf.Infinity) { force = Vector3.ClampMagnitude(force, _MaximumBuoyancyForce); } _RigidBody.AddForceAtPosition(force, _QueryPoints[i]); } } } if (!InWater) { s_FixedUpdateMarker.End(); return; } } else { var height = _QueryResultDisplacements[0].y + water.SeaLevel; var bottomDepth = height - transform.position.y - _CenterToBottomOffset; var normal = _QueryResultNormal[0]; if (_Debug._DrawQueries) { var surfPos = transform.position; surfPos.y = height; DebugUtility.DrawCross(Debug.DrawLine, surfPos, normal, 1f, Color.red); } InWater = bottomDepth > 0f; if (!InWater) { s_FixedUpdateMarker.End(); return; } var buoyancy = _BuoyancyForceStrength * bottomDepth * bottomDepth * bottomDepth * -Physics.gravity.normalized; if (_MaximumBuoyancyForce < Mathf.Infinity) { buoyancy = Vector3.ClampMagnitude(buoyancy, _MaximumBuoyancyForce); } _RigidBody.AddForce(buoyancy, ForceMode.Acceleration); // Approximate hydrodynamics of sliding along water if (_AccelerateDownhill > 0f) { _RigidBody.AddForce(_AccelerateDownhill * -Physics.gravity.y * new Vector3(normal.x, 0f, normal.z), ForceMode.Acceleration); } // Orientation // Align to water normal. One normal by default, but can use a separate normal // based on boat length vs width. This gives varying rotations based on boat // dimensions. { var normalLatitudinal = normal; var normalLongitudinal = Vector3.up; if (_UseObjectLength) { if (_SampleHeightHelper.SampleHeight(transform.position, out _, out _, out normalLongitudinal, minimumLength: _ObjectLength, _Layer)) { var f = transform.forward; f.y = 0f; f.Normalize(); normalLatitudinal -= Vector3.Dot(f, normalLatitudinal) * f; var r = transform.right; r.y = 0f; r.Normalize(); normalLongitudinal -= Vector3.Dot(r, normalLongitudinal) * r; } } if (_Debug._DrawQueries) Debug.DrawLine(transform.position, transform.position + 5f * normalLatitudinal, Color.green); if (_Debug._DrawQueries && _UseObjectLength) Debug.DrawLine(transform.position, transform.position + 5f * normalLongitudinal, Color.yellow); var torqueWidth = Vector3.Cross(transform.up, normalLatitudinal); _RigidBody.AddTorque(torqueWidth * _BuoyancyTorqueStrength, ForceMode.Acceleration); if (_UseObjectLength) { var torqueLength = Vector3.Cross(transform.up, normalLongitudinal); _RigidBody.AddTorque(torqueLength * _BuoyancyTorqueStrength, ForceMode.Acceleration); } _RigidBody.AddTorque(-_AngularDrag * _RigidBody.angularVelocity); } } // Apply drag relative to water if (_Drag != Vector3.zero) { #if UNITY_6000_0_OR_NEWER var velocityRelativeToWater = _RigidBody.linearVelocity - surfaceVelocity; #else var velocityRelativeToWater = _RigidBody.velocity - surfaceVelocity; #endif var forcePosition = _RigidBody.worldCenterOfMass + _ForceHeightOffset * Vector3.up; _RigidBody.AddForceAtPosition(_Drag.x * Vector3.Dot(transform.right, -velocityRelativeToWater) * transform.right, forcePosition, ForceMode.Acceleration); _RigidBody.AddForceAtPosition(_Drag.y * Vector3.Dot(Vector3.up, -velocityRelativeToWater) * Vector3.up, forcePosition, ForceMode.Acceleration); _RigidBody.AddForceAtPosition(_Drag.z * Vector3.Dot(transform.forward, -velocityRelativeToWater) * transform.forward, forcePosition, ForceMode.Acceleration); } s_FixedUpdateMarker.End(); } } }