diff --git a/Assets/Render/KiteFocusSprite.shader b/Assets/Render/KiteFocusSprite.shader
index 87bfc7025..fa0175d49 100644
--- a/Assets/Render/KiteFocusSprite.shader
+++ b/Assets/Render/KiteFocusSprite.shader
@@ -8,7 +8,7 @@ Shader "AIBIS/KiteFocusSprite"
[Header(Focus Blur)]
_BlurAmount ("Blur Amount", Range(0, 1)) = 0
_BlurSize ("Blur Size", Range(0, 0.05)) = 0.012
- [HideInInspector] _UseGaussianBlur ("Use Gaussian Blur", Float) = 0
+ [HideInInspector] _UseSoftFocus ("Use Soft Focus", Float) = 0
_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
@@ -76,7 +76,7 @@ Shader "AIBIS/KiteFocusSprite"
fixed4 _Color;
float _BlurAmount;
float _BlurSize;
- float _UseGaussianBlur;
+ float _UseSoftFocus;
float _DefocusBloom;
float _HaloStrength;
float _ChromaticFringe;
@@ -152,30 +152,31 @@ Shader "AIBIS/KiteFocusSprite"
return result * 0.125;
}
- // Same directional Gaussian kernel used by EyeUnifiedSprite in the
- // Peipei eye module, including its lower-cost mobile fallback.
- half3 EyeGaussianBlurRgb(float2 uv, float stepSize)
+ half4 SoftFocusBlur(float2 uv, float radius)
{
- float angle = 1.937315;
- float2 stepDir = float2(sin(angle), cos(angle));
- float2 stepUv = stepDir * stepSize;
+ float nearRadius = radius * 0.48;
+ half4 result = samplePremultiplied(uv) * 0.20;
-#if defined(SHADER_API_MOBILE)
- half3 rgb = tex2D(_MainTex, saturate(uv)).rgb * 0.34;
- rgb += tex2D(_MainTex, saturate(uv + stepUv)).rgb * 0.33;
- rgb += tex2D(_MainTex, saturate(uv - stepUv)).rgb * 0.33;
-#else
- half3 rgb = tex2D(_MainTex, saturate(uv)).rgb * 0.18;
- rgb += tex2D(_MainTex, saturate(uv + stepUv * 1.0)).rgb * 0.15;
- rgb += tex2D(_MainTex, saturate(uv - stepUv * 1.0)).rgb * 0.15;
- rgb += tex2D(_MainTex, saturate(uv + stepUv * 2.0)).rgb * 0.12;
- rgb += tex2D(_MainTex, saturate(uv - stepUv * 2.0)).rgb * 0.12;
- rgb += tex2D(_MainTex, saturate(uv + stepUv * 3.0)).rgb * 0.09;
- rgb += tex2D(_MainTex, saturate(uv - stepUv * 3.0)).rgb * 0.09;
- rgb += tex2D(_MainTex, saturate(uv + stepUv * 4.0)).rgb * 0.05;
- rgb += tex2D(_MainTex, saturate(uv - stepUv * 4.0)).rgb * 0.05;
-#endif
- return rgb;
+ result += samplePremultiplied(uv + float2( nearRadius, 0)) * 0.08;
+ result += samplePremultiplied(uv + float2(-nearRadius, 0)) * 0.08;
+ result += samplePremultiplied(uv + float2(0, nearRadius)) * 0.08;
+ result += samplePremultiplied(uv + float2(0, -nearRadius)) * 0.08;
+
+ result += samplePremultiplied(uv + float2( nearRadius, nearRadius)) * 0.055;
+ result += samplePremultiplied(uv + float2(-nearRadius, nearRadius)) * 0.055;
+ result += samplePremultiplied(uv + float2( nearRadius, -nearRadius)) * 0.055;
+ result += samplePremultiplied(uv + float2(-nearRadius, -nearRadius)) * 0.055;
+
+ result += samplePremultiplied(uv + float2( radius, 0)) * 0.04;
+ result += samplePremultiplied(uv + float2(-radius, 0)) * 0.04;
+ result += samplePremultiplied(uv + float2(0, radius)) * 0.04;
+ result += samplePremultiplied(uv + float2(0, -radius)) * 0.04;
+
+ result += samplePremultiplied(uv + float2( radius, radius)) * 0.025;
+ result += samplePremultiplied(uv + float2(-radius, radius)) * 0.025;
+ result += samplePremultiplied(uv + float2( radius, -radius)) * 0.025;
+ result += samplePremultiplied(uv + float2(-radius, -radius)) * 0.025;
+ return result;
}
fixed4 frag(v2f i) : SV_Target
@@ -193,18 +194,15 @@ Shader "AIBIS/KiteFocusSprite"
float2 texel = max(abs(_MainTex_TexelSize.xy), float2(1e-5, 1e-5));
float radius = (length(texel) * 0.5 + _BlurSize) * (0.28 + blurAmt * 1.72);
- if (_UseGaussianBlur > 0.5)
+ if (_UseSoftFocus > 0.5)
{
- float stepSize = length(texel) + _BlurSize;
- half3 blurredRgb = EyeGaussianBlurRgb(i.uv, stepSize);
- float blurStrength = pow(blurAmt, 0.75);
- half4 sharpPremul = sharp;
- sharpPremul.rgb *= sharpPremul.a;
- half4 gaussianColor = half4(blurredRgb * sharp.a, sharp.a);
- gaussianColor = lerp(sharpPremul, gaussianColor, blurStrength);
- gaussianColor.rgb *= i.color.rgb * i.color.a;
- gaussianColor.a *= i.color.a;
- return gaussianColor;
+ half4 softBlurred = SoftFocusBlur(i.uv, radius);
+ half4 softSharp = sharp;
+ softSharp.rgb *= softSharp.a;
+ half4 softColor = lerp(softSharp, softBlurred, smoothstep(0.0, 1.0, blurAmt));
+ softColor.rgb *= i.color.rgb * i.color.a;
+ softColor.a *= i.color.a;
+ return softColor;
}
half4 disc = ApertureDisc(i.uv, radius);
diff --git a/Assets/Scenes/梦境.unity b/Assets/Scenes/梦境.unity
index bf0e55103..e013a019b 100644
--- a/Assets/Scenes/梦境.unity
+++ b/Assets/Scenes/梦境.unity
@@ -424,18 +424,19 @@ MonoBehaviour:
initialBlur: 0.58
blurSize: 0.0045
firstAttemptStart: 0.45
- firstFocusDuration: 0.82
+ firstFocusDuration: 0.72
firstNearFocusBlur: 0.16
- firstHoldDuration: 0.12
- firstSlipDuration: 0.48
+ firstHoldDuration: 0.1
+ firstSlipDuration: 0.42
firstSlipBlur: 0.52
- secondAttemptStart: 2
- secondFocusDuration: 0.92
+ secondAttemptStart: 1.85
+ secondFocusDuration: 0.82
secondNearFocusBlur: 0.08
- secondHoldDuration: 0.14
- secondSlipDuration: 0.58
+ secondHoldDuration: 0.12
+ secondSlipDuration: 0.5
unresolvedBlur: 0.44
- descentStart: 4
+ blurFadeStart: 4
+ blurFadeDuration: 3.2
--- !u!1 &218986882
GameObject:
m_ObjectHideFlags: 0
diff --git a/Assets/Scripts/SceneManagement/SpriteShowcase.cs b/Assets/Scripts/SceneManagement/SpriteShowcase.cs
index d9851a04d..c1d2a4722 100644
--- a/Assets/Scripts/SceneManagement/SpriteShowcase.cs
+++ b/Assets/Scripts/SceneManagement/SpriteShowcase.cs
@@ -37,6 +37,13 @@ namespace AibisDream
private Quaternion _largeBaseLocalRotation;
private Vector3 _largeBaseLocalScale;
private float _largeBaseAlpha;
+ private Material _largeBaseMaterial;
+ private MaterialPropertyBlock _largePropertyBlock;
+ private Coroutine _largeSpriteAnimationCoroutine;
+
+ private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount");
+ private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize");
+ private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus");
// 通过 activeSelf 判断当前状态
private bool IsSmallShowing => _spriteRenderer.gameObject.activeSelf;
@@ -89,6 +96,95 @@ namespace AibisDream
_largeBaseLocalRotation = tr.localRotation;
_largeBaseLocalScale = tr.localScale;
_largeBaseAlpha = _largeSpriteRenderer.color.a;
+ _largeBaseMaterial = _largeSpriteRenderer.sharedMaterial;
+ }
+
+ private void StopLargeSpriteAnimation()
+ {
+ if (_largeSpriteAnimationCoroutine == null) return;
+
+ StopCoroutine(_largeSpriteAnimationCoroutine);
+ _largeSpriteAnimationCoroutine = null;
+ }
+
+ private IEnumerator LoopLargeSpriteAnimation(Sprite[] frames, float secondsPerFrame)
+ {
+ var frameIndex = 0;
+ var wait = new WaitForSeconds(Mathf.Max(0.01f, secondsPerFrame));
+
+ while (frames.Length > 0)
+ {
+ if (_largeSpriteRenderer != null)
+ {
+ _largeSpriteRenderer.sprite = frames[frameIndex];
+ }
+
+ frameIndex = (frameIndex + 1) % frames.Length;
+ yield return wait;
+ }
+ }
+
+ private void ApplyLargeSpriteBlur(float amount, float blurSize)
+ {
+ if (_largeSpriteRenderer == null) return;
+
+ amount = Mathf.Clamp01(amount);
+ if (amount <= 0.001f)
+ {
+ _largeSpriteRenderer.SetPropertyBlock(null);
+ _largeSpriteRenderer.sharedMaterial = _largeBaseMaterial;
+ return;
+ }
+
+ if (_kiteFocusMaterial == null)
+ {
+ Debug.LogWarning("[SpriteShowcase] 未配置全屏图片可用的对焦材质。", this);
+ return;
+ }
+
+ _largeSpriteRenderer.sharedMaterial = _kiteFocusMaterial;
+ _largePropertyBlock ??= new MaterialPropertyBlock();
+ _largeSpriteRenderer.GetPropertyBlock(_largePropertyBlock);
+ _largePropertyBlock.SetFloat(BlurAmountId, amount);
+ _largePropertyBlock.SetFloat(BlurSizeId, Mathf.Max(0f, blurSize));
+ _largePropertyBlock.SetFloat(UseSoftFocusId, 1f);
+ _largeSpriteRenderer.SetPropertyBlock(_largePropertyBlock);
+ }
+
+ public void SetLargeSpriteBlur(float amount, float blurSize = 0.012f)
+ {
+ ApplyLargeSpriteBlur(amount, blurSize);
+ }
+
+ public IEnumerator TweenLargeSpriteBlur(float targetAmount, float duration, float blurSize = 0.012f)
+ {
+ if (_largeSpriteRenderer == null) yield break;
+
+ _largePropertyBlock ??= new MaterialPropertyBlock();
+ _largeSpriteRenderer.GetPropertyBlock(_largePropertyBlock);
+ var startAmount = _largeSpriteRenderer.sharedMaterial == _kiteFocusMaterial
+ ? _largePropertyBlock.GetFloat(BlurAmountId)
+ : 0f;
+ targetAmount = Mathf.Clamp01(targetAmount);
+ duration = Mathf.Max(0f, duration);
+
+ if (duration <= 0f)
+ {
+ ApplyLargeSpriteBlur(targetAmount, blurSize);
+ yield break;
+ }
+
+ var elapsed = 0f;
+ while (elapsed < duration)
+ {
+ elapsed += Time.deltaTime;
+ var t = Mathf.Clamp01(elapsed / duration);
+ t = t * t * (3f - 2f * t);
+ ApplyLargeSpriteBlur(Mathf.Lerp(startAmount, targetAmount, t), blurSize);
+ yield return null;
+ }
+
+ ApplyLargeSpriteBlur(targetAmount, blurSize);
}
public IEnumerator AnimateLargePresentation(float scaleMultiplier, float targetAlpha, float duration)
@@ -164,6 +260,8 @@ namespace AibisDream
{
if (_largeSpriteRenderer == null) return;
+ StopLargeSpriteAnimation();
+ ApplyLargeSpriteBlur(0f, 0f);
ResetLargePresentationImmediate();
_largeSpriteRenderer.gameObject.SetActive(false);
}
@@ -510,6 +608,9 @@ namespace AibisDream
private void ClearLargeImmediate()
{
+ StopLargeSpriteAnimation();
+ ApplyLargeSpriteBlur(0f, 0f);
+
if (_largeSpriteRenderer != null)
{
_largeSpriteRenderer.DOKill();
@@ -685,6 +786,8 @@ namespace AibisDream
/// 协程
public IEnumerator ShowLargeObj(string picName, float duration = 1)
{
+ StopLargeSpriteAnimation();
+
// 互斥:小图立刻关掉,避免风筝层与底图不同步淡出造成的怪 fade
if (IsSmallShowing)
{
@@ -721,6 +824,65 @@ namespace AibisDream
}
}
+ ///
+ /// 按“前缀1.png … 前缀N.png”加载并循环播放全屏序列帧。
+ ///
+ public IEnumerator ShowLargeSpriteAnimation(
+ string picNamePrefix,
+ int frameCount,
+ float secondsPerFrame = 0.2f,
+ float duration = 1f)
+ {
+ if (_largeSpriteRenderer == null || frameCount <= 0) yield break;
+
+ StopLargeSpriteAnimation();
+
+ if (IsSmallShowing)
+ {
+ yield return HideObj(0f);
+ }
+
+ if (IsKiteShowing)
+ {
+ yield return HideKite(0f);
+ }
+
+ var frames = new Sprite[frameCount];
+ for (var i = 0; i < frameCount; i++)
+ {
+ var picName = $"{NormalizeDreamPicName(picNamePrefix)}{i + 1}";
+ var key = string.Format(DreamSpritePath, picName);
+ var handle = ResourceSystem.LoadAsync(key);
+ yield return handle;
+
+ if (handle.Status != AsyncOperationStatus.Succeeded || handle.Result == null)
+ {
+ Debug.LogError($"[SpriteShowcase] Failed to load animation frame: {key}");
+ ClearLargeImmediate();
+ yield break;
+ }
+
+ frames[i] = handle.Result;
+ }
+
+ ResetLargePresentationImmediate();
+ _largeSpriteRenderer.gameObject.SetActive(true);
+ _largeSpriteRenderer.sprite = frames[0];
+ _largeSpriteRenderer.SetAlpha(0f);
+ _currentLargePicName = $"{NormalizeDreamPicName(picNamePrefix)}1";
+ _largeSpriteAnimationCoroutine = StartCoroutine(
+ LoopLargeSpriteAnimation(frames, secondsPerFrame));
+
+ if (duration <= 0f)
+ {
+ _largeSpriteRenderer.SetAlpha(1f);
+ }
+ else
+ {
+ yield return _largeSpriteRenderer.DOFade(1f, duration).WaitForCompletion();
+ }
+ }
+
public void SetLargeSpriteImmediate(Sprite sprite)
{
if (_largeSpriteRenderer != null && sprite != null)
@@ -799,6 +961,7 @@ namespace AibisDream
{
if (_largeSpriteRenderer == null) yield break;
+ StopLargeSpriteAnimation();
KillLargePresentationTweens();
if (duration <= 0f)
{
@@ -811,6 +974,7 @@ namespace AibisDream
_largeSpriteRenderer.gameObject.SetActive(false);
_currentLargePicName = null;
+ ApplyLargeSpriteBlur(0f, 0f);
ResetLargePresentationImmediate();
}
diff --git a/Assets/Scripts/SceneManagement/TimelineKit/StarfieldDefocusController.cs b/Assets/Scripts/SceneManagement/TimelineKit/StarfieldDefocusController.cs
index 389fc9f00..747efc7e9 100644
--- a/Assets/Scripts/SceneManagement/TimelineKit/StarfieldDefocusController.cs
+++ b/Assets/Scripts/SceneManagement/TimelineKit/StarfieldDefocusController.cs
@@ -7,14 +7,14 @@ namespace AibisDream
{
///
/// Drives the deliberately unsuccessful focus pulls at the start of the
- /// starfield-to-sea Timeline and keeps the starfield defocused until the
- /// Timeline reaches the sea.
+ /// starfield-to-sea Timeline. The effect is evaluated from Timeline time so
+ /// pausing, frame drops and repeated playback remain deterministic.
///
public sealed class StarfieldDefocusController : MonoBehaviour
{
private static readonly int BlurAmountId = Shader.PropertyToID("_BlurAmount");
private static readonly int BlurSizeId = Shader.PropertyToID("_BlurSize");
- private static readonly int UseGaussianBlurId = Shader.PropertyToID("_UseGaussianBlur");
+ private static readonly int UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus");
[Header("References")]
[SerializeField] private PlayableDirector director;
@@ -30,34 +30,36 @@ namespace AibisDream
[Min(0f)]
[SerializeField] private float firstAttemptStart = 0.45f;
[Min(0.01f)]
- [SerializeField] private float firstFocusDuration = 0.82f;
+ [SerializeField] private float firstFocusDuration = 0.48f;
[Range(0f, 1f)]
[SerializeField] private float firstNearFocusBlur = 0.38f;
[Min(0f)]
- [SerializeField] private float firstHoldDuration = 0.12f;
+ [SerializeField] private float firstHoldDuration = 0.14f;
[Min(0.01f)]
- [SerializeField] private float firstSlipDuration = 0.48f;
+ [SerializeField] private float firstSlipDuration = 0.38f;
[Range(0f, 1f)]
[SerializeField] private float firstSlipBlur = 0.72f;
[Header("Focus Attempt 2")]
[Min(0f)]
- [SerializeField] private float secondAttemptStart = 2f;
+ [SerializeField] private float secondAttemptStart = 1.72f;
[Min(0.01f)]
- [SerializeField] private float secondFocusDuration = 0.92f;
+ [SerializeField] private float secondFocusDuration = 0.55f;
[Range(0f, 1f)]
[SerializeField] private float secondNearFocusBlur = 0.28f;
[Min(0f)]
- [SerializeField] private float secondHoldDuration = 0.14f;
+ [SerializeField] private float secondHoldDuration = 0.18f;
[Min(0.01f)]
- [SerializeField] private float secondSlipDuration = 0.58f;
+ [SerializeField] private float secondSlipDuration = 0.46f;
[Range(0f, 1f)]
[SerializeField] private float unresolvedBlur = 0.62f;
[Header("Downward Pan")]
[Tooltip("Timeline time at which the starfield starts moving down toward the sea.")]
[Min(0f)]
- [SerializeField] private float descentStart = 4f;
+ [SerializeField] private float blurFadeStart = 4f;
+ [Min(0.01f)]
+ [SerializeField] private float blurFadeDuration = 3.2f;
private MaterialPropertyBlock propertyBlock;
@@ -158,7 +160,7 @@ namespace AibisDream
}
director.Pause();
- SetTimelineTime(descentStart);
+ SetTimelineTime(blurFadeStart);
director.Play();
while (director.state == PlayState.Playing)
@@ -246,7 +248,7 @@ namespace AibisDream
starfieldRenderer.GetPropertyBlock(propertyBlock);
propertyBlock.SetFloat(BlurAmountId, EvaluateBlur(time));
propertyBlock.SetFloat(BlurSizeId, blurSize);
- propertyBlock.SetFloat(UseGaussianBlurId, 1f);
+ propertyBlock.SetFloat(UseSoftFocusId, 1f);
starfieldRenderer.SetPropertyBlock(propertyBlock);
}
@@ -306,7 +308,12 @@ namespace AibisDream
return SmoothLerp(secondNearFocusBlur, unresolvedBlur, InverseLerp(secondHoldEnd, secondSlipEnd, time));
}
- return unresolvedBlur;
+ if (time < blurFadeStart)
+ {
+ return unresolvedBlur;
+ }
+
+ return SmoothLerp(unresolvedBlur, 0f, InverseLerp(blurFadeStart, blurFadeStart + blurFadeDuration, time));
}
///