#if VFX_HAS_TIMELINE using UnityEngine.Playables; using System.Collections.Generic; using System; namespace UnityEngine.VFX { #if UNITY_EDITOR class VisualEffectControlErrorHelper { public struct MaxScrubbingWarning { public float requestedTime; public float fixedTimeStep; public VisualEffectControlTrackController controller; } private void UpdateConflictingControlTrack() { //Detect potential issue with multiple track controlling the same vfx with some scrubbing m_ConflictingControlTrack.Clear(); var controlTrackByTarget = new Dictionary>(); foreach (var controlTrack in m_RegisteredControlTrack) { var target = controlTrack.GetTarget(); if (!controlTrackByTarget.TryGetValue(controlTrack.GetTarget(), out var listOfTrack)) { listOfTrack = new List(); controlTrackByTarget.Add(target, listOfTrack); } listOfTrack.Add(controlTrack); } foreach (var group in controlTrackByTarget) { if (group.Value.Count > 1) { foreach (var track in group.Value) { if (track.GetScrubbing()) { m_ConflictingControlTrack.Add(group.Value.ToArray()); break; } } } } } public void RegisterControlTrack(VisualEffectControlTrackController controller) { m_RegisteredControlTrack.Add(controller); UpdateConflictingControlTrack(); } public void UnregisterControlTrack(VisualEffectControlTrackController controller) { UnregisterScrubbingWarning(controller); m_RegisteredControlTrack.RemoveAll(o => o == controller); UpdateConflictingControlTrack(); } public void RegisterScrubbingWarning(VisualEffectControlTrackController controller, float requestedTime, float fixedTimeStep) { UnregisterScrubbingWarning(controller); m_ScrubbingWarnings.Add(new MaxScrubbingWarning() { controller = controller, requestedTime = requestedTime, fixedTimeStep = fixedTimeStep }); } public void UnregisterScrubbingWarning(VisualEffectControlTrackController controller) { m_ScrubbingWarnings.RemoveAll(o => o.controller == controller); } public IEnumerable GetConflictingControlTrack() { return m_ConflictingControlTrack; } public IEnumerable GetMaxScrubbingWarnings() { return m_ScrubbingWarnings; } public bool AnyError() { return m_ScrubbingWarnings.Count > 0 || m_ConflictingControlTrack.Count > 0; } private static VisualEffectControlErrorHelper m_Instance = new VisualEffectControlErrorHelper(); public static VisualEffectControlErrorHelper instance => m_Instance; List m_RegisteredControlTrack = new List(); List m_ConflictingControlTrack = new List(); List m_ScrubbingWarnings = new List(); } #endif class VisualEffectControlTrackController { struct Event { public int nameId; public VFXEventAttribute attribute; public double time; public enum ClipType { None, Enter, Exit } public int clipIndex; public ClipType clipType; } struct Clip { public int enter; public int exit; } struct Chunk { public bool scrubbing; public bool reinitEnter; public bool reinitExit; public uint startSeed; public double begin; public double end; public uint prewarmCount; public float prewarmDeltaTime; public double prewarmOffset; public int prewarmEvent; public Event[] events; public Clip[] clips; } const int kErrorIndex = int.MinValue; int m_LastChunk = kErrorIndex; int m_LastEvent = kErrorIndex; double m_LastPlayableTime = double.MinValue; List m_EventListIndexCache = new (); #if UNITY_EDITOR bool[] m_ClipState; bool m_HasScrubbingWarnings; bool m_Scrubbing; PlayableDirector m_Director; VisualEffectControlTrack m_Track; public bool GetScrubbing() { return m_Scrubbing; } public VisualEffect GetTarget() { return m_Target; } public VisualEffectControlTrack GetTrack() { return m_Track; } public PlayableDirector GetDirector() { return m_Director; } #endif VisualEffect m_Target; bool m_BackupReseedOnPlay; uint m_BackupStartSeed; Chunk[] m_Chunks; private void OnEnterChunk(int currentChunk) { var chunk = m_Chunks[currentChunk]; #if UNITY_EDITOR if (!chunk.scrubbing) m_ClipState = new bool[chunk.clips.Length]; #endif if (chunk.reinitEnter) { m_Target.resetSeedOnPlay = false; m_Target.startSeed = chunk.startSeed; m_Target.Reinit(false); if (chunk.prewarmCount != 0u) { m_Target.SendEvent(chunk.prewarmEvent); m_Target.Simulate(chunk.prewarmDeltaTime, chunk.prewarmCount); } } } private void OnLeaveChunk(int previousChunkIndex, bool leavingGoingBeforeClip) { var previousChunk = m_Chunks[previousChunkIndex]; if (previousChunk.reinitExit) { m_Target.Reinit(false); } else { #if UNITY_EDITOR if (previousChunk.scrubbing) throw new InvalidOperationException(); #endif //Using infinity as virtual limit to force include events where time is exactly 0 or duration. ProcessNoScrubbingEvents(previousChunk, m_LastPlayableTime, leavingGoingBeforeClip ? double.NegativeInfinity : double.PositiveInfinity); } RestoreVFXState(previousChunk.scrubbing, previousChunk.reinitEnter); #if UNITY_EDITOR m_ClipState = null; #endif } bool IsTimeInChunk(double time, int index) { var chunk = m_Chunks[index]; return chunk.begin <= time && time < chunk.end; } private static readonly double kEpsilonEvent = 1e-12; public void Update(double playableTime, float deltaTime) { #if UNITY_EDITOR if (m_HasScrubbingWarnings) { VisualEffectControlErrorHelper.instance.UnregisterScrubbingWarning(this); m_HasScrubbingWarnings = false; } #endif var playableTimeForEvent = playableTime + kEpsilonEvent; var paused = deltaTime == 0.0; var currentChunkIndex = kErrorIndex; if (m_LastChunk != currentChunkIndex) { if (IsTimeInChunk(playableTime, m_LastChunk)) currentChunkIndex = m_LastChunk; } if (currentChunkIndex == kErrorIndex) { var startIndex = m_LastChunk != kErrorIndex ? (uint)m_LastEvent : 0u; for (uint i = startIndex; i < startIndex + m_Chunks.Length; i++) { var actualIndex = (int)(i % m_Chunks.Length); if (IsTimeInChunk(playableTime, actualIndex)) { currentChunkIndex = actualIndex; break; } } } var firstFrameOfChunk = false; if (m_LastChunk != currentChunkIndex) { if (m_LastChunk != kErrorIndex) { var before = playableTime < m_Chunks[m_LastChunk].begin; OnLeaveChunk(m_LastChunk, before); } if (currentChunkIndex != kErrorIndex) { OnEnterChunk(currentChunkIndex); firstFrameOfChunk = true; } m_LastChunk = currentChunkIndex; m_LastEvent = kErrorIndex; } if (currentChunkIndex != kErrorIndex) { var chunk = m_Chunks[currentChunkIndex]; if (chunk.scrubbing) { m_Target.pause = paused; var actualCurrentTime = chunk.begin + m_Target.time; if (!firstFrameOfChunk) actualCurrentTime -= chunk.prewarmOffset; var playingBackward = playableTime < m_LastPlayableTime; if (!playingBackward) { if (Math.Abs(m_LastPlayableTime - actualCurrentTime) < VFXManager.maxDeltaTime) { //Remove the float part from VFX and only keep double precision actualCurrentTime = m_LastPlayableTime; } else { //VFX is too late on timeline (or a bit ahead), we will have to launch simulate //Warning, in that case, event could have been already sent //m_LastEvent status prevents sending twice the same event } } else { actualCurrentTime = chunk.begin; m_LastEvent = kErrorIndex; OnEnterChunk(m_LastChunk); } double expectedCurrentTime; if (paused) expectedCurrentTime = playableTime; else expectedCurrentTime = playableTime - VFXManager.fixedTimeStep; //Sending missed event (in case of VFX ahead) if (m_LastPlayableTime < actualCurrentTime) { var eventList = m_EventListIndexCache; GetEventsIndex(chunk, m_LastPlayableTime, actualCurrentTime, m_LastEvent, eventList); foreach (var itEvent in eventList) ProcessEvent(itEvent, chunk); } if (actualCurrentTime < expectedCurrentTime) { //Process adjustment if actualCurrentTime < expectedCurrentTime (heavy loop with potential Simulate) var eventList = m_EventListIndexCache; GetEventsIndex(chunk, actualCurrentTime, expectedCurrentTime, m_LastEvent, eventList); var eventCount = eventList.Count; var nextEvent = 0; var maxScrubTime = VFXManager.maxScrubTime; var fixedStep = VFXManager.maxDeltaTime; if (expectedCurrentTime - actualCurrentTime > maxScrubTime) { //Choose a bigger time step to reach the actual expected time fixedStep = (float)((expectedCurrentTime - actualCurrentTime) * (double)VFXManager.maxDeltaTime / (double)maxScrubTime); #if UNITY_EDITOR VisualEffectControlErrorHelper.instance.RegisterScrubbingWarning(this, (float)(expectedCurrentTime - actualCurrentTime), fixedStep); m_HasScrubbingWarnings = true; #endif } while (actualCurrentTime < expectedCurrentTime) { var currentEventIndex = kErrorIndex; uint currentStepCount; if (nextEvent < eventCount) { currentEventIndex = eventList[nextEvent++]; var currentEvent = chunk.events[currentEventIndex]; currentStepCount = (uint)((currentEvent.time - actualCurrentTime) / fixedStep); } else { currentStepCount = (uint)((expectedCurrentTime - actualCurrentTime) / fixedStep); if (currentStepCount == 0) { //We reached the maximum precision according to the current fixedStep & no more event break; } } if (currentStepCount != 0) { m_Target.Simulate((float)fixedStep, currentStepCount); actualCurrentTime += fixedStep * currentStepCount; } ProcessEvent(currentEventIndex, chunk); } } //Sending incoming event (considering an epsilon for current frame events) if (actualCurrentTime < playableTimeForEvent) { var eventList = m_EventListIndexCache; GetEventsIndex(chunk, actualCurrentTime, playableTimeForEvent, m_LastEvent, eventList); foreach (var itEvent in eventList) ProcessEvent(itEvent, chunk); } } else //No scrubbing, only update events { m_Target.pause = false; ProcessNoScrubbingEvents(chunk, m_LastPlayableTime, playableTimeForEvent); } } m_LastPlayableTime = playableTime; } void ProcessNoScrubbingEvents(Chunk chunk, double oldTime, double newTime) { #if UNITY_EDITOR if (chunk.scrubbing) throw new InvalidOperationException(); #endif if (newTime < oldTime) // == playingBackward { var eventBehind = m_EventListIndexCache; GetEventsIndex(chunk, newTime, oldTime, kErrorIndex, eventBehind); if (eventBehind.Count > 0) { for (int index = eventBehind.Count - 1; index >= 0; index--) { var itEvent = eventBehind[index]; var currentEvent = chunk.events[itEvent]; if (currentEvent.clipType == Event.ClipType.Enter) { ProcessEvent(chunk.clips[currentEvent.clipIndex].exit, chunk); } else if (currentEvent.clipType == Event.ClipType.Exit) { ProcessEvent(chunk.clips[currentEvent.clipIndex].enter, chunk); } //else: Ignore, we aren't playing single event backward } //The last event will be always invalid in case of scrubbing backward m_LastEvent = kErrorIndex; } } else { var eventAhead = m_EventListIndexCache; GetEventsIndex(chunk, oldTime, newTime, m_LastEvent, eventAhead); foreach (var itEvent in eventAhead) ProcessEvent(itEvent, chunk); } } void ProcessEvent(int eventIndex, Chunk currentChunk) { if (eventIndex == kErrorIndex) return; m_LastEvent = eventIndex; var currentEvent = currentChunk.events[eventIndex]; #if UNITY_EDITOR if (currentEvent.clipType == Event.ClipType.Enter) { if (m_ClipState[currentEvent.clipIndex]) throw new InvalidOperationException(); m_ClipState[currentEvent.clipIndex] = true; } else if (currentEvent.clipType == Event.ClipType.Exit) { if (!m_ClipState[currentEvent.clipIndex]) throw new InvalidOperationException(); m_ClipState[currentEvent.clipIndex] = false; } #endif m_Target.SendEvent(currentEvent.nameId, currentEvent.attribute); } static void GetEventsIndex(Chunk chunk, double minTime, double maxTime, int lastIndex, List eventListIndex) { eventListIndex.Clear(); var startIndex = lastIndex == kErrorIndex ? 0 : lastIndex + 1; for (int i = startIndex; i < chunk.events.Length; ++i) { var currentEvent = chunk.events[i]; //We are retrieving events between [minTime, maxTime[ //N.B.: maxTime could be offset by an epsilon to cover matching event frame (e.g: event sent at frame 0) if (currentEvent.time >= maxTime) break; if (minTime <= currentEvent.time) eventListIndex.Add(i); } } static VFXEventAttribute ComputeAttribute(VisualEffect vfx, EventAttributes attributes) { if (attributes.content == null) return null; var vfxAttribute = vfx.CreateVFXEventAttribute(); bool hasApply = false; foreach (var content in attributes.content) { if (content?.ApplyToVFX(vfxAttribute) == true) hasApply = true; } //We didn't setup any vfxEventAttribute, ignoring the event payload return hasApply ? vfxAttribute : null; } static IEnumerable ComputeRuntimeEvent(VisualEffectControlPlayableBehaviour behavior, VisualEffect vfx) { var events = VFXTimeSpaceHelper.GetEventNormalizedSpace(PlayableTimeSpace.Absolute, behavior); foreach (var itEvent in events) { //Apply clamping on the fly var absoluteTime = Math.Max(behavior.clipStart, Math.Min(behavior.clipEnd, itEvent.time)); yield return new Event() { attribute = ComputeAttribute(vfx, itEvent.eventAttributes), nameId = itEvent.name, time = absoluteTime, clipIndex = -1, clipType = Event.ClipType.None }; } } class VisualEffectControlPlayableBehaviourComparer : IComparer { public int Compare(VisualEffectControlPlayableBehaviour x, VisualEffectControlPlayableBehaviour y) { return x.clipStart.CompareTo(y.clipStart); } } public void RestoreVFXState(bool restorePause = true, bool restoreSeedState = true) { //Target could have been destroyed if (m_Target == null) return; if (restorePause) m_Target.pause = false; if (restoreSeedState) { m_Target.startSeed = m_BackupStartSeed; m_Target.resetSeedOnPlay = m_BackupReseedOnPlay; } } public void Init(Playable playable, VisualEffect vfx, VisualEffectControlTrack parentTrack) { m_Target = vfx; #if UNITY_EDITOR m_Director = playable.GetGraph().GetResolver() as PlayableDirector; m_Track = parentTrack; #endif m_BackupStartSeed = m_Target.startSeed; m_BackupReseedOnPlay = m_Target.resetSeedOnPlay; var chunks = new Stack<(Chunk actual, List tempEvents, List tempClips)>(); int inputCount = playable.GetInputCount(); var playableBehaviors = new List(); for (int i = 0; i < inputCount; ++i) { var inputPlayable = playable.GetInput(i); if (inputPlayable.GetPlayableType() != typeof(VisualEffectControlPlayableBehaviour)) continue; var inputVFXPlayable = (ScriptPlayable)inputPlayable; var inputBehavior = inputVFXPlayable.GetBehaviour(); if (inputBehavior != null) playableBehaviors.Add(inputBehavior); } playableBehaviors.Sort(new VisualEffectControlPlayableBehaviourComparer()); foreach (var inputBehavior in playableBehaviors) { if (chunks.Count == 0 || inputBehavior.clipStart > chunks.Peek().actual.end || inputBehavior.scrubbing != chunks.Peek().actual.scrubbing || (!inputBehavior.scrubbing && (inputBehavior.reinitEnter || chunks.Peek().actual.reinitExit)) || inputBehavior.startSeed != chunks.Peek().actual.startSeed || inputBehavior.prewarmStepCount != 0u) { chunks.Push(new () { actual = new Chunk() { begin = inputBehavior.clipStart, scrubbing = inputBehavior.scrubbing, startSeed = inputBehavior.startSeed, reinitEnter = inputBehavior.reinitEnter, reinitExit = inputBehavior.reinitExit, prewarmCount = inputBehavior.prewarmStepCount, prewarmDeltaTime = inputBehavior.prewarmDeltaTime, prewarmEvent = inputBehavior.prewarmEvent ?? 0, prewarmOffset = (double)inputBehavior.prewarmStepCount * inputBehavior.prewarmDeltaTime //Event & Clip are null at this stage, using temporary list during initialization }, tempEvents = new List(), tempClips = new List(), }); } var currentChunk = chunks.Pop(); currentChunk.actual.end = inputBehavior.clipEnd; var currentsEvents = new List(ComputeRuntimeEvent(inputBehavior, vfx)); if (!currentChunk.actual.scrubbing) { var sortedEventWithSourceIndex = new List<(Event evt, int sourceIndex)>(); for (var sourceIndex = 0; sourceIndex < currentsEvents.Count; sourceIndex++) sortedEventWithSourceIndex.Add((currentsEvents[sourceIndex], sourceIndex)); sortedEventWithSourceIndex.Sort((x, y) => x.evt.time.CompareTo(y.evt.time)); var newClips = new Clip[inputBehavior.clipEventsCount]; var newEvents = new List(); for (int actualIndex = 0; actualIndex < sortedEventWithSourceIndex.Count; actualIndex++) { var newEvent = sortedEventWithSourceIndex[actualIndex].evt; var sourceIndex = sortedEventWithSourceIndex[actualIndex].sourceIndex; if (sourceIndex < inputBehavior.clipEventsCount * 2) { var actualSortedClipIndex = currentChunk.tempEvents.Count + actualIndex; var localClipIndex = sourceIndex / 2; newEvent.clipIndex = localClipIndex + currentChunk.tempClips.Count; if (sourceIndex % 2 == 0) { newEvent.clipType = Event.ClipType.Enter; newClips[localClipIndex].enter = actualSortedClipIndex; } else { newEvent.clipType = Event.ClipType.Exit; newClips[localClipIndex].exit = actualSortedClipIndex; } newEvents.Add(newEvent); } else //Not a clip event { newEvents.Add(newEvent); } } currentChunk.tempClips.AddRange(newClips); currentChunk.tempEvents.AddRange(newEvents); } else { #if UNITY_EDITOR m_Scrubbing = true; #endif //No need to compute clip information currentsEvents.Sort((x, y) => x.time.CompareTo(y.time)); currentChunk.tempEvents.AddRange(currentsEvents); } chunks.Push(currentChunk); } m_Chunks = new Chunk[chunks.Count]; for (int i = 0; i < m_Chunks.Length; ++i) { var tempChunk = chunks.Pop(); m_Chunks[i] = tempChunk.actual; m_Chunks[i].clips = tempChunk.tempClips.ToArray(); m_Chunks[i].events = tempChunk.tempEvents.ToArray(); } #if UNITY_EDITOR VisualEffectControlErrorHelper.instance.RegisterControlTrack(this); #endif } public void Release() { #if UNITY_EDITOR VisualEffectControlErrorHelper.instance.UnregisterControlTrack(this); #endif RestoreVFXState(); } } class VisualEffectControlTrackMixerBehaviour : PlayableBehaviour { VisualEffectControlTrackController m_ScrubbingCacheHelper; #if UNITY_EDITOR VisualEffectControlTrack m_ParentTrack; #endif VisualEffect m_Target; bool m_ReinitWithBinding; bool m_ReinitWithUnbinding; public void Init(VisualEffectControlTrack parentTrack, bool reinitWithBinding, bool reinitWithUnbinding) { #if UNITY_EDITOR m_ParentTrack = parentTrack; #endif m_ReinitWithBinding = reinitWithBinding; m_ReinitWithUnbinding = reinitWithUnbinding; } private void ApplyFrame(Playable playable, FrameData data) { if (m_Target == null) return; if (m_ScrubbingCacheHelper == null) { m_ScrubbingCacheHelper = new VisualEffectControlTrackController(); VisualEffectControlTrack parentTrack = null; #if UNITY_EDITOR parentTrack = m_ParentTrack; #endif m_ScrubbingCacheHelper.Init(playable, m_Target, parentTrack); } var duration = playable.GetOutput(0).GetDuration(); var globalTime = playable.GetTime(); var numberOfFullLoops = (int)(globalTime / duration); globalTime -= numberOfFullLoops * duration; var deltaTime = data.deltaTime; m_ScrubbingCacheHelper.Update(globalTime, deltaTime); } void BindVFX(VisualEffect vfx) { m_Target = vfx; if (m_Target != null && m_ReinitWithBinding) { m_Target.Reinit(false); } } void UnbindVFX() { if (m_Target != null && m_ReinitWithUnbinding) { m_Target.Reinit(true); } m_Target = null; } public override void PrepareFrame(Playable playable, FrameData data) { var vfx = data.output.GetUserData() as VisualEffect; if (m_Target != vfx) { UnbindVFX(); if (vfx != null) { if (vfx.visualEffectAsset == null) vfx = null; else if (!vfx.isActiveAndEnabled) vfx = null; } BindVFX(vfx); InvalidateScrubbingHelper(); } ApplyFrame(playable, data); } public override void OnBehaviourPause(Playable playable, FrameData data) { base.OnBehaviourPause(playable, data); ApplyFrame(playable, data); } void InvalidateScrubbingHelper() { if (m_ScrubbingCacheHelper != null) { m_ScrubbingCacheHelper.Release(); m_ScrubbingCacheHelper = null; } } public override void OnPlayableCreate(Playable playable) { InvalidateScrubbingHelper(); } public override void OnPlayableDestroy(Playable playable) { InvalidateScrubbingHelper(); UnbindVFX(); } } } #endif