using System; using UnityEngine; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif namespace Pegasus { /// /// Add this object to a a game object in the scene to include it in a flythrough /// /// public class PegasusPoi : MonoBehaviour { [Tooltip("The type of POI. Auto generated POI's are subject to automatic deletion. Manual POI's will always be kept.")] public PegasusConstants.PoiType m_poiType = PegasusConstants.PoiType.Manual; [Tooltip("The mechanism used to check height for the POI.")] public PegasusConstants.PoiHeightCheckType m_heightCheckType = PegasusConstants.PoiHeightCheckType.ManagerSettings; [Tooltip("The lookat type for this POI segement. Changing this will re-generate the lookat location.")] public PegasusConstants.LookatType m_lookatType = PegasusConstants.LookatType.Path; [Tooltip("The lookat angle from the POI.")] public float m_lookAtAngle = 0f; [Tooltip("The lookat distance from the POI.")] public float m_lookAtDistance = 0f; [Tooltip("The lookat height above the ground from the POI.")] public float m_lookAtHeight = 0f; [Tooltip("The actual lookat location for the POI segment.")] public Vector3 m_lookatLocation = Vector3.zero; [Tooltip("The start speed type for this segment. The segement will always start at this speed and ease to the next segments start speed.")] public PegasusConstants.SpeedType m_startSpeedType = PegasusConstants.SpeedType.Medium; [Tooltip("The actual start speed for this segment. Speed varies between stat speed for this segment and start speed for next segment.")] public float m_startSpeed = 1f; [HideInInspector] public PegasusConstants.EasingType m_rotationEasingType = PegasusConstants.EasingType.Linear; [HideInInspector] public PegasusConstants.EasingType m_velocityEasingType = PegasusConstants.EasingType.EaseInOut; [HideInInspector] public PegasusConstants.EasingType m_positionEasingType = PegasusConstants.EasingType.Linear; [HideInInspector] public PegasusManager m_manager; //The manager which created this poi and owns it [HideInInspector] public bool m_alwaysShowGizmos = true; //Whether we should show gizmos even when not selected [HideInInspector] public float m_segmentDistance = 0f; //The actual travel distance of this segment based on spline [HideInInspector] public TimeSpan m_segmentStartTime = TimeSpan.Zero; //The time after start time that this segment will start playing [HideInInspector] public TimeSpan m_segmentDuration = TimeSpan.Zero; //The amount of time it will take to traverse this segment [HideInInspector] public int m_segmentIndex = 0; //The index of this segment in the overal flythrough [HideInInspector] public bool m_isFirstPOI = true; //Whether or not we are the first POI [HideInInspector] public bool m_isLastPOI = true; //Whether or not we are the last POI [HideInInspector] public PegasusPoi m_prevPoi; //The previous POI, will initially be set this [HideInInspector] public PegasusPoi m_nextPoi; //The next POI, will initially be set to this [HideInInspector] public List m_poiSteps = new List(); //The steps that this POI segment will traverse through [HideInInspector] public bool m_isSelected = false; //Whether or not this poi is selected - draws a different line [HideInInspector] public List m_poiTriggers = new List(); //The triggers associated with this poi [HideInInspector] public bool m_autoRollOn = false; //Whether or not this POI should calculate banking automatically [HideInInspector] public Vector3 m_lastRotation = Vector3.zero; //Last rotation euler angles - used to disable auto roll #region Unity gizmos n editing /// /// Draw gizmos when selected /// private void OnDrawGizmosSelected() { DrawGizmos(true); } /// /// Draw gizmos when not selected /// private void OnDrawGizmos() { DrawGizmos(false); } /// /// Draw the gizmos /// /// private void DrawGizmos(bool isSelected) { //Determine whether to drop out if (!isSelected && !m_alwaysShowGizmos) { return; } //Check to see if we have a change in the state of our list - force an initialise if so if (transform.parent.childCount != m_manager.m_poiList.Count) { m_manager.InitialiseFlythrough(); } #if UNITY_EDITOR //Make a check to see if we are selected - disable if not if (Selection.activeGameObject != null && Selection.activeGameObject.GetInstanceID() != this.gameObject.GetInstanceID()) { m_isSelected = false; } #endif //Now draw the gizmos //Segment path spline float velRange = PegasusConstants.SpeedStratospheric - PegasusConstants.SpeedReallySlow; if (m_nextPoi != null) { //See if we need to draw the segments for this bool drawSegments = true; if (m_isLastPOI && m_manager.m_flythroughType == PegasusConstants.FlythroughType.SingleShot) { drawSegments = false; } if (drawSegments) { float inc = 0.05f; float vel1, vel2; Vector3 pos1, pos2; pos1 = CalculatePositionSpline(0f); vel1 = CalculateVelocity(0f); for (float pct = inc; pct <= 1.02f; pct += inc) { //pos2 = CalculatePositionSpline(pct); pos2 = CalculatePositionLinear(pct); vel2 = CalculateVelocity(pct); if (m_isSelected) { Gizmos.color = Color.magenta * Color.Lerp(Color.cyan, Color.red, ((vel1 + vel2 / 2f) - PegasusConstants.SpeedReallySlow) / velRange); } else { Gizmos.color = Color.Lerp(Color.cyan, Color.red, ((vel1 + vel2 / 2f) - PegasusConstants.SpeedReallySlow) / velRange); } Gizmos.DrawLine(pos1, pos2); vel1 = vel2; pos1 = pos2; } } } /* Vector3 point1 = m_poiList[segment + 1].m_targetLocation; Vector3 point2 = m_poiList[segment + 2].m_targetLocation; Vector3 tangent2 = m_poiList[segment + 3].m_targetLocation; Vector3 t2 = Vector3.Normalize(m_poiList[segment + 2].m_targetLocation - m_poiList[segment + 3].m_targetLocation) * smooth; Gizmos.color = Color.magenta; Gizmos.DrawLine(m_poiList[segment + 2].m_targetLocation, m_poiList[segment + 2].m_targetLocation + t2); tangent2 = m_poiList[segment + 2].m_targetLocation + t2; Gizmos.color = Color.green; for (int i = 0; i < 30; i++) { Gizmos.DrawLine(Hermite(point1, tangent1, point2, tangent2, (float) i/40f), Hermite(point1, tangent1, point2, tangent2, (float)(i + 1)/40f)); } */ //Only draw this in correct scenario if (m_lookatType == PegasusConstants.LookatType.Target) { //Line to lookat location Gizmos.color = Color.Lerp(Color.cyan, Color.red, (m_startSpeed - PegasusConstants.SpeedReallySlow) / velRange); Gizmos.DrawLine(transform.position, m_lookatLocation); //Lookat sphere Gizmos.color = Color.cyan; Gizmos.DrawSphere(m_lookatLocation, 0.25f); } //Are we last one, and is play next chosen Gizmos.color = Color.yellow; if (m_isLastPOI == true && m_manager.m_flythroughType == PegasusConstants.FlythroughType.SingleShot && m_manager.m_flythroughEndAction == PegasusConstants.FlythroughEndAction.PlayNextPegasus && m_manager.m_nextPegasus != null) { PegasusPoi nextPoi = m_manager.m_nextPegasus.GetFirstPOI(); if (nextPoi != null) { Gizmos.DrawLine(transform.position, nextPoi.transform.position); } } //Flythrough location Gizmos.DrawSphere(transform.position, m_manager.m_poiGizmoSize); } #endregion /// /// Return true if the other object is the same as this one /// /// The other object to check /// public bool IsSameObject(PegasusPoi poi) { if (poi == null) { return false; } if (this.GetInstanceID() == poi.GetInstanceID()) { return true; } return false; } /// /// Initialise this POI's settings, leveraging the manager /// /// Update the segments public void Initialise(bool updateSegments) { //Make sure prev and next set up - if not then exit if (m_prevPoi == null || m_nextPoi == null) { return; } //Move us if we are below the minimum height on the terrain transform.position = m_manager.GetValidatedPoiPosition(transform.position, m_heightCheckType); //Update last rotation m_lastRotation = transform.rotation.eulerAngles; //Set up easing calculators switch (m_velocityEasingType) { case PegasusConstants.EasingType.Linear: m_velocityEasingCalculator = new Easing(EaseLinear); break; case PegasusConstants.EasingType.EaseIn: m_velocityEasingCalculator = new Easing(EaseIn); break; case PegasusConstants.EasingType.EaseOut: m_velocityEasingCalculator = new Easing(EaseOut); break; case PegasusConstants.EasingType.EaseInOut: m_velocityEasingCalculator = new Easing(EaseInOut); break; } switch (m_rotationEasingType) { case PegasusConstants.EasingType.Linear: m_rotationEasingCalculator = new Easing(EaseLinear); break; case PegasusConstants.EasingType.EaseIn: m_rotationEasingCalculator = new Easing(EaseIn); break; case PegasusConstants.EasingType.EaseOut: m_rotationEasingCalculator = new Easing(EaseOut); break; case PegasusConstants.EasingType.EaseInOut: m_rotationEasingCalculator = new Easing(EaseInOut); break; } switch (m_positionEasingType) { case PegasusConstants.EasingType.Linear: m_positionEasingCalculator = new Easing(EaseLinear); break; case PegasusConstants.EasingType.EaseIn: m_positionEasingCalculator = new Easing(EaseIn); break; case PegasusConstants.EasingType.EaseOut: m_positionEasingCalculator = new Easing(EaseOut); break; case PegasusConstants.EasingType.EaseInOut: m_positionEasingCalculator = new Easing(EaseInOut); break; } //Now move things around and update variables based on lookat type switch (m_lookatType) { case PegasusConstants.LookatType.Path: m_lookatLocation = CalculatePositionSpline(0.005f); GetRelativeOffsets(transform.position, m_lookatLocation, out m_lookAtDistance, out m_lookAtHeight, out m_lookAtAngle); break; case PegasusConstants.LookatType.Target: GetRelativeOffsets(transform.position, m_lookatLocation, out m_lookAtDistance, out m_lookAtHeight, out m_lookAtAngle); break; } //Setup the start and end rotations - required for progression calculations - note this assumes that the next poi lookat location has been calculated already //Handle auto roll - overrides the z rotation if (m_autoRollOn) { if (m_manager.m_flythroughType == PegasusConstants.FlythroughType.Looped || (!m_isFirstPOI && !m_isLastPOI)) { Vector3 posPrev = m_prevPoi.transform.position; Vector3 posOrigin = transform.position; Vector3 posTarget = m_nextPoi.transform.position; //float distOriginTarget = Vector3.Distance(posOrigin, posTarget); //Create forward location and heading based on prev location to current location -> project it forward Vector3 hdgPrevOrigin = posOrigin - posPrev; // hdgPrevOrigin.Normalize(); // Vector3 posForward = posOrigin + Quaternion.Euler(transform.localEulerAngles) * (hdgPrevOrigin * distOriginTarget); Quaternion qOriginForward = Quaternion.identity; if (hdgPrevOrigin != Vector3.zero) { qOriginForward = Quaternion.LookRotation(hdgPrevOrigin); } //Origin to target Vector3 hdgOriginTarget = posTarget - posOrigin; Quaternion qOriginTarget = Quaternion.identity; if (hdgOriginTarget != Vector3.zero) { qOriginTarget = Quaternion.LookRotation(hdgOriginTarget); } //Scale things float angle = qOriginTarget.eulerAngles.y - qOriginForward.eulerAngles.y; float scaledSpeed = Mathf.Clamp(m_startSpeed, PegasusConstants.SpeedReallySlow, m_manager.m_autoRollMaxSpeed) / m_manager.m_autoRollMaxSpeed; float scaledAngle = angle; if (angle < 0f) { scaledAngle = Mathf.Clamp(angle, -90f, 0f) / 90f; } else { scaledAngle = Mathf.Clamp(angle, 0f, 90f) / 90f; } float z = scaledAngle * scaledSpeed * m_manager.m_autoRollMaxAngle * -1f; transform.localRotation = Quaternion.Euler(0f, 0f, z); m_lastRotation = transform.localEulerAngles; /* Vector3 pos1 = new Vector3(m_prevPoi.transform.position.x, 0f, m_prevPoi.transform.position.z); Vector3 pos2 = new Vector3(transform.position.x, 0f, transform.position.z); Vector3 pos3 = new Vector3(m_nextPoi.transform.position.x, 0f, m_nextPoi.transform.position.z); Vector3 hdg12 = pos2 - pos1; Vector3 hdg13 = pos3 - pos1; Quaternion q1 = Quaternion.identity; if (hdg12 != Vector3.zero) { q1 = Quaternion.LookRotation(hdg12); } Quaternion q2 = Quaternion.identity; if (hdg13 != Vector3.zero) { q2 = Quaternion.LookRotation(hdg13); } float angle = q1.eulerAngles.y - q2.eulerAngles.y; float scaledSpeed = Mathf.Clamp(m_startSpeed, PegasusConstants.SpeedReallySlow, m_manager.m_autoRollMaxSpeed) / m_manager.m_autoRollMaxSpeed; float scaledAngle = angle; if (angle < 0f) { scaledAngle = Mathf.Clamp(angle, -90f, 0f) / 90f; } else { scaledAngle = Mathf.Clamp(angle, 0f, 90f) / 90f; } float z = scaledAngle*scaledSpeed*m_manager.m_autoRollMaxAngle; transform.localRotation = Quaternion.Euler(0f, 0f, z); m_lastRotation = transform.localEulerAngles; */ } } //Then set up the rest of the rotations Vector3 rotationDir = m_lookatLocation - transform.position; if (rotationDir != Vector3.zero) { m_rotationStart = Quaternion.LookRotation(m_lookatLocation - transform.position) * transform.localRotation; } else { m_rotationStart = transform.localRotation; } rotationDir = m_nextPoi.m_lookatLocation - m_nextPoi.transform.position; if (rotationDir != Vector3.zero) { m_rotationEnd = Quaternion.LookRotation(rotationDir) * m_nextPoi.transform.localRotation; } else { m_rotationEnd = m_nextPoi.transform.localRotation; } //Speed switch (m_startSpeedType) { case PegasusConstants.SpeedType.ReallySlow: m_startSpeed = PegasusConstants.SpeedReallySlow; break; case PegasusConstants.SpeedType.Slow: m_startSpeed = PegasusConstants.SpeedSlow; break; case PegasusConstants.SpeedType.Medium: m_startSpeed = PegasusConstants.SpeedMedium; break; case PegasusConstants.SpeedType.Fast: m_startSpeed = PegasusConstants.SpeedFast; break; case PegasusConstants.SpeedType.ReallyFast: m_startSpeed = PegasusConstants.SpeedReallyFast; break; case PegasusConstants.SpeedType.Stratospheric: m_startSpeed = PegasusConstants.SpeedStratospheric; break; } //Update travel distance for the segment if (updateSegments) { UpdateSegment(); } //Get the POI triggers m_poiTriggers.Clear(); m_poiTriggers.AddRange(gameObject.GetComponentsInChildren()); } /// /// Called when this poi starts its flythrough /// public void OnStartTriggers() { for (int idx = 0; idx < m_poiTriggers.Count; idx++) { m_poiTriggers[idx].OnStart(this); } } /// /// Called when this poi continues its flythrough /// public void OnUpdateTriggers(float progress) { for (int idx = 0; idx < m_poiTriggers.Count; idx++) { m_poiTriggers[idx].OnUpdate(this, progress); } } /// /// Called when this poi starts its flythrough /// public void OnEndTriggers() { for (int idx = 0; idx < m_poiTriggers.Count; idx++) { m_poiTriggers[idx].OnEnd(this); } } #region Handy value getters /// /// Get the poi start speed based on the speed type /// /// /// public float GetStartSpeed(PegasusConstants.SpeedType speedType) { switch (speedType) { case PegasusConstants.SpeedType.ReallySlow: return PegasusConstants.SpeedReallySlow; case PegasusConstants.SpeedType.Slow: return PegasusConstants.SpeedSlow; case PegasusConstants.SpeedType.Medium: return PegasusConstants.SpeedMedium; case PegasusConstants.SpeedType.Fast: return PegasusConstants.SpeedFast; case PegasusConstants.SpeedType.ReallyFast: return PegasusConstants.SpeedReallyFast; case PegasusConstants.SpeedType.Stratospheric: return PegasusConstants.SpeedStratospheric; } return m_startSpeed; } /// /// Update the current segment distance /// public void UpdateSegment() { m_segmentDistance = 0f; m_segmentDuration = TimeSpan.Zero; m_segmentStartTime = TimeSpan.Zero; if (!m_isFirstPOI) { m_segmentStartTime = m_prevPoi.m_segmentStartTime + m_prevPoi.m_segmentDuration; } m_poiSteps.Clear(); if (m_manager.m_flythroughType == PegasusConstants.FlythroughType.SingleShot && m_manager.GetNextPOI(this, false) == null) { return; } float pct = 0f; Vector3 pos1 = Vector3.zero; Vector3 pos2 = Vector3.zero; if (m_nextPoi != null) { //Calculate the segment distance by iterating over the spline - at a resolution related to the overall distance int stepsPerMeter = 3; int measurementsPerMeter = stepsPerMeter * 20; //Another magic multiplier - the more steps per meter the more measurements are required int measurement; float straightLineDistance = Vector3.Distance(transform.position, m_nextPoi.transform.position); int totalMeasurments = (int)Mathf.Ceil((float)measurementsPerMeter * straightLineDistance); float measurementIncrement = 1f / (float)totalMeasurments; float measurementDistance = 0f; float steppedDistance = 0f; float minMeasurementDistance = 0f; float maxMeasurementDistance = 0f; float totalSteppedDistance = 0f; float minMeasuredStepDistance = 0f; float maxMeasuredStepDistance = 0f; pos1 = transform.position; for (measurement = 1, pct = 0f, minMeasurementDistance = 0f, maxMeasurementDistance = 0f; measurement <= totalMeasurments; measurement++) { pct += measurementIncrement; pos2 = CalculatePositionSpline(pct); pos2 = m_manager.GetValidatedPoiPosition(pos2, m_heightCheckType); measurementDistance = Vector3.Distance(pos1, pos2); m_segmentDistance += measurementDistance; pos1 = pos2; //For debugging if (ApproximatelyEqual(minMeasurementDistance, 0f) || (measurementDistance < minMeasurementDistance)) { minMeasurementDistance = measurementDistance; } if (ApproximatelyEqual(maxMeasurementDistance, 0f) || (measurementDistance > maxMeasurementDistance)) { maxMeasurementDistance = measurementDistance; } } //Debug.Log(string.Format("{0} - meas dist {1:0.0000}, min dist {2:0.0000}, max dist {3:0.0000}, staight length {4:0.000}, spline length {5:0.000}", transform.name, 1f / (float)measurementsPerMeter , minMeasurementDistance, maxMeasurementDistance, straightLineDistance, m_segmentDistance)); //Refine steps - increase them if there is a small distance in oder to create a smoother movement if (m_segmentDistance < 2f) { stepsPerMeter *= 3; //Arbitrar magic value } //Now add in the actual steps - the biggest thing they need to be to get consistent speed is as close to eqidistant as possible float expectedStepDistance = 1f / (float)stepsPerMeter; //We want an exact multiple that gets to the whole length, but is nearest to 1 / stepsPerMeter expectedStepDistance = m_segmentDistance / Mathf.Floor(m_segmentDistance / expectedStepDistance); pos1 = transform.position; m_poiSteps.Add(pos1); for (measurement = 1, pct = 0f, minMeasuredStepDistance = 0f, maxMeasuredStepDistance = 0f; measurement <= totalMeasurments; measurement++) { pct += measurementIncrement; pos2 = CalculatePositionSpline(pct); pos2 = m_manager.GetValidatedPoiPosition(pos2, m_heightCheckType); measurementDistance = Vector3.Distance(pos1, pos2); steppedDistance += measurementDistance; if (steppedDistance >= expectedStepDistance) { //For debugging if (ApproximatelyEqual(minMeasuredStepDistance, 0f) || (steppedDistance < minMeasuredStepDistance)) { minMeasuredStepDistance = steppedDistance; } if (ApproximatelyEqual(maxMeasuredStepDistance, 0f) || (steppedDistance > maxMeasuredStepDistance)) { maxMeasuredStepDistance = steppedDistance; } //Lerp the intermediary steps to maintain distances while (steppedDistance >= expectedStepDistance) { m_poiSteps.Add(Vector3.Lerp(m_poiSteps[m_poiSteps.Count-1], pos2, expectedStepDistance / steppedDistance)); steppedDistance -= expectedStepDistance; totalSteppedDistance += expectedStepDistance; } } pos1 = pos2; } //Debug.Log(string.Format("{0} - exp step dist {1:0.0000}, min step dist {2:0.0000}, max step dist {3:0.0000}, straight length {4:0.000}, spline length {5:0.000}, stepped length {6:0.000}, steps {7}, ERROR {8:0.000}, ERROR % {9:0.00}", transform.name, expectedStepDistance, minMeasuredStepDistance, maxMeasuredStepDistance, straightLineDistance, m_segmentDistance, totalSteppedDistance, m_poiSteps.Count, totalSteppedDistance - m_segmentDistance, ((totalSteppedDistance - m_segmentDistance) / expectedStepDistance) * 100f)); //Check the last position, if we just missed out then add another, otherwise move the last one so it is exact if (((totalSteppedDistance - m_segmentDistance) / expectedStepDistance) < -0.5f) { m_poiSteps.Add(m_nextPoi.transform.position); } else { m_poiSteps[m_poiSteps.Count - 1] = m_nextPoi.transform.position; } } //Update the duration UpdateSegmentDuration(); } /// /// Update the segments duration - needs to be called after update segment /// public void UpdateSegmentDuration() { //Update the expected traversal time (based on either fps setting, or an average of 60fps) m_segmentDuration = TimeSpan.Zero; float pct = 0f; Vector3 pos1 = CalculatePositionLinear(0f); Vector3 pos2 = Vector3.zero; float duration = 0f; for (pct = 0f; pct < 1f; pct += 0.05f) { pos2 = CalculatePositionLinear(pct); duration += Vector3.Distance(pos1, pos2) / CalculateVelocity(pct); pos1 = pos2; } m_segmentDuration = TimeSpan.FromSeconds(duration); } #endregion #region Progression variables and calculations - uses more memory to reduce allocations that can cause framerate jitter /// /// Easing calculator used to smooth calculations - stops the jolts that kill immersion /// /// /// /// delegate float Easing(float time, float duration = 1f); private Easing m_velocityEasingCalculator = new Easing(EaseLinear); private Easing m_positionEasingCalculator = new Easing(EaseLinear); private Easing m_rotationEasingCalculator = new Easing(EaseLinear); /// /// Variables used to determine lookat target and rotation /// public Quaternion m_rotationStart = Quaternion.identity; public Quaternion m_rotationEnd = Quaternion.identity; /// /// Calculate progress on the variables supplied /// /// The percentage through the segment that the calculations should be made for from 0 == 0% to 1 == 100% /// Velocity at that point /// Position at that point /// Rotation at that point public void CalculateProgress(float percent, out float velocity, out Vector3 position, out Quaternion rotation) { velocity = CalculateVelocity(percent); rotation = CalculateRotation(percent); position = CalculatePositionLinear(percent); } /// /// Calculate position based on the variables supplied /// /// The percentage through the segment that the calculations should be made for from 0 == 0% to 1 == 100% /// Position at that point public Vector3 CalculatePositionSpline(float percent) { return CatmullRom(m_prevPoi.transform.position, transform.position, m_nextPoi.transform.position, m_nextPoi.m_nextPoi.transform.position, percent); } /// /// Calculate position based on the variables supplied. This must only be called after initialisation as it depends on the initialisation setup to work. /// /// The percentage through the segment that the calculations should be made for from 0 == 0% to 1 == 100% /// Return the next position public Vector3 CalculatePositionLinear(float percent) { //Ease the percentage percent = m_positionEasingCalculator(percent); //Handle no data if (m_poiSteps.Count == 0) { return Vector3.zero; } //Handle only one item if (m_poiSteps.Count == 1) { return m_poiSteps[0]; } int maxSegments = m_poiSteps.Count - 1; int firstSegment = (int)(percent * (float)maxSegments); if (firstSegment == maxSegments) { return m_poiSteps[firstSegment]; } float progress = (percent * (float)maxSegments) - (float)firstSegment; //Debug.Log(string.Format("Pct is {0:0.000}, {1} {2}, progress is {3:0.000} lerp is {4:0.000} {5:0.000} ", percent, firstSegment, firstSegment+1, progress, Vector3.Lerp(m_poiSteps[(int)(firstSegment)], m_poiSteps[(int)(firstSegment)+1], progress).x, Vector3.Lerp(m_poiSteps[(int)(firstSegment)], m_poiSteps[(int)(firstSegment)+1], progress).z)); return Vector3.Lerp(m_poiSteps[firstSegment], m_poiSteps[firstSegment+1], progress); } /// /// Calculate the velocity at the given position in the segment /// /// Valuer between 0..1 /// Velocity at that location public float CalculateVelocity(float percent) { return Mathf.Lerp(m_startSpeed, m_nextPoi.m_startSpeed, m_velocityEasingCalculator(percent)); } /// /// Calculate the rotation at the given position in the segment /// /// Value between 0..1 /// Rotation at that location public Quaternion CalculateRotation(float percent) { return Quaternion.Lerp(m_rotationStart, m_rotationEnd, m_rotationEasingCalculator(percent)); } #endregion #region Routines influenced by terrain /// /// Get the offsets of targetDistance, targetHeight relative to ground, and targetAngle of y rotation from source position to target position, as if they were on the same plane /// /// Source postiion /// Target position /// Distance from source to target as if on same plane /// Height of target relative to the terrain /// Angle from source to target, allowing only for rotation on y axis public void GetRelativeOffsets(Vector3 source, Vector3 target, out float targetDistance, out float targetHeight, out float targetAngle) { targetHeight = m_manager.GetValidatedLookatHeightRelativeToMinimum(target, m_heightCheckType); Vector3 planarTargetPosition = new Vector3(target.x, source.y, target.z); targetDistance = Vector3.Distance(source, planarTargetPosition); Vector3 targetDirection = source - target; if (targetDirection != Vector3.zero) { targetAngle = Quaternion.LookRotation(targetDirection, Vector3.up).eulerAngles.y; } else { targetAngle = 0; } } #endregion #region Handy general maths routines /// /// Return true if the values are approximately equal /// /// Parameter A /// Parameter B /// True if approximately equal public static bool ApproximatelyEqual(float a, float b) { if (a == b || Mathf.Abs(a - b) < float.Epsilon) { return true; } else { return false; } } /// /// Rotate the point around the pivot - used to handle rotation /// /// Point to move /// Pivot /// Angle to pivot /// New location public static Vector3 RotatePointAroundPivot(Vector3 point, Vector3 pivot, Vector3 angle) { Vector3 dir = point - pivot; dir = Quaternion.Euler(angle)*dir; point = dir + pivot; return point; } /// /// Linear easing /// /// /// /// private static float EaseLinear(float time, float duration = 1f) { return time/duration; } /// /// Ease in /// /// /// /// private static float EaseIn(float time, float duration = 1f) { return (time /= duration)*time; } /// /// Ease out /// /// /// /// private static float EaseOut(float time, float duration = 1f) { return -1f*(time /= duration)*(time - 2f); } /// /// Ease in and out /// /// /// /// private static float EaseInOut(float time, float duration = 1f) { if ((time /= duration/2f) < 1f) return 0.5f*time*time; return -0.5f*((--time)*(time - 2f) - 1f); } /// /// Creates a new that contains CatmullRom interpolation of the specified vectors. /// /// The first vector in interpolation. /// The second vector in interpolation. /// The third vector in interpolation. /// The fourth vector in interpolation. /// Weighting factor. /// The result of CatmullRom interpolation. public static Vector3 CatmullRom(Vector3 value1, Vector3 value2, Vector3 value3, Vector3 value4, float amount) { return new Vector3(CalcCatmullRom(value1.x, value2.x, value3.x, value4.x, amount), CalcCatmullRom(value1.y, value2.y, value3.y, value4.y, amount), CalcCatmullRom(value1.z, value2.z, value3.z, value4.z, amount)); } /// /// Creates a new that contains CatmullRom interpolation of the specified vectors. /// /// The first vector in interpolation. /// The second vector in interpolation. /// The third vector in interpolation. /// The fourth vector in interpolation. /// Weighting factor. /// The result of CatmullRom interpolation as an output parameter. public static void CatmullRom(ref Vector3 value1, ref Vector3 value2, ref Vector3 value3, ref Vector3 value4, float amount, out Vector3 result) { result.x = CalcCatmullRom(value1.x, value2.x, value3.x, value4.x, amount); result.y = CalcCatmullRom(value1.y, value2.y, value3.y, value4.y, amount); result.z = CalcCatmullRom(value1.z, value2.z, value3.z, value4.z, amount); } /// /// Performs a Catmull-Rom interpolation using the specified positions. /// /// The first position in the interpolation. /// The second position in the interpolation. /// The third position in the interpolation. /// The fourth position in the interpolation. /// Weighting factor. /// A position that is the result of the Catmull-Rom interpolation. public static float CalcCatmullRom(float value1, float value2, float value3, float value4, float amount) { // Using formula from http://www.mvps.org/directx/articles/catmull/ // Internally using doubles not to lose precision double amountSquared = amount*amount; double amountCubed = amountSquared*amount; return (float) (0.5*(2.0*value2 + (value3 - value1)*amount + (2.0*value1 - 5.0*value2 + 4.0*value3 - value4)*amountSquared + (3.0*value2 - value1 - 3.0*value3 + value4)*amountCubed)); } /// /// Creates a new that contains hermite spline interpolation. /// /// The first position vector. /// The first tangent vector. /// The second position vector. /// The second tangent vector. /// Weighting factor. /// The hermite spline interpolation vector. public static Vector3 Hermite(Vector3 value1, Vector3 tangent1, Vector3 value2, Vector3 tangent2, float amount) { return new Vector3(CalcHermite(value1.x, tangent1.x, value2.x, tangent2.x, amount), CalcHermite(value1.y, tangent1.y, value2.y, tangent2.y, amount), CalcHermite(value1.z, tangent1.z, value2.z, tangent2.z, amount)); } /// /// Creates a new that contains hermite spline interpolation. /// /// The first position vector. /// The first tangent vector. /// The second position vector. /// The second tangent vector. /// Weighting factor. /// The hermite spline interpolation vector as an output parameter. public static void Hermite(ref Vector3 value1, ref Vector3 tangent1, ref Vector3 value2, ref Vector3 tangent2, float amount, out Vector3 result) { result.x = CalcHermite(value1.x, tangent1.x, value2.x, tangent2.x, amount); result.y = CalcHermite(value1.y, tangent1.y, value2.y, tangent2.y, amount); result.z = CalcHermite(value1.z, tangent1.z, value2.z, tangent2.z, amount); } /// /// Performs a Hermite spline interpolation. /// /// Source position. /// Source tangent. /// Source position. /// Source tangent. /// Weighting factor. /// The result of the Hermite spline interpolation. public static float CalcHermite(float value1, float tangent1, float value2, float tangent2, float amount) { // All transformed to double not to lose precission // Otherwise, for high numbers of param:amount the result is NaN instead of Infinity double v1 = value1, v2 = value2, t1 = tangent1, t2 = tangent2, s = amount, result; double sCubed = s*s*s; double sSquared = s*s; if (amount == 0f) result = value1; else if (amount == 1f) result = value2; else result = (2*v1 - 2*v2 + t2 + t1)*sCubed + (3*v2 - 3*v1 - 2*t1 - t2)*sSquared + t1*s + v1; return (float) result; } #endregion } }