Files
Fishing2/Packages/com.waveharmonic.crest/Runtime/Scripts/Interaction/FloatingObject.cs
2026-01-31 00:32:49 +08:00

379 lines
16 KiB
C#

// Crest Water System
// Copyright © 2024 Wave Harmonic. All rights reserved.
using UnityEngine;
using WaveHarmonic.Crest.Internal;
using WaveHarmonic.Crest.Utility;
namespace WaveHarmonic.Crest
{
/// <summary>
/// Different physics models for <see cref="FloatingObject"/>
/// </summary>
[@GenerateDoc]
public enum FloatingObjectModel
{
/// <inheritdoc cref="Generated.FloatingObjectModel.AlignNormal"/>
[Tooltip("A simple model which aligns the object with the wave normal.")]
AlignNormal,
/// <inheritdoc cref="Generated.FloatingObjectModel.Probes"/>
[Tooltip("A more advanced model which samples water at the probes positions.")]
Probes,
}
/// <summary>
/// Probes for the <see cref="FloatingObject"/> <see cref="FloatingObjectModel.Probes"/> model.
/// </summary>
[System.Serializable]
public struct FloatingObjectProbe
{
/// <summary>
/// How much this probe affects the outcome (not a physical weight).
/// </summary>
[SerializeField]
public float _Weight;
/// <summary>
/// The position of the probe.
/// </summary>
[SerializeField]
public Vector3 _Position;
}
/// <summary>
/// Physics including buoyancy and drag.
/// </summary>
[@HelpURL("Manual/FloatingObjects.html#physics")]
[AddComponentMenu(Constants.k_MenuPrefixPhysics + "Floating Object")]
public sealed partial class FloatingObject : ManagedBehaviour<WaterRenderer>
{
[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.")]
[@Predicated(nameof(_Model), true, nameof(FloatingObjectModel.Probes), hide: true)]
[@GenerateAPI]
[@DecoratedField]
[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);
/// <summary>
/// Is any part of this object in water.
/// </summary>
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<WaterRenderer> 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();
}
}
}