feat: initialize Streamingle Utilities package
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
|
||||
namespace Streamingle.Gaze
|
||||
{
|
||||
/// <summary>
|
||||
/// Runtime data copied from a <see cref="BlendshapeGazeClip"/>.
|
||||
/// </summary>
|
||||
public sealed class BlendshapeGazeBehaviour : PlayableBehaviour
|
||||
{
|
||||
public Transform Target { get; internal set; }
|
||||
|
||||
public float Strength { get; internal set; }
|
||||
|
||||
public bool EnableCorrectives { get; internal set; }
|
||||
|
||||
public bool OverrideCutResponse { get; internal set; }
|
||||
|
||||
public GazeCutResponse CutResponse { get; internal set; }
|
||||
|
||||
public float CutTransitionDuration { get; internal set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 07c9f3da83d6a724b91c78d798091a03
|
||||
@@ -0,0 +1,104 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace Streamingle.Gaze
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes a camera/target gaze request on a Timeline track.
|
||||
/// The track mixer combines requests and submits the result to a
|
||||
/// <see cref="BlendshapeGazeDriver"/>; clips never write blendshapes directly.
|
||||
/// </summary>
|
||||
public sealed class BlendshapeGazeClip : PlayableAsset, ITimelineClipAsset
|
||||
{
|
||||
[SerializeField]
|
||||
[Tooltip("Optional world-space target. When empty, the Driver Target Camera is used before Camera.main.")]
|
||||
private ExposedReference<Transform> target;
|
||||
|
||||
[SerializeField, Range(0f, 1f)]
|
||||
[Tooltip("Maximum gaze influence contributed by this clip.")]
|
||||
private float strength = 1f;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Allow optional eyelid/corrective channels from the calibration profile.")]
|
||||
private bool enableCorrectives;
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Override the bound driver's camera-cut response while this clip is dominant.")]
|
||||
private bool overrideCutResponse;
|
||||
|
||||
[SerializeField]
|
||||
private GazeCutResponse cutResponse;
|
||||
|
||||
[SerializeField, Min(0f)]
|
||||
[Tooltip("Transition duration used by cut-response modes that smooth between cameras.")]
|
||||
private float cutTransitionDuration = 0.1f;
|
||||
|
||||
public ExposedReference<Transform> Target
|
||||
{
|
||||
get => target;
|
||||
set => target = value;
|
||||
}
|
||||
|
||||
public float Strength
|
||||
{
|
||||
get => strength;
|
||||
set => strength = SanitizeStrength(value);
|
||||
}
|
||||
|
||||
public bool EnableCorrectives
|
||||
{
|
||||
get => enableCorrectives;
|
||||
set => enableCorrectives = value;
|
||||
}
|
||||
|
||||
public bool OverrideCutResponse
|
||||
{
|
||||
get => overrideCutResponse;
|
||||
set => overrideCutResponse = value;
|
||||
}
|
||||
|
||||
public GazeCutResponse CutResponse
|
||||
{
|
||||
get => cutResponse;
|
||||
set => cutResponse = value;
|
||||
}
|
||||
|
||||
public float CutTransitionDuration
|
||||
{
|
||||
get => cutTransitionDuration;
|
||||
set => cutTransitionDuration = SanitizeDuration(value);
|
||||
}
|
||||
|
||||
public ClipCaps clipCaps => ClipCaps.Blending;
|
||||
|
||||
public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
|
||||
{
|
||||
var playable = ScriptPlayable<BlendshapeGazeBehaviour>.Create(graph);
|
||||
var behaviour = playable.GetBehaviour();
|
||||
|
||||
behaviour.Target = target.Resolve(graph.GetResolver());
|
||||
behaviour.Strength = SanitizeStrength(strength);
|
||||
behaviour.EnableCorrectives = enableCorrectives;
|
||||
behaviour.OverrideCutResponse = overrideCutResponse;
|
||||
behaviour.CutResponse = cutResponse;
|
||||
behaviour.CutTransitionDuration = SanitizeDuration(cutTransitionDuration);
|
||||
|
||||
return playable;
|
||||
}
|
||||
|
||||
private static float SanitizeStrength(float value)
|
||||
{
|
||||
return float.IsNaN(value) || float.IsInfinity(value)
|
||||
? 0f
|
||||
: Mathf.Clamp01(value);
|
||||
}
|
||||
|
||||
private static float SanitizeDuration(float value)
|
||||
{
|
||||
return float.IsNaN(value) || float.IsInfinity(value)
|
||||
? 0f
|
||||
: Mathf.Max(0f, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3df39f801d7538f46933258e013ceb96
|
||||
@@ -0,0 +1,286 @@
|
||||
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 (!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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84259821b954cf84f853e4a88b17e80d
|
||||
@@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Playables;
|
||||
using UnityEngine.Timeline;
|
||||
|
||||
namespace Streamingle.Gaze
|
||||
{
|
||||
[TrackColor(0.23f, 0.74f, 0.92f)]
|
||||
[TrackBindingType(typeof(BlendshapeGazeDriver))]
|
||||
[TrackClipType(typeof(BlendshapeGazeClip))]
|
||||
public sealed class BlendshapeGazeTrack : TrackAsset
|
||||
{
|
||||
public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
|
||||
{
|
||||
return ScriptPlayable<BlendshapeGazeMixerBehaviour>.Create(graph, inputCount);
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
public override void GatherProperties(PlayableDirector director, IPropertyCollector collector)
|
||||
{
|
||||
var gazeDriver = director != null
|
||||
? director.GetGenericBinding(this) as BlendshapeGazeDriver
|
||||
: null;
|
||||
|
||||
var targetRenderer = gazeDriver != null ? gazeDriver.TargetRenderer : null;
|
||||
var profile = gazeDriver != null ? gazeDriver.Profile : null;
|
||||
var channels = profile != null ? profile.Channels : null;
|
||||
var mesh = targetRenderer != null ? targetRenderer.sharedMesh : null;
|
||||
if (targetRenderer != null && mesh != null && channels != null)
|
||||
{
|
||||
var channelNames = new HashSet<string>();
|
||||
var previewBindings = new AnimationClip
|
||||
{
|
||||
name = "Blendshape Gaze Preview Bindings",
|
||||
hideFlags = HideFlags.HideAndDontSave
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
for (var i = 0; i < channels.Count; i++)
|
||||
{
|
||||
var channel = channels[i];
|
||||
if (object.ReferenceEquals(channel, null))
|
||||
continue;
|
||||
|
||||
var blendShapeIndex = profile.FindBlendShapeIndex(mesh, i);
|
||||
if (blendShapeIndex < 0)
|
||||
continue;
|
||||
|
||||
// AddFromName expects a SerializedProperty path, while
|
||||
// blendShape.* is a synthetic Animator curve binding.
|
||||
// Register it through a clip so Timeline's AnimationMode
|
||||
// resolves and restores the real renderer property.
|
||||
var channelName = mesh.GetBlendShapeName(blendShapeIndex);
|
||||
if (string.IsNullOrWhiteSpace(channelName) || !channelNames.Add(channelName))
|
||||
continue;
|
||||
|
||||
previewBindings.SetCurve(
|
||||
string.Empty,
|
||||
typeof(SkinnedMeshRenderer),
|
||||
"blendShape." + channelName,
|
||||
AnimationCurve.Constant(0f, 1f, 0f));
|
||||
}
|
||||
|
||||
if (channelNames.Count > 0)
|
||||
collector.AddFromClip(targetRenderer.gameObject, previewBindings);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Object.DestroyImmediate(previewBindings);
|
||||
}
|
||||
}
|
||||
|
||||
base.GatherProperties(director, collector);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c2beb3d591978e49bbc25526fa7fc51
|
||||
Reference in New Issue
Block a user