Files
aibis-dream/Assets/Scripts/SceneManagement/KiteRigDriver.cs
T

1315 lines
51 KiB
C#

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);
}
/// <summary>
/// 连续航迹驱动的风筝表现:风筝自身保持稳定迎风姿态,速度感主要由延迟镜头追踪表现。
/// </summary>
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 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
}
}