From fa66dbc7c99350dec59ce9d407e65599ef21207c Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Wed, 15 Jul 2026 19:39:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(kite):=20=E4=BC=98=E5=8C=96=E9=A3=8E?= =?UTF-8?q?=E7=AD=9D=E9=95=9C=E5=A4=B4=E8=BF=BD=E8=B5=B6=E4=B8=8E=E5=8F=8C?= =?UTF-8?q?=E6=8D=95=E6=8D=89=E8=A1=A8=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Assets/Scenes/梦境.unity | 14 +- .../Scripts/SceneManagement/KiteRigDriver.cs | 324 +++++++++++++++--- Docs/风筝命令使用指南.md | 11 +- 3 files changed, 303 insertions(+), 46 deletions(-) diff --git a/Assets/Scenes/梦境.unity b/Assets/Scenes/梦境.unity index cdb494efa..3261017bf 100644 --- a/Assets/Scenes/梦境.unity +++ b/Assets/Scenes/梦境.unity @@ -1198,8 +1198,10 @@ MonoBehaviour: bigWindHoldMax: 4.5 superWindHoldMin: 1.7 superWindHoldMax: 3 - doubleEscapeGapMin: 0.28 - doubleEscapeGapMax: 0.6 + doubleCatchIntervalMin: 3 + doubleCatchIntervalMax: 5 + doubleCatchFirstReach: 0.74 + doubleCatchSecondReach: 0.96 windPressureAmp: 0.36 windPressureFrequency: 0.11 windPressureVerticalRatio: 0.48 @@ -1223,10 +1225,16 @@ MonoBehaviour: _kiteCameraSettings: reactionLatency: 0.34 smoothTimePos: 0.92 - stormFollowMultiplier: 0.63 + stormFollowMultiplier: 0.55 smoothTimeZoom: 1.18 maxZoom: 3.2 maxPan: 4.5 + zoomCompensation: 0.55 + calmVerticalFollow: 0.42 + stormVerticalFollow: 0.78 + doubleCatchNearDistance: 0.48 + catchOvershootDistance: 0.18 + catchOvershootDuration: 0.45 _kiteFocusSettings: maxBlur: 0.68 blurSize: 0.011 diff --git a/Assets/Scripts/SceneManagement/KiteRigDriver.cs b/Assets/Scripts/SceneManagement/KiteRigDriver.cs index 7757a7d5d..732c0c92c 100644 --- a/Assets/Scripts/SceneManagement/KiteRigDriver.cs +++ b/Assets/Scripts/SceneManagement/KiteRigDriver.cs @@ -55,10 +55,14 @@ namespace AibisDream public float superWindHoldMin = 1.7f; [Tooltip("超级大风时镜头追上后的最长停留时间")] public float superWindHoldMax = 3f; - [Tooltip("超级大风连续第二次甩开镜头前的最短蓄力时间")] - public float doubleEscapeGapMin = 0.28f; - [Tooltip("超级大风连续第二次甩开镜头前的最长蓄力时间")] - public float doubleEscapeGapMax = 0.6f; + [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("低频风压推动风筝缓慢侧移的最大范围;不是机械抖动")] @@ -122,6 +126,18 @@ namespace AibisDream 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] @@ -155,6 +171,16 @@ namespace AibisDream /// 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"); @@ -198,7 +224,10 @@ namespace AibisDream private int _routeSide = 1; private bool _routeMoveActive; private bool _routeWaitingForCamera; - private int _escapeBurstCount; + private bool _routeCountsTowardDoubleCatch; + private bool _wasSuperWind; + private int _normalEscapesUntilDoubleCatch; + private DoubleCatchPhase _doubleCatchPhase; private float _routeAngle; private float _routeAngleTarget; private float _routeAngularVelocity; @@ -216,6 +245,9 @@ namespace AibisDream 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; @@ -295,7 +327,10 @@ namespace AibisDream _routeSide = wind < 0 ? -1 : 1; _routeMoveActive = false; _routeWaitingForCamera = false; - _escapeBurstCount = 0; + _routeCountsTowardDoubleCatch = false; + _wasSuperWind = false; + _doubleCatchPhase = DoubleCatchPhase.None; + ResetDoubleCatchCadence(); _routeAngle = 0f; _routeAngleTarget = 0f; _routeAngularVelocity = 0f; @@ -308,6 +343,7 @@ namespace AibisDream _pitchAngleVel = 0f; _yawAngle = 0f; _yawAngleVel = 0f; + ResetCameraOvershoot(); _blur = 0f; _blurTarget = 0f; _wasLargeMove = false; @@ -378,13 +414,19 @@ namespace AibisDream Vector3 kitePosition = SampleKiteLocalPose(); float kiteScale = ScaleFactor(_height); - float zoomTarget = Mathf.Clamp(1f / Mathf.Max(kiteScale, 0.0001f), 1f, _cameraSettings.maxZoom); - _camTrackPos = ClampPan(kitePosition); + EvaluateCameraComposition( + kitePosition, + kiteScale, + MotionIntensity(), + out Vector3 positionTarget, + out float zoomTarget); + _camTrackPos = positionTarget; _camTrackPosVel = Vector3.zero; _camZoom = zoomTarget; _camZoomVel = 0f; + ResetCameraOvershoot(); ApplyCameraTransform(); - PushPoseSample(kitePosition, zoomTarget, Time.time); + PushPoseSample(positionTarget, zoomTarget, Time.time); } public void ResetAndSleep() @@ -401,7 +443,10 @@ namespace AibisDream _routeTarget = Vector3.zero; _routeMoveActive = false; _routeWaitingForCamera = false; - _escapeBurstCount = 0; + _routeCountsTowardDoubleCatch = false; + _wasSuperWind = false; + _doubleCatchPhase = DoubleCatchPhase.None; + ResetDoubleCatchCadence(); _routeAngle = 0f; _routeAngleTarget = 0f; _routeAngularVelocity = 0f; @@ -414,6 +459,7 @@ namespace AibisDream _pitchAngleVel = 0f; _yawAngle = 0f; _yawAngleVel = 0f; + ResetCameraOvershoot(); _blur = 0f; _blurTarget = 0f; _settleElapsed = -1f; @@ -520,30 +566,80 @@ namespace AibisDream dt); ApplyMotionPose(now, false, dt); - UpdateRouteArrival(intensity, now); + 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; - _escapeBurstCount = 0; + _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; - _escapeBurstCount = 0; + if (_routeCountsTowardDoubleCatch && isSuperWind) + { + _normalEscapesUntilDoubleCatch = Mathf.Max( + 0, + _normalEscapesUntilDoubleCatch - 1); + } + + _routeCountsTowardDoubleCatch = false; // 镜头回到风筝后再多停一拍,避免机械地一追上就立刻出发。 float superBlend = Smooth01(Mathf.InverseLerp( _windSettings.superWindThreshold, @@ -564,11 +660,49 @@ namespace AibisDream if (now >= _routeRetargetAt) { - ChooseNextRouteTarget(intensity, now); - _routeMoveActive = true; + 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); @@ -603,7 +737,7 @@ namespace AibisDream _routeTarget = new Vector3(x, y, 0f); } - private void UpdateRouteArrival(float intensity, float now) + private void UpdateRouteArrival(float intensity) { if (!_routeMoveActive) return; float angleError = Mathf.Abs(_routeAngleTarget - _routeAngle); @@ -616,23 +750,22 @@ namespace AibisDream } _routeMoveActive = false; - bool cameraCaught = IsCameraCaught(); - bool canDoubleEscape = intensity >= _windSettings.superWindThreshold - && !cameraCaught - && _escapeBurstCount < 1 - && UnityEngine.Random.value < 0.48f; - - if (canDoubleEscape) + if (_doubleCatchPhase == DoubleCatchPhase.FirstEscape) { - // 最疯狂时最多连续甩开镜头两次;第二次结束后必定等待镜头回中。 - _escapeBurstCount++; - float gapMin = Mathf.Max(0f, _windSettings.doubleEscapeGapMin); - float gapMax = Mathf.Max(gapMin, _windSettings.doubleEscapeGapMax); - _routeRetargetAt = now + UnityEngine.Random.Range(gapMin, gapMax); + _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; } @@ -640,21 +773,42 @@ namespace AibisDream { if (_renderer == null) return true; Vector3 kitePosition = SampleKiteLocalPose(); - return Vector3.Distance(kitePosition, _camTrackPos) < 0.24f + EvaluateCameraComposition( + kitePosition, + ScaleFactor(_height), + MotionIntensity(), + out Vector3 positionTarget, + out _); + return Vector3.Distance(positionTarget, _camTrackPos + _camOvershootOffset) < 0.24f && _camTrackPosVel.magnitude < 0.38f; } - private void ChooseNextRouteTarget(float intensity, float now) + 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 (alternateSides) + if (!keepSide && alternateSides) { _routeSide = -_routeSide; if (_routeSide == 0) _routeSide = 1; } - else + else if (!keepSide) { // 大风阶段主要顺风靠向同一侧,只有少量同侧高度变化。 _routeSide = _wind < 0 ? -1 : 1; @@ -664,13 +818,23 @@ namespace AibisDream _windSettings.maxTetherAngle * 0.42f, _windSettings.maxTetherAngle, Smooth01(edge01)); - _routeAngleTarget = _routeSide * targetAngle * UnityEngine.Random.Range(0.88f, 1f); + _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)); - _routeVerticalTarget = UnityEngine.Random.Range(-yDistance, yDistance) * 0.55f; + 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; @@ -796,12 +960,86 @@ namespace AibisDream 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 zoomTarget = Mathf.Clamp(1f / Mathf.Max(kiteScale, 0.0001f), 1f, _cameraSettings.maxZoom); - PushPoseSample(kitePosition, zoomTarget, now); + 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); @@ -810,7 +1048,7 @@ namespace AibisDream float followMultiplier = Mathf.Lerp( 1f, Mathf.Clamp(_cameraSettings.stormFollowMultiplier, 0.25f, 1f), - Smooth01(MotionIntensity())); + Smooth01(intensity)); float positionSmooth = _cameraSettings.smoothTimePos * followMultiplier; _camTrackPos = Vector3.SmoothDamp( @@ -829,13 +1067,16 @@ namespace AibisDream dt); _camZoom = Mathf.Clamp(_camZoom, 1f, _cameraSettings.maxZoom); + StepCameraOvershoot(dt); ApplyCameraTransform(); - UpdateFocusTarget(kitePosition, zoomTarget); + UpdateFocusTarget(positionTarget, zoomTarget); } - private void UpdateFocusTarget(Vector3 kitePosition, float zoomTarget) + private void UpdateFocusTarget(Vector3 positionTarget, float zoomTarget) { - float positionError = Vector3.Distance(kitePosition, _camTrackPos) + 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))) @@ -924,7 +1165,8 @@ namespace AibisDream if (_cameraNode == null) return; _cameraNode.localScale = Vector3.one * _camZoom; _cameraNode.localRotation = Quaternion.identity; - _cameraNode.localPosition = _framePosition - _camZoom * _camTrackPos; + _cameraNode.localPosition = _framePosition + - _camZoom * (_camTrackPos + _camOvershootOffset); } private Vector3 SampleKiteLocalPose() diff --git a/Docs/风筝命令使用指南.md b/Docs/风筝命令使用指南.md index d49c7a9b3..295daae22 100644 --- a/Docs/风筝命令使用指南.md +++ b/Docs/风筝命令使用指南.md @@ -40,13 +40,20 @@ Kite Mask(SpriteMask,以底图为形状,风筝层被裁在天空范围内 ### 参数含义 -- `height` 对应 Yarn 变量 `$h`,范围会被 clamp 到 `[0, 13]`。缩放曲线是 `0.84^height`,且不低于 12%(避免缩到看不见)。 +- `height` 对应 Yarn 变量 `$h`,范围会被 clamp 到 `[0, 13]`。缩放曲线是 `0.76^height`,且不低于 12%(避免缩到看不见)。镜头只补回约 55% 的尺寸变化,因此风筝仍会随高度增加而逐渐变小。 - `wind` 对应 Yarn 变量 `$wind`:`1` 强风 / `0` 无风 / `-1` 风弱。 - `turbulence`(乱度)范围 `0~1`,**默认表示平稳(0),不是“保持不变”**: - 省略或传负数 → 平稳,风筝沿 CatmullRom 曲线大而缓地漫游(有风时顶风侧倾巡游,无风时自由漂,风弱时缓缓下坠),绝不抖动。 - - 传正数 → 阵风:静一下 → 猛地被拽走一截(位移+旋转,X/Y 不同步走出弧线)→ 慢慢荡回 → 再来一阵。**每一阵的方向、力度、间隔都是随机的,节奏不重复**;数值越大拉扯越猛、间隔越短,乱度 ≥0.7 时还会概率性甩过头再弹回,读出“失控”。 + - 传正数 → 阵风:静一下 → 猛地被拽走一截(位移+旋转,X/Y 不同步走出弧线)→ 镜头追回 → 再来一阵。数值越大拉扯越猛、间隔越短;进入超级大风后路线会左右换边,并在每 3~5 次普通出镜后穿插一次“双捕捉”。 - **乱度是粘滞的**:由 `kite_wind` 显式设定后会保留,后续 `kite_state` 只切高度和风向,仍沿用当前乱度,直到下一次 `kite_wind` 改变它。`kite_show` 会把乱度重置为 0。 +### 镜头构图与双捕捉 + +- 镜头横向完整追赶风筝,纵向只做部分跟随:平静阶段约 42%,乱风阶段约 78%。连续放线时,风筝会在画面里逐渐升高、变小,不会每轮回到相同的特写景别。 +- 超级大风的普通出镜保持左右和纵向变化;每经过 3~5 次普通出镜,下一次改为“双捕捉”:第一次出镜后等镜头快要框住,再沿同一方向突然加速;第二次追回时镜头轻微跟过头并一次性收住。 +- 双捕捉不会连续触发。乱度降低、隐藏风筝或重新显示风筝时,当前双捕捉与镜头过冲都会取消并重新计数。 +- 虚焦和“镜头已捕捉”都按目标构图判断;风筝故意处于画面上方或保持较小尺寸时,不会因此持续虚焦。 + ### `kite_accent` 支持的 profile | profile | 效果 | 典型使用场景 |