using System.Collections.Generic;
using System.Threading;
using UnityEngine;
using UnityEngine.Playables;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Streamingle.Gaze
{
///
/// Blends Timeline clip requests into one command for the bound driver.
///
public sealed class BlendshapeGazeMixerBehaviour : PlayableBehaviour
{
private const float MinimumWeight = 0.0001f;
private static int nextSourceId;
#if UNITY_EDITOR
private static readonly HashSet PendingEditorEvaluations =
new HashSet();
private static bool editorEvaluationScheduled;
#endif
private readonly int sourceId = Interlocked.Increment(ref nextSourceId);
private BlendshapeGazeDriver boundDriver;
private bool hasSubmittedState;
private bool hasProcessedFrame;
public override void PrepareFrame(Playable playable, FrameData info)
{
if (hasSubmittedState && boundDriver != null)
boundDriver.PrepareForTimelineEvaluation();
}
public override void ProcessFrame(Playable playable, FrameData info, object playerData)
{
var driver = playerData as BlendshapeGazeDriver;
if (driver == null)
{
ClearBoundDriver(ShouldEvaluateImmediately());
return;
}
if (boundDriver != driver)
{
ClearBoundDriver(ShouldEvaluateImmediately());
boundDriver = driver;
}
var outputWeight = Mathf.Max(0f, info.effectiveWeight);
var totalWeight = 0f;
var gazeOriginPosition = driver.GetGazeOriginPosition();
var weightedTargetDirection = Vector3.zero;
var hasCommonTarget = false;
var hasDifferentTargets = false;
var hasUnresolvedTarget = false;
Transform commonTarget = null;
var dominantWeight = float.NegativeInfinity;
BlendshapeGazeBehaviour dominantBehaviour = null;
Camera resolvedCamera = null;
var inputCount = playable.GetInputCount();
for (var i = 0; i < inputCount; i++)
{
var inputPlayable = (ScriptPlayable)playable.GetInput(i);
if (!inputPlayable.IsValid())
continue;
var behaviour = inputPlayable.GetBehaviour();
var inputWeight = Mathf.Max(0f, playable.GetInputWeight(i));
var contribution = inputWeight * outputWeight * Mathf.Clamp01(behaviour.Strength);
if (contribution <= MinimumWeight)
continue;
totalWeight += contribution;
var requestedTarget = behaviour.Target;
if (!hasCommonTarget)
{
commonTarget = requestedTarget;
hasCommonTarget = true;
}
else if (commonTarget != requestedTarget)
{
hasDifferentTargets = true;
}
var resolvedTarget = requestedTarget;
if (resolvedTarget == null)
{
if (resolvedCamera == null)
resolvedCamera = driver.TargetCamera != null
? driver.TargetCamera
: Camera.main;
if (resolvedCamera != null)
resolvedTarget = resolvedCamera.transform;
}
if (resolvedTarget != null)
{
var targetOffset = resolvedTarget.position - gazeOriginPosition;
if (targetOffset.sqrMagnitude > MinimumWeight * MinimumWeight)
{
weightedTargetDirection += targetOffset.normalized * contribution;
}
else
{
hasUnresolvedTarget = true;
}
}
else
{
hasUnresolvedTarget = true;
}
if (contribution > dominantWeight)
{
dominantWeight = contribution;
dominantBehaviour = behaviour;
}
}
var canDeferTargetResolution = hasCommonTarget && !hasDifferentTargets;
// A shared Transform (including a null target meaning Main Camera) is
// deliberately deferred to the driver. This lets LateUpdate observe the
// final Cinemachine camera pose. Distinct overlapping targets must be
// collapsed to a fixed blended direction during Timeline evaluation.
// Directions, rather than positions, prevent a distant target from
// outweighing a nearby target with the same Timeline contribution.
var cannotBlendTargets = !canDeferTargetResolution
&& (weightedTargetDirection.sqrMagnitude <= MinimumWeight * MinimumWeight
|| hasUnresolvedTarget);
if (totalWeight <= MinimumWeight || dominantBehaviour == null || cannotBlendTargets)
{
ClearSubmittedState(ShouldEvaluateImmediately());
hasProcessedFrame = true;
return;
}
var resetSmoothing = !hasProcessedFrame || info.seekOccurred || info.timeLooped;
var deterministic = !Application.isPlaying
|| info.evaluationType == FrameData.EvaluationType.Evaluate
|| info.seekOccurred
|| info.timeLooped;
var state = new GazeTimelineState
{
TargetDirection = canDeferTargetResolution
? Vector3.zero
: weightedTargetDirection.normalized,
HasTargetDirection = !canDeferTargetResolution,
Target = canDeferTargetResolution ? commonTarget : null,
UseMainCamera = canDeferTargetResolution && commonTarget == null,
Influence = Mathf.Clamp01(totalWeight),
EnableCorrectives = dominantBehaviour.EnableCorrectives,
OverrideCutResponse = dominantBehaviour.OverrideCutResponse,
CutResponse = dominantBehaviour.CutResponse,
CutTransitionDuration = dominantBehaviour.CutTransitionDuration,
ResetSmoothing = resetSmoothing,
Deterministic = deterministic
};
driver.SetTimelineState(sourceId, state);
hasSubmittedState = true;
hasProcessedFrame = true;
if (!Application.isPlaying)
{
// Evaluate after the whole Timeline graph has applied its animation
// outputs. This pass sees the animated GazeOrigin, final camera pose
// and facial blendshape baseline from this frame.
ScheduleEditorFinalEvaluation(driver);
}
}
public override void OnGraphStop(Playable playable)
{
ClearBoundDriver(ShouldEvaluateImmediately());
hasProcessedFrame = false;
}
public override void OnPlayableDestroy(Playable playable)
{
ClearBoundDriver(ShouldEvaluateImmediately());
hasProcessedFrame = false;
}
private void ClearSubmittedState(bool evaluateImmediately)
{
if (!hasSubmittedState)
return;
if (boundDriver != null)
{
boundDriver.ClearTimelineState(sourceId);
if (evaluateImmediately)
{
#if UNITY_EDITOR
ScheduleEditorFinalEvaluation(boundDriver);
#else
boundDriver.EvaluateNow(true);
#endif
}
}
hasSubmittedState = false;
}
private void ClearBoundDriver(bool evaluateImmediately)
{
ClearSubmittedState(evaluateImmediately);
boundDriver = null;
}
private static bool ShouldEvaluateImmediately()
{
return !Application.isPlaying;
}
#if UNITY_EDITOR
internal static void ScheduleEditorFinalEvaluation(BlendshapeGazeDriver driver)
{
if (driver == null)
return;
PendingEditorEvaluations.Add(driver);
if (editorEvaluationScheduled)
return;
editorEvaluationScheduled = true;
EditorApplication.delayCall += FlushPendingEditorEvaluations;
// Manual/paused Timeline evaluations do not guarantee an ExecuteAlways
// LateUpdate. Request one so the driver gets a post-animation final pass.
EditorApplication.QueuePlayerLoopUpdate();
}
internal static void FlushPendingEditorEvaluations()
{
EditorApplication.delayCall -= FlushPendingEditorEvaluations;
editorEvaluationScheduled = false;
if (PendingEditorEvaluations.Count == 0)
return;
var drivers = new BlendshapeGazeDriver[PendingEditorEvaluations.Count];
PendingEditorEvaluations.CopyTo(drivers);
PendingEditorEvaluations.Clear();
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
var evaluatedAny = false;
for (var index = 0; index < drivers.Length; index++)
{
if (drivers[index] != null && drivers[index].isActiveAndEnabled)
{
drivers[index].EvaluateNow(true);
evaluatedAny = true;
}
}
if (evaluatedAny)
SceneView.RepaintAll();
}
internal static void CancelEditorFinalEvaluation(BlendshapeGazeDriver driver)
{
if (object.ReferenceEquals(driver, null))
return;
PendingEditorEvaluations.Remove(driver);
if (PendingEditorEvaluations.Count > 0 || !editorEvaluationScheduled)
return;
EditorApplication.delayCall -= FlushPendingEditorEvaluations;
editorEvaluationScheduled = false;
}
#endif
}
}