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)); } ///