feat: initialize Streamingle Utilities package

This commit is contained in:
KINDNICK
2026-07-23 22:43:07 +09:00
commit e9bbf8481a
107 changed files with 9746 additions and 0 deletions
@@ -0,0 +1,865 @@
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;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 37c10ba76e48457438d8da1d6673d1f8
@@ -0,0 +1,224 @@
using NUnit.Framework;
using Streamingle.Gaze.Editor;
using UnityEngine;
namespace Streamingle.Gaze.Tests
{
public sealed class GazeCalibrationPreviewSessionTests
{
[Test]
public void Session_UsesRuntimeBlendSemantics_BlocksDriverAndRestoresWeights()
{
var root = new GameObject("CalibrationPreviewSessionTest");
var mesh = CreateMesh();
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var session = new GazeCalibrationPreviewSession();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.SetBlendShapeWeight(0, 25f);
renderer.SetBlendShapeWeight(1, 30f);
renderer.SetBlendShapeWeight(2, 45f);
renderer.SetBlendShapeWeight(3, 55f);
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft"),
new GazeBlendShapeChannel(
"eyeCorrectiveAdd",
GazeChannelUsage.Corrective,
GazeCorrectiveBlendMode.AdditiveFromCenter),
new GazeBlendShapeChannel(
"eyeCorrectiveOverride",
GazeChannelUsage.Corrective,
GazeCorrectiveBlendMode.Override)
});
for (var index = 0; index < 9; index++)
{
profile.SetSample(
(GazeCalibrationPoint)index,
new[]
{
index * 10f,
10f + index * 2f,
100f - index * 5f
});
}
var driver = root.AddComponent<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
var gazeOrigin = new GameObject("GazeOrigin").transform;
gazeOrigin.SetParent(root.transform, false);
driver.GazeOrigin = gazeOrigin;
Assert.That(session.Start(driver, out var error), Is.True, error);
Assert.That(session.GetOriginalWeightsCopy(), Is.EqualTo(new[] { 25f, 30f, 45f }));
session.PreviewSample(GazeCalibrationPoint.UpRight);
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(80f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(38f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(60f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(3), Is.EqualTo(55f).Within(0.001f));
session.PreviewWeights(new[] { 67f, 23f, 72f }, GazeCalibrationPoint.CenterRight);
Assert.That(session.IsFreePreview, Is.False);
Assert.That(session.PreviewCoordinates, Is.EqualTo(Vector2.right));
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(67f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(35f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(72f).Within(0.001f));
// The four surrounding samples are deliberately distinct. At
// (0.5, -0.5), indices 1/2/4/5 contribute equally.
session.PreviewNormalized(new Vector2(0.5f, -0.5f));
Assert.That(session.IsFreePreview, Is.True);
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(30f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(28f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(85f).Within(0.001f));
driver.SetTimelineState(1234, new GazeTimelineState
{
TargetPosition = new Vector3(1f, 0f, 1f),
HasTargetPosition = true,
Influence = 1f,
EnableCorrectives = true
});
driver.EvaluateNow(true);
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(30f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(28f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(85f).Within(0.001f));
session.Stop();
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(25f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(30f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(45f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(3), Is.EqualTo(55f).Within(0.001f));
// Releasing the session also releases the driver override. A yaw
// beyond the profile limit resolves to CenterRight (sample 5).
driver.EvaluateNow(true);
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(50f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(32f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(2), Is.EqualTo(75f).Within(0.001f));
}
finally
{
session.Dispose();
Object.DestroyImmediate(root);
Object.DestroyImmediate(mesh);
Object.DestroyImmediate(profile);
}
}
[Test]
public void Session_DoesNotRestoreIntoAnIndexWhoseResolvedNameChanged()
{
var root = new GameObject("CalibrationPreviewResolvedNameTest");
var mesh = CreateMesh();
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var session = new GazeCalibrationPreviewSession();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.SetBlendShapeWeight(0, 25f);
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;
Assert.That(session.Start(driver, out var error), Is.True, error);
session.PreviewSample(GazeCalibrationPoint.Center);
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(80f).Within(0.001f));
mesh.ClearBlendShapes();
AddBlendShape(mesh, "replacementShape");
renderer.SetBlendShapeWeight(0, 66f);
session.Stop();
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(66f).Within(0.001f));
}
finally
{
session.Dispose();
Object.DestroyImmediate(root);
Object.DestroyImmediate(mesh);
Object.DestroyImmediate(profile);
}
}
[Test]
public void Session_AdditiveDraftUsesOriginalWeightUntilCenterIsCaptured()
{
var root = new GameObject("CalibrationPreviewDraftCorrectiveTest");
var mesh = CreateMesh();
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var session = new GazeCalibrationPreviewSession();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.SetBlendShapeWeight(1, 30f);
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel(
"eyeCorrectiveAdd",
GazeChannelUsage.Corrective,
GazeCorrectiveBlendMode.AdditiveFromCenter)
});
var driver = root.AddComponent<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
Assert.That(session.Start(driver, out var error), Is.True, error);
session.PreviewWeights(new[] { 42f }, GazeCalibrationPoint.UpRight);
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(42f).Within(0.001f));
session.Stop();
Assert.That(renderer.GetBlendShapeWeight(1), Is.EqualTo(30f).Within(0.001f));
}
finally
{
session.Dispose();
Object.DestroyImmediate(root);
Object.DestroyImmediate(mesh);
Object.DestroyImmediate(profile);
}
}
private static Mesh CreateMesh()
{
var mesh = new Mesh { name = "CalibrationPreviewSessionMesh" };
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 };
AddBlendShape(mesh, "Face.eyeLookUpLeft");
AddBlendShape(mesh, "Face.eyeCorrectiveAdd");
AddBlendShape(mesh, "Face.eyeCorrectiveOverride");
AddBlendShape(mesh, "unrelatedFaceShape");
return mesh;
}
private static void AddBlendShape(Mesh mesh, string name)
{
mesh.AddBlendShapeFrame(
name,
100f,
new[] { Vector3.up * 0.01f, Vector3.up * 0.01f, Vector3.up * 0.01f },
new Vector3[3],
new Vector3[3]);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 7ce304fdd09812a49baacc0f0d5c2cba
@@ -0,0 +1,250 @@
using System.Reflection;
using NUnit.Framework;
using Streamingle.Gaze.Editor;
using UnityEngine;
namespace Streamingle.Gaze.Tests
{
public sealed class GazeCalibrationWindowTests
{
[Test]
public void AutoCreateOrigin_ParentsFaceGazeOriginUnderRendererHeadBone()
{
var root = new GameObject("GazeOriginParentTest");
var head = new GameObject("Bip001 Head");
var mesh = CreateMesh();
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();
try
{
head.transform.SetParent(root.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.bones = new[] { head.transform };
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
var driver = root.AddComponent<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
Invoke(window, "SetDriver", driver);
Invoke(window, "AutoCreateOrRecalculateOrigin");
Assert.That(driver.GazeOrigin, Is.Not.Null);
Assert.That(driver.GazeOrigin.name, Is.EqualTo("Face_GazeOrigin"));
Assert.That(driver.GazeOrigin.parent, Is.EqualTo(head.transform));
}
finally
{
if (window != null)
Object.DestroyImmediate(window);
Object.DestroyImmediate(root);
Object.DestroyImmediate(mesh);
Object.DestroyImmediate(profile);
}
}
[Test]
public void AutoCreateOrigin_ReusesExistingHeadChildWhenDriverReferenceIsMissing()
{
var root = new GameObject("GazeOriginReuseTest");
var head = new GameObject("Bip001 Head");
var existingOrigin = new GameObject("Face_GazeOrigin");
var mesh = CreateMesh();
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();
try
{
head.transform.SetParent(root.transform, false);
existingOrigin.transform.SetParent(head.transform, false);
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.bones = new[] { head.transform };
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
var driver = root.AddComponent<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
Invoke(window, "SetDriver", driver);
Invoke(window, "AutoCreateOrRecalculateOrigin");
Assert.That(driver.GazeOrigin, Is.SameAs(existingOrigin.transform));
Assert.That(head.transform.childCount, Is.EqualTo(1));
}
finally
{
if (window != null)
Object.DestroyImmediate(window);
Object.DestroyImmediate(root);
Object.DestroyImmediate(mesh);
Object.DestroyImmediate(profile);
}
}
[Test]
public void SelectingMissingPointsDoesNotCaptureAndCancelRevertsProfile()
{
var root = new GameObject("GazeCalibrationWindowTest");
var mesh = CreateMesh();
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.SetBlendShapeWeight(0, 12f);
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
var origin = new GameObject("GazeOrigin").transform;
origin.SetParent(root.transform, false);
var driver = root.AddComponent<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
driver.GazeOrigin = origin;
Invoke(window, "SetDriver", driver);
Invoke(window, "StartCalibration");
Invoke(window, "SelectPoint", GazeCalibrationPoint.CenterLeft);
Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.False);
Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.CenterLeft), Is.False);
// Guided calibration edits an unsaved working copy. Cancel must
// discard it without ever mutating the original profile asset.
var session = GetPrivateField<GazeCalibrationPreviewSession>(window, "previewSession");
Assert.That(session, Is.Not.Null);
Assert.That(session.Profile, Is.Not.SameAs(profile));
UnityEditor.Undo.RecordObject(session.Profile, "Test Gaze Calibration Edit");
session.Profile.SetSample(GazeCalibrationPoint.CenterLeft, new[] { 77f });
renderer.SetBlendShapeWeight(0, 77f);
Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.CenterLeft), Is.False);
// A mapping/play-mode/reload guard may stop the preview before the
// window repaints. The stop callback must cancel immediately.
session.Stop();
Invoke(window, "FinishCalibration", false, false);
Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.CenterLeft), Is.False);
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(12f).Within(0.001f));
}
finally
{
if (window != null)
{
Invoke(window, "FinishCalibration", false, false);
Object.DestroyImmediate(window);
}
Object.DestroyImmediate(root);
Object.DestroyImmediate(mesh);
Object.DestroyImmediate(profile);
}
}
[Test]
public void ApplyCalibration_CommitsWorkingCopyAndRestoresRendererPose()
{
var root = new GameObject("GazeCalibrationApplyTest");
var mesh = CreateMesh();
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var window = ScriptableObject.CreateInstance<GazeCalibrationWindow>();
try
{
var renderer = root.AddComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.SetBlendShapeWeight(0, 14f);
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
var origin = new GameObject("GazeOrigin").transform;
origin.SetParent(root.transform, false);
var driver = root.AddComponent<BlendshapeGazeDriver>();
driver.TargetRenderer = renderer;
driver.Profile = profile;
driver.GazeOrigin = origin;
Invoke(window, "SetDriver", driver);
Invoke(window, "StartCalibration");
var session = GetPrivateField<GazeCalibrationPreviewSession>(window, "previewSession");
Assert.That(session, Is.Not.Null);
Assert.That(session.Profile, Is.Not.SameAs(profile));
UnityEditor.Undo.RecordObject(session.Profile, "Test Apply Gaze Calibration");
session.Profile.SetSample(GazeCalibrationPoint.Center, new[] { 66f });
session.PreviewSample(GazeCalibrationPoint.Center);
Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.False);
Invoke(window, "FinishCalibration", true, false);
Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.True);
Assert.That(
profile.GetSampleWeight(GazeCalibrationPoint.Center, 0),
Is.EqualTo(66f).Within(0.001f));
Assert.That(renderer.GetBlendShapeWeight(0), Is.EqualTo(14f).Within(0.001f));
}
finally
{
if (window != null)
{
Invoke(window, "FinishCalibration", false, false);
Object.DestroyImmediate(window);
}
Object.DestroyImmediate(root);
Object.DestroyImmediate(mesh);
Object.DestroyImmediate(profile);
}
}
private static void Invoke(object target, string methodName, params object[] arguments)
{
var method = target.GetType().GetMethod(
methodName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(method, Is.Not.Null, $"Expected private method '{methodName}'.");
method.Invoke(target, arguments);
}
private static T GetPrivateField<T>(object target, string fieldName)
where T : class
{
var field = target.GetType().GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, $"Expected private field '{fieldName}'.");
return field.GetValue(target) as T;
}
private static Mesh CreateMesh()
{
var mesh = new Mesh { name = "GazeCalibrationWindowMesh" };
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 };
mesh.AddBlendShapeFrame(
"eyeLookUpLeft",
100f,
new[] { Vector3.up * 0.01f, Vector3.up * 0.01f, Vector3.up * 0.01f },
new Vector3[3],
new Vector3[3]);
return mesh;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: de38c6c9919441369ab6564cd13399f7
@@ -0,0 +1,149 @@
using System;
using System.Collections.Generic;
using NUnit.Framework;
using UnityEngine;
namespace Streamingle.Gaze.Tests
{
public sealed class GazeDirectionSolverTests
{
[Test]
public void Calculate_ClampsAndNormalizesAsymmetricRanges()
{
var result = GazeDirectionSolver.Calculate(
Quaternion.identity,
GazeDirectionSolver.DirectionFromAngles(60f, 30f),
-20f,
40f,
-10f,
20f);
Assert.That(result.RawYaw, Is.EqualTo(60f).Within(0.01f));
Assert.That(result.RawPitch, Is.EqualTo(30f).Within(0.01f));
Assert.That(result.ClampedYaw, Is.EqualTo(40f).Within(0.01f));
Assert.That(result.ClampedPitch, Is.EqualTo(20f).Within(0.01f));
Assert.That(result.NormalizedYaw, Is.EqualTo(1f).Within(0.001f));
Assert.That(result.NormalizedPitch, Is.EqualTo(1f).Within(0.001f));
Assert.That(result.WasClamped, Is.True);
}
[Test]
public void Calculate_UsesCharacterLocalReference()
{
var reference = Quaternion.Euler(5f, 73f, 0f);
var localDirection = GazeDirectionSolver.DirectionFromAngles(-10f, -5f);
var result = GazeDirectionSolver.Calculate(
reference,
reference * localDirection,
-20f,
40f,
-10f,
20f);
Assert.That(result.RawYaw, Is.EqualTo(-10f).Within(0.01f));
Assert.That(result.RawPitch, Is.EqualTo(-5f).Within(0.01f));
Assert.That(result.NormalizedYaw, Is.EqualTo(-0.5f).Within(0.001f));
Assert.That(result.NormalizedPitch, Is.EqualTo(-0.5f).Within(0.001f));
Assert.That(result.WasClamped, Is.False);
}
[Test]
public void Profile_BilinearlyInterpolatesTheThreeByThreeGrid()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
try
{
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
for (var index = 0; index < 9; index++)
profile.SetSample((GazeCalibrationPoint)index, new[] { (float)index });
var output = new float[1];
var direction = DirectionWithNormalizedCoordinates(-0.5f, -0.5f);
Assert.That(profile.TryEvaluate(direction, false, output), Is.True);
// Bottom-left/center cell: lerp of sample values 0,1,3,4.
Assert.That(output[0], Is.EqualTo(2f).Within(0.001f));
direction = DirectionWithNormalizedCoordinates(0.5f, 0.5f);
Assert.That(profile.TryEvaluate(direction, false, output), Is.True);
// Center/top-right cell: lerp of sample values 4,5,7,8.
Assert.That(output[0], Is.EqualTo(6f).Within(0.001f));
}
finally
{
UnityEngine.Object.DestroyImmediate(profile);
}
}
[Test]
public void Profile_ExcludesCorrectiveChannelsUnlessRequested()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
try
{
profile.ReplaceChannels(new List<GazeBlendShapeChannel>
{
new GazeBlendShapeChannel("eyeLookUpLeft"),
new GazeBlendShapeChannel("eyeBlinkLeft", GazeChannelUsage.Corrective)
});
for (var index = 0; index < 9; index++)
profile.SetSample((GazeCalibrationPoint)index, new[] { 10f, 20f });
var output = new float[2];
Assert.That(profile.TryEvaluate(DirectionWithNormalizedCoordinates(0f, 0f), false, output), Is.True);
Assert.That(output[0], Is.EqualTo(10f));
Assert.That(float.IsNaN(output[1]), Is.True);
Assert.That(profile.TryEvaluate(DirectionWithNormalizedCoordinates(0f, 0f), true, output), Is.True);
Assert.That(output[1], Is.EqualTo(20f));
}
finally
{
UnityEngine.Object.DestroyImmediate(profile);
}
}
[Test]
public void Profile_DoesNotEvaluateAnIncompleteCalibration()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
try
{
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
profile.SetSample(GazeCalibrationPoint.Center, new[] { 50f });
Assert.That(
profile.TryEvaluate(
DirectionWithNormalizedCoordinates(0f, 0f),
false,
new float[1]),
Is.False);
}
finally
{
UnityEngine.Object.DestroyImmediate(profile);
}
}
private static GazeDirectionResult DirectionWithNormalizedCoordinates(float yaw, float pitch)
{
return new GazeDirectionResult(
Vector3.forward,
Vector3.forward,
0f,
0f,
0f,
0f,
yaw,
pitch);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 85088138a737dc34f87bd3be06dc45f6
@@ -0,0 +1,227 @@
using System.Reflection;
using NUnit.Framework;
using UnityEngine;
namespace Streamingle.Gaze.Tests
{
public sealed class GazeProfileAuthoringTests
{
private static readonly string[] StandardChannels =
{
"eyeLookUpLeft",
"eyeLookUpRight",
"eyeLookDownLeft",
"eyeLookDownRight",
"eyeLookInLeft",
"eyeLookInRight",
"eyeLookOutLeft",
"eyeLookOutRight"
};
[Test]
public void SeedStandardEyeLook_CreatesFivePointsAndFourCorners()
{
var profile = CreateStandardProfile();
try
{
Assert.That(profile.SeedStandardEyeLook(80f), Is.True);
for (var index = 0; index < 9; index++)
Assert.That(profile.IsSampleCaptured((GazeCalibrationPoint)index), Is.True);
Assert.That(
profile.GetSampleWeightsCopy(GazeCalibrationPoint.Center),
Is.EqualTo(new[] { 0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f }));
Assert.That(
profile.GetSampleWeightsCopy(GazeCalibrationPoint.CenterLeft),
Is.EqualTo(new[] { 0f, 0f, 0f, 0f, 0f, 80f, 80f, 0f }));
Assert.That(
profile.GetSampleWeightsCopy(GazeCalibrationPoint.UpRight),
Is.EqualTo(new[] { 80f, 80f, 0f, 0f, 80f, 0f, 0f, 80f }));
}
finally
{
Object.DestroyImmediate(profile);
}
}
[Test]
public void GenerateCorners_CombinesCardinalDeltasAndClampsByUsage()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
try
{
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft"),
new GazeBlendShapeChannel("eyeLidCorrective", GazeChannelUsage.Corrective)
});
profile.SetSample(GazeCalibrationPoint.Center, new[] { 80f, 0f });
profile.SetSample(GazeCalibrationPoint.CenterLeft, new[] { 100f, 100f });
profile.SetSample(GazeCalibrationPoint.CenterRight, new[] { 0f, -100f });
profile.SetSample(GazeCalibrationPoint.UpCenter, new[] { 100f, 100f });
profile.SetSample(GazeCalibrationPoint.DownCenter, new[] { 0f, -100f });
Assert.That(profile.CanGenerateCorners, Is.True);
Assert.That(profile.GenerateCornersFromCardinals(), Is.True);
Assert.That(
profile.GetSampleWeightsCopy(GazeCalibrationPoint.UpLeft),
Is.EqualTo(new[] { 100f, 100f }));
Assert.That(
profile.GetSampleWeightsCopy(GazeCalibrationPoint.DownRight),
Is.EqualTo(new[] { 0f, -100f }));
}
finally
{
Object.DestroyImmediate(profile);
}
}
[Test]
public void GenerateCorners_UsesCardinalDeltasRelativeToCenter()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
try
{
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookTest")
});
profile.SetSample(GazeCalibrationPoint.Center, new[] { 10f });
profile.SetSample(GazeCalibrationPoint.CenterLeft, new[] { 30f });
profile.SetSample(GazeCalibrationPoint.CenterRight, new[] { 10f });
profile.SetSample(GazeCalibrationPoint.UpCenter, new[] { 40f });
profile.SetSample(GazeCalibrationPoint.DownCenter, new[] { 10f });
Assert.That(profile.GenerateCornersFromCardinals(), Is.True);
Assert.That(
profile.GetSampleWeight(GazeCalibrationPoint.UpLeft, 0),
Is.EqualTo(60f));
}
finally
{
Object.DestroyImmediate(profile);
}
}
[Test]
public void DirectionLimits_SanitizeNonFiniteValuesAndClampToSupportedRanges()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
try
{
profile.SetDirectionLimits(
float.NaN,
float.PositiveInfinity,
float.NegativeInfinity,
float.NaN);
Assert.That(profile.MinYaw, Is.EqualTo(-35f));
Assert.That(profile.MaxYaw, Is.EqualTo(35f));
Assert.That(profile.MinPitch, Is.EqualTo(-25f));
Assert.That(profile.MaxPitch, Is.EqualTo(20f));
profile.SetDirectionLimits(-180f, -5f, 10f, 180f);
Assert.That(profile.MinYaw, Is.EqualTo(-90f));
Assert.That(profile.MaxYaw, Is.EqualTo(0.1f));
Assert.That(profile.MinPitch, Is.EqualTo(-0.1f));
Assert.That(profile.MaxPitch, Is.EqualTo(90f));
SetPrivateField(profile, "minYaw", float.PositiveInfinity);
SetPrivateField(profile, "maxYaw", float.NegativeInfinity);
SetPrivateField(profile, "minPitch", float.NaN);
SetPrivateField(profile, "maxPitch", float.NaN);
InvokeOnValidate(profile);
Assert.That(profile.MinYaw, Is.EqualTo(-35f));
Assert.That(profile.MaxYaw, Is.EqualTo(35f));
Assert.That(profile.MinPitch, Is.EqualTo(-25f));
Assert.That(profile.MaxPitch, Is.EqualTo(20f));
}
finally
{
Object.DestroyImmediate(profile);
}
}
[Test]
public void CopySample_WithMirrorSwapsLeftAndRightChannels()
{
var profile = CreateStandardProfile();
try
{
profile.SetSample(
GazeCalibrationPoint.CenterLeft,
new[] { 1f, 2f, 3f, 4f, 5f, 6f, 7f, 8f });
Assert.That(
profile.CopySample(
GazeCalibrationPoint.CenterLeft,
GazeCalibrationPoint.CenterRight,
true),
Is.True);
Assert.That(
profile.GetSampleWeightsCopy(GazeCalibrationPoint.CenterRight),
Is.EqualTo(new[] { 2f, 1f, 4f, 3f, 6f, 5f, 8f, 7f }));
}
finally
{
Object.DestroyImmediate(profile);
}
}
[Test]
public void SetSample_RejectsNonFiniteWeights()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
try
{
profile.ReplaceChannels(new[]
{
new GazeBlendShapeChannel("eyeLookUpLeft")
});
Assert.Throws<System.ArgumentException>(() =>
profile.SetSample(GazeCalibrationPoint.Center, new[] { float.NaN }));
Assert.Throws<System.ArgumentException>(() =>
profile.SetSample(GazeCalibrationPoint.Center, new[] { float.PositiveInfinity }));
Assert.That(profile.IsSampleCaptured(GazeCalibrationPoint.Center), Is.False);
}
finally
{
Object.DestroyImmediate(profile);
}
}
private static BlendshapeGazeProfile CreateStandardProfile()
{
var profile = ScriptableObject.CreateInstance<BlendshapeGazeProfile>();
var channels = new GazeBlendShapeChannel[StandardChannels.Length];
for (var index = 0; index < channels.Length; index++)
channels[index] = new GazeBlendShapeChannel(StandardChannels[index]);
profile.ReplaceChannels(channels);
return profile;
}
private static void SetPrivateField(
BlendshapeGazeProfile profile,
string fieldName,
float value)
{
var field = typeof(BlendshapeGazeProfile).GetField(
fieldName,
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(field, Is.Not.Null, $"Expected private field '{fieldName}'.");
field.SetValue(profile, value);
}
private static void InvokeOnValidate(BlendshapeGazeProfile profile)
{
var method = typeof(BlendshapeGazeProfile).GetMethod(
"OnValidate",
BindingFlags.Instance | BindingFlags.NonPublic);
Assert.That(method, Is.Not.Null, "Expected private OnValidate method.");
method.Invoke(profile, null);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: f18f5068667f845489a27afe1e360737
@@ -0,0 +1,24 @@
{
"name": "Streamingle.Gaze.Tests.Editor",
"rootNamespace": "Streamingle.Gaze.Tests",
"references": [
"Streamingle.Gaze.Runtime",
"Streamingle.Gaze.Editor",
"Unity.Timeline"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": true,
"precompiledReferences": [
"nunit.framework.dll"
],
"autoReferenced": false,
"defineConstraints": [
"UNITY_INCLUDE_TESTS"
],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: fa6dac9989212e44585a6d0be5e542d4
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: