355 lines
12 KiB
C#
355 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;
|
|
}
|
|
|
|
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(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;
|
|
}
|
|
|
|
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(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);
|
|
}
|
|
}
|
|
}
|