Files
aibis-dream/Assets/Scripts/SceneManagement/TimelineKit/StarfieldDefocusController.cs
T

348 lines
11 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 and keeps the starfield defocused until the
/// Timeline reaches the sea.
/// </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 UseGaussianBlurId = Shader.PropertyToID("_UseGaussianBlur");
[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.82f;
[Range(0f, 1f)]
[SerializeField] private float firstNearFocusBlur = 0.38f;
[Min(0f)]
[SerializeField] private float firstHoldDuration = 0.12f;
[Min(0.01f)]
[SerializeField] private float firstSlipDuration = 0.48f;
[Range(0f, 1f)]
[SerializeField] private float firstSlipBlur = 0.72f;
[Header("Focus Attempt 2")]
[Min(0f)]
[SerializeField] private float secondAttemptStart = 2f;
[Min(0.01f)]
[SerializeField] private float secondFocusDuration = 0.92f;
[Range(0f, 1f)]
[SerializeField] private float secondNearFocusBlur = 0.28f;
[Min(0f)]
[SerializeField] private float secondHoldDuration = 0.14f;
[Min(0.01f)]
[SerializeField] private float secondSlipDuration = 0.58f;
[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;
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;
}
director.Stop();
SetTimelineTime(0f);
}
private IEnumerator PlayFocusAttempt(int attempt)
{
if (!TryValidateReferences())
{
yield break;
}
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;
}
director.Pause();
SetTimelineTime(descentStart);
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;
}
ApplyAtTime((float)director.time);
}
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(UseGaussianBlurId, 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));
}
return unresolvedBlur;
}
/// <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);
}
}
}