251 lines
10 KiB
C#
251 lines
10 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|