streamingle-unity-utilities/Tests/Editor/Gaze/BlendshapeGazeDriverTests.cs

866 lines
38 KiB
C#

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<BlendshapeGazeProfile>();
try
{
originObject.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
originObject.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
animatedHead.transform.SetParent(root.transform, false);
originObject.transform.SetParent(animatedHead.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
originObject.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
originObject.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
originObject.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
originObject.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
try
{
originObject.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
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<BlendshapeGazeDriver>();
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<BlendshapeGazeProfile>();
var track = ScriptableObject.CreateInstance<BlendshapeGazeTrack>();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.SetBlendShapeWeight(0, 37f);
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
var driver = root.AddComponent<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
var director = root.AddComponent<PlayableDirector>();
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<BlendshapeGazeProfile>();
var timeline = ScriptableObject.CreateInstance<TimelineAsset>();
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<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.SetBlendShapeWeight(0, 80f);
var animator = root.AddComponent<Animator>();
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<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
driver.GazeOrigin = originObject.transform;
AnimationTrack animationTrack;
BlendshapeGazeTrack gazeTrack;
if (gazeTrackFirst)
{
gazeTrack = timeline.CreateTrack<BlendshapeGazeTrack>(null, "Gaze");
animationTrack = timeline.CreateTrack<AnimationTrack>(null, "Facial");
}
else
{
animationTrack = timeline.CreateTrack<AnimationTrack>(null, "Facial");
gazeTrack = timeline.CreateTrack<BlendshapeGazeTrack>(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<BlendshapeGazeClip>();
leftClip.start = 0d;
leftClip.duration = 0.5d;
var leftAsset = (BlendshapeGazeClip)leftClip.asset;
leftAsset.Target = new ExposedReference<Transform>
{
defaultValue = leftTarget.transform
};
leftAsset.Strength = 0.5f;
var rightClip = gazeTrack.CreateClip<BlendshapeGazeClip>();
rightClip.start = 0.5d;
rightClip.duration = 0.5d;
var rightAsset = (BlendshapeGazeClip)rightClip.asset;
rightAsset.Target = new ExposedReference<Transform>
{
defaultValue = rightTarget.transform
};
rightAsset.Strength = 0.5f;
var director = root.AddComponent<PlayableDirector>();
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<CurveProperty> CurveProperties =
new List<CurveProperty>();
public void PushActiveGameObject(GameObject gameObject) { }
public void PopActiveGameObject() { }
public void AddFromClip(AnimationClip clip) { }
public void AddFromClips(IEnumerable<AnimationClip> clips) { }
public void AddFromName<T>(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<AnimationClip> clips) { }
public void AddFromName<T>(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;
}
}
}