Files
2026-02-27 17:44:21 +08:00

438 lines
16 KiB
C#

// ╔════════════════════════════════════════════════════════════════╗
// ║ 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
{
/// <summary>
/// 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.
/// </summary>
public class SailController : MonoBehaviour
{
private void Awake()
{
_rb = GetComponentInParent<Rigidbody>();
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
/// <summary>
/// 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.
/// </summary>
[Tooltip("Bottom left sail corner if square sail.\r\nOtherwise bottom front.")]
public Transform a;
/// <summary>
/// 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.
/// </summary>
[Tooltip("Top left sail corner if square sail.\r\nOtherwise top front.")]
public Transform b;
/// <summary>
/// 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.
/// </summary>
[Tooltip("Top right sail corner if square sail.\r\nOtherwise top rear.")]
public Transform c;
/// <summary>
/// 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.
/// </summary>
[Tooltip("Bottom right sail corner if square sail.\r\nOtherwise bottom rear.")]
public Transform d;
/// <summary>
/// Defines the aerodynamic characteristics of the sail through lift and drag coefficient curves.
/// Contains the relationship between angle of attack and force coefficients.
/// </summary>
public SailPreset sailPreset;
/// <summary>
/// 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.
/// </summary>
[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
/// <summary>
/// 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.
/// </summary>
public Vector3 SailCenter { get; private set; }
/// <summary>
/// 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.
/// </summary>
public float SailArea { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Vector3 SailForward { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Vector3 SailUp { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Vector3 SailRight { get; private set; }
private Vector3 _liftForceDirection;
private Vector3 _dragForceDirection;
private float _liftForce;
private float _dragForce;
#endregion
#region WindCalculated
/// <summary>
/// True wind vector from the WindGenerator, representing the actual environmental wind.
/// Does not account for vessel motion.
/// Measured in meters per second.
/// </summary>
public Vector3 TrueWind { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Vector3 ShipVelocity { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Vector3 SailForce { get; private set; }
/// <summary>
/// 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.
/// </summary>
public Vector3 ApparentWind { get; private set; }
/// <summary>
/// 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.
/// </summary>
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