using System; using System.Collections.Generic; using UnityEditor; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.SceneManagement; namespace Streamingle.Gaze.Editor { /// /// Owns the temporary blendshape writes used by the calibration UI. /// The session never modifies the profile or scene and always attempts to /// restore the weights that were present when it started. /// internal sealed class GazeCalibrationPreviewSession : IDisposable { private int[] blendShapeIndices = Array.Empty(); private string[] resolvedBlendShapeNames = Array.Empty(); private float[] originalWeights = Array.Empty(); private float[] previewWeights = Array.Empty(); private Mesh sourceMesh; private bool hasPreview; private bool eventsSubscribed; private bool driverOverrideAcquired; private BlendshapeGazeProfile driverProfile; internal event Action Stopped; public bool IsActive { get; private set; } public BlendshapeGazeDriver Driver { get; private set; } public SkinnedMeshRenderer Renderer { get; private set; } public BlendshapeGazeProfile Profile { get; private set; } public Vector2 PreviewCoordinates { get; private set; } public bool IsFreePreview { get; private set; } public bool Start(BlendshapeGazeDriver driver, out string error) { return Start(driver, null, out error); } internal bool Start( BlendshapeGazeDriver driver, BlendshapeGazeProfile previewProfile, out string error) { Stop(); error = null; if (Application.isPlaying || EditorApplication.isPlayingOrWillChangePlaymode) { error = "Calibration preview is only available in Edit Mode."; return false; } if (driver == null) { error = "A Blendshape Gaze Driver is required."; return false; } var renderer = driver.TargetRenderer; if (renderer == null) { error = "The driver does not have a Target Renderer."; return false; } var mesh = renderer.sharedMesh; if (mesh == null) { error = "The Target Renderer does not have a shared mesh."; return false; } var assignedProfile = driver.Profile; if (assignedProfile == null) { error = "The driver does not have a Gaze Profile."; return false; } var profile = previewProfile != null ? previewProfile : assignedProfile; if (!profile.IsCompatibleWith(mesh)) { error = "The Gaze Profile was calibrated for a different source mesh."; return false; } var channels = profile.Channels; if (channels == null || channels.Count == 0) { error = "The Gaze Profile does not contain any blendshape channels."; return false; } var indices = new int[channels.Count]; var names = new string[channels.Count]; var originals = new float[channels.Count]; for (var channelIndex = 0; channelIndex < channels.Count; channelIndex++) { var channel = channels[channelIndex]; if (channel == null || string.IsNullOrWhiteSpace(channel.BlendShapeName)) { error = $"Gaze channel {channelIndex + 1} does not have a blendshape name."; return false; } var blendShapeIndex = profile.FindBlendShapeIndex(mesh, channelIndex); if (blendShapeIndex < 0 || blendShapeIndex >= mesh.blendShapeCount) { error = $"Blendshape '{channel.BlendShapeName}' was not found on '{mesh.name}'."; return false; } indices[channelIndex] = blendShapeIndex; names[channelIndex] = mesh.GetBlendShapeName(blendShapeIndex); } // The driver relinquishes its current gaze result only after the entire // profile-to-mesh mapping has passed validation. Capture the baseline // after that release so Stop restores the actual external facial pose. driver.BeginCalibrationPreviewOverride(); try { for (var channelIndex = 0; channelIndex < channels.Count; channelIndex++) { var blendShapeIndex = indices[channelIndex]; originals[channelIndex] = renderer.GetBlendShapeWeight(blendShapeIndex); } Driver = driver; Renderer = renderer; Profile = profile; driverProfile = assignedProfile; sourceMesh = mesh; blendShapeIndices = indices; resolvedBlendShapeNames = names; originalWeights = originals; previewWeights = new float[channels.Count]; PreviewCoordinates = Vector2.zero; IsFreePreview = false; hasPreview = false; driverOverrideAcquired = true; IsActive = true; SubscribeEvents(); return true; } catch (Exception exception) { driver.EndCalibrationPreviewOverride(); error = $"Could not start calibration preview: {exception.Message}"; return false; } } public void PreviewSample(GazeCalibrationPoint point) { if (!IsActive || !IsSessionValid()) return; var sampleIndex = (int)point; var samples = Profile.Samples; if (sampleIndex < 0 || sampleIndex >= samples.Count) return; var sample = samples[sampleIndex]; if (sample == null || !sample.Captured || sample.Weights.Count != previewWeights.Length) return; PreviewWeights(sample.Weights, point); } public void PreviewWeights(IReadOnlyList weights, GazeCalibrationPoint point) { if (!IsActive || !IsSessionValid() || !IsValidPoint(point) || !TrySetPreviewWeights(weights, point == GazeCalibrationPoint.Center)) return; PreviewCoordinates = CoordinatesFor(point); IsFreePreview = false; hasPreview = true; ApplyPreviewWeights(); } public void PreviewNormalized(Vector2 normalized) { if (!IsActive || !IsSessionValid()) return; normalized = new Vector2( Mathf.Clamp(normalized.x, -1f, 1f), Mathf.Clamp(normalized.y, -1f, 1f)); var evaluated = new float[previewWeights.Length]; var direction = new GazeDirectionResult( Vector3.forward, Vector3.forward, 0f, 0f, 0f, 0f, normalized.x, normalized.y); // TryEvaluate also guarantees that all nine calibration samples exist. // Correctives are intentionally included so this is an exact preview of // the completed profile rather than only its eye-look subset. if (!Profile.TryEvaluate(direction, true, evaluated)) return; if (!TrySetPreviewWeights(evaluated, false)) return; PreviewCoordinates = normalized; IsFreePreview = true; hasPreview = true; ApplyPreviewWeights(); } public float[] GetOriginalWeightsCopy() { return (float[])originalWeights.Clone(); } public void Stop() { var wasActive = IsActive; UnsubscribeEvents(); var overriddenDriver = Driver; try { if (IsActive) RestoreOriginalWeights(); } finally { // Restoring while the override is still held guarantees that a // Timeline/editor evaluation cannot race the final face reset. if (driverOverrideAcquired && overriddenDriver != null) overriddenDriver.EndCalibrationPreviewOverride(); IsActive = false; Driver = null; Renderer = null; Profile = null; driverProfile = null; sourceMesh = null; blendShapeIndices = Array.Empty(); resolvedBlendShapeNames = Array.Empty(); originalWeights = Array.Empty(); previewWeights = Array.Empty(); PreviewCoordinates = Vector2.zero; IsFreePreview = false; hasPreview = false; driverOverrideAcquired = false; if (wasActive) Stopped?.Invoke(); } } public void Dispose() { Stop(); } private void OnEditorUpdate() { if (!IsActive) return; if (EditorApplication.isPlayingOrWillChangePlaymode || !IsSessionValid()) { Stop(); return; } if (hasPreview) ApplyPreviewWeights(); } private void OnBeforeAssemblyReload() { Stop(); } private void OnEditorQuitting() { Stop(); } private void OnPlayModeStateChanged(PlayModeStateChange state) { Stop(); } private void OnSceneSaving(Scene scene, string path) { Stop(); } private void OnPrefabSaving(GameObject root) { Stop(); } private void SubscribeEvents() { if (eventsSubscribed) return; EditorApplication.update += OnEditorUpdate; EditorApplication.quitting += OnEditorQuitting; EditorApplication.playModeStateChanged += OnPlayModeStateChanged; AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload; EditorSceneManager.sceneSaving += OnSceneSaving; PrefabStage.prefabSaving += OnPrefabSaving; eventsSubscribed = true; } private void UnsubscribeEvents() { if (!eventsSubscribed) return; EditorApplication.update -= OnEditorUpdate; EditorApplication.quitting -= OnEditorQuitting; EditorApplication.playModeStateChanged -= OnPlayModeStateChanged; AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload; EditorSceneManager.sceneSaving -= OnSceneSaving; PrefabStage.prefabSaving -= OnPrefabSaving; eventsSubscribed = false; } private bool IsSessionValid() { if (!IsActive || Driver == null || Renderer == null || Profile == null || driverProfile == null || sourceMesh == null) return false; // Cached indices belong exclusively to this exact mesh. Never use them // after a renderer mesh swap, even if the replacement has matching names. if (Renderer.sharedMesh != sourceMesh || Driver.TargetRenderer != Renderer || Driver.Profile != driverProfile || !Profile.IsCompatibleWith(sourceMesh)) return false; var channels = Profile.Channels; if (channels == null || channels.Count != blendShapeIndices.Length || channels.Count != resolvedBlendShapeNames.Length) return false; for (var channelIndex = 0; channelIndex < channels.Count; channelIndex++) { var channel = channels[channelIndex]; var blendShapeIndex = blendShapeIndices[channelIndex]; if (channel == null || blendShapeIndex < 0 || blendShapeIndex >= sourceMesh.blendShapeCount || !string.Equals( sourceMesh.GetBlendShapeName(blendShapeIndex), resolvedBlendShapeNames[channelIndex], StringComparison.Ordinal) || Profile.FindBlendShapeIndex(sourceMesh, channelIndex) != blendShapeIndices[channelIndex]) return false; } return previewWeights.Length == blendShapeIndices.Length && originalWeights.Length == blendShapeIndices.Length; } private void ApplyPreviewWeights() { if (!hasPreview || !IsSessionValid()) return; for (var channelIndex = 0; channelIndex < blendShapeIndices.Length; channelIndex++) Renderer.SetBlendShapeWeight(blendShapeIndices[channelIndex], previewWeights[channelIndex]); } private void RestoreOriginalWeights() { if (Renderer == null || sourceMesh == null || Renderer.sharedMesh != sourceMesh) return; var count = Mathf.Min(blendShapeIndices.Length, originalWeights.Length); if (count != resolvedBlendShapeNames.Length) return; // Validate the complete cached index/name mapping before writing any // value. A mesh rebuilt in place may reuse an index for another shape. for (var channelIndex = 0; channelIndex < count; channelIndex++) { var blendShapeIndex = blendShapeIndices[channelIndex]; if (blendShapeIndex < 0 || blendShapeIndex >= sourceMesh.blendShapeCount || !string.Equals( sourceMesh.GetBlendShapeName(blendShapeIndex), resolvedBlendShapeNames[channelIndex], StringComparison.Ordinal)) return; } for (var channelIndex = 0; channelIndex < count; channelIndex++) Renderer.SetBlendShapeWeight(blendShapeIndices[channelIndex], originalWeights[channelIndex]); } private bool TrySetPreviewWeights(IReadOnlyList weights, bool inputRepresentsCenter) { var channels = Profile != null ? Profile.Channels : null; if (weights == null || channels == null || weights.Count != previewWeights.Length || channels.Count != previewWeights.Length || originalWeights.Length != previewWeights.Length) return false; var translated = new float[weights.Count]; for (var channelIndex = 0; channelIndex < weights.Count; channelIndex++) { var sampleWeight = weights[channelIndex]; if (!IsFinite(sampleWeight)) return false; var channel = channels[channelIndex]; if (channel == null) return false; if (channel.Usage == GazeChannelUsage.Corrective && channel.CorrectiveBlendMode == GazeCorrectiveBlendMode.AdditiveFromCenter) { var centerWeight = inputRepresentsCenter ? sampleWeight : Profile.IsSampleCaptured(GazeCalibrationPoint.Center) ? Profile.GetSampleWeight(GazeCalibrationPoint.Center, channelIndex) : originalWeights[channelIndex]; if (!IsFinite(centerWeight)) return false; translated[channelIndex] = originalWeights[channelIndex] + sampleWeight - centerWeight; } else { translated[channelIndex] = sampleWeight; } if (!IsFinite(translated[channelIndex])) return false; } Array.Copy(translated, previewWeights, translated.Length); return true; } private static Vector2 CoordinatesFor(GazeCalibrationPoint point) { var index = (int)point; return new Vector2(index % 3 - 1, index / 3 - 1); } private static bool IsValidPoint(GazeCalibrationPoint point) { var index = (int)point; return index >= 0 && index < 9; } private static bool IsFinite(float value) { return !float.IsNaN(value) && !float.IsInfinity(value); } } }