feat(dream): 实现风筝镜头补偿与虚焦驱动

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-13 20:56:51 +08:00
co-authored by Cursor
parent 2c74ec2030
commit b98214632e
8 changed files with 1538 additions and 261 deletions
+46
View File
@@ -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: []
+8
View File
@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 78265ded39f017d45a2afc117effab89
NativeFormatImporter:
externalObjects: {}
mainObjectFileID: 2100000
userData:
assetBundleName:
assetBundleVariant:
+195
View File
@@ -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
}
}
}
@@ -0,0 +1,9 @@
fileFormatVersion: 2
guid: fb0a8d34c65ffb540b0e1f9538285d18
ShaderImporter:
externalObjects: {}
defaultTextures: []
nonModifiableTextures: []
userData:
assetBundleName:
assetBundleVariant:
+65 -1
View File
@@ -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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 1290ba4dc7b8bfa40b61dd3bb1372cf0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
+132 -260
View File
@@ -569,43 +569,47 @@ namespace AibisDream
#region Rig
// 风筝段落弃用切图(D2S风筝1/2/3),改为单张风筝图 + 三层 Transform 连续驱动:
// Kite Rig(高度补间:缩放+位移)→ Sway(常驻风况循环)→ Accent(一次性顿挫 + Renderer
// 三层各持有独立 Tween,互不覆盖。白色底图挂在独立的静态 renderer 上,不随高度缩放。
// Kite Camera(虚拟镜头完全补偿)→ Rig(高度+漂移)→ Sway(鱼式流体摆动)→ AccentDOTween 顿挫)
// 底图 / Mask 保持静态,不进镜头节点。
private const string KiteSpritePicName = "D2S风筝1_风筝层";
private const string KiteBasePicName = "D2S风筝1_底图";
private const float KiteMaxHeight = 13f;
/// <summary>每升高 1 点高度,缩放乘以该系数。</summary>
private const float KiteScalePerHeight = 0.84f;
/// <summary>缩放下限:最远处的风筝也不小于该比例。</summary>
private const float KiteMinScaleFactor = 0.12f;
/// <summary>高度最大时风筝相对基准位置的漂移量(向上飘 + 少量横移)。</summary>
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;
/// <summary>当前乱度 0~1,由 kite_wind 显式设定;kite_state 沿用上次的值(粘滞)。</summary>
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<SpriteRenderer>();
@@ -640,53 +645,52 @@ namespace AibisDream
_kiteMask.isCustomRangeActive = true;
_kiteMask.enabled = false;
root.SetActive(false);
EnsureKiteFocusMaterial();
_kiteDriver = cameraObject.AddComponent<KiteRigDriver>();
_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);
/// <summary>极高处降低不透明度,配合“几乎看不见”的文本。</summary>
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();
}
/// <summary>
/// 显示风筝 Rig。已显示时等价于 <see cref="SetKiteState"/>(幂等)。
/// </summary>
/// <param name="height">风筝高度(对应 Yarn 变量 $h</param>
/// <param name="wind">风向(对应 Yarn 变量 $wind1 强 / 0 无 / -1 弱)</param>
/// <param name="duration">保留参数以兼容既有调用;出现不再淡入,仅用于互斥隐藏其它图</param>
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));
}
/// <summary>
/// 高度补间(每次选择的主反馈)+ 切换风况循环
/// 高度 SmoothDamp + 切换风向。乱度粘滞由 driver 保留
/// </summary>
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);
}
/// <summary>仅切换风况循环(选择前 telegraph 风向)。</summary>
/// <param name="wind">风向:1 强 / 0 无 / -1 弱</param>
/// <param name="turbulence">乱度 0~1;负数或省略表示平稳。设定后粘滞,kite_state 沿用。</param>
/// <summary>仅切换风况(选择前 telegraph 风向)。</summary>
/// <param name="turbulence">乱度 0~1;负数或省略表示平稳(清零)。设定后粘滞,kite_state 沿用。</param>
public void SetKiteWind(int wind, float turbulence = -1f)
{
if (!IsKiteShowing) return;
_kiteTurbulence = turbulence >= 0f ? Mathf.Clamp01(turbulence) : 0f;
PlayKiteSway(wind);
}
/// <summary>屏幕可读性补偿:高度层缩小时适度放大局部位移(开方衰减),避免远处几乎不动。</summary>
private float KiteSwayComp() =>
Mathf.Clamp(1f / Mathf.Sqrt(KiteScaleFactor(_kiteHeight)), 1f, 3f);
/// <summary>
/// 常驻风况循环。位移在高度层的子空间里会随缩放变小,用 comp 做部分补偿保证远处仍有可读的移动。
/// 平稳(乱度 0):沿 CatmullRom 曲线大而缓地漫游,绝不抖。
/// 乱流(乱度 &gt;0):转交 <see cref="PlayKiteGust"/>,每一阵的方向、力度、间隔都随机。
/// </summary>
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);
}
/// <summary>
/// 单次阵风,播完通过 OnComplete 用一组新的随机参数接下一阵——节奏永不重复
/// 猛拽段 X 快、Y 慢半拍(不同时长+缓动),走出弧线而不是直线
/// 乱度越高越狂:间隔与荡回时间大幅压缩、拽得更快更斜,
/// 甩过头弹回之外,还有概率来不及回稳就被反向再抽一鞭。
/// </summary>
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;
}
/// <summary>
/// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位,不打断高度/风况补间。
/// 支持:lift(上冲回落)/ sink(下坠沉降)/ taut(前扯抖动)/ drop(猛地下坠)/ flutter(原地挣扎扑腾)
/// / wrench(侧面兜住猛甩,最强档)。
/// 一次性顿挫动效,叠加在 Accent 层上,结束回到原位
/// 镜头采样合成位姿,会自然追着 accent 跑
/// </summary>
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;
}
/// <summary>
/// 隐藏风筝 Rig。与显示一致不做淡出,直接消失(过渡由外层屏幕 fade 承担)
/// 隐藏风筝 Rig。与显示一致不做淡出,直接消失。
/// </summary>
/// <param name="duration">保留参数以兼容既有调用,实际忽略</param>
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)
{