// ╔════════════════════════════════════════════════════════════════╗ // ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║ // ║ Licensed under Unity Asset Store Terms of Service: ║ // ║ https://unity.com/legal/as-terms ║ // ║ Use permitted only in compliance with the License. ║ // ║ Distributed "AS IS", without warranty of any kind. ║ // ╚════════════════════════════════════════════════════════════════╝ #region using UnityEngine; #if UNITY_EDITOR using NWH.NUI; using UnityEditor; #endif #endregion namespace NWH.DWP2.SailController { /// /// Manages sail physics by calculating and applying lift and drag forces based on sail geometry, orientation, and wind conditions. /// Uses four corner transforms (a, b, c, d) to define sail shape, enabling dynamic sail configurations including furling and multi-part sails. /// Calculates apparent wind from true wind and vessel velocity, then applies aerodynamic forces at the sail's surface-weighted center. /// The corner transforms follow a clockwise pattern: a (bottom left/front), b (top left/front), c (top right/rear), d (bottom right/rear). /// Corner transforms can be attached to different parents to enable sail furling or complex rigging systems. /// Use SailRotator for user-controlled sail rotation. /// Requires a WindGenerator in the scene and a SailPreset for aerodynamic coefficients. /// public class SailController : MonoBehaviour { private void Awake() { _rb = GetComponentInParent(); Debug.Assert(_rb != null, "Rigidbody not found on the sail or any of the parent GameObjects."); } private void Start() { if (WindGenerator.Instance == null) { Debug.LogError("WindGenerator not found in the scene. SailController will not work."); } if (sailPreset == null) { Debug.LogError($"SailPreset not assigned to SailController {name}"); } } private void FixedUpdate() { Debug.Assert(a != null, $"{name}: Transform 'a' is not assigned."); Debug.Assert(b != null, $"{name}: Transform 'b' is not assigned."); Debug.Assert(c != null, $"{name}: Transform 'c' is not assigned."); Debug.Assert(d != null, $"{name}: Transform 'd' is not assigned."); // Update the sail positions, directions and geometry UpdateCachedPositions(); SailCenter = CalculateSurfaceWeightedCenter(); SailArea = CalculateSailArea(); SailForward = CalculateSailForward(); SailUp = CalculateSailUp(); SailRight = CalculateSailRight(SailUp, SailForward); // Calculate velocities ShipVelocity = _rb.linearVelocity; TrueWind = WindGenerator.Instance.CurrentWind; ApparentWind = CalculateApparentWind(ShipVelocity, TrueWind); AngleOfAttack = CalculateAngleOfAttack(ApparentWind, SailForward); // Calculate and apply force SailForce = CalculateSailForce(); _rb.AddForceAtPosition(SailForce, SailCenter); } private void OnDrawGizmos() { #if UNITY_EDITOR if (a == null || b == null || c == null || d == null) { return; } UpdateCachedPositions(); Vector3 center = CalculateSurfaceWeightedCenter(); // Draw sail directions if (!Application.isPlaying) // Calculate directions if out of play mode { SailForward = CalculateSailForward(); SailUp = CalculateSailUp(); SailRight = CalculateSailRight(SailUp, SailForward); } Gizmos.color = Color.blue; Gizmos.DrawRay(center, SailForward); Gizmos.color = Color.green; Gizmos.DrawRay(center, SailUp); Gizmos.color = Color.red; Gizmos.DrawRay(center, SailRight); // Draw sail shape if (a != null && b != null && c != null && d != null) { Gizmos.color = Color.white; Gizmos.DrawLine(_pA, _pB); Gizmos.DrawLine(_pB, _pC); Gizmos.DrawLine(_pC, _pD); Gizmos.DrawLine(_pD, _pA); Gizmos.DrawLine(_pA, _pC); Gizmos.DrawLine(_pB, _pD); Gizmos.DrawSphere(_pA, 0.1f); Gizmos.DrawSphere(_pB, 0.1f); Gizmos.DrawSphere(_pC, 0.1f); Gizmos.DrawSphere(_pD, 0.1f); Handles.Label(_pA, "A (bottom left)"); Handles.Label(_pB, "B (top left)"); Handles.Label(_pC, "C (top right)"); Handles.Label(_pD, "D (bottom right"); } // RUNTIME ONLY FROM THIS POINT FORWARD if (!Application.isPlaying) { return; } // Draw force point (center) Gizmos.color = Color.red; Gizmos.DrawSphere(center, 0.05f); // Draw wind Gizmos.color = Color.green; Gizmos.DrawRay(center, TrueWind * 0.2f); Handles.Label(center + TrueWind * 0.2f, $"True Wind ({TrueWind.magnitude} m/s)"); Gizmos.color = Color.yellow; Gizmos.DrawRay(center, ApparentWind * 0.2f); Handles.Label(center + ApparentWind * 0.2f, $"Apparent Wind ({ApparentWind.magnitude} m/s)"); Gizmos.color = Color.red; Gizmos.DrawRay(center, SailForce * 0.001f); Handles.Label(center + SailForce * 0.001f, $"Force ({SailForce.magnitude} N)"); Gizmos.color = Color.white; Gizmos.DrawRay(center, _liftForceDirection); Handles.Label(center + _liftForceDirection, "Lift Force Dir."); Gizmos.color = Color.gray; Gizmos.DrawRay(center, _dragForceDirection); Handles.Label(center + _dragForceDirection, "Drag Force Dir."); Gizmos.color = Color.magenta; Gizmos.DrawRay(center - Vector3.up * 0.1f, ShipVelocity * 0.1f); Handles.Label(center - Vector3.up * 0.1f + ShipVelocity * 0.1f, $"Ship Velocity ({ShipVelocity.magnitude} m/s)"); #endif } private void UpdateCachedPositions() { _pA = a.position; _pB = b.position; _pC = c.position; _pD = d.position; } private Vector3 CalculateSurfaceWeightedCenter() { return GeometryUtils.CalculateSurfaceWeightedCenter(_pA, _pB, _pC, _pD); } private float CalculateSailArea() { return GeometryUtils.CalculateQuadrilateralArea(_pA, _pB, _pC, _pD); } private Vector3 CalculateSailForward() { return (_pA - _pD).normalized; } private Vector3 CalculateSailUp() { return ((_pB + _pC) * 0.5f - (_pA + _pD) * 0.5f).normalized; } private Vector3 CalculateSailRight(Vector3 sailUp, Vector3 sailForward) { return Vector3.Cross(sailUp, sailForward).normalized; } private Vector3 CalculateApparentWind(Vector3 boatVelocity, Vector3 trueWind) { return trueWind - boatVelocity; } private float CalculateAngleOfAttack(Vector3 apparentWind, Vector3 sailForward) { return Vector3.SignedAngle(sailForward, apparentWind, Vector3.up); } private Vector3 CalculateSailForce() { float apparentWindSpeed = ApparentWind.magnitude; float dynamicPressure = 0.5f * airDensity * apparentWindSpeed * apparentWindSpeed; float liftCoefficient = sailPreset.liftCoefficientVsAoACurve.Evaluate(AngleOfAttack) * sailPreset.liftScale; _liftForce = liftCoefficient * dynamicPressure * SailArea; float dragCoefficient = sailPreset.dragCoefficientVsAoACurve.Evaluate(AngleOfAttack) * sailPreset.dragScale; _dragForce = dragCoefficient * dynamicPressure * SailArea; _liftForceDirection = SailRight * Mathf.Sign(Vector3.Dot(ApparentWind, SailRight)); _dragForceDirection = ApparentWind.normalized; Vector3 totalForce = _liftForceDirection * _liftForce + _dragForceDirection * _dragForce; // Compensate for the lean totalForce *= Vector3.Dot(SailUp, Vector3.up); return totalForce; } #region UserSettings /// /// Bottom left corner of the sail for square sails, or bottom front corner for triangular sails. /// First point in the clockwise corner definition pattern. /// Can be attached to any parent transform to enable dynamic sail configurations. /// [Tooltip("Bottom left sail corner if square sail.\r\nOtherwise bottom front.")] public Transform a; /// /// Top left corner of the sail for square sails, or top front corner for triangular sails. /// Second point in the clockwise corner definition pattern. /// Can be attached to any parent transform to enable dynamic sail configurations. /// [Tooltip("Top left sail corner if square sail.\r\nOtherwise top front.")] public Transform b; /// /// Top right corner of the sail for square sails, or top rear corner for triangular sails. /// Third point in the clockwise corner definition pattern. /// Can be attached to any parent transform to enable dynamic sail configurations. /// [Tooltip("Top right sail corner if square sail.\r\nOtherwise top rear.")] public Transform c; /// /// Bottom right corner of the sail for square sails, or bottom rear corner for triangular sails. /// Fourth point in the clockwise corner definition pattern. /// Can be attached to any parent transform to enable dynamic sail configurations. /// [Tooltip("Bottom right sail corner if square sail.\r\nOtherwise bottom rear.")] public Transform d; /// /// Defines the aerodynamic characteristics of the sail through lift and drag coefficient curves. /// Contains the relationship between angle of attack and force coefficients. /// public SailPreset sailPreset; /// /// Air density in kg/m³ used in aerodynamic force calculations. /// Standard sea level value is 1.225 kg/m³. /// Can be adjusted as a multiplier to scale all sail forces uniformly without modifying the preset. /// [Tooltip( "The air density. Can also be used\r\nas a force coefficient as this affects both lift and drag forces equally.")] public float airDensity = 1.225f; #endregion #region SailCalculated /// /// Surface-weighted center point of the sail in world space. /// All aerodynamic forces are applied at this position. /// Calculated from the quadrilateral formed by corner points a, b, c, and d. /// public Vector3 SailCenter { get; private set; } /// /// Total surface area of the sail in square meters. /// Calculated from the quadrilateral formed by corner points a, b, c, and d. /// Used in aerodynamic force calculations along with dynamic pressure. /// public float SailArea { get; private set; } /// /// Forward direction vector of the sail in world space. /// Determined by the vector from corner point d to corner point a. /// Used to calculate the angle of attack relative to the apparent wind. /// public Vector3 SailForward { get; private set; } /// /// Up direction vector of the sail in world space. /// Calculated from the average of the top edge to the average of the bottom edge. /// Used to determine sail orientation and compensate for heel angle. /// public Vector3 SailUp { get; private set; } /// /// Right direction vector of the sail in world space. /// Derived from the cross product of SailUp and SailForward. /// Defines the direction of lift force generation perpendicular to the sail plane. /// public Vector3 SailRight { get; private set; } private Vector3 _liftForceDirection; private Vector3 _dragForceDirection; private float _liftForce; private float _dragForce; #endregion #region WindCalculated /// /// True wind vector from the WindGenerator, representing the actual environmental wind. /// Does not account for vessel motion. /// Measured in meters per second. /// public Vector3 TrueWind { get; private set; } /// /// Current velocity of the vessel's Rigidbody in world space. /// Used to calculate apparent wind by combining with true wind. /// Measured in meters per second. /// public Vector3 ShipVelocity { get; private set; } /// /// Total aerodynamic force vector applied to the vessel at the sail center point. /// Combines lift and drag forces based on sail geometry, apparent wind, and aerodynamic coefficients. /// Measured in Newtons. /// public Vector3 SailForce { get; private set; } /// /// Wind experienced by the sail relative to the moving vessel. /// Calculated as the vector difference between true wind and vessel velocity. /// This is the effective wind that generates aerodynamic forces on the sail. /// Measured in meters per second. /// public Vector3 ApparentWind { get; private set; } /// /// Angle in degrees between the sail's forward direction and the apparent wind direction. /// Measured on the horizontal plane using Vector3.up as the reference axis. /// Used to determine lift and drag coefficients from the SailPreset curves. /// Positive values indicate wind from the starboard side, negative values from port side. /// public float AngleOfAttack { get; private set; } #endregion #region Cached private Vector3 _pA; private Vector3 _pB; private Vector3 _pC; private Vector3 _pD; private Rigidbody _rb; #endregion } } #if UNITY_EDITOR namespace NWH.DWP2.SailController { [CustomEditor(typeof(SailController))] [CanEditMultipleObjects] public class SailControllerEditor : DWP2NUIEditor { public override bool OnInspectorNUI() { if (!base.OnInspectorNUI()) { return false; } SailController sailController = (SailController)target; if (Application.isPlaying) { drawer.BeginSubsection("Debug Info"); drawer.Label($"AoA: {sailController.AngleOfAttack}"); drawer.Label($"Force Mag.: {sailController.SailForce.magnitude}"); drawer.Label($"Force: {sailController.SailForce}"); drawer.EndSubsection(); } drawer.BeginSubsection("Geometry"); drawer.Field("a"); drawer.Field("b"); drawer.Field("c"); drawer.Field("d"); drawer.EndSubsection(); drawer.BeginSubsection("Physics"); drawer.Field("sailPreset"); drawer.Field("airDensity"); drawer.EndSubsection(); drawer.EndEditor(this); return true; } } } #endif