using System.Collections.Generic; using System.Reflection; using NUnit.Framework; using UnityEditor; using UnityEngine; using UnityEngine.Playables; using UnityEngine.Timeline; namespace Streamingle.Gaze.Tests { public sealed class BlendshapeGazeDriverTests { [Test] public void Driver_OverridesBlendsAndRestoresExternalFacialWeight() { var root = new GameObject("GazeDriverTest"); var originObject = new GameObject("Origin"); var mesh = CreateBlendShapeMesh("eyeLookUpLeft"); var profile = ScriptableObject.CreateInstance(); try { originObject.transform.SetParent(root.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 20f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookUpLeft") }); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 80f }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; driver.SetRuntimeTargetPosition(Vector3.forward, 0.5f); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(50f).Within(0.001f)); // Simulate the facial Animator/receiver writing a new base before LateUpdate. renderer.SetBlendShapeWeight(0, 40f); Assert.That(driver.RebuildCache(), Is.True); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(40f).Within(0.001f)); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(60f).Within(0.001f)); driver.ClearRuntimeTarget(); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(40f).Within(0.001f)); } finally { UnityEngine.Object.DestroyImmediate(root); UnityEngine.Object.DestroyImmediate(mesh); UnityEngine.Object.DestroyImmediate(profile); } } [Test] public void Driver_IgnoresNonFiniteInfluenceWithoutWritingBlendshapes() { var root = new GameObject("GazeNonFiniteInfluenceTest"); var mesh = CreateBlendShapeMesh("eyeLookHorizontal"); var profile = ScriptableObject.CreateInstance(); try { var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 23f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookHorizontal") }); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 80f }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.SetRuntimeTargetPosition(Vector3.forward * 10f, float.NaN); driver.EvaluateNow(true); Assert.That(driver.HasActiveRequest, Is.False); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(23f).Within(0.001f)); } finally { Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void Driver_AddsCorrectiveDeltaWithoutErasingExistingBlink() { var root = new GameObject("GazeCorrectiveTest"); var originObject = new GameObject("Origin"); var mesh = CreateBlendShapeMesh("eyeLookUpLeft", "eyeBlinkLeft"); var profile = ScriptableObject.CreateInstance(); try { originObject.transform.SetParent(root.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 10f); renderer.SetBlendShapeWeight(1, 50f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookUpLeft"), new GazeBlendShapeChannel("eyeBlinkLeft", GazeChannelUsage.Corrective) }); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 80f, 30f }); profile.SetSample(GazeCalibrationPoint.Center, new[] { 80f, 20f }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; var upTarget = GazeDirectionSolver.DirectionFromAngles(0f, profile.MaxPitch); driver.SetRuntimeTargetPosition(upTarget, 1f, true); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(80f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(60f).Within(0.001f)); renderer.SetBlendShapeWeight(1, 70f); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(80f).Within(0.001f)); driver.ClearRuntimeTarget(); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(70f).Within(0.001f)); } finally { UnityEngine.Object.DestroyImmediate(root); UnityEngine.Object.DestroyImmediate(mesh); UnityEngine.Object.DestroyImmediate(profile); } } [Test] public void Driver_UsesAnimatedGazeOriginRotationBeforeHeadReferenceFallback() { var root = new GameObject("AnimatedGazeOriginTest"); var animatedHead = new GameObject("Head"); var originObject = new GameObject("Face_GazeOrigin"); var mesh = CreateBlendShapeMesh("eyeLookHorizontal"); var profile = ScriptableObject.CreateInstance(); try { animatedHead.transform.SetParent(root.transform, false); originObject.transform.SetParent(animatedHead.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 20f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookHorizontal") }); for (var index = 0; index < 9; index++) { var column = index % 3; profile.SetSample( (GazeCalibrationPoint)index, new[] { column == 0 ? 0f : column == 1 ? 50f : 100f }); } var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; // Deliberately keep this fallback fixed. Gaze Origin must win. driver.HeadReference = root.transform; driver.SetRuntimeTargetPosition(Vector3.forward * 10f, 1f); driver.EvaluateNow(true); Assert.That(driver.LastDirection.RawYaw, Is.EqualTo(0f).Within(0.01f)); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(50f).Within(0.001f)); animatedHead.transform.rotation = Quaternion.Euler(0f, 17.5f, 0f); driver.EvaluateNow(true); Assert.That(driver.LastDirection.RawYaw, Is.EqualTo(-17.5f).Within(0.01f)); Assert.That(driver.LastDirection.NormalizedYaw, Is.EqualTo(-0.5f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(25f).Within(0.001f)); } finally { Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void Driver_ResolvesBlendedTargetDirectionFromLatestOriginPosition() { var root = new GameObject("LatestOriginDirectionTest"); var originObject = new GameObject("Face_GazeOrigin"); var mesh = CreateBlendShapeMesh("eyeLookHorizontal"); var profile = ScriptableObject.CreateInstance(); try { originObject.transform.SetParent(root.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookHorizontal") }); for (var index = 0; index < 9; index++) { var column = index % 3; profile.SetSample( (GazeCalibrationPoint)index, new[] { column == 0 ? 0f : column == 1 ? 50f : 100f }); } var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; var direction = GazeDirectionSolver.DirectionFromAngles(profile.MaxYaw, 0f); driver.SetTimelineState(7119, new GazeTimelineState { TargetDirection = direction, HasTargetDirection = true, Influence = 1f }); // Simulate an Animation Track moving the head after the gaze mixer // submitted its blended direction. originObject.transform.position = new Vector3(12f, 3f, -7f); driver.EvaluateNow(true); Assert.That(driver.LastDirection.RawYaw, Is.EqualTo(profile.MaxYaw).Within(0.01f)); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(100f).Within(0.001f)); Assert.That( Vector3.Distance( driver.LastTargetPosition, originObject.transform.position + direction.normalized), Is.LessThan(0.0001f)); } finally { Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void Driver_UsesLowestSourceIdWhenTimelineInfluencesAreEqual() { var root = new GameObject("GazeTimelineTieBreakTest"); var mesh = CreateBlendShapeMesh("eyeLookHorizontal"); var profile = ScriptableObject.CreateInstance(); try { var renderer = root.AddComponent(); renderer.sharedMesh = mesh; profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookHorizontal") }); for (var index = 0; index < 9; index++) { var column = index % 3; profile.SetSample( (GazeCalibrationPoint)index, new[] { column == 0 ? 0f : column == 1 ? 50f : 100f }); } var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.SetTimelineState(20, new GazeTimelineState { TargetDirection = GazeDirectionSolver.DirectionFromAngles(profile.MaxYaw, 0f), HasTargetDirection = true, Influence = 1f }); driver.SetTimelineState(10, new GazeTimelineState { TargetDirection = GazeDirectionSolver.DirectionFromAngles(profile.MinYaw, 0f), HasTargetDirection = true, Influence = 1f }); driver.EvaluateNow(true); Assert.That(driver.LastDirection.RawYaw, Is.EqualTo(profile.MinYaw).Within(0.01f)); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(0f).Within(0.001f)); } finally { Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void EditorFinalPass_ComposesOverLatestFacialWrite() { var root = new GameObject("GazeEditorFinalPassTest"); var originObject = new GameObject("Face_GazeOrigin"); var mesh = CreateBlendShapeMesh("eyeLookUpLeft", "unrelatedBlink"); var profile = ScriptableObject.CreateInstance(); try { originObject.transform.SetParent(root.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 20f); renderer.SetBlendShapeWeight(1, 55f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookUpLeft") }); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 80f }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; const int sourceId = 8127; driver.SetTimelineState(sourceId, new GazeTimelineState { TargetPosition = Vector3.forward * 10f, HasTargetPosition = true, Influence = 0.5f }); // Timeline submits gaze first. The Animation/Facial output writes // later in the same editor evaluation, before the queued final pass. BlendshapeGazeMixerBehaviour.ScheduleEditorFinalEvaluation(driver); root.transform.rotation = Quaternion.Euler(0f, 17.5f, 0f); renderer.SetBlendShapeWeight(0, 40f); renderer.SetBlendShapeWeight(1, 70f); BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Assert.That(driver.LastDirection.RawYaw, Is.EqualTo(-17.5f).Within(0.01f)); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(60f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(70f).Within(0.001f)); BlendshapeGazeMixerBehaviour.ScheduleEditorFinalEvaluation(driver); renderer.SetBlendShapeWeight(0, 10f); renderer.SetBlendShapeWeight(1, 35f); BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(45f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(35f).Within(0.001f)); driver.ClearTimelineState(sourceId); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(10f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(35f).Within(0.001f)); } finally { BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void EditorLateUpdate_ReappliesAfterAFacialWriteWithoutAnotherTimelineFrame() { var root = new GameObject("GazeEditorLateUpdateTest"); var originObject = new GameObject("Face_GazeOrigin"); var mesh = CreateBlendShapeMesh("eyeLookHorizontal"); var profile = ScriptableObject.CreateInstance(); try { originObject.transform.SetParent(root.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 20f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookHorizontal") }); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 80f }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; const int sourceId = 9231; driver.SetTimelineState(sourceId, new GazeTimelineState { TargetPosition = Vector3.forward * 10f, HasTargetPosition = true, Influence = 0.5f }); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(50f).Within(0.001f)); // A later editor writer overwrites gaze without causing another // Timeline ProcessFrame. ExecuteAlways LateUpdate must still win. renderer.SetBlendShapeWeight(0, 10f); originObject.transform.rotation = Quaternion.Euler(0f, 17.5f, 0f); InvokePrivateLateUpdate(driver); Assert.That(driver.LastDirection.RawYaw, Is.EqualTo(-17.5f).Within(0.01f)); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(45f).Within(0.001f)); driver.ClearTimelineState(sourceId); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(10f).Within(0.001f)); } finally { Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void TimelinePreparation_CapturesEqualFacialValueWithoutFeedback() { var root = new GameObject("GazeEditorBaselineCollisionTest"); var originObject = new GameObject("Face_GazeOrigin"); var mesh = CreateBlendShapeMesh("animatedEye", "unanimatedEye"); var profile = ScriptableObject.CreateInstance(); try { originObject.transform.SetParent(root.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 80f); renderer.SetBlendShapeWeight(1, 20f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("animatedEye"), new GazeBlendShapeChannel("unanimatedEye") }); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 0f, 80f }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; const int sourceId = 9232; driver.SetTimelineState(sourceId, new GazeTimelineState { TargetPosition = Vector3.forward * 10f, HasTargetPosition = true, Influence = 0.5f }); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(40f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(50f).Within(0.001f)); driver.PrepareForTimelineEvaluation(); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(80f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(20f).Within(0.001f)); // The next facial sample happens to equal the previous gaze result. renderer.SetBlendShapeWeight(0, 40f); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 100f, 80f }); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(70f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(50f).Within(0.001f)); for (var iteration = 0; iteration < 5; iteration++) { driver.PrepareForTimelineEvaluation(); renderer.SetBlendShapeWeight(0, 40f); driver.EvaluateNow(true); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(70f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(50f).Within(0.001f)); } driver.ClearTimelineState(sourceId); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(40f).Within(0.001f)); Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(20f).Within(0.001f)); } finally { Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void DisablingDriver_CancelsQueuedEditorFinalEvaluation() { var root = new GameObject("GazeEditorCancellationTest"); var originObject = new GameObject("Face_GazeOrigin"); var mesh = CreateBlendShapeMesh("eyeLookHorizontal"); var profile = ScriptableObject.CreateInstance(); try { originObject.transform.SetParent(root.transform, false); var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 23f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookHorizontal") }); for (var index = 0; index < 9; index++) profile.SetSample((GazeCalibrationPoint)index, new[] { 80f }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; driver.SetTimelineState(9233, new GazeTimelineState { TargetPosition = Vector3.forward * 10f, HasTargetPosition = true, Influence = 1f }); BlendshapeGazeMixerBehaviour.ScheduleEditorFinalEvaluation(driver); driver.enabled = false; BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(23f).Within(0.001f)); Assert.That(driver.HasActiveRequest, Is.False); } finally { BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); } } [Test] public void GatherProperties_RegistersResolvedBlendShapeAsAnimationBinding() { var root = new GameObject("GazeGatherPropertiesTest"); var mesh = CreateBlendShapeMesh("Face.eyeLookUpLeft"); var profile = ScriptableObject.CreateInstance(); var track = ScriptableObject.CreateInstance(); try { var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 37f); profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("eyeLookUpLeft") }); var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; var director = root.AddComponent(); director.SetGenericBinding(track, driver); var collector = new RecordingPropertyCollector(); track.GatherProperties(director, collector); Assert.That(collector.CurveProperties, Has.Count.EqualTo(1)); Assert.That(collector.CurveProperties[0].Root, Is.SameAs(root)); Assert.That(collector.CurveProperties[0].Binding.path, Is.Empty); Assert.That(collector.CurveProperties[0].Binding.type, Is.EqualTo(typeof(SkinnedMeshRenderer))); Assert.That( collector.CurveProperties[0].Binding.propertyName, Is.EqualTo("blendShape.Face.eyeLookUpLeft")); // Exercise Timeline's real internal PropertyCollector. Unexpected // DrivenPropertyManager errors fail the Unity test automatically. // Use Timeline's own preview driver, while preserving any session // already owned by an open Timeline/Animation window. var previewDriver = GetTimelinePreviewDriver(); if (!AnimationMode.InAnimationMode()) { var timelineCollector = CreateTimelinePropertyCollector(); AnimationMode.StartAnimationMode(previewDriver); try { track.GatherProperties(director, timelineCollector); renderer.SetBlendShapeWeight(0, 80f); } finally { AnimationMode.StopAnimationMode(previewDriver); } Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(37f).Within(0.001f)); } } finally { Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); Object.DestroyImmediate(track); } } [Test] public void ManualTimelineEvaluation_ComposesFacialAnimationAcrossRepeatedScrubs() { RunManualTimelineBaselineCollision(gazeTrackFirst: false); RunManualTimelineBaselineCollision(gazeTrackFirst: true); } private static void RunManualTimelineBaselineCollision(bool gazeTrackFirst) { var root = new GameObject(gazeTrackFirst ? "GazeFirstManualTimelineTest" : "AnimationFirstManualTimelineTest"); var originObject = new GameObject("Face_GazeOrigin"); var leftTarget = new GameObject("LeftTarget"); var rightTarget = new GameObject("RightTarget"); var mesh = CreateBlendShapeMesh("animatedEye"); var profile = ScriptableObject.CreateInstance(); var timeline = ScriptableObject.CreateInstance(); var facialClip = new AnimationClip { name = "FacialBaseline" }; try { originObject.transform.SetParent(root.transform, false); leftTarget.transform.SetParent(root.transform, false); rightTarget.transform.SetParent(root.transform, false); leftTarget.transform.position = GazeDirectionSolver.DirectionFromAngles(profile.MinYaw, 0f) * 10f; rightTarget.transform.position = GazeDirectionSolver.DirectionFromAngles(profile.MaxYaw, 0f) * 10f; var renderer = root.AddComponent(); renderer.sharedMesh = mesh; renderer.SetBlendShapeWeight(0, 80f); var animator = root.AddComponent(); animator.cullingMode = AnimatorCullingMode.AlwaysAnimate; profile.ReplaceChannels(new[] { new GazeBlendShapeChannel("animatedEye") }); for (var index = 0; index < 9; index++) { var column = index % 3; profile.SetSample( (GazeCalibrationPoint)index, new[] { column == 0 ? 0f : column == 1 ? 50f : 100f }); } var driver = root.AddComponent(); driver.TargetRenderer = renderer; driver.Profile = profile; driver.GazeOrigin = originObject.transform; AnimationTrack animationTrack; BlendshapeGazeTrack gazeTrack; if (gazeTrackFirst) { gazeTrack = timeline.CreateTrack(null, "Gaze"); animationTrack = timeline.CreateTrack(null, "Facial"); } else { animationTrack = timeline.CreateTrack(null, "Facial"); gazeTrack = timeline.CreateTrack(null, "Gaze"); } facialClip.SetCurve( string.Empty, typeof(SkinnedMeshRenderer), "blendShape.animatedEye", new AnimationCurve( new Keyframe(0f, 80f), new Keyframe(0.25f, 80f), new Keyframe(0.75f, 40f), new Keyframe(1f, 40f))); var facialTimelineClip = animationTrack.CreateClip(facialClip); facialTimelineClip.start = 0d; facialTimelineClip.duration = 1d; var leftClip = gazeTrack.CreateClip(); leftClip.start = 0d; leftClip.duration = 0.5d; var leftAsset = (BlendshapeGazeClip)leftClip.asset; leftAsset.Target = new ExposedReference { defaultValue = leftTarget.transform }; leftAsset.Strength = 0.5f; var rightClip = gazeTrack.CreateClip(); rightClip.start = 0.5d; rightClip.duration = 0.5d; var rightAsset = (BlendshapeGazeClip)rightClip.asset; rightAsset.Target = new ExposedReference { defaultValue = rightTarget.transform }; rightAsset.Strength = 0.5f; var director = root.AddComponent(); director.timeUpdateMode = DirectorUpdateMode.Manual; director.playableAsset = timeline; director.SetGenericBinding(animationTrack, animator); director.SetGenericBinding(gazeTrack, driver); director.time = 0.25d; director.Evaluate(); BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(40f).Within(0.01f)); director.time = 0.75d; director.Evaluate(); BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(70f).Within(0.01f)); for (var iteration = 0; iteration < 5; iteration++) { director.Evaluate(); BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(70f).Within(0.01f)); } director.time = 0.25d; director.Evaluate(); BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(40f).Within(0.01f)); } finally { BlendshapeGazeMixerBehaviour.FlushPendingEditorEvaluations(); Object.DestroyImmediate(root); Object.DestroyImmediate(mesh); Object.DestroyImmediate(profile); Object.DestroyImmediate(facialClip); Object.DestroyImmediate(timeline); } } private static void InvokePrivateLateUpdate(BlendshapeGazeDriver driver) { var method = typeof(BlendshapeGazeDriver).GetMethod( "LateUpdate", BindingFlags.Instance | BindingFlags.NonPublic); Assert.That(method, Is.Not.Null); method.Invoke(driver, null); } private static IPropertyCollector CreateTimelinePropertyCollector() { foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { var type = assembly.GetType("UnityEditor.Timeline.PropertyCollector"); if (type != null) return (IPropertyCollector)System.Activator.CreateInstance(type, true); } Assert.Fail("Unity Timeline PropertyCollector type was not loaded."); return null; } private static AnimationModeDriver GetTimelinePreviewDriver() { foreach (var assembly in System.AppDomain.CurrentDomain.GetAssemblies()) { var type = assembly.GetType("UnityEditor.Timeline.WindowState"); var property = type?.GetProperty( "previewDriver", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic); if (property?.GetValue(null) is AnimationModeDriver previewDriver) return previewDriver; } Assert.Fail("Unity Timeline preview driver was not loaded."); return null; } private sealed class RecordingPropertyCollector : IPropertyCollector { internal readonly List CurveProperties = new List(); public void PushActiveGameObject(GameObject gameObject) { } public void PopActiveGameObject() { } public void AddFromClip(AnimationClip clip) { } public void AddFromClips(IEnumerable clips) { } public void AddFromName(string name) where T : Component { } public void AddFromName(string name) { } public void AddFromClip(GameObject obj, AnimationClip clip) { var bindings = AnimationUtility.GetCurveBindings(clip); for (var index = 0; index < bindings.Length; index++) CurveProperties.Add(new CurveProperty(obj, bindings[index])); } public void AddFromClips(GameObject obj, IEnumerable clips) { } public void AddFromName(GameObject obj, string name) where T : Component { } public void AddFromName(GameObject obj, string name) { } public void AddFromName(Component component, string name) { } public void AddFromComponent(GameObject obj, Component component) { } public void AddObjectProperties(Object obj, AnimationClip clip) { } } private readonly struct CurveProperty { internal CurveProperty(GameObject root, EditorCurveBinding binding) { Root = root; Binding = binding; } internal GameObject Root { get; } internal EditorCurveBinding Binding { get; } } private static Mesh CreateBlendShapeMesh(params string[] blendShapeNames) { var mesh = new Mesh { name = "GazeDriverTestMesh" }; mesh.vertices = new[] { new Vector3(-0.1f, 0f, 0f), new Vector3(0.1f, 0f, 0f), new Vector3(0f, 0.1f, 0f) }; mesh.triangles = new[] { 0, 1, 2 }; for (var index = 0; index < blendShapeNames.Length; index++) { mesh.AddBlendShapeFrame( blendShapeNames[index], 100f, new[] { Vector3.up * 0.01f, Vector3.up * 0.01f, Vector3.up * 0.01f }, new Vector3[3], new Vector3[3]); } return mesh; } } }