feat: initialize Streamingle Utilities package
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 152fec42be1e1c549ad14303896b3ecd
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user