AStarHeightmapGrid / Assets / PlatypusIdeas / AirPath / Runtime / Utilities / PathSmoother.cs
PathSmoother.cs
Raw
using System.Collections.Generic;
using UnityEngine;

namespace PlatypusIdeas.AirPath.Runtime.Utilities {
    
    public static class PathSmoother {
        /// <summary>
        /// Defines configuration settings used for path smoothing processes in a 3D environment.
        /// This struct provides parameters to adjust corner smoothing thresholds, turn radii,
        /// terrain clearance, and other characteristics that influence the generation of smooth
        /// and realistic paths.
        /// </summary>
        public struct SmoothingConfig {
            public float CornerAngleThreshold;
            public float MinTurnRadius;
            public float MaxTurnRadius;
            public int BezierSegments;
            public float TerrainClearance;
            public float PreferredClearance;
            public float TurnSpeedMultiplier;

            /// <summary>
            /// Provides a default configuration for the path smoothing process, including parameters such as corner angle
            /// thresholds, turn radii, Bezier segments, and terrain clearance. This configuration serves as a standard
            /// baseline for smoothing behavior when no specific settings are provided.
            /// </summary>
            public static SmoothingConfig Default => new() {
                CornerAngleThreshold = 45f,
                MinTurnRadius = 3f,
                MaxTurnRadius = 10f,
                BezierSegments = 5,
                TerrainClearance = 2f,
                PreferredClearance = 5f,
                TurnSpeedMultiplier = 0.7f
            };
        }

        /// <summary>
        /// Represents variations in the behavior and characteristics of a bird in-flight.
        /// This struct is used to influence path smoothing processes, allowing for more
        /// natural and diverse paths by introducing variations in turn radius, speed,
        /// reaction timing, and height preferences.
        /// </summary>
        public struct BirdVariation {
            public float turnRadiusMultiplier;
            public float speedMultiplier;
            public float reactionDelay;
            public float heightPreference;
            
            public static BirdVariation Random() {
                return new BirdVariation {
                    turnRadiusMultiplier = UnityEngine.Random.Range(0.8f, 1.2f),
                    speedMultiplier = UnityEngine.Random.Range(0.9f, 1.1f),
                    reactionDelay = UnityEngine.Random.Range(0f, 0.3f),
                    heightPreference = UnityEngine.Random.Range(-2f, 2f)
                };
            }
        }

        /// <summary>
        /// Smooths a raw path by applying Bezier curve generation, height adjustments, and terrain clearance checks.
        /// </summary>
        /// <param name="rawPath">The initial path represented as a list of 3D points.</param>
        /// <param name="config">Configuration settings for the smoothing process, such as corner angle thresholds and clearance preferences.</param>
        /// <param name="variation">Individual variation settings, such as turn radius and speed multipliers, affecting the smoothing behavior.</param>
        /// <param name="speedModifiers">An output list of speed modifiers corresponding to each point in the smoothed path.</param>
        /// <returns>A list of 3D points representing the smoothed path.</returns>
        public static List<Vector3> SmoothPath(List<Vector3> rawPath, 
            SmoothingConfig config, BirdVariation variation, out List<float> speedModifiers) {
            
            speedModifiers = new List<float>();
            
            if (rawPath == null || rawPath.Count < 3) {
                if (rawPath != null) {
                    for (int i = 0; i < rawPath.Count; i++) {
                        speedModifiers.Add(1f);
                    }
                }
                return rawPath;
            }
            
            var smoothedPath = new List<Vector3>();
            var tempSpeedMods = new List<float>();
            
            smoothedPath.Add(AdjustHeightForVariation(rawPath[0], variation.heightPreference));
            tempSpeedMods.Add(1f);
            
            for (int i = 1; i < rawPath.Count - 1; i++) {
                var prev = rawPath[i - 1];
                var current = rawPath[i];
                var next = rawPath[i + 1];
                
                var dirIn = (current - prev).normalized;
                var dirOut = (next - current).normalized;
                var angle = Vector3.Angle(dirIn, dirOut);
                
                if (angle >= config.CornerAngleThreshold) {
                    var (curvePoints, curveSpeeds) = GenerateBezierCorner(
                        prev, current, next, 
                        angle, config, variation
                    );
                    
                    for (int j = 1; j < curvePoints.Count; j++) {
                        smoothedPath.Add(curvePoints[j]);
                        tempSpeedMods.Add(curveSpeeds[j]);
                    }
                } else {
                    smoothedPath.Add(AdjustHeightForVariation(current, variation.heightPreference));
                    tempSpeedMods.Add(1f * variation.speedMultiplier);
                }
            }
            
            smoothedPath.Add(AdjustHeightForVariation(
                rawPath[rawPath.Count - 1], 
                variation.heightPreference
            ));
            tempSpeedMods.Add(1f);
            
            smoothedPath = EnsureTerrainClearance(smoothedPath, config);
            
            speedModifiers = tempSpeedMods;
            return smoothedPath;
        }

        /// <summary>
        /// Generates a Bezier curve to create a smooth transition around a corner in a path.
        /// </summary>
        /// <param name="prev">The point preceding the corner in the raw path.</param>
        /// <param name="corner">The corner point around which the Bezier curve will be generated.</param>
        /// <param name="next">The point following the corner in the raw path.</param>
        /// <param name="angle">The angle at the corner, calculated using the preceding and following points.</param>
        /// <param name="config">Configuration settings for smoothing, including turn radius, clearance, and segment count.</param>
        /// <param name="variation">Variation settings for individual characteristics such as turn radius multiplier and height preference.</param>
        /// <returns>A tuple containing a list of 3D points representing the generated Bezier curve and a corresponding list of speed modifiers for each point.</returns>
        private static (List<Vector3> points, List<float> speeds) GenerateBezierCorner(
            Vector3 prev, Vector3 corner, Vector3 next,
            float angle, SmoothingConfig config, BirdVariation variation) {
            
            var curvePoints = new List<Vector3>();
            var speeds = new List<float>();
            
            var normalizedAngle = Mathf.Clamp01(angle / 180f);
            var turnRadius = Mathf.Lerp(config.MinTurnRadius, config.MaxTurnRadius, normalizedAngle);
            turnRadius *= variation.turnRadiusMultiplier;
            
            var offsetDistance = turnRadius * Mathf.Tan(angle * 0.5f * Mathf.Deg2Rad);
            offsetDistance = Mathf.Min(offsetDistance, Vector3.Distance(prev, corner) * 0.4f);
            offsetDistance = Mathf.Min(offsetDistance, Vector3.Distance(corner, next) * 0.4f);
            
            var entryPoint = corner - (corner - prev).normalized * offsetDistance;
            var exitPoint = corner + (next - corner).normalized * offsetDistance;
            
            var controlPoint = corner;
            
            var heightVariation = variation.heightPreference * 0.5f;
            controlPoint.y += heightVariation;
            
            for (int i = 0; i <= config.BezierSegments; i++) {
                var t = i / (float)config.BezierSegments;
                
                var oneMinusT = 1f - t;
                var point = oneMinusT * oneMinusT * entryPoint +
                            2f * oneMinusT * t * controlPoint +
                            t * t * exitPoint;
                
                point.y += variation.heightPreference;
                
                curvePoints.Add(point);
                
                var speedMod = Mathf.Lerp(1f, config.TurnSpeedMultiplier, 
                    Mathf.Sin(t * Mathf.PI));
                speeds.Add(speedMod * variation.speedMultiplier);
            }
            
            return (curvePoints, speeds);
        }
        
        private static Vector3 AdjustHeightForVariation(Vector3 point, float heightPreference) {
            point.y += heightPreference;
            return point;
        }
        
        private static List<Vector3> EnsureTerrainClearance(List<Vector3> path, SmoothingConfig config) {
            var adjustedPath = new List<Vector3>(path.Count);
            
            foreach (var point in path) {
                var adjustedPoint = point;
                
                if (Physics.Raycast(point + Vector3.up * 100f, Vector3.down, 
                    out RaycastHit hit, 200f)) {
                    
                    var terrainHeight = hit.point.y;
                    var desiredHeight = terrainHeight + config.PreferredClearance;
                    
                    if (adjustedPoint.y < terrainHeight + config.TerrainClearance) {
                        adjustedPoint.y = desiredHeight;
                    }
                }
                
                adjustedPath.Add(adjustedPoint);
            }
            
            for (int i = 1; i < adjustedPath.Count - 1; i++) {
                var prevHeight = adjustedPath[i - 1].y;
                var currentHeight = adjustedPath[i].y;
                var nextHeight = adjustedPath[i + 1].y;
                
                var avgHeight = (prevHeight + nextHeight) * 0.5f;
                var heightDiff = Mathf.Abs(currentHeight - avgHeight);
                
                if (heightDiff > 5f) {
                    var smoothedPoint = adjustedPath[i];
                    smoothedPoint.y = Mathf.Lerp(currentHeight, avgHeight, 0.5f);
                    adjustedPath[i] = smoothedPoint;
                }
            }
            
            return adjustedPath;
        }
        
        public static float GetMaxTurnRate(float normalizedSpeed) {
            if (normalizedSpeed > 0.8f) {
                return 120f;
            }

            if (normalizedSpeed > 0.4f) {
                return 150f;
            }

            return 180f;
        }
    }
}