From b98214632eba9d3bc4d3ede419d4cd05f24e0e1b Mon Sep 17 00:00:00 2001 From: bottlefish <781230111@qq.com> Date: Mon, 13 Jul 2026 20:56:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(dream):=20=E5=AE=9E=E7=8E=B0=E9=A3=8E?= =?UTF-8?q?=E7=AD=9D=E9=95=9C=E5=A4=B4=E8=A1=A5=E5=81=BF=E4=B8=8E=E8=99=9A?= =?UTF-8?q?=E7=84=A6=E9=A9=B1=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- Assets/Render/KiteFocusSprite.mat | 46 + Assets/Render/KiteFocusSprite.mat.meta | 8 + Assets/Render/KiteFocusSprite.shader | 195 +++ Assets/Render/KiteFocusSprite.shader.meta | 9 + Assets/Scenes/梦境.unity | 66 +- .../Scripts/SceneManagement/KiteRigDriver.cs | 1072 +++++++++++++++++ .../SceneManagement/KiteRigDriver.cs.meta | 11 + .../Scripts/SceneManagement/SpriteShowcase.cs | 392 ++---- 8 files changed, 1538 insertions(+), 261 deletions(-) create mode 100644 Assets/Render/KiteFocusSprite.mat create mode 100644 Assets/Render/KiteFocusSprite.mat.meta create mode 100644 Assets/Render/KiteFocusSprite.shader create mode 100644 Assets/Render/KiteFocusSprite.shader.meta create mode 100644 Assets/Scripts/SceneManagement/KiteRigDriver.cs create mode 100644 Assets/Scripts/SceneManagement/KiteRigDriver.cs.meta 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) {