866 lines
38 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|