diff --git a/Assets/Render/KiteFocusSprite.mat b/Assets/Render/KiteFocusSprite.mat
new file mode 100644
index 000000000..dda528fd4
--- /dev/null
+++ b/Assets/Render/KiteFocusSprite.mat
@@ -0,0 +1,46 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: KiteFocusSprite
+ m_Shader: {fileID: 4800000, guid: fb0a8d34c65ffb540b0e1f9538285d18, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords: []
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: -1
+ stringTagMap: {}
+ disabledShaderPasses: []
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _BlurAmount: 0
+ - _BlurSize: 0.004
+ - _ChromaticFringe: 0.14
+ - _ColorMask: 15
+ - _DefocusBloom: 0.32
+ - _DefocusVeil: 0.055
+ - _HaloStrength: 0.22
+ - _Stencil: 0
+ - _StencilComp: 8
+ - _StencilOp: 0
+ - _StencilReadMask: 255
+ - _StencilWriteMask: 255
+ m_Colors:
+ - _Color: {r: 1, g: 1, b: 1, a: 1}
+ m_BuildTextureStacks: []
diff --git a/Assets/Render/KiteFocusSprite.mat.meta b/Assets/Render/KiteFocusSprite.mat.meta
new file mode 100644
index 000000000..3cc5a8272
--- /dev/null
+++ b/Assets/Render/KiteFocusSprite.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 78265ded39f017d45a2afc117effab89
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Render/KiteFocusSprite.shader b/Assets/Render/KiteFocusSprite.shader
new file mode 100644
index 000000000..3f623f71b
--- /dev/null
+++ b/Assets/Render/KiteFocusSprite.shader
@@ -0,0 +1,195 @@
+Shader "AIBIS/KiteFocusSprite"
+{
+ Properties
+ {
+ [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
+ _Color ("Tint", Color) = (1, 1, 1, 1)
+
+ [Header(Focus Blur)]
+ _BlurAmount ("Blur Amount", Range(0, 1)) = 0
+ _BlurSize ("Blur Size", Range(0, 0.05)) = 0.012
+ _DefocusBloom ("Defocus Highlight Bloom", Range(0, 1)) = 0.32
+ _HaloStrength ("Large Circle Halo", Range(0, 0.6)) = 0.22
+ _ChromaticFringe ("Lens Color Fringe", Range(0, 0.6)) = 0.14
+ _DefocusVeil ("Defocus White Veil", Range(0, 0.3)) = 0.055
+
+ // Required for SpriteMask (same contract as Sprites-Default / UI.Mask)
+ [HideInInspector][PerRendererData] _StencilComp ("Stencil Comparison", Float) = 8
+ [HideInInspector][PerRendererData] _Stencil ("Stencil ID", Float) = 0
+ [HideInInspector][PerRendererData] _StencilOp ("Stencil Operation", Float) = 0
+ [HideInInspector][PerRendererData] _StencilWriteMask ("Stencil Write Mask", Float) = 255
+ [HideInInspector][PerRendererData] _StencilReadMask ("Stencil Read Mask", Float) = 255
+ [HideInInspector] _ColorMask ("Color Mask", Float) = 15
+ }
+
+ SubShader
+ {
+ Tags
+ {
+ "Queue" = "Transparent"
+ "IgnoreProjector" = "True"
+ "RenderType" = "Transparent"
+ "PreviewType" = "Plane"
+ "CanUseSpriteAtlas" = "True"
+ }
+
+ Cull Off
+ Lighting Off
+ ZWrite Off
+ Blend One OneMinusSrcAlpha
+ ColorMask [_ColorMask]
+
+ Stencil
+ {
+ Ref [_Stencil]
+ Comp [_StencilComp]
+ Pass [_StencilOp]
+ ReadMask [_StencilReadMask]
+ WriteMask [_StencilWriteMask]
+ }
+
+ Pass
+ {
+ CGPROGRAM
+ #pragma vertex vert
+ #pragma fragment frag
+ #pragma target 3.0
+ #include "UnityCG.cginc"
+
+ struct appdata
+ {
+ float4 vertex : POSITION;
+ float4 color : COLOR;
+ float2 uv : TEXCOORD0;
+ };
+
+ struct v2f
+ {
+ float4 vertex : SV_POSITION;
+ fixed4 color : COLOR;
+ float2 uv : TEXCOORD0;
+ };
+
+ sampler2D _MainTex;
+ float4 _MainTex_TexelSize;
+ fixed4 _Color;
+ float _BlurAmount;
+ float _BlurSize;
+ float _DefocusBloom;
+ float _HaloStrength;
+ float _ChromaticFringe;
+ float _DefocusVeil;
+
+ v2f vert(appdata v)
+ {
+ v2f o;
+ o.vertex = UnityObjectToClipPos(v.vertex);
+ o.color = v.color * _Color;
+ o.uv = v.uv;
+ return o;
+ }
+
+ half4 sampleTex(float2 uv)
+ {
+ // 边界外按透明,避免 saturate 夹边造成硬切
+ if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0)
+ return half4(0, 0, 0, 0);
+ return tex2D(_MainTex, uv);
+ }
+
+ half4 samplePremultiplied(float2 uv)
+ {
+ half4 color = sampleTex(uv);
+ color.rgb *= color.a;
+ return color;
+ }
+
+ // 圆形光圈核:比高斯更接近真实镜头的弥散圆,且不会出现十字形拖影。
+ half4 ApertureDisc(float2 uv, float radius)
+ {
+ half4 result = samplePremultiplied(uv) * 0.12;
+ const half w = 0.044;
+
+ // 内圈
+ result += samplePremultiplied(uv + float2( 0.38, 0.00) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.38, 0.00) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.00, 0.38) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.00, -0.38) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.27, 0.27) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.27, 0.27) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.27, -0.27) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.27, -0.27) * radius) * w;
+
+ // 外圈使用不规则角度,避免规则采样形成振铃或星芒。
+ result += samplePremultiplied(uv + float2( 0.98, 0.05) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.82, 0.55) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.43, 0.90) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.16, 0.98) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.69, 0.72) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.99, 0.10) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.80, -0.58) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.36, -0.92) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.22, -0.97) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.72, -0.68) * radius) * w;
+ result += samplePremultiplied(uv + float2( 0.94, -0.31) * radius) * w;
+ result += samplePremultiplied(uv + float2(-0.48, 0.86) * radius) * w;
+ return result;
+ }
+
+ half4 WideHalo(float2 uv, float radius)
+ {
+ half4 result = 0;
+ result += samplePremultiplied(uv + float2( 1.00, 0.00) * radius);
+ result += samplePremultiplied(uv + float2( 0.71, 0.71) * radius);
+ result += samplePremultiplied(uv + float2( 0.00, 1.00) * radius);
+ result += samplePremultiplied(uv + float2(-0.71, 0.71) * radius);
+ result += samplePremultiplied(uv + float2(-1.00, 0.00) * radius);
+ result += samplePremultiplied(uv + float2(-0.71, -0.71) * radius);
+ result += samplePremultiplied(uv + float2( 0.00, -1.00) * radius);
+ result += samplePremultiplied(uv + float2( 0.71, -0.71) * radius);
+ return result * 0.125;
+ }
+
+ fixed4 frag(v2f i) : SV_Target
+ {
+ float blurAmt = saturate(_BlurAmount);
+ half4 sharp = sampleTex(i.uv);
+
+ if (blurAmt < 0.001)
+ {
+ half4 col = sharp * i.color;
+ col.rgb *= col.a;
+ return col;
+ }
+
+ float2 texel = max(abs(_MainTex_TexelSize.xy), float2(1e-5, 1e-5));
+ float radius = (length(texel) * 0.5 + _BlurSize) * (0.28 + blurAmt * 1.72);
+ half4 disc = ApertureDisc(i.uv, radius);
+ half4 halo = WideHalo(i.uv, radius * 1.85);
+ half4 blurred = lerp(disc, halo, saturate(_HaloStrength * blurAmt));
+
+ // 很轻的 RGB 焦平面差异,避免单纯软件模糊的塑料感。
+ float2 fringe = float2(0.73, 0.41) * radius * _ChromaticFringe * blurAmt;
+ half4 fringeR = samplePremultiplied(i.uv + fringe);
+ half4 fringeB = samplePremultiplied(i.uv - fringe);
+ blurred.r = lerp(blurred.r, fringeR.r, _ChromaticFringe * blurAmt);
+ blurred.b = lerp(blurred.b, fringeB.b, _ChromaticFringe * blurAmt);
+
+ // 参考片中的失焦会让亮部扩散并稍微泛白,但保留在物体 alpha 内。
+ half luminance = dot(blurred.rgb, half3(0.2126, 0.7152, 0.0722));
+ half highlight = saturate(luminance - blurred.a * 0.42);
+ blurred.rgb += highlight * _DefocusBloom * blurAmt;
+ blurred.rgb += blurred.a * _DefocusVeil * blurAmt;
+
+ half4 sharpPremul = sharp;
+ sharpPremul.rgb *= sharpPremul.a;
+ float mixT = smoothstep(0.0, 1.0, blurAmt);
+ half4 col = lerp(sharpPremul, blurred, mixT);
+ col.rgb *= i.color.rgb * i.color.a;
+ col.a *= i.color.a;
+ return col;
+ }
+ ENDCG
+ }
+ }
+}
diff --git a/Assets/Render/KiteFocusSprite.shader.meta b/Assets/Render/KiteFocusSprite.shader.meta
new file mode 100644
index 000000000..9143e29dc
--- /dev/null
+++ b/Assets/Render/KiteFocusSprite.shader.meta
@@ -0,0 +1,9 @@
+fileFormatVersion: 2
+guid: fb0a8d34c65ffb540b0e1f9538285d18
+ShaderImporter:
+ externalObjects: {}
+ defaultTextures: []
+ nonModifiableTextures: []
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scenes/梦境.unity b/Assets/Scenes/梦境.unity
index 6a0be98ae..aaa0635f8 100644
--- a/Assets/Scenes/梦境.unity
+++ b/Assets/Scenes/梦境.unity
@@ -1143,6 +1143,71 @@ MonoBehaviour:
_spriteRenderer: {fileID: 218986884}
_backgroundSpriteRenderer: {fileID: 969796818}
_largeSpriteRenderer: {fileID: 762853994}
+ _kiteFocusMaterial: {fileID: 2100000, guid: 78265ded39f017d45a2afc117effab89, type: 2}
+ _kiteWindSettings:
+ calmFloatAmp: 0.53
+ calmFloatFreq: 0.065
+ steadyWindOffset: 0.45
+ weakWindDrop: 0.32
+ edgeWindThreshold: 0.48
+ edgeTravel: 3.3
+ edgeTravelY: 1.35
+ calmMotionSmoothTime: 1.2
+ stormMotionSmoothTime: 0.68
+ maxTravelSpeed: 3.2
+ maxTetherAngle: 62
+ tetherAngularStiffness: 2.4
+ tetherAngularDamping: 3.35
+ maxAngularAcceleration: 68
+ maxAngularSpeed: 48
+ tetherArcDrop: 1.15
+ verticalGustSmoothTime: 0.85
+ superWindThreshold: 0.86
+ bigWindHoldMin: 2.6
+ bigWindHoldMax: 4.5
+ superWindHoldMin: 1.7
+ superWindHoldMax: 3
+ doubleEscapeGapMin: 0.28
+ doubleEscapeGapMax: 0.6
+ windPressureAmp: 0.36
+ windPressureFrequency: 0.11
+ windPressureVerticalRatio: 0.48
+ windPressurePoseGain: 3.2
+ heightDurationScale: 1.3
+ heightArcPerLevel: 0.055
+ heightArcMax: 0.42
+ heightArcSmoothTime: 0.62
+ leanAmp: 3.2
+ bankFromVel: 1.5
+ pitchFromHeightVel: 1.6
+ yawFromLateralVel: 1.15
+ flutterAmp: 0.7
+ poseSmoothTime: 0.34
+ maxPoseAngle: 7
+ maxPerspectiveTilt: 5.5
+ windSmoothTime: 0.45
+ altitudeWindGain: 0.18
+ tailLagPhase: 0.9
+ tailGain: 1.2
+ _kiteCameraSettings:
+ reactionLatency: 0.34
+ smoothTimePos: 0.92
+ stormFollowMultiplier: 0.63
+ smoothTimeZoom: 1.18
+ maxZoom: 3.2
+ maxPan: 4.5
+ _kiteFocusSettings:
+ maxBlur: 0.68
+ blurSize: 0.011
+ posErrorRef: 0.7
+ zoomErrorRef: 0.2
+ blurDeadZone: 0.3
+ blurInSpeed: 3.5
+ blurOutSpeed: 0.95
+ settleEnterThreshold: 0.48
+ settleDuration: 0.46
+ settleBump: 0.08
+ settlePhaseRatios: {x: 0.5, y: 0.2, z: 0.3}
--- !u!1 &969796816
GameObject:
m_ObjectHideFlags: 0
@@ -2288,7 +2353,6 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 0e1a1832f60f72e449b07f1298eefa53, type: 3}
m_Name:
m_EditorClassIdentifier:
- bubbleSlotGroup: {fileID: 676277672}
--- !u!1 &1847534532
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/Scripts/SceneManagement/KiteRigDriver.cs b/Assets/Scripts/SceneManagement/KiteRigDriver.cs
new file mode 100644
index 000000000..7757a7d5d
--- /dev/null
+++ b/Assets/Scripts/SceneManagement/KiteRigDriver.cs
@@ -0,0 +1,1072 @@
+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("超级大风连续第二次甩开镜头前的最短蓄力时间")]
+ public float doubleEscapeGapMin = 0.28f;
+ [Tooltip("超级大风连续第二次甩开镜头前的最长蓄力时间")]
+ public float doubleEscapeGapMax = 0.6f;
+
+ [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;
+ }
+
+ [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 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 int _escapeBurstCount;
+ 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 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;
+ _escapeBurstCount = 0;
+ _routeAngle = 0f;
+ _routeAngleTarget = 0f;
+ _routeAngularVelocity = 0f;
+ _routeVertical = 0f;
+ _routeVerticalTarget = 0f;
+ _routeVerticalVel = 0f;
+ _poseAngle = 0f;
+ _poseAngleVel = 0f;
+ _pitchAngle = 0f;
+ _pitchAngleVel = 0f;
+ _yawAngle = 0f;
+ _yawAngleVel = 0f;
+ _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);
+ float zoomTarget = Mathf.Clamp(1f / Mathf.Max(kiteScale, 0.0001f), 1f, _cameraSettings.maxZoom);
+ _camTrackPos = ClampPan(kitePosition);
+ _camTrackPosVel = Vector3.zero;
+ _camZoom = zoomTarget;
+ _camZoomVel = 0f;
+ ApplyCameraTransform();
+ PushPoseSample(kitePosition, 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;
+ _escapeBurstCount = 0;
+ _routeAngle = 0f;
+ _routeAngleTarget = 0f;
+ _routeAngularVelocity = 0f;
+ _routeVertical = 0f;
+ _routeVerticalTarget = 0f;
+ _routeVerticalVel = 0f;
+ _poseAngle = 0f;
+ _poseAngleVel = 0f;
+ _pitchAngle = 0f;
+ _pitchAngleVel = 0f;
+ _yawAngle = 0f;
+ _yawAngleVel = 0f;
+ _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, now);
+ }
+
+ private void UpdateRoutePlan(float intensity, float now)
+ {
+ if (intensity < _windSettings.edgeWindThreshold)
+ {
+ _routeAngleTarget = 0f;
+ _routeVerticalTarget = 0f;
+ _routeMoveActive = false;
+ _routeWaitingForCamera = false;
+ _escapeBurstCount = 0;
+ _routeRetargetAt = now + UnityEngine.Random.Range(0.8f, 1.5f);
+ return;
+ }
+
+ if (_routeMoveActive) return;
+
+ if (_routeWaitingForCamera)
+ {
+ if (!IsCameraCaught()) return;
+
+ _routeWaitingForCamera = false;
+ _escapeBurstCount = 0;
+ // 镜头回到风筝后再多停一拍,避免机械地一追上就立刻出发。
+ 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)
+ {
+ ChooseNextRouteTarget(intensity, now);
+ _routeMoveActive = true;
+ }
+ }
+
+ 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, float now)
+ {
+ 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;
+ bool cameraCaught = IsCameraCaught();
+ bool canDoubleEscape = intensity >= _windSettings.superWindThreshold
+ && !cameraCaught
+ && _escapeBurstCount < 1
+ && UnityEngine.Random.value < 0.48f;
+
+ if (canDoubleEscape)
+ {
+ // 最疯狂时最多连续甩开镜头两次;第二次结束后必定等待镜头回中。
+ _escapeBurstCount++;
+ float gapMin = Mathf.Max(0f, _windSettings.doubleEscapeGapMin);
+ float gapMax = Mathf.Max(gapMin, _windSettings.doubleEscapeGapMax);
+ _routeRetargetAt = now + UnityEngine.Random.Range(gapMin, gapMax);
+ return;
+ }
+
+ _routeWaitingForCamera = true;
+ _routeRetargetAt = float.PositiveInfinity;
+ }
+
+ private bool IsCameraCaught()
+ {
+ if (_renderer == null) return true;
+ Vector3 kitePosition = SampleKiteLocalPose();
+ return Vector3.Distance(kitePosition, _camTrackPos) < 0.24f
+ && _camTrackPosVel.magnitude < 0.38f;
+ }
+
+ private void ChooseNextRouteTarget(float intensity, float now)
+ {
+ float edge01 = Mathf.InverseLerp(_windSettings.edgeWindThreshold, 1f, intensity);
+ bool alternateSides = intensity >= _windSettings.superWindThreshold;
+
+ if (alternateSides)
+ {
+ _routeSide = -_routeSide;
+ if (_routeSide == 0) _routeSide = 1;
+ }
+ else
+ {
+ // 大风阶段主要顺风靠向同一侧,只有少量同侧高度变化。
+ _routeSide = _wind < 0 ? -1 : 1;
+ }
+
+ float targetAngle = Mathf.Lerp(
+ _windSettings.maxTetherAngle * 0.42f,
+ _windSettings.maxTetherAngle,
+ Smooth01(edge01));
+ _routeAngleTarget = _routeSide * targetAngle * UnityEngine.Random.Range(0.88f, 1f);
+
+ float yDistance = Mathf.Lerp(
+ _windSettings.edgeTravelY * 0.3f,
+ _windSettings.edgeTravelY,
+ Smooth01(edge01));
+ _routeVerticalTarget = UnityEngine.Random.Range(-yDistance, yDistance) * 0.55f;
+
+ // 这里不安排下一次移动;到点后必须经过镜头追赶和随机停留阶段。
+ _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 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);
+
+ SampleDelayedPose(now, out Vector3 delayedPosition, out float delayedZoom);
+ delayedPosition = ClampPan(delayedPosition);
+
+ // 平静时镜头从容地跟;风越大,摄影师越积极追赶快速换边的风筝。
+ float followMultiplier = Mathf.Lerp(
+ 1f,
+ Mathf.Clamp(_cameraSettings.stormFollowMultiplier, 0.25f, 1f),
+ Smooth01(MotionIntensity()));
+ 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);
+
+ ApplyCameraTransform();
+ UpdateFocusTarget(kitePosition, zoomTarget);
+ }
+
+ private void UpdateFocusTarget(Vector3 kitePosition, float zoomTarget)
+ {
+ float positionError = Vector3.Distance(kitePosition, _camTrackPos)
+ / 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;
+ }
+
+ 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
+ }
+}
diff --git a/Assets/Scripts/SceneManagement/KiteRigDriver.cs.meta b/Assets/Scripts/SceneManagement/KiteRigDriver.cs.meta
new file mode 100644
index 000000000..492391fc9
--- /dev/null
+++ b/Assets/Scripts/SceneManagement/KiteRigDriver.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: 1290ba4dc7b8bfa40b61dd3bb1372cf0
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Scripts/SceneManagement/SpriteShowcase.cs b/Assets/Scripts/SceneManagement/SpriteShowcase.cs
index d387ca1c5..3979e0ab1 100644
--- a/Assets/Scripts/SceneManagement/SpriteShowcase.cs
+++ b/Assets/Scripts/SceneManagement/SpriteShowcase.cs
@@ -569,43 +569,47 @@ namespace AibisDream
#region 风筝 Rig
- // 风筝段落弃用切图(D2S风筝1/2/3),改为单张风筝图 + 三层 Transform 连续驱动:
- // Kite Rig(高度补间:缩放+位移)→ Sway(常驻风况循环)→ Accent(一次性顿挫 + Renderer)
- // 三层各持有独立 Tween,互不覆盖。白色底图挂在独立的静态 renderer 上,不随高度缩放。
+ // Kite Camera(虚拟镜头完全补偿)→ Rig(高度+漂移)→ Sway(鱼式流体摆动)→ Accent(DOTween 顿挫)
+ // 底图 / Mask 保持静态,不进镜头节点。
private const string KiteSpritePicName = "D2S风筝1_风筝层";
private const string KiteBasePicName = "D2S风筝1_底图";
private const float KiteMaxHeight = 13f;
- /// 每升高 1 点高度,缩放乘以该系数。
- private const float KiteScalePerHeight = 0.84f;
- /// 缩放下限:最远处的风筝也不小于该比例。
- private const float KiteMinScaleFactor = 0.12f;
- /// 高度最大时风筝相对基准位置的漂移量(向上飘 + 少量横移)。
- private static readonly Vector3 KiteTopDrift = new Vector3(0.5f, 1.2f, 0f);
+ [Header("风筝 / 对焦材质")]
+ [SerializeField] private Material _kiteFocusMaterial;
+ [SerializeField] private KiteWindSettings _kiteWindSettings = new KiteWindSettings();
+ [SerializeField] private KiteCameraSettings _kiteCameraSettings = new KiteCameraSettings();
+ [SerializeField] private KiteFocusSettings _kiteFocusSettings = new KiteFocusSettings();
+
+ private Transform _kiteCameraNode;
private Transform _kiteRootPivot;
private Transform _kiteSwayPivot;
private SpriteRenderer _kiteRenderer;
private SpriteRenderer _kiteBaseRenderer;
private SpriteMask _kiteMask;
- private DG.Tweening.Sequence _kiteHeightSequence;
- private DG.Tweening.Sequence _kiteSwaySequence;
+ private KiteRigDriver _kiteDriver;
private DG.Tweening.Sequence _kiteAccentSequence;
- private Tween _kiteFadeTween;
- private float _kiteHeight;
- /// 当前乱度 0~1,由 kite_wind 显式设定;kite_state 沿用上次的值(粘滞)。
- private float _kiteTurbulence;
private bool _kiteSpriteLoaded;
private bool _kiteBaseSpriteLoaded;
- private bool IsKiteShowing => _kiteRootPivot != null && _kiteRootPivot.gameObject.activeSelf;
+ private bool IsKiteShowing =>
+ _kiteCameraNode != null && _kiteCameraNode.gameObject.activeSelf;
private void EnsureKiteRig()
{
if (_kiteRootPivot != null || _spriteRenderer == null) return;
+ var parent = _spriteRenderer.transform.parent;
+
+ var cameraObject = new GameObject("Kite Camera");
+ cameraObject.transform.SetParent(parent);
+ cameraObject.transform.localPosition = _smallBaseLocalPosition;
+ cameraObject.transform.localRotation = Quaternion.identity;
+ cameraObject.transform.localScale = Vector3.one;
+
var root = new GameObject("Kite Rig");
- root.transform.SetParent(_spriteRenderer.transform.parent);
- root.transform.localPosition = _smallBaseLocalPosition;
+ root.transform.SetParent(cameraObject.transform, false);
+ root.transform.localPosition = Vector3.zero;
root.transform.localRotation = _smallBaseLocalRotation;
root.transform.localScale = _smallBaseLocalScale;
@@ -615,20 +619,21 @@ namespace AibisDream
var accent = new GameObject("Kite Accent");
accent.transform.SetParent(sway.transform, false);
- // 白色底图:独立静态节点,不随高度层缩放
+ // 白色底图:独立静态节点,不随镜头/高度缩放
var baseObject = new GameObject("Kite Base");
- baseObject.transform.SetParent(_spriteRenderer.transform.parent);
+ baseObject.transform.SetParent(parent);
baseObject.transform.localPosition = _smallBaseLocalPosition;
baseObject.transform.localRotation = _smallBaseLocalRotation;
baseObject.transform.localScale = _smallBaseLocalScale;
- // 遮罩:把风筝层限制在底图(天空)范围内,不会飘出画框
+ // 遮罩:把风筝层限制在底图(天空)范围内
var maskObject = new GameObject("Kite Mask");
- maskObject.transform.SetParent(_spriteRenderer.transform.parent);
+ maskObject.transform.SetParent(parent);
maskObject.transform.localPosition = _smallBaseLocalPosition;
maskObject.transform.localRotation = _smallBaseLocalRotation;
maskObject.transform.localScale = _smallBaseLocalScale;
+ _kiteCameraNode = cameraObject.transform;
_kiteRootPivot = root.transform;
_kiteSwayPivot = sway.transform;
_kiteRenderer = accent.AddComponent();
@@ -640,53 +645,52 @@ namespace AibisDream
_kiteMask.isCustomRangeActive = true;
_kiteMask.enabled = false;
- root.SetActive(false);
+ EnsureKiteFocusMaterial();
+ _kiteDriver = cameraObject.AddComponent();
+ _kiteDriver.Configure(
+ _kiteCameraNode,
+ _kiteRootPivot,
+ _kiteSwayPivot,
+ _kiteRenderer,
+ _smallBaseLocalPosition,
+ _smallBaseLocalRotation,
+ _smallBaseLocalScale,
+ _kiteWindSettings,
+ _kiteCameraSettings,
+ _kiteFocusSettings,
+ _kiteFocusMaterial);
+ _kiteDriver.enabled = false;
+
+ cameraObject.SetActive(false);
baseObject.SetActive(false);
_kiteRenderer.SetAlpha(0);
_kiteBaseRenderer.SetAlpha(0);
}
- private static float KiteScaleFactor(float height) =>
- Mathf.Max(Mathf.Pow(KiteScalePerHeight, height), KiteMinScaleFactor);
+ private void EnsureKiteFocusMaterial()
+ {
+ if (_kiteFocusMaterial != null) return;
- private Vector3 KitePosition(float height) =>
- _smallBaseLocalPosition + KiteTopDrift * (height / KiteMaxHeight);
+ var shader = Shader.Find("AIBIS/KiteFocusSprite");
+ if (shader == null)
+ {
+ Debug.LogWarning("[SpriteShowcase] 找不到 AIBIS/KiteFocusSprite,风筝虚焦不可用。", this);
+ return;
+ }
- private Vector3 KiteScale(float height) => _smallBaseLocalScale * KiteScaleFactor(height);
-
- /// 极高处降低不透明度,配合“几乎看不见”的文本。
- private static float KiteAlpha(float height) =>
- height <= 8f ? 1f : Mathf.Lerp(1f, 0.75f, (height - 8f) / (KiteMaxHeight - 8f));
+ _kiteFocusMaterial = new Material(shader) { name = "KiteFocusSprite (Runtime)" };
+ }
private void KillKiteTweens()
{
- _kiteHeightSequence?.Kill();
- _kiteHeightSequence = null;
- _kiteSwaySequence?.Kill();
- _kiteSwaySequence = null;
_kiteAccentSequence?.Kill();
_kiteAccentSequence = null;
- _kiteFadeTween?.Kill();
- _kiteFadeTween = null;
- }
-
- private void ApplyKiteStateImmediate(float height)
- {
- _kiteRootPivot.localPosition = KitePosition(height);
- _kiteRootPivot.localRotation = _smallBaseLocalRotation;
- _kiteRootPivot.localScale = KiteScale(height);
- _kiteSwayPivot.localPosition = Vector3.zero;
- _kiteSwayPivot.localRotation = Quaternion.identity;
- _kiteRenderer.transform.localPosition = Vector3.zero;
- _kiteRenderer.transform.localRotation = Quaternion.identity;
+ _kiteDriver?.ResetAndSleep();
}
///
/// 显示风筝 Rig。已显示时等价于 (幂等)。
///
- /// 风筝高度(对应 Yarn 变量 $h)
- /// 风向(对应 Yarn 变量 $wind:1 强 / 0 无 / -1 弱)
- /// 保留参数以兼容既有调用;出现不再淡入,仅用于互斥隐藏其它图
public IEnumerator ShowKite(float height, int wind, float duration = 0.35f)
{
if (IsLargeShowing)
@@ -700,7 +704,7 @@ namespace AibisDream
}
EnsureKiteRig();
- if (_kiteRenderer == null) yield break;
+ if (_kiteRenderer == null || _kiteDriver == null) yield break;
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
@@ -711,15 +715,19 @@ namespace AibisDream
}
KillKiteTweens();
- ApplyKiteStateImmediate(height);
- _kiteHeight = height;
- _kiteRootPivot.gameObject.SetActive(true);
+ _kiteCameraNode.gameObject.SetActive(true);
_kiteBaseRenderer.gameObject.SetActive(true);
if (!_kiteSpriteLoaded)
{
yield return LoadDreamSpriteInto(_kiteRenderer, KiteSpritePicName);
_kiteSpriteLoaded = _kiteRenderer.sprite != null;
+ // 贴图加载后重新赋 focus 材质(Load 可能换过 material)
+ EnsureKiteFocusMaterial();
+ if (_kiteFocusMaterial != null)
+ {
+ _kiteRenderer.material = _kiteFocusMaterial;
+ }
}
if (!_kiteBaseSpriteLoaded)
@@ -738,189 +746,36 @@ namespace AibisDream
_kiteMask.enabled = true;
}
- _kiteTurbulence = 0f;
- PlayKiteSway(wind);
- // 不做淡入:背景是硬切的,风筝单独 fade 反而突兀,直接以目标透明度出现
+ _kiteDriver.Wake(height, wind);
_kiteBaseRenderer.SetAlpha(1f);
- _kiteRenderer.SetAlpha(KiteAlpha(height));
+ _kiteRenderer.SetAlpha(KiteRigDriver.Alpha(height));
}
///
- /// 高度补间(每次选择的主反馈)+ 切换风况循环。
+ /// 高度 SmoothDamp + 切换风向。乱度粘滞由 driver 保留。
///
public void SetKiteState(float height, int wind, float duration = 0.9f)
{
- if (!IsKiteShowing) return;
+ if (!IsKiteShowing || _kiteDriver == null) return;
height = Mathf.Clamp(height, 0f, KiteMaxHeight);
- _kiteHeight = height;
-
- _kiteHeightSequence?.Kill();
- var seq = DOTween.Sequence();
- seq.Append(_kiteRootPivot.DOLocalMove(KitePosition(height), duration).SetEase(Ease.InOutSine));
- seq.Join(_kiteRootPivot.DOScale(KiteScale(height), duration).SetEase(Ease.InOutSine));
- _kiteHeightSequence = seq;
-
- _kiteFadeTween?.Kill();
- _kiteFadeTween = _kiteRenderer.DOFade(KiteAlpha(height), duration);
-
- PlayKiteSway(wind);
+ _kiteDriver.SetHeight(height, duration);
+ _kiteDriver.SetWind(wind);
}
- /// 仅切换风况循环(选择前 telegraph 风向)。
- /// 风向:1 强 / 0 无 / -1 弱
- /// 乱度 0~1;负数或省略表示平稳。设定后粘滞,kite_state 沿用。
+ /// 仅切换风况(选择前 telegraph 风向)。
+ /// 乱度 0~1;负数或省略表示平稳(清零)。设定后粘滞,kite_state 沿用。
public void SetKiteWind(int wind, float turbulence = -1f)
{
- if (!IsKiteShowing) return;
- _kiteTurbulence = turbulence >= 0f ? Mathf.Clamp01(turbulence) : 0f;
- PlayKiteSway(wind);
- }
-
- /// 屏幕可读性补偿:高度层缩小时适度放大局部位移(开方衰减),避免远处几乎不动。
- private float KiteSwayComp() =>
- Mathf.Clamp(1f / Mathf.Sqrt(KiteScaleFactor(_kiteHeight)), 1f, 3f);
-
- ///
- /// 常驻风况循环。位移在高度层的子空间里会随缩放变小,用 comp 做部分补偿保证远处仍有可读的移动。
- /// 平稳(乱度 0):沿 CatmullRom 曲线大而缓地漫游,绝不抖。
- /// 乱流(乱度 >0):转交 ,每一阵的方向、力度、间隔都随机。
- ///
- private void PlayKiteSway(int wind)
- {
- _kiteSwaySequence?.Kill();
- _kiteSwaySequence = null;
-
- if (wind > 0 && _kiteTurbulence > 0.01f)
- {
- PlayKiteGust();
- return;
- }
-
- var tr = _kiteSwayPivot;
- var comp = KiteSwayComp();
- // 幅度随高度补偿放大后,周期也同步拉长:漂移更明显,但速度不变快,保持轻松
- var drift = 1f + (comp - 1f) * 0.4f;
- var seq = DOTween.Sequence();
-
- if (wind > 0)
- {
- // 稳定的风:顶着风侧倾,沿弧线缓慢巡游(位移为主,平稳无抖)
- var path = new[]
- {
- new Vector3(0.26f, 0.15f, 0f) * comp,
- new Vector3(0.46f, 0.05f, 0f) * comp,
- new Vector3(0.33f, 0.24f, 0f) * comp,
- new Vector3(0.11f, 0.11f, 0f) * comp,
- };
- var dur = 9.2f * drift;
- seq.Append(tr.DOLocalPath(path, dur, PathType.CatmullRom).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -7.5f), dur / 4f).SetEase(Ease.InOutSine)
- .SetLoops(4, LoopType.Yoyo));
- }
- else if (wind == 0)
- {
- // 平稳无风:自由自在地沿曲线漫游,幅度加大、节奏放缓
- var path = new[]
- {
- new Vector3(0.3f, 0.16f, 0f) * comp,
- new Vector3(0.08f, 0.36f, 0f) * comp,
- new Vector3(-0.28f, 0.24f, 0f) * comp,
- new Vector3(-0.38f, 0.03f, 0f) * comp,
- };
- var dur = 12f * drift;
- seq.Append(tr.DOLocalPath(path, dur, PathType.CatmullRom).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4f), dur / 4f).SetEase(Ease.InOutSine)
- .SetLoops(4, LoopType.Yoyo));
- }
- else
- {
- // 风弱:沿弧线缓缓向下飘
- var path = new[]
- {
- new Vector3(0.12f, -0.14f, 0f) * comp,
- new Vector3(0.03f, -0.28f, 0f) * comp,
- new Vector3(-0.17f, -0.21f, 0f) * comp,
- };
- var dur = 7.8f * drift;
- seq.Append(tr.DOLocalPath(path, dur, PathType.CatmullRom).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 3f), dur / 3f).SetEase(Ease.InOutSine)
- .SetLoops(3, LoopType.Yoyo));
- }
-
- seq.SetLoops(-1, LoopType.Yoyo);
- _kiteSwaySequence = seq;
+ if (!IsKiteShowing || _kiteDriver == null) return;
+ // 与旧语义对齐:省略/负数 → 平稳(乱度归零);显式传入 ≥0 则设定并粘滞
+ float t = turbulence >= 0f ? Mathf.Clamp01(turbulence) : 0f;
+ _kiteDriver.SetWind(wind, t);
}
///
- /// 单次阵风,播完通过 OnComplete 用一组新的随机参数接下一阵——节奏永不重复。
- /// 猛拽段 X 快、Y 慢半拍(不同时长+缓动),走出弧线而不是直线。
- /// 乱度越高越狂:间隔与荡回时间大幅压缩、拽得更快更斜,
- /// 甩过头弹回之外,还有概率来不及回稳就被反向再抽一鞭。
- ///
- private void PlayKiteGust()
- {
- if (!IsKiteShowing) return;
-
- _kiteSwaySequence?.Kill();
-
- var tr = _kiteSwayPivot;
- var storm = _kiteTurbulence;
- var amp = KiteSwayComp() * (1f + storm * 1.15f);
- // 高乱度整体提速:猛拽更快、几乎没有喘息
- var frantic = Mathf.Lerp(1f, 0.7f, storm);
-
- // 阵风方向:多数顺着主风向,偶尔反向兜一下;上下随机
- var side = UnityEngine.Random.value < 0.72f ? 1f : -1f;
- var jerk = new Vector3(
- UnityEngine.Random.Range(0.34f, 0.58f) * side,
- UnityEngine.Random.Range(-0.1f, 0.3f),
- 0f) * amp;
- var jerkDur = UnityEngine.Random.Range(0.14f, 0.26f) * frantic;
- var rot = (14f + storm * 18f) * UnityEngine.Random.Range(0.7f, 1.15f) * -side;
- var settle = jerk * UnityEngine.Random.Range(0.15f, 0.4f);
- var settleDur = UnityEngine.Random.Range(0.7f, 1.2f) * Mathf.Lerp(1f, 0.45f, storm);
- var rest = Mathf.Lerp(
- UnityEngine.Random.Range(0.35f, 0.75f),
- UnityEngine.Random.Range(0.02f, 0.1f),
- storm);
-
- var seq = DOTween.Sequence();
- seq.AppendInterval(rest);
- // 猛地被拽走:X 快 Y 慢,轨迹成弧
- seq.Append(tr.DOLocalMoveX(jerk.x, jerkDur).SetEase(Ease.OutCubic));
- seq.Join(tr.DOLocalMoveY(jerk.y, jerkDur * UnityEngine.Random.Range(1.4f, 1.9f)).SetEase(Ease.OutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, rot), jerkDur * 1.2f).SetEase(Ease.OutCubic));
- // 失控:甩过头再弹回来一下
- if (storm > 0.7f && UnityEngine.Random.value < 0.6f)
- {
- seq.Append(tr.DOLocalRotate(new Vector3(0f, 0f, -rot * 0.65f), 0.22f).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalMoveX(jerk.x * 0.7f, 0.22f).SetEase(Ease.InOutSine));
- }
- // 更失控:来不及回稳,就被反方向再抽一鞭
- if (storm > 0.6f && UnityEngine.Random.value < 0.5f)
- {
- var whipDur = UnityEngine.Random.Range(0.12f, 0.2f);
- seq.Append(tr.DOLocalMoveX(-jerk.x * UnityEngine.Random.Range(0.5f, 0.9f), whipDur)
- .SetEase(Ease.OutCubic));
- seq.Join(tr.DOLocalMoveY(UnityEngine.Random.Range(-0.16f, 0.2f) * amp, whipDur * 1.5f)
- .SetEase(Ease.OutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -rot * UnityEngine.Random.Range(0.7f, 1.1f)), whipDur * 1.3f)
- .SetEase(Ease.OutCubic));
- }
- // 荡回来(X/Y 不同步 → 回程也是弧线;高乱度下这一步也短促)
- seq.Append(tr.DOLocalMoveX(settle.x, settleDur).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalMoveY(settle.y, settleDur * UnityEngine.Random.Range(0.7f, 0.95f)).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, rot * UnityEngine.Random.Range(-0.3f, 0.15f)), settleDur)
- .SetEase(Ease.InOutSine));
- seq.OnComplete(PlayKiteGust);
- _kiteSwaySequence = seq;
- }
-
- ///
- /// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位,不打断高度/风况补间。
- /// 支持:lift(上冲回落)/ sink(下坠沉降)/ taut(前扯抖动)/ drop(猛地下坠)/ flutter(原地挣扎扑腾)
- /// / wrench(侧面兜住猛甩,最强档)。
+ /// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位。
+ /// 镜头采样合成位姿,会自然追着 accent 跑。
///
public void PlayKiteAccent(string profile)
{
@@ -930,74 +785,91 @@ namespace AibisDream
_kiteAccentSequence = null;
var tr = _kiteRenderer.transform;
- // 位移在高度层的子空间里,随缩放自动变小,无需额外乘缩放系数
var seq = DOTween.Sequence();
switch (NormalizeMotionProfile(profile))
{
case "lift":
- seq.Append(tr.DOLocalMove(new Vector3(0.14f, 0.3f, 0f), 0.35f).SetEase(Ease.OutQuad));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -6f), 0.35f).SetEase(Ease.OutQuad));
+ seq.Append(tr.DOLocalPath(new[]
+ {
+ new Vector3(0.03f, 0.07f, 0f),
+ new Vector3(0.11f, 0.17f, 0f),
+ new Vector3(0.12f, 0.24f, 0f),
+ }, 0.75f, PathType.CatmullRom).SetEase(Ease.InOutSine));
+ seq.Join(tr.DOLocalRotate(new Vector3(2f, -1.5f, -2f), 0.62f).SetEase(Ease.InOutSine));
break;
case "sink":
- seq.Append(tr.DOLocalMove(new Vector3(-0.06f, -0.28f, 0f), 0.4f).SetEase(Ease.OutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 5f), 0.4f).SetEase(Ease.OutSine));
+ seq.Append(tr.DOLocalPath(new[]
+ {
+ new Vector3(0.03f, -0.06f, 0f),
+ new Vector3(-0.02f, -0.17f, 0f),
+ new Vector3(-0.08f, -0.24f, 0f),
+ }, 0.82f, PathType.CatmullRom).SetEase(Ease.InOutSine));
+ seq.Join(tr.DOLocalRotate(new Vector3(-2f, 1.5f, 2f), 0.68f).SetEase(Ease.InOutSine));
break;
- // 绷紧:猛地被扯直,然后绷住缓慢微颤(小幅慢速拉锯,不是震动)
case "taut":
- seq.Append(tr.DOLocalMove(new Vector3(0.3f, 0.06f, 0f), 0.2f).SetEase(Ease.OutCubic));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -9f), 0.2f).SetEase(Ease.OutCubic));
- seq.Append(tr.DOLocalMove(new Vector3(0.26f, 0.03f, 0f), 0.3f).SetEase(Ease.InOutSine));
- seq.Append(tr.DOLocalMove(new Vector3(0.31f, 0.07f, 0f), 0.26f).SetEase(Ease.InOutSine));
- seq.Append(tr.DOLocalMove(new Vector3(0.27f, 0.04f, 0f), 0.3f).SetEase(Ease.InOutSine));
+ seq.Append(tr.DOLocalPath(new[]
+ {
+ new Vector3(0.08f, 0.01f, 0f),
+ new Vector3(0.2f, 0.07f, 0f),
+ new Vector3(0.26f, 0.05f, 0f),
+ }, 0.62f, PathType.CatmullRom).SetEase(Ease.OutSine));
+ seq.Join(tr.DOLocalRotate(new Vector3(0f, -2f, -3f), 0.62f).SetEase(Ease.OutSine));
+ seq.AppendInterval(0.32f);
break;
case "drop":
- seq.Append(tr.DOLocalMove(new Vector3(0.04f, -0.42f, 0f), 0.55f).SetEase(Ease.InQuad));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 7f), 0.55f).SetEase(Ease.InQuad));
+ seq.Append(tr.DOLocalPath(new[]
+ {
+ new Vector3(0.06f, -0.08f, 0f),
+ new Vector3(0.08f, -0.24f, 0f),
+ new Vector3(0.01f, -0.36f, 0f),
+ }, 0.92f, PathType.CatmullRom).SetEase(Ease.InSine));
+ seq.Join(tr.DOLocalRotate(new Vector3(-2.5f, 1f, 3f), 0.86f).SetEase(Ease.InOutSine));
break;
- // 侧面兜住(最强档,切轱辘前用):先被回带半拍蓄力,随即狠狠横甩出去,
- // 挣了一下没挣回来,又被兜得更远、倾得更狠——位移远超其它档位
case "wrench":
- seq.Append(tr.DOLocalMove(new Vector3(-0.11f, 0.04f, 0f), 0.09f).SetEase(Ease.OutQuad));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4f), 0.09f).SetEase(Ease.OutQuad));
- seq.Append(tr.DOLocalMove(new Vector3(0.7f, 0.2f, 0f), 0.16f).SetEase(Ease.OutCubic));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -24f), 0.16f).SetEase(Ease.OutCubic));
- seq.Append(tr.DOLocalMove(new Vector3(0.45f, 0.09f, 0f), 0.2f).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -10f), 0.2f).SetEase(Ease.InOutSine));
- seq.Append(tr.DOLocalMove(new Vector3(0.92f, 0.3f, 0f), 0.15f).SetEase(Ease.OutCubic));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -30f), 0.15f).SetEase(Ease.OutCubic));
- seq.AppendInterval(0.25f);
+ seq.Append(tr.DOLocalPath(new[]
+ {
+ new Vector3(-0.06f, 0.03f, 0f),
+ new Vector3(0.3f, 0.18f, 0f),
+ new Vector3(0.64f, 0.12f, 0f),
+ new Vector3(0.78f, 0.24f, 0f),
+ }, 1.15f, PathType.CatmullRom).SetEase(Ease.InOutSine));
+ seq.Join(tr.DOLocalRotate(new Vector3(1.5f, -3f, -6f), 0.92f).SetEase(Ease.InOutSine));
+ seq.AppendInterval(0.42f);
break;
- // 挣扎:左右交替的几下小拉扯,力度递减
case "flutter":
- seq.Append(tr.DOLocalMove(new Vector3(0.1f, 0.14f, 0f), 0.2f).SetEase(Ease.OutCubic));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -5f), 0.2f).SetEase(Ease.OutCubic));
- seq.Append(tr.DOLocalMove(new Vector3(-0.08f, 0.06f, 0f), 0.26f).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, 4.5f), 0.26f).SetEase(Ease.InOutSine));
- seq.Append(tr.DOLocalMove(new Vector3(0.06f, 0.1f, 0f), 0.22f).SetEase(Ease.OutCubic));
- seq.Join(tr.DOLocalRotate(new Vector3(0f, 0f, -3.5f), 0.22f).SetEase(Ease.OutCubic));
+ {
+ float side = UnityEngine.Random.value < 0.5f ? -1f : 1f;
+ seq.Append(tr.DOLocalPath(new[]
+ {
+ new Vector3(0.05f * side, 0.04f, 0f),
+ new Vector3(0.16f * side, 0.11f, 0f),
+ new Vector3(0.24f * side, 0.07f, 0f),
+ }, 1.05f, PathType.CatmullRom).SetEase(Ease.InOutSine));
+ seq.Join(tr.DOLocalRotate(new Vector3(1.5f, -2.5f * side, -3f * side), 0.9f)
+ .SetEase(Ease.InOutSine));
+ seq.AppendInterval(0.22f);
break;
+ }
default:
seq.Kill();
return;
}
- // 回到原位,保证可与高度/风况补间叠加
- seq.Append(tr.DOLocalMove(Vector3.zero, 0.5f).SetEase(Ease.InOutSine));
- seq.Join(tr.DOLocalRotate(Vector3.zero, 0.5f).SetEase(Ease.InOutSine));
+ seq.Append(tr.DOLocalMove(Vector3.zero, 0.82f).SetEase(Ease.InOutSine));
+ seq.Join(tr.DOLocalRotate(Vector3.zero, 0.82f).SetEase(Ease.InOutSine));
_kiteAccentSequence = seq;
}
///
- /// 隐藏风筝 Rig。与显示一致不做淡出,直接消失(过渡由外层屏幕 fade 承担)。
+ /// 隐藏风筝 Rig。与显示一致不做淡出,直接消失。
///
- /// 保留参数以兼容既有调用,实际忽略
public IEnumerator HideKite(float duration = 1)
{
if (!IsKiteShowing) yield break;
@@ -1007,7 +879,7 @@ namespace AibisDream
_kiteRenderer.SetAlpha(0);
_kiteBaseRenderer.SetAlpha(0);
- _kiteRootPivot.gameObject.SetActive(false);
+ _kiteCameraNode.gameObject.SetActive(false);
_kiteBaseRenderer.gameObject.SetActive(false);
if (_kiteMask != null)
{