ScheduleEditorFinalEvaluation is defined under UNITY_EDITOR but was called unguarded in ProcessFrame, breaking player builds with CS0103. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
289 lines
10 KiB
C#
289 lines
10 KiB
C#
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using UnityEngine;
|
|
using UnityEngine.Playables;
|
|
#if UNITY_EDITOR
|
|
using UnityEditor;
|
|
#endif
|
|
|
|
namespace Streamingle.Gaze
|
|
{
|
|
/// <summary>
|
|
/// Blends Timeline clip requests into one command for the bound driver.
|
|
/// </summary>
|
|
public sealed class BlendshapeGazeMixerBehaviour : PlayableBehaviour
|
|
{
|
|
private const float MinimumWeight = 0.0001f;
|
|
|
|
private static int nextSourceId;
|
|
#if UNITY_EDITOR
|
|
private static readonly HashSet<BlendshapeGazeDriver> PendingEditorEvaluations =
|
|
new HashSet<BlendshapeGazeDriver>();
|
|
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<BlendshapeGazeBehaviour>)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 UNITY_EDITOR
|
|
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);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|