Files

373 lines
12 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
}
}