using UnityEngine; using System.Collections.Generic; using System; namespace Cinemachine { internal class TargetPositionCache { public static bool UseCache; public const float CacheStepSize = 1 / 60.0f; public enum Mode { Disabled, Record, Playback } static Mode m_CacheMode = Mode.Disabled; #if UNITY_EDITOR static TargetPositionCache() { UnityEngine.SceneManagement.SceneManager.sceneLoaded += OnSceneLoaded; } static void OnSceneLoaded(UnityEngine.SceneManagement.Scene scene, UnityEngine.SceneManagement.LoadSceneMode mode) { ClearCache(); } #endif public static Mode CacheMode { get => m_CacheMode; set { if (value == m_CacheMode) return; m_CacheMode = value; switch (value) { default: case Mode.Disabled: ClearCache(); break; case Mode.Record: ClearCache(); break; case Mode.Playback: CreatePlaybackCurves(); break; } } } public static bool IsRecording => UseCache && m_CacheMode == Mode.Record; public static bool CurrentPlaybackTimeValid => UseCache && m_CacheMode == Mode.Playback && HasCurrentTime; public static bool IsEmpty => CacheTimeRange.IsEmpty; public static float CurrentTime; // These are used during recording to manage camera cuts public static int CurrentFrame; public static bool IsCameraCut; class CacheCurve { public struct Item { public Vector3 Pos; public Quaternion Rot; public static Item Lerp(Item a, Item b, float t) { return new Item { Pos = Vector3.LerpUnclamped(a.Pos, b.Pos, t), Rot = Quaternion.SlerpUnclamped(a.Rot, b.Rot, t) }; } public static Item Empty => new Item { Rot = Quaternion.identity }; } public float StartTime; public float StepSize; public int Count => m_Cache.Count; List m_Cache; public CacheCurve(float startTime, float endTime, float stepSize) { StepSize = stepSize; StartTime = startTime; m_Cache = new List(Mathf.CeilToInt((StepSize * 0.5f + endTime - startTime) / StepSize)); } public void Add(Item item) => m_Cache.Add(item); public void AddUntil(Item item, float time, bool isCut) { var prevIndex = m_Cache.Count - 1; var prevTime = prevIndex * StepSize; var timeRange = time - StartTime - prevTime; // If this sample is the first after a camera cut, we don't want to lerp the positions // in the event that some frames got skipped by timeline and the targets were // warped at the cut if (isCut) for (float t = StepSize; t <= timeRange; t += StepSize) Add(item); else { var prev = m_Cache[prevIndex]; for (float t = StepSize; t <= timeRange; t += StepSize) Add(Item.Lerp(prev, item, t / timeRange)); } } public Item Evaluate(float time) { var numItems = m_Cache.Count; if (numItems == 0) return Item.Empty; var s = time - StartTime; var index = Mathf.Clamp(Mathf.FloorToInt(s / StepSize), 0, numItems - 1); var v = m_Cache[index]; if (index == numItems - 1) return v; return Item.Lerp(v, m_Cache[index + 1], (s - index * StepSize) / StepSize); } } class CacheEntry { public CacheCurve Curve; struct RecordingItem { public float Time; public bool IsCut; public CacheCurve.Item Item; } List RawItems = new List(); public void AddRawItem(float time, bool isCut, Transform target) { // Preserve monotonic ordering var endTime = time - CacheStepSize; var maxItem = RawItems.Count - 1; var lastToKeep = maxItem; while (lastToKeep >= 0 && RawItems[lastToKeep].Time > endTime) --lastToKeep; if (lastToKeep == maxItem) { // Append only, nothing to remove RawItems.Add(new RecordingItem { Time = time, IsCut = isCut, Item = new CacheCurve.Item { Pos = target.position, Rot = target.rotation } }); } else { // Trim off excess, overwrite the one after lastToKeep var trimStart = lastToKeep + 2; if (trimStart <= maxItem) RawItems.RemoveRange(trimStart, RawItems.Count - trimStart); RawItems[lastToKeep + 1] = new RecordingItem { Time = time, IsCut = isCut, Item = new CacheCurve.Item { Pos = target.position, Rot = target.rotation } }; } } public void CreateCurves() { int maxItem = RawItems.Count - 1; float startTime = maxItem < 0 ? 0 : RawItems[0].Time; float endTime = maxItem < 0 ? 0 : RawItems[maxItem].Time; Curve = new CacheCurve(startTime, endTime, CacheStepSize); Curve.Add(maxItem < 0 ? CacheCurve.Item.Empty : RawItems[0].Item); for (int i = 1; i <= maxItem; ++i) Curve.AddUntil(RawItems[i].Item, RawItems[i].Time, RawItems[i].IsCut); RawItems.Clear(); } } static Dictionary m_Cache; public struct TimeRange { public float Start; public float End; public bool IsEmpty => End < Start; public bool Contains(float time) => time >= Start && time <= End; public static TimeRange Empty { get => new TimeRange { Start = float.MaxValue, End = float.MinValue }; } public void Include(float time) { Start = Mathf.Min(Start, time); End = Mathf.Max(End, time); } } static TimeRange m_CacheTimeRange; public static TimeRange CacheTimeRange { get => m_CacheTimeRange; } public static bool HasCurrentTime { get => m_CacheTimeRange.Contains(CurrentTime); } public static void ClearCache() { m_Cache = CacheMode == Mode.Disabled ? null : new Dictionary(); m_CacheTimeRange = TimeRange.Empty; CurrentTime = 0; CurrentFrame = 0; IsCameraCut = false; } static void CreatePlaybackCurves() { if (m_Cache == null) m_Cache = new Dictionary(); var iter = m_Cache.GetEnumerator(); while (iter.MoveNext()) iter.Current.Value.CreateCurves(); } const float kWraparoundSlush = 0.1f; /// /// If Recording, will log the target position at the CurrentTime. /// Otherwise, will fetch the cached position at CurrentTime. /// /// Target whose transform is tracked /// Target's position at CurrentTime public static Vector3 GetTargetPosition(Transform target) { if (!UseCache || CacheMode == Mode.Disabled) return target.position; // Wrap around during record? if (CacheMode == Mode.Record && !m_CacheTimeRange.IsEmpty && CurrentTime < m_CacheTimeRange.Start - kWraparoundSlush) { ClearCache(); } if (CacheMode == Mode.Playback && !HasCurrentTime) return target.position; if (!m_Cache.TryGetValue(target, out var entry)) { if (CacheMode != Mode.Record) return target.position; entry = new CacheEntry(); m_Cache.Add(target, entry); } if (CacheMode == Mode.Record) { entry.AddRawItem(CurrentTime, IsCameraCut, target); m_CacheTimeRange.Include(CurrentTime); return target.position; } if (entry.Curve == null) return target.position; return entry.Curve.Evaluate(CurrentTime).Pos; } /// /// If Recording, will log the target rotation at the CurrentTime. /// Otherwise, will fetch the cached position at CurrentTime. /// /// Target whose transform is tracked /// Target's rotation at CurrentTime public static Quaternion GetTargetRotation(Transform target) { if (CacheMode == Mode.Disabled) return target.rotation; // Wrap around during record? if (CacheMode == Mode.Record && !m_CacheTimeRange.IsEmpty && CurrentTime < m_CacheTimeRange.Start - kWraparoundSlush) { ClearCache(); } if (CacheMode == Mode.Playback && !HasCurrentTime) return target.rotation; if (!m_Cache.TryGetValue(target, out var entry)) { if (CacheMode != Mode.Record) return target.rotation; entry = new CacheEntry(); m_Cache.Add(target, entry); } if (CacheMode == Mode.Record) { if (m_CacheTimeRange.End <= CurrentTime) { entry.AddRawItem(CurrentTime, IsCameraCut, target); m_CacheTimeRange.Include(CurrentTime); } return target.rotation; } return entry.Curve.Evaluate(CurrentTime).Rot; } } }