373 lines
12 KiB
C#
373 lines
12 KiB
C#
using System.Collections;
|
||
using UnityEngine;
|
||
using UnityEngine.Playables;
|
||
using Yarn.Unity;
|
||
|
||
namespace AibisDream
|
||
{
|
||
/// <summary>
|
||
/// Drives the deliberately unsuccessful focus pulls at the start of the
|
||
/// starfield-to-sea Timeline. The effect is evaluated from Timeline time so
|
||
/// pausing, frame drops and repeated playback remain deterministic.
|
||
/// </summary>
|
||
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 UseSoftFocusId = Shader.PropertyToID("_UseSoftFocus");
|
||
|
||
[Header("References")]
|
||
[SerializeField] private PlayableDirector director;
|
||
[SerializeField] private SpriteRenderer starfieldRenderer;
|
||
|
||
[Header("Defocus Look")]
|
||
[Range(0f, 1f)]
|
||
[SerializeField] private float initialBlur = 0.78f;
|
||
[Range(0f, 0.05f)]
|
||
[SerializeField] private float blurSize = 0.009f;
|
||
|
||
[Header("Focus Attempt 1")]
|
||
[Min(0f)]
|
||
[SerializeField] private float firstAttemptStart = 0.45f;
|
||
[Min(0.01f)]
|
||
[SerializeField] private float firstFocusDuration = 0.48f;
|
||
[Range(0f, 1f)]
|
||
[SerializeField] private float firstNearFocusBlur = 0.38f;
|
||
[Min(0f)]
|
||
[SerializeField] private float firstHoldDuration = 0.14f;
|
||
[Min(0.01f)]
|
||
[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 = 1.72f;
|
||
[Min(0.01f)]
|
||
[SerializeField] private float secondFocusDuration = 0.55f;
|
||
[Range(0f, 1f)]
|
||
[SerializeField] private float secondNearFocusBlur = 0.28f;
|
||
[Min(0f)]
|
||
[SerializeField] private float secondHoldDuration = 0.18f;
|
||
[Min(0.01f)]
|
||
[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 blurFadeStart = 4f;
|
||
[Min(0.01f)]
|
||
[SerializeField] private float blurFadeDuration = 3.2f;
|
||
|
||
private MaterialPropertyBlock propertyBlock;
|
||
|
||
[YarnCommand("starfield_prepare")]
|
||
public static void PrepareStarfieldCommand()
|
||
{
|
||
var controller = FindFirstObjectByType<StarfieldDefocusController>();
|
||
if (controller == null)
|
||
{
|
||
Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_prepare.");
|
||
return;
|
||
}
|
||
|
||
controller.PrepareStarfield();
|
||
}
|
||
|
||
[YarnCommand("starfield_focus")]
|
||
public static IEnumerator PlayFocusAttemptCommand(int attempt)
|
||
{
|
||
var controller = FindFirstObjectByType<StarfieldDefocusController>();
|
||
if (controller == null)
|
||
{
|
||
Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_focus.");
|
||
yield break;
|
||
}
|
||
|
||
yield return controller.PlayFocusAttempt(attempt);
|
||
}
|
||
|
||
[YarnCommand("starfield_descend")]
|
||
public static IEnumerator DescendToSeaCommand()
|
||
{
|
||
var controller = FindFirstObjectByType<StarfieldDefocusController>();
|
||
if (controller == null)
|
||
{
|
||
Debug.LogWarning("[StarfieldDefocusController] No active controller found for starfield_descend.");
|
||
yield break;
|
||
}
|
||
|
||
yield return controller.DescendToSea();
|
||
}
|
||
|
||
private void PrepareStarfield()
|
||
{
|
||
if (!TryValidateReferences())
|
||
{
|
||
return;
|
||
}
|
||
|
||
EnsureStarfieldActive();
|
||
director.Stop();
|
||
SetTimelineTime(0f);
|
||
}
|
||
|
||
private IEnumerator PlayFocusAttempt(int attempt)
|
||
{
|
||
if (!TryValidateReferences())
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
EnsureStarfieldActive();
|
||
|
||
float startTime;
|
||
float endTime;
|
||
switch (attempt)
|
||
{
|
||
case 1:
|
||
startTime = firstAttemptStart;
|
||
endTime = firstAttemptStart + firstFocusDuration + firstHoldDuration + firstSlipDuration;
|
||
break;
|
||
case 2:
|
||
startTime = secondAttemptStart;
|
||
endTime = secondAttemptStart + secondFocusDuration + secondHoldDuration + secondSlipDuration;
|
||
break;
|
||
default:
|
||
Debug.LogWarning($"[StarfieldDefocusController] Unsupported focus attempt '{attempt}'. Expected 1 or 2.", this);
|
||
yield break;
|
||
}
|
||
|
||
director.Pause();
|
||
SetTimelineTime(startTime);
|
||
|
||
float duration = Mathf.Max(0.01f, endTime - startTime);
|
||
float elapsed = 0f;
|
||
while (elapsed < duration)
|
||
{
|
||
elapsed += Time.deltaTime;
|
||
SetTimelineTime(Mathf.Lerp(startTime, endTime, Mathf.Clamp01(elapsed / duration)));
|
||
yield return null;
|
||
}
|
||
|
||
SetTimelineTime(endTime);
|
||
}
|
||
|
||
private IEnumerator DescendToSea()
|
||
{
|
||
if (!TryValidateReferences())
|
||
{
|
||
yield break;
|
||
}
|
||
|
||
EnsureStarfieldActive();
|
||
director.Pause();
|
||
SetTimelineTime(blurFadeStart);
|
||
director.Play();
|
||
|
||
while (director.state == PlayState.Playing)
|
||
{
|
||
yield return null;
|
||
}
|
||
|
||
ApplyAtTime((float)director.time);
|
||
}
|
||
|
||
private void Reset()
|
||
{
|
||
director = GetComponent<PlayableDirector>();
|
||
starfieldRenderer = GetComponentInChildren<SpriteRenderer>(true);
|
||
}
|
||
|
||
private void Awake()
|
||
{
|
||
propertyBlock = new MaterialPropertyBlock();
|
||
ApplyAtTime(director != null ? (float)director.time : 0f);
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
ApplyAtTime(director != null ? (float)director.time : 0f);
|
||
}
|
||
|
||
private void LateUpdate()
|
||
{
|
||
if (director == null || starfieldRenderer == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
// Fiction 等路径可能只 play_timeline、不走 starfield_prepare;
|
||
// Timeline 只 key 了「海浪」的 Active,不会打开「星空」。
|
||
if (director.state == PlayState.Playing)
|
||
EnsureStarfieldActive();
|
||
|
||
ApplyAtTime((float)director.time);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 场景「星空」默认 inactive(与「海浪」一致)。演出开始时打开。
|
||
/// </summary>
|
||
private void EnsureStarfieldActive()
|
||
{
|
||
if (starfieldRenderer != null && !starfieldRenderer.gameObject.activeSelf)
|
||
starfieldRenderer.gameObject.SetActive(true);
|
||
}
|
||
|
||
private bool TryValidateReferences()
|
||
{
|
||
if (director == null)
|
||
{
|
||
Debug.LogWarning("[StarfieldDefocusController] PlayableDirector is not assigned.", this);
|
||
return false;
|
||
}
|
||
|
||
if (director.playableAsset == null)
|
||
{
|
||
Debug.LogWarning("[StarfieldDefocusController] PlayableDirector has no Timeline asset.", this);
|
||
return false;
|
||
}
|
||
|
||
if (starfieldRenderer == null)
|
||
{
|
||
Debug.LogWarning("[StarfieldDefocusController] Starfield SpriteRenderer is not assigned.", this);
|
||
return false;
|
||
}
|
||
|
||
var material = starfieldRenderer.sharedMaterial;
|
||
if (material == null || !material.HasProperty(BlurAmountId) || !material.HasProperty(BlurSizeId))
|
||
{
|
||
Debug.LogWarning(
|
||
"[StarfieldDefocusController] Starfield material must support _BlurAmount and _BlurSize.",
|
||
starfieldRenderer);
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
private void SetTimelineTime(float time)
|
||
{
|
||
director.time = Mathf.Clamp(time, 0f, (float)director.duration);
|
||
director.Evaluate();
|
||
ApplyAtTime((float)director.time);
|
||
}
|
||
|
||
private void ApplyAtTime(float time)
|
||
{
|
||
if (starfieldRenderer == null)
|
||
{
|
||
return;
|
||
}
|
||
|
||
propertyBlock ??= new MaterialPropertyBlock();
|
||
starfieldRenderer.GetPropertyBlock(propertyBlock);
|
||
propertyBlock.SetFloat(BlurAmountId, EvaluateBlur(time));
|
||
propertyBlock.SetFloat(BlurSizeId, blurSize);
|
||
propertyBlock.SetFloat(UseSoftFocusId, 1f);
|
||
starfieldRenderer.SetPropertyBlock(propertyBlock);
|
||
}
|
||
|
||
private float EvaluateBlur(float time)
|
||
{
|
||
float firstFocusEnd = firstAttemptStart + firstFocusDuration;
|
||
float firstHoldEnd = firstFocusEnd + firstHoldDuration;
|
||
float firstSlipEnd = firstHoldEnd + firstSlipDuration;
|
||
|
||
float secondFocusEnd = secondAttemptStart + secondFocusDuration;
|
||
float secondHoldEnd = secondFocusEnd + secondHoldDuration;
|
||
float secondSlipEnd = secondHoldEnd + secondSlipDuration;
|
||
|
||
if (time < firstAttemptStart)
|
||
{
|
||
return initialBlur;
|
||
}
|
||
|
||
if (time < firstFocusEnd)
|
||
{
|
||
return EvaluateFocusTurn(
|
||
initialBlur,
|
||
firstNearFocusBlur,
|
||
InverseLerp(firstAttemptStart, firstFocusEnd, time));
|
||
}
|
||
|
||
if (time < firstHoldEnd)
|
||
{
|
||
return firstNearFocusBlur;
|
||
}
|
||
|
||
if (time < firstSlipEnd)
|
||
{
|
||
return SmoothLerp(firstNearFocusBlur, firstSlipBlur, InverseLerp(firstHoldEnd, firstSlipEnd, time));
|
||
}
|
||
|
||
if (time < secondAttemptStart)
|
||
{
|
||
return firstSlipBlur;
|
||
}
|
||
|
||
if (time < secondFocusEnd)
|
||
{
|
||
return EvaluateFocusTurn(
|
||
firstSlipBlur,
|
||
secondNearFocusBlur,
|
||
InverseLerp(secondAttemptStart, secondFocusEnd, time));
|
||
}
|
||
|
||
if (time < secondHoldEnd)
|
||
{
|
||
return secondNearFocusBlur;
|
||
}
|
||
|
||
if (time < secondSlipEnd)
|
||
{
|
||
return SmoothLerp(secondNearFocusBlur, unresolvedBlur, InverseLerp(secondHoldEnd, secondSlipEnd, time));
|
||
}
|
||
|
||
if (time < blurFadeStart)
|
||
{
|
||
return unresolvedBlur;
|
||
}
|
||
|
||
return SmoothLerp(unresolvedBlur, 0f, InverseLerp(blurFadeStart, blurFadeStart + blurFadeDuration, time));
|
||
}
|
||
|
||
/// <summary>
|
||
/// Simulates turning a focus ring toward focus, passing the focal plane,
|
||
/// then reversing back. The following slip phase still lets the image
|
||
/// drift out of focus, keeping both attempts deliberately unsuccessful.
|
||
/// </summary>
|
||
private static float EvaluateFocusTurn(float fromBlur, float nearFocusBlur, float t)
|
||
{
|
||
t = Mathf.Clamp01(t);
|
||
float overshootBlur = Mathf.Lerp(nearFocusBlur, fromBlur, 0.68f);
|
||
|
||
if (t < 0.44f)
|
||
{
|
||
return SmoothLerp(fromBlur, nearFocusBlur, t / 0.44f);
|
||
}
|
||
|
||
if (t < 0.72f)
|
||
{
|
||
return SmoothLerp(nearFocusBlur, overshootBlur, (t - 0.44f) / 0.28f);
|
||
}
|
||
|
||
return SmoothLerp(overshootBlur, nearFocusBlur, (t - 0.72f) / 0.28f);
|
||
}
|
||
|
||
private static float InverseLerp(float from, float to, float value)
|
||
{
|
||
return Mathf.Clamp01((value - from) / Mathf.Max(0.0001f, to - from));
|
||
}
|
||
|
||
private static float SmoothLerp(float from, float to, float t)
|
||
{
|
||
t = Mathf.Clamp01(t);
|
||
t = t * t * (3f - 2f * t);
|
||
return Mathf.LerpUnclamped(from, to, t);
|
||
}
|
||
}
|
||
}
|