using System; using UnityEngine; namespace AibisDream { [Serializable] public class KiteWindSettings { [Header("平静漂移")] [Tooltip("无风时缓慢上下左右游移的范围")] public float calmFloatAmp = 0.46f; [Tooltip("平静游移的基础频率;数值越小越从容")] public float calmFloatFreq = 0.065f; [Tooltip("有风时持续向风向一侧偏移的距离")] public float steadyWindOffset = 0.45f; [Tooltip("弱风时额外向下飘落的距离")] public float weakWindDrop = 0.32f; [Header("大风航迹")] [Tooltip("乱度达到此值后开始明显靠近画面边缘")] [Range(0f, 1f)] public float edgeWindThreshold = 0.48f; [Tooltip("大风目标点最远可到达的横向距离")] public float edgeTravel = 3.65f; [Tooltip("大风目标点最远可到达的纵向距离")] public float edgeTravelY = 1.35f; [Tooltip("平静航迹的平滑时间")] public float calmMotionSmoothTime = 1.2f; [Tooltip("暴风航迹的平滑时间;仍保留惯性,不会瞬移")] public float stormMotionSmoothTime = 0.68f; [Tooltip("航迹最大速度,避免目标切换时突然抽走")] public float maxTravelSpeed = 3f; [Tooltip("风筝线允许的最大侧摆角度")] public float maxTetherAngle = 62f; [Tooltip("线张力把风筝拉向新平衡角的强度")] public float tetherAngularStiffness = 2.4f; [Tooltip("空气阻力和线阻尼;越大越不容易冲过头")] public float tetherAngularDamping = 3.35f; [Tooltip("最大角加速度,控制阵风起步有多猛")] public float maxAngularAcceleration = 68f; [Tooltip("最大角速度,控制横跨画面的速度上限")] public float maxAngularSpeed = 48f; [Tooltip("固定线长带来的边缘下沉弧度")] public float tetherArcDrop = 1.15f; [Tooltip("阵风纵向升沉的平滑时间")] public float verticalGustSmoothTime = 0.85f; [Header("大风与超级大风")] [Tooltip("达到此强度后才允许左右换边;此前的大风主要顺着同一风向拉扯")] [Range(0f, 1f)] public float superWindThreshold = 0.86f; [Tooltip("大风时镜头追上后的最短停留时间")] public float bigWindHoldMin = 2.6f; [Tooltip("大风时镜头追上后的最长停留时间")] public float bigWindHoldMax = 4.5f; [Tooltip("超级大风时镜头追上后的最短停留时间")] public float superWindHoldMin = 1.7f; [Tooltip("超级大风时镜头追上后的最长停留时间")] public float superWindHoldMax = 3f; [Tooltip("两次捕捉之间至少经过的普通出镜次数")] [Min(1)] public int doubleCatchIntervalMin = 3; [Tooltip("两次捕捉之间至多经过的普通出镜次数")] [Min(1)] public int doubleCatchIntervalMax = 5; [Tooltip("双捕捉第一次出镜相对完整边缘行程的比例;镜头将要追上后才继续向外加速")] [Range(0.5f, 0.9f)] public float doubleCatchFirstReach = 0.74f; [Tooltip("双捕捉第二次加速相对完整边缘行程的最小比例")] [Range(0.85f, 1f)] public float doubleCatchSecondReach = 0.96f; [Header("风变大时的常驻表现")] [Tooltip("低频风压推动风筝缓慢侧移的最大范围;不是机械抖动")] public float windPressureAmp = 0.36f; [Tooltip("风压起伏频率;0.1 约等于十秒一个主周期")] public float windPressureFrequency = 0.11f; [Tooltip("风压纵向位移相对于横向位移的比例")] [Range(0f, 1f)] public float windPressureVerticalRatio = 0.48f; [Tooltip("常驻风压位移带来的姿态响应")] public float windPressurePoseGain = 3.2f; [Header("升降曲线")] [Tooltip("Yarn 指定升降时长的整体放慢倍率")] public float heightDurationScale = 1.3f; [Tooltip("每级高度变化带来的受风侧偏")] public float heightArcPerLevel = 0.055f; [Tooltip("升降侧偏的最大距离")] public float heightArcMax = 0.42f; [Tooltip("升降侧偏建立和回正的平滑时间")] public float heightArcSmoothTime = 0.62f; [Header("稳定姿态")] [Tooltip("稳态迎风侧倾;参考实拍保持小角度")] public float leanAmp = 3.2f; [Tooltip("横向速度带来的轻微侧倾")] public float bankFromVel = 1.25f; [Tooltip("升降速度带来的轻微俯仰")] public float pitchFromHeightVel = 1.6f; [Tooltip("横向移动带来的轻微侧面透视")] public float yawFromLateralVel = 1.15f; [Tooltip("微小风压颤动幅度")] public float flutterAmp = 0.55f; [Tooltip("姿态变化的平滑时间")] public float poseSmoothTime = 0.34f; [Tooltip("任何风况下的最大姿态角,避免突然翻身")] public float maxPoseAngle = 7f; [Tooltip("俯仰和侧面透视的最大角度")] public float maxPerspectiveTilt = 5.5f; [Tooltip("有效风变化的平滑时间")] public float windSmoothTime = 0.45f; [Tooltip("高度对大风表现的增益")] public float altitudeWindGain = 0.18f; [Tooltip("尾部相位滞后")] public float tailLagPhase = 0.9f; [Tooltip("尾部幅度增益")] public float tailGain = 1.2f; } [Serializable] public class KiteCameraSettings { [Tooltip("摄影师看到风筝改变航迹后的反应延迟")] public float reactionLatency = 0.34f; [Tooltip("平静时镜头缓慢跟随的平滑时间")] public float smoothTimePos = 0.92f; [Tooltip("暴风时镜头追赶速度相对平静状态的倍率")] [Range(0.25f, 1f)] public float stormFollowMultiplier = 0.58f; [Tooltip("变焦追踪平滑;升降时风筝尺寸先变化,镜头再跟焦")] public float smoothTimeZoom = 1.18f; [Tooltip("最大变焦")] public float maxZoom = 3.2f; [Tooltip("镜头追踪目标允许的最大偏移")] public float maxPan = 4.5f; [Tooltip("高度变小时由镜头补回的缩放比例;低于 1 会让风筝越飞越小")] [Range(0f, 1f)] public float zoomCompensation = 0.55f; [Tooltip("平静阶段镜头跟随风筝纵向高度的比例")] [Range(0f, 1f)] public float calmVerticalFollow = 0.42f; [Tooltip("乱风阶段镜头跟随风筝纵向高度的比例")] [Range(0f, 1f)] public float stormVerticalFollow = 0.78f; [Tooltip("双捕捉中判定镜头快要框住风筝的构图误差")] public float doubleCatchNearDistance = 0.48f; [Tooltip("二次追回后镜头沿追赶方向多走的距离")] public float catchOvershootDistance = 0.18f; [Tooltip("二次追回后镜头过冲并收住的总时长")] public float catchOvershootDuration = 0.45f; } [Serializable] public class KiteFocusSettings { [Tooltip("最大虚焦")] public float maxBlur = 0.68f; [Tooltip("高斯半径 (_BlurSize)")] public float blurSize = 0.011f; [Tooltip("位置误差参考")] public float posErrorRef = 0.7f; [Tooltip("变焦误差参考")] public float zoomErrorRef = 0.2f; [Tooltip("低于此追踪误差完全不虚焦,避免平静漂移一直糊")] [Range(0f, 0.95f)] public float blurDeadZone = 0.3f; [Tooltip("失焦速度")] public float blurInSpeed = 3.5f; [Tooltip("重新合焦速度")] public float blurOutSpeed = 0.95f; [Tooltip("大移动结束触发一次轻微过焦的阈值")] public float settleEnterThreshold = 0.48f; [Tooltip("重新合焦阶段时长")] public float settleDuration = 0.46f; [Tooltip("重新合焦时轻微越过清晰点的幅度")] public float settleBump = 0.08f; public Vector3 settlePhaseRatios = new Vector3(0.5f, 0.2f, 0.3f); } /// /// 连续航迹驱动的风筝表现:风筝自身保持稳定迎风姿态,速度感主要由延迟镜头追踪表现。 /// public class KiteRigDriver : MonoBehaviour { private enum DoubleCatchPhase { None, FirstEscape, WaitingForFirstCatch, SecondEscape, WaitingForSecondCatch, Overshooting, } private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount"); private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize"); private const float MaxHeight = 13f; private const float ScalePerHeight = 0.76f; private const float MinScaleFactor = 0.12f; private static readonly Vector3 TopDrift = new Vector3(0.55f, 1.35f, 0f); private Transform _cameraNode; private Transform _rig; private Transform _sway; private SpriteRenderer _renderer; private Material _focusMaterial; private Vector3 _framePosition; private Quaternion _baseLocalRotation; private Vector3 _baseLocalScale; private KiteWindSettings _windSettings; private KiteCameraSettings _cameraSettings; private KiteFocusSettings _focusSettings; private int _wind; private float _turbulence; private float _effectiveWind; private float _effectiveWindVel; private float _height; private float _heightTarget; private float _heightSmoothTime = 0.3f; private float _heightVel; private float _heightArc; private float _heightArcTarget; private float _heightArcVel; private Vector3 _drift; private Vector3 _driftVel; private Vector3 _windPressure; private Vector3 _routeTarget; private float _routeRetargetAt; private int _routeSide = 1; private bool _routeMoveActive; private bool _routeWaitingForCamera; private bool _routeCountsTowardDoubleCatch; private bool _wasSuperWind; private int _normalEscapesUntilDoubleCatch; private DoubleCatchPhase _doubleCatchPhase; private float _routeAngle; private float _routeAngleTarget; private float _routeAngularVelocity; private float _routeVertical; private float _routeVerticalTarget; private float _routeVerticalVel; private float _poseAngle; private float _poseAngleVel; private float _pitchAngle; private float _pitchAngleVel; private float _yawAngle; private float _yawAngleVel; private Vector3 _camTrackPos; private Vector3 _camTrackPosVel; private float _camZoom = 1f; private float _camZoomVel; private Vector3 _camOvershootDirection; private Vector3 _camOvershootOffset; private float _camOvershootElapsed = -1f; private float _blur; private float _blurTarget; private bool _wasLargeMove; private float _settleElapsed = -1f; private float _settleSharp; private readonly PoseSample[] _poseRing = new PoseSample[96]; private int _poseWrite; private int _poseCount; private float _noiseSeedA; private float _noiseSeedB; private bool _configured; private bool _running; public float TailSway { get; private set; } public float CurrentHeight => _height; public float TargetHeight => _heightTarget; public int CurrentWind => _wind; public float Turbulence => _turbulence; public bool IsRunning => _running; public void Configure( Transform cameraNode, Transform rig, Transform sway, SpriteRenderer renderer, Vector3 framePosition, Quaternion baseLocalRotation, Vector3 baseLocalScale, KiteWindSettings windSettings, KiteCameraSettings cameraSettings, KiteFocusSettings focusSettings, Material focusMaterial) { _cameraNode = cameraNode; _rig = rig; _sway = sway; _renderer = renderer; _framePosition = framePosition; _baseLocalRotation = baseLocalRotation; _baseLocalScale = baseLocalScale; _windSettings = windSettings ?? new KiteWindSettings(); _cameraSettings = cameraSettings ?? new KiteCameraSettings(); _focusSettings = focusSettings ?? new KiteFocusSettings(); _focusMaterial = focusMaterial; _noiseSeedA = UnityEngine.Random.Range(0f, 100f); _noiseSeedB = UnityEngine.Random.Range(0f, 100f); _configured = true; if (_renderer != null && _focusMaterial != null) { _renderer.material = _focusMaterial; ApplyBlurToMaterial(0f); } } public void Wake(float height, int wind) { if (!_configured) return; enabled = true; _running = true; _wind = wind; _turbulence = 0f; _effectiveWind = WindBase(wind); _effectiveWindVel = 0f; _height = Mathf.Clamp(height, 0f, MaxHeight); _heightTarget = _height; _heightVel = 0f; _heightArc = 0f; _heightArcTarget = 0f; _heightArcVel = 0f; _drift = Vector3.zero; _driftVel = Vector3.zero; _windPressure = Vector3.zero; _routeTarget = Vector3.zero; _routeRetargetAt = Time.time + 1f; _routeSide = wind < 0 ? -1 : 1; _routeMoveActive = false; _routeWaitingForCamera = false; _routeCountsTowardDoubleCatch = false; _wasSuperWind = false; _doubleCatchPhase = DoubleCatchPhase.None; ResetDoubleCatchCadence(); _routeAngle = 0f; _routeAngleTarget = 0f; _routeAngularVelocity = 0f; _routeVertical = 0f; _routeVerticalTarget = 0f; _routeVerticalVel = 0f; _poseAngle = 0f; _poseAngleVel = 0f; _pitchAngle = 0f; _pitchAngleVel = 0f; _yawAngle = 0f; _yawAngleVel = 0f; ResetCameraOvershoot(); _blur = 0f; _blurTarget = 0f; _wasLargeMove = false; _settleElapsed = -1f; _poseWrite = 0; _poseCount = 0; TailSway = 0f; ApplyHeightPose(_height); ResetSwayLocal(); ApplyMotionPose(Time.time, true, 0f); SnapCameraToStable(); ApplyBlurToMaterial(0f); } public void SetHeight(float target, float duration) { if (!_running) return; float nextHeight = Mathf.Clamp(target, 0f, MaxHeight); float deltaHeight = nextHeight - _height; _heightTarget = nextHeight; float physicalDuration = Mathf.Max(0.05f, duration) * Mathf.Max(0.2f, _windSettings.heightDurationScale); _heightSmoothTime = Mathf.Max(0.04f, physicalDuration / 2.45f); float windSide = Mathf.Abs(_effectiveWind) > 0.08f ? Mathf.Sign(_effectiveWind) : (_routeSide == 0 ? 1f : _routeSide); float arcMagnitude = Mathf.Min( Mathf.Abs(deltaHeight) * _windSettings.heightArcPerLevel, _windSettings.heightArcMax); _heightArcTarget = windSide * arcMagnitude; if (duration <= 0.001f) { _height = _heightTarget; _heightVel = 0f; _heightArc = 0f; _heightArcTarget = 0f; _heightArcVel = 0f; } } public void SetWind(int wind, float turbulence = -1f) { if (!_running) return; int previousWind = _wind; float previousTurbulence = _turbulence; _wind = wind; if (turbulence >= 0f) { _turbulence = Mathf.Clamp01(turbulence); } // 只提前安排下一段航迹,不重置当前位置和速度。 // 因此 Yarn 连续切换风况时不会跳帧或瞬间反向。 if (previousWind != _wind || Mathf.Abs(previousTurbulence - _turbulence) > 0.05f) { _routeRetargetAt = Mathf.Min(_routeRetargetAt, Time.time + 0.2f); } } public void SnapCameraToStable() { if (_cameraNode == null || _renderer == null) return; Vector3 kitePosition = SampleKiteLocalPose(); float kiteScale = ScaleFactor(_height); EvaluateCameraComposition( kitePosition, kiteScale, MotionIntensity(), out Vector3 positionTarget, out float zoomTarget); _camTrackPos = positionTarget; _camTrackPosVel = Vector3.zero; _camZoom = zoomTarget; _camZoomVel = 0f; ResetCameraOvershoot(); ApplyCameraTransform(); PushPoseSample(positionTarget, zoomTarget, Time.time); } public void ResetAndSleep() { _running = false; enabled = false; _effectiveWind = 0f; _effectiveWindVel = 0f; _drift = Vector3.zero; _driftVel = Vector3.zero; _heightArc = 0f; _heightArcTarget = 0f; _heightArcVel = 0f; _routeTarget = Vector3.zero; _routeMoveActive = false; _routeWaitingForCamera = false; _routeCountsTowardDoubleCatch = false; _wasSuperWind = false; _doubleCatchPhase = DoubleCatchPhase.None; ResetDoubleCatchCadence(); _routeAngle = 0f; _routeAngleTarget = 0f; _routeAngularVelocity = 0f; _routeVertical = 0f; _routeVerticalTarget = 0f; _routeVerticalVel = 0f; _poseAngle = 0f; _poseAngleVel = 0f; _pitchAngle = 0f; _pitchAngleVel = 0f; _yawAngle = 0f; _yawAngleVel = 0f; ResetCameraOvershoot(); _blur = 0f; _blurTarget = 0f; _settleElapsed = -1f; _wasLargeMove = false; _poseCount = 0; TailSway = 0f; ResetSwayLocal(); ApplyBlurToMaterial(0f); } private void Update() { if (!_running || !_configured) return; float dt = Mathf.Max(Time.deltaTime, 0.0001f); float now = Time.time; StepWind(dt, now); StepHeight(dt); StepMotion(dt, now); StepCamera(dt, now); StepFocus(dt); } private void StepWind(float dt, float now) { // 只使用低频风压变化;快速变化交给航迹目标,不直接写入速度或姿态。 float slowNoise = PerlinSigned(now * 0.08f, _noiseSeedA) * _turbulence * 0.16f; float altitudeBoost = 1f + (_height / MaxHeight) * _windSettings.altitudeWindGain; float raw = (WindBase(_wind) + slowNoise) * altitudeBoost; _effectiveWind = Mathf.SmoothDamp( _effectiveWind, raw, ref _effectiveWindVel, Mathf.Max(0.05f, _windSettings.windSmoothTime), Mathf.Infinity, dt); } private void StepHeight(float dt) { if (Mathf.Abs(_height - _heightTarget) < 0.0005f) { _height = _heightTarget; _heightVel = 0f; _heightArcTarget = 0f; } else { _height = Mathf.SmoothDamp( _height, _heightTarget, ref _heightVel, Mathf.Max(0.02f, _heightSmoothTime), Mathf.Infinity, dt); } _heightArc = Mathf.SmoothDamp( _heightArc, _heightArcTarget, ref _heightArcVel, Mathf.Max(0.08f, _windSettings.heightArcSmoothTime), Mathf.Infinity, dt); ApplyHeightPose(_height); if (_renderer != null) { Color color = _renderer.color; color.a = Alpha(_height); _renderer.color = color; } } private void StepMotion(float dt, float now) { if (_sway == null || _rig == null) return; float intensity = MotionIntensity(); UpdateRoutePlan(intensity, now); StepTetherPhysics(dt); Vector3 calmTarget = SampleCalmDrift(now, intensity); _windPressure = SampleWindPressure(now, intensity); Vector3 windTarget = new Vector3( _effectiveWind * _windSettings.steadyWindOffset, _wind < 0 ? -_windSettings.weakWindDrop : Mathf.Abs(_effectiveWind) * 0.08f, 0f); Vector3 target = calmTarget + windTarget + _windPressure + _routeTarget; target.x = Mathf.Clamp(target.x, -_cameraSettings.maxPan, _cameraSettings.maxPan); target.y = Mathf.Clamp(target.y, -_cameraSettings.maxPan * 0.72f, _cameraSettings.maxPan * 0.72f); float smoothTime = Mathf.Lerp( _windSettings.calmMotionSmoothTime, _windSettings.stormMotionSmoothTime, Smooth01(intensity)); _drift = Vector3.SmoothDamp( _drift, target, ref _driftVel, Mathf.Max(0.08f, smoothTime), Mathf.Max(0.2f, _windSettings.maxTravelSpeed), dt); ApplyMotionPose(now, false, dt); UpdateRouteArrival(intensity); } private void UpdateRoutePlan(float intensity, float now) { bool isSuperWind = intensity >= _windSettings.superWindThreshold; if (intensity < _windSettings.edgeWindThreshold) { if (_wasSuperWind) { ResetDoubleCatchCadence(); } _wasSuperWind = false; _routeAngleTarget = 0f; _routeVerticalTarget = 0f; _routeMoveActive = false; _routeWaitingForCamera = false; _routeCountsTowardDoubleCatch = false; _doubleCatchPhase = DoubleCatchPhase.None; ResetCameraOvershoot(); _routeRetargetAt = now + UnityEngine.Random.Range(0.8f, 1.5f); return; } if (_wasSuperWind && !isSuperWind) { CancelDoubleCatchForWindChange(); ResetDoubleCatchCadence(); } else if (!_wasSuperWind && isSuperWind) { ResetDoubleCatchCadence(); } _wasSuperWind = isSuperWind; if (_routeMoveActive) return; if (_doubleCatchPhase == DoubleCatchPhase.WaitingForFirstCatch) { if (!IsCameraNearCatch()) return; ChooseNextRouteTarget( intensity, keepSide: true, reachScale: Mathf.Clamp(_windSettings.doubleCatchSecondReach, 0.85f, 1f)); _doubleCatchPhase = DoubleCatchPhase.SecondEscape; _routeMoveActive = true; return; } if (_doubleCatchPhase == DoubleCatchPhase.WaitingForSecondCatch) { if (!IsCameraNearCatch()) return; BeginCameraOvershoot(); return; } if (_doubleCatchPhase == DoubleCatchPhase.Overshooting) return; if (_routeWaitingForCamera) { if (!IsCameraCaught()) return; _routeWaitingForCamera = false; if (_routeCountsTowardDoubleCatch && isSuperWind) { _normalEscapesUntilDoubleCatch = Mathf.Max( 0, _normalEscapesUntilDoubleCatch - 1); } _routeCountsTowardDoubleCatch = false; // 镜头回到风筝后再多停一拍,避免机械地一追上就立刻出发。 float superBlend = Smooth01(Mathf.InverseLerp( _windSettings.superWindThreshold, 1f, intensity)); float holdMin = Mathf.Lerp( _windSettings.bigWindHoldMin, _windSettings.superWindHoldMin, superBlend); float holdMax = Mathf.Lerp( _windSettings.bigWindHoldMax, _windSettings.superWindHoldMax, superBlend); holdMax = Mathf.Max(holdMin, holdMax); _routeRetargetAt = now + UnityEngine.Random.Range(holdMin, holdMax); return; } if (now >= _routeRetargetAt) { if (isSuperWind && _normalEscapesUntilDoubleCatch <= 0) { StartDoubleCatch(intensity); } else { ChooseNextRouteTarget(intensity, keepSide: false, reachScale: 1f); _routeMoveActive = true; } } } private void StartDoubleCatch(float intensity) { _routeWaitingForCamera = false; _routeCountsTowardDoubleCatch = false; ChooseNextRouteTarget( intensity, keepSide: false, reachScale: Mathf.Clamp(_windSettings.doubleCatchFirstReach, 0.5f, 0.9f)); _doubleCatchPhase = DoubleCatchPhase.FirstEscape; _routeMoveActive = true; } private void CancelDoubleCatchForWindChange() { if (_doubleCatchPhase == DoubleCatchPhase.None && _camOvershootElapsed < 0f) return; _doubleCatchPhase = DoubleCatchPhase.None; _routeMoveActive = false; _routeWaitingForCamera = true; _routeCountsTowardDoubleCatch = false; _routeRetargetAt = float.PositiveInfinity; ResetCameraOvershoot(); } private void ResetDoubleCatchCadence() { int min = Mathf.Max(1, _windSettings != null ? _windSettings.doubleCatchIntervalMin : 3); int max = Mathf.Max(min, _windSettings != null ? _windSettings.doubleCatchIntervalMax : 5); _normalEscapesUntilDoubleCatch = UnityEngine.Random.Range(min, max + 1); } private void StepTetherPhysics(float dt) { float stiffness = Mathf.Max(0.1f, _windSettings.tetherAngularStiffness); float damping = Mathf.Max(0f, _windSettings.tetherAngularDamping); float angularAcceleration = (_routeAngleTarget - _routeAngle) * stiffness - _routeAngularVelocity * damping; angularAcceleration = Mathf.Clamp( angularAcceleration, -Mathf.Abs(_windSettings.maxAngularAcceleration), Mathf.Abs(_windSettings.maxAngularAcceleration)); _routeAngularVelocity += angularAcceleration * dt; _routeAngularVelocity = Mathf.Clamp( _routeAngularVelocity, -Mathf.Abs(_windSettings.maxAngularSpeed), Mathf.Abs(_windSettings.maxAngularSpeed)); _routeAngle += _routeAngularVelocity * dt; _routeVertical = Mathf.SmoothDamp( _routeVertical, _routeVerticalTarget, ref _routeVerticalVel, Mathf.Max(0.08f, _windSettings.verticalGustSmoothTime), Mathf.Infinity, dt); // 固定线长约束:横向侧摆越大,垂直高度自然略降,轨迹形成圆弧。 float angleRadians = _routeAngle * Mathf.Deg2Rad; float x = Mathf.Sin(angleRadians) * _windSettings.edgeTravel; float y = (Mathf.Cos(angleRadians) - 1f) * _windSettings.tetherArcDrop + _routeVertical; _routeTarget = new Vector3(x, y, 0f); } private void UpdateRouteArrival(float intensity) { if (!_routeMoveActive) return; float angleError = Mathf.Abs(_routeAngleTarget - _routeAngle); float verticalError = Mathf.Abs(_routeVerticalTarget - _routeVertical); if (angleError > 2.2f || Mathf.Abs(_routeAngularVelocity) > 4.5f || verticalError > 0.12f) { return; } _routeMoveActive = false; if (_doubleCatchPhase == DoubleCatchPhase.FirstEscape) { _doubleCatchPhase = DoubleCatchPhase.WaitingForFirstCatch; _routeRetargetAt = float.PositiveInfinity; return; } if (_doubleCatchPhase == DoubleCatchPhase.SecondEscape) { _doubleCatchPhase = DoubleCatchPhase.WaitingForSecondCatch; _routeRetargetAt = float.PositiveInfinity; return; } _routeWaitingForCamera = true; _routeCountsTowardDoubleCatch = intensity >= _windSettings.superWindThreshold; _routeRetargetAt = float.PositiveInfinity; } private bool IsCameraCaught() { if (_renderer == null) return true; Vector3 kitePosition = SampleKiteLocalPose(); EvaluateCameraComposition( kitePosition, ScaleFactor(_height), MotionIntensity(), out Vector3 positionTarget, out _); return Vector3.Distance(positionTarget, _camTrackPos + _camOvershootOffset) < 0.24f && _camTrackPosVel.magnitude < 0.38f; } private bool IsCameraNearCatch() { if (_renderer == null) return true; Vector3 kitePosition = SampleKiteLocalPose(); EvaluateCameraComposition( kitePosition, ScaleFactor(_height), MotionIntensity(), out Vector3 positionTarget, out _); float threshold = Mathf.Max(0.25f, _cameraSettings.doubleCatchNearDistance); return Vector3.Distance(positionTarget, _camTrackPos + _camOvershootOffset) <= threshold; } private void ChooseNextRouteTarget(float intensity, bool keepSide, float reachScale) { float edge01 = Mathf.InverseLerp(_windSettings.edgeWindThreshold, 1f, intensity); bool alternateSides = intensity >= _windSettings.superWindThreshold; if (!keepSide && alternateSides) { _routeSide = -_routeSide; if (_routeSide == 0) _routeSide = 1; } else if (!keepSide) { // 大风阶段主要顺风靠向同一侧,只有少量同侧高度变化。 _routeSide = _wind < 0 ? -1 : 1; } float targetAngle = Mathf.Lerp( _windSettings.maxTetherAngle * 0.42f, _windSettings.maxTetherAngle, Smooth01(edge01)); _routeAngleTarget = _routeSide * targetAngle * Mathf.Clamp(reachScale, 0.1f, 1f) * UnityEngine.Random.Range(0.94f, 1f); float yDistance = Mathf.Lerp( _windSettings.edgeTravelY * 0.3f, _windSettings.edgeTravelY, Smooth01(edge01)); float nextVertical = UnityEngine.Random.Range(-yDistance, yDistance) * 0.55f; if (keepSide && Mathf.Abs(nextVertical - _routeVerticalTarget) < yDistance * 0.18f) { nextVertical = -Mathf.Sign(_routeVerticalTarget == 0f ? 1f : _routeVerticalTarget) * Mathf.Max(yDistance * 0.22f, Mathf.Abs(nextVertical)); } _routeVerticalTarget = nextVertical; // 这里不安排下一次移动;到点后必须经过镜头追赶和随机停留阶段。 _routeRetargetAt = float.PositiveInfinity; } private Vector3 SampleCalmDrift(float now, float intensity) { float frequency = Mathf.Max(0.005f, _windSettings.calmFloatFreq) * Mathf.Lerp(1f, 2.8f, intensity); float amplitude = _windSettings.calmFloatAmp * Mathf.Lerp(1f, 1.45f, intensity); float phaseA = _noiseSeedA * 0.13f; float phaseB = _noiseSeedB * 0.17f; float t = now * frequency * Mathf.PI * 2f; // 正弦负责保证始终有运动,低权重连续噪声负责打破规律。 float x = Mathf.Sin(t + phaseA) * 0.56f + Mathf.Sin(t * 0.47f + phaseB) * 0.24f + PerlinSigned(now * frequency * 0.32f, _noiseSeedA + 3f) * 0.2f; float y = Mathf.Cos(t * 0.73f + phaseB) * 0.52f + Mathf.Sin(t * 0.31f + phaseA) * 0.26f + PerlinSigned(now * frequency * 0.25f, _noiseSeedB + 5f) * 0.22f; return new Vector3(x * amplitude, y * amplitude * 0.82f, 0f); } private Vector3 SampleWindPressure(float now, float intensity) { // 0.25 / 0.38 阶段就开始让风有存在感;到大风阶段逐渐成为主要常驻运动。 // 同一股低频风压同时驱动位置和姿态,避免每帧独立随机形成机械振动。 float superThreshold = Mathf.Max(0.2f, _windSettings.superWindThreshold); float pressure01 = Mathf.InverseLerp(0.18f, superThreshold, intensity); float pressureWeight = Mathf.Sqrt(Smooth01(pressure01)); if (pressureWeight <= 0.001f) return Vector3.zero; float frequency = Mathf.Max(0.01f, _windSettings.windPressureFrequency); float phase = now * frequency * Mathf.PI * 2f; float lateral = Mathf.Sin(phase + _noiseSeedA * 0.19f) * 0.62f + Mathf.Sin(phase * 0.43f + _noiseSeedB * 0.23f) * 0.2f + PerlinSigned(now * frequency * 0.38f, _noiseSeedA + 41f) * 0.18f; float vertical = Mathf.Sin(phase * 0.61f + _noiseSeedB * 0.23f) * 0.68f + PerlinSigned(now * frequency * 0.29f, _noiseSeedB + 47f) * 0.32f; // 大范围甩动时仍保留少量风压,但把主要画面让给航迹动作。 float idleWeight = _routeMoveActive ? 0.35f : 1f; float amplitude = _windSettings.windPressureAmp * pressureWeight * idleWeight; return new Vector3( lateral * amplitude, vertical * amplitude * _windSettings.windPressureVerticalRatio, 0f); } private void ApplyMotionPose(float now, bool snap, float dt) { float intensity = MotionIntensity(); float microFlutter = PerlinSigned(now * Mathf.Lerp(0.28f, 0.72f, intensity), _noiseSeedB + 23f) * _windSettings.flutterAmp * Mathf.Lerp(0.25f, 1f, intensity); float targetAngle = -_effectiveWind * _windSettings.leanAmp - _driftVel.x * _windSettings.bankFromVel - _windPressure.x * _windSettings.windPressurePoseGain + microFlutter; targetAngle = Mathf.Clamp( targetAngle, -Mathf.Abs(_windSettings.maxPoseAngle), Mathf.Abs(_windSettings.maxPoseAngle)); if (snap) { _poseAngle = targetAngle; _poseAngleVel = 0f; } else { _poseAngle = Mathf.SmoothDampAngle( _poseAngle, targetAngle, ref _poseAngleVel, Mathf.Max(0.05f, _windSettings.poseSmoothTime), Mathf.Infinity, dt); } float maxTilt = Mathf.Abs(_windSettings.maxPerspectiveTilt); float targetPitch = Mathf.Clamp( -_heightVel * _windSettings.pitchFromHeightVel + _driftVel.y * 0.85f, -maxTilt, maxTilt); float targetYaw = Mathf.Clamp( -_driftVel.x * _windSettings.yawFromLateralVel, -maxTilt, maxTilt); if (snap) { _pitchAngle = targetPitch; _yawAngle = targetYaw; _pitchAngleVel = 0f; _yawAngleVel = 0f; } else { float tiltSmooth = Mathf.Max(0.08f, _windSettings.poseSmoothTime * 1.15f); _pitchAngle = Mathf.SmoothDampAngle( _pitchAngle, targetPitch, ref _pitchAngleVel, tiltSmooth, Mathf.Infinity, dt); _yawAngle = Mathf.SmoothDampAngle( _yawAngle, targetYaw, ref _yawAngleVel, tiltSmooth, Mathf.Infinity, dt); } _sway.localPosition = Vector3.zero; _sway.localRotation = Quaternion.Euler(_pitchAngle, _yawAngle, _poseAngle); TailSway = PerlinSigned(now * 0.9f - _windSettings.tailLagPhase, _noiseSeedA + 31f) * (0.25f + intensity * _windSettings.tailGain); Vector3 heightArcOffset = new Vector3(_heightArc, Mathf.Abs(_heightArc) * 0.12f, 0f); _rig.localPosition = HeightPosition(_height) + heightArcOffset + _drift; _rig.localRotation = _baseLocalRotation; _rig.localScale = HeightScale(_height); } private float MotionIntensity() { float baseWind = _wind > 0 ? 0.12f : _wind < 0 ? 0.06f : 0f; float altitude = (_height / MaxHeight) * _windSettings.altitudeWindGain * _turbulence; return Mathf.Clamp01(Mathf.Max(baseWind, _turbulence + altitude)); } private void BeginCameraOvershoot() { float verticalSign = Mathf.Sign(_routeVerticalTarget); _camOvershootDirection = new Vector3(_routeSide, verticalSign * 0.18f, 0f).normalized; _camOvershootOffset = Vector3.zero; _camOvershootElapsed = 0f; _doubleCatchPhase = DoubleCatchPhase.Overshooting; _routeRetargetAt = float.PositiveInfinity; } private void StepCameraOvershoot(float dt) { if (_camOvershootElapsed < 0f) return; float duration = Mathf.Max(0.01f, _cameraSettings.catchOvershootDuration); _camOvershootElapsed += dt; float progress = Mathf.Clamp01(_camOvershootElapsed / duration); const float peakAt = 0.34f; float envelope = progress <= peakAt ? Smooth01(progress / peakAt) : 1f - Smooth01((progress - peakAt) / (1f - peakAt)); _camOvershootOffset = _camOvershootDirection * Mathf.Max(0f, _cameraSettings.catchOvershootDistance) * envelope; if (progress < 1f) return; ResetCameraOvershoot(); _doubleCatchPhase = DoubleCatchPhase.None; _routeWaitingForCamera = true; _routeCountsTowardDoubleCatch = false; _routeRetargetAt = float.PositiveInfinity; ResetDoubleCatchCadence(); } private void ResetCameraOvershoot() { _camOvershootDirection = Vector3.zero; _camOvershootOffset = Vector3.zero; _camOvershootElapsed = -1f; } private void EvaluateCameraComposition( Vector3 kitePosition, float kiteScale, float intensity, out Vector3 positionTarget, out float zoomTarget) { float verticalFollow = Mathf.Lerp( Mathf.Clamp01(_cameraSettings.calmVerticalFollow), Mathf.Clamp01(_cameraSettings.stormVerticalFollow), Smooth01(intensity)); positionTarget = ClampPan(new Vector3( kitePosition.x, kitePosition.y * verticalFollow, kitePosition.z)); float fullZoom = Mathf.Clamp( 1f / Mathf.Max(kiteScale, 0.0001f), 1f, _cameraSettings.maxZoom); zoomTarget = Mathf.Lerp( 1f, fullZoom, Mathf.Clamp01(_cameraSettings.zoomCompensation)); } private void StepCamera(float dt, float now) { Vector3 kitePosition = SampleKiteLocalPose(); float kiteScale = ScaleFactor(_height); float intensity = MotionIntensity(); EvaluateCameraComposition( kitePosition, kiteScale, intensity, out Vector3 positionTarget, out float zoomTarget); PushPoseSample(positionTarget, zoomTarget, now); SampleDelayedPose(now, out Vector3 delayedPosition, out float delayedZoom); delayedPosition = ClampPan(delayedPosition); // 平静时镜头从容地跟;风越大,摄影师越积极追赶快速换边的风筝。 float followMultiplier = Mathf.Lerp( 1f, Mathf.Clamp(_cameraSettings.stormFollowMultiplier, 0.25f, 1f), Smooth01(intensity)); float positionSmooth = _cameraSettings.smoothTimePos * followMultiplier; _camTrackPos = Vector3.SmoothDamp( _camTrackPos, delayedPosition, ref _camTrackPosVel, Mathf.Max(0.03f, positionSmooth), Mathf.Infinity, dt); _camZoom = Mathf.SmoothDamp( _camZoom, delayedZoom, ref _camZoomVel, Mathf.Max(0.03f, _cameraSettings.smoothTimeZoom), Mathf.Infinity, dt); _camZoom = Mathf.Clamp(_camZoom, 1f, _cameraSettings.maxZoom); StepCameraOvershoot(dt); ApplyCameraTransform(); UpdateFocusTarget(positionTarget, zoomTarget); } private void UpdateFocusTarget(Vector3 positionTarget, float zoomTarget) { float positionError = Vector3.Distance( positionTarget, _camTrackPos + _camOvershootOffset) / Mathf.Max(0.01f, _focusSettings.posErrorRef); float zoomError = Mathf.Abs(Mathf.Log( Mathf.Max(zoomTarget, 0.001f) / Mathf.Max(_camZoom, 0.001f))) / Mathf.Max(0.01f, _focusSettings.zoomErrorRef); float error = Mathf.Clamp01(positionError + zoomError); float deadZone = Mathf.Clamp01(_focusSettings.blurDeadZone); float gatedError = error <= deadZone ? 0f : (error - deadZone) / Mathf.Max(0.001f, 1f - deadZone); _blurTarget = _focusSettings.maxBlur * Mathf.Clamp01(gatedError); bool largeMove = error > _focusSettings.settleEnterThreshold; if (_wasLargeMove && !largeMove && _settleElapsed < 0f && _blur > 0.02f) { _settleElapsed = 0f; _settleSharp = 0f; } _wasLargeMove = largeMove; } private void StepFocus(float dt) { float target = _settleElapsed >= 0f ? EvaluateSettleBlur(dt) : _blurTarget; float speed = target > _blur ? _focusSettings.blurInSpeed : _focusSettings.blurOutSpeed; _blur = Mathf.MoveTowards( _blur, target, speed * dt * Mathf.Max(0.01f, _focusSettings.maxBlur)); ApplyBlurToMaterial(_blur); } private float EvaluateSettleBlur(float dt) { _settleElapsed += dt; Vector3 ratios = _focusSettings.settlePhaseRatios; float sum = ratios.x + ratios.y + ratios.z; if (sum <= 0.001f) { ratios = new Vector3(0.5f, 0.2f, 0.3f); sum = 1f; } float total = Mathf.Max(0.05f, _focusSettings.settleDuration); float sharpenDuration = total * (ratios.x / sum); float bumpDuration = total * (ratios.y / sum); float recoverDuration = total * (ratios.z / sum); float bump = Mathf.Clamp(_focusSettings.settleBump, 0f, _focusSettings.maxBlur); if (_settleElapsed <= sharpenDuration) { float u = sharpenDuration <= 0.001f ? 1f : _settleElapsed / sharpenDuration; return Mathf.Lerp(_blur, _settleSharp, Smooth01(u)); } if (_settleElapsed <= sharpenDuration + bumpDuration) { float u = bumpDuration <= 0.001f ? 1f : (_settleElapsed - sharpenDuration) / bumpDuration; return Mathf.Lerp(_settleSharp, bump, Smooth01(u)); } if (_settleElapsed <= sharpenDuration + bumpDuration + recoverDuration) { float u = recoverDuration <= 0.001f ? 1f : (_settleElapsed - sharpenDuration - bumpDuration) / recoverDuration; return Mathf.Lerp(bump, _settleSharp, Smooth01(u)); } _settleElapsed = -1f; return _settleSharp; } private void ApplyHeightPose(float height) { if (_rig == null) return; _rig.localRotation = _baseLocalRotation; _rig.localScale = HeightScale(height); } private void ApplyCameraTransform() { if (_cameraNode == null) return; _cameraNode.localScale = Vector3.one * _camZoom; _cameraNode.localRotation = Quaternion.identity; _cameraNode.localPosition = _framePosition - _camZoom * (_camTrackPos + _camOvershootOffset); } private Vector3 SampleKiteLocalPose() { if (_cameraNode == null || _renderer == null) return Vector3.zero; return _cameraNode.InverseTransformPoint(_renderer.transform.position); } private void PushPoseSample(Vector3 position, float zoom, float time) { _poseRing[_poseWrite] = new PoseSample { position = position, zoom = zoom, time = time }; _poseWrite = (_poseWrite + 1) % _poseRing.Length; if (_poseCount < _poseRing.Length) _poseCount++; } private void SampleDelayedPose(float now, out Vector3 position, out float zoom) { float targetTime = now - Mathf.Max(0f, _cameraSettings.reactionLatency); if (_poseCount <= 0) { position = _camTrackPos; zoom = _camZoom; return; } int oldest = (_poseWrite - _poseCount + _poseRing.Length) % _poseRing.Length; PoseSample before = _poseRing[oldest]; PoseSample after = before; bool foundAfter = false; for (int i = 0; i < _poseCount; i++) { int index = (oldest + i) % _poseRing.Length; PoseSample sample = _poseRing[index]; if (sample.time <= targetTime) { before = sample; after = sample; continue; } after = sample; foundAfter = true; break; } if (!foundAfter || after.time <= before.time + 0.0001f) { position = before.position; zoom = before.zoom; return; } float t = Mathf.InverseLerp(before.time, after.time, targetTime); position = Vector3.LerpUnclamped(before.position, after.position, t); zoom = Mathf.LerpUnclamped(before.zoom, after.zoom, t); } private Vector3 ClampPan(Vector3 position) { float maxPan = _cameraSettings.maxPan; if (maxPan <= 0.001f) return position; position.x = Mathf.Clamp(position.x, -maxPan, maxPan); position.y = Mathf.Clamp(position.y, -maxPan, maxPan); return position; } private void ResetSwayLocal() { if (_sway == null) return; _sway.localPosition = Vector3.zero; _sway.localRotation = Quaternion.identity; } private void ApplyBlurToMaterial(float amount) { if (_renderer == null) return; Material material = Application.isPlaying ? _renderer.material : _renderer.sharedMaterial; if (material == null) return; if (material.HasProperty(BlurAmountId)) material.SetFloat(BlurAmountId, amount); if (material.HasProperty(BlurSizeId)) { material.SetFloat(BlurSizeId, _focusSettings != null ? _focusSettings.blurSize : 0.011f); } } private static float WindBase(int wind) { if (wind > 0) return 1f; if (wind < 0) return -0.45f; return 0f; } private static float PerlinSigned(float t, float seed) { return Mathf.PerlinNoise(t + seed, seed * 0.37f) * 2f - 1f; } private static float Smooth01(float t) { t = Mathf.Clamp01(t); return t * t * (3f - 2f * t); } public static float ScaleFactor(float height) { return Mathf.Max(Mathf.Pow(ScalePerHeight, height), MinScaleFactor); } public Vector3 HeightPosition(float height) { return TopDrift * (Mathf.Clamp(height, 0f, MaxHeight) / MaxHeight); } public Vector3 HeightScale(float height) { return _baseLocalScale * ScaleFactor(height); } public static float Alpha(float height) { return height <= 8f ? 1f : Mathf.Lerp(1f, 0.75f, (height - 8f) / (MaxHeight - 8f)); } private struct PoseSample { public Vector3 position; public float zoom; public float time; } #if UNITY_EDITOR private void OnDrawGizmosSelected() { if (_cameraNode == null) return; Gizmos.color = new Color(0.3f, 0.85f, 1f, 0.7f); Gizmos.DrawWireSphere(_cameraNode.TransformPoint(_camTrackPos), 0.05f); Gizmos.color = new Color(1f, 0.6f, 0.2f, 0.8f); if (_renderer != null) Gizmos.DrawWireSphere(_renderer.transform.position, 0.04f); } #endif } }