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
+3
View File
@@ -0,0 +1,3 @@
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Streamingle.Gaze.Tests.Editor")]
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ff643376513f90b4f88310d09238aa75
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: faf76a1077abde041aee942d9ec420ee
@@ -0,0 +1,473 @@
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace Streamingle.Gaze.Editor
{
/// <summary>
/// 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.
/// </summary>
internal sealed class GazeCalibrationPreviewSession : IDisposable
{
private int[] blendShapeIndices = Array.Empty<int>();
private string[] resolvedBlendShapeNames = Array.Empty<string>();
private float[] originalWeights = Array.Empty<float>();
private float[] previewWeights = Array.Empty<float>();
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<float> 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<int>();
resolvedBlendShapeNames = Array.Empty<string>();
originalWeights = Array.Empty<float>();
previewWeights = Array.Empty<float>();
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<float> 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);
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4fd84d575a717f54ba289016048bb22b
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 6b4ab3e2eac3c4e46868fdc5ee5a449e
+426
View File
@@ -0,0 +1,426 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Streamingle.Gaze.Editor
{
/// <summary>
/// Result of an editor-only gaze-origin estimation pass.
/// </summary>
internal readonly struct GazeOriginEstimate
{
internal GazeOriginEstimate(
Vector3 worldPosition,
Vector3 leftEyeWorldPosition,
Vector3 rightEyeWorldPosition,
bool usedEyeBlendShapes,
int leftVertexCount,
int rightVertexCount,
string message)
{
WorldPosition = worldPosition;
LeftEyeWorldPosition = leftEyeWorldPosition;
RightEyeWorldPosition = rightEyeWorldPosition;
UsedEyeBlendShapes = usedEyeBlendShapes;
LeftVertexCount = leftVertexCount;
RightVertexCount = rightVertexCount;
Message = message;
}
internal Vector3 WorldPosition { get; }
internal Vector3 LeftEyeWorldPosition { get; }
internal Vector3 RightEyeWorldPosition { get; }
internal bool UsedEyeBlendShapes { get; }
internal int LeftVertexCount { get; }
internal int RightVertexCount { get; }
internal string Message { get; }
}
/// <summary>
/// Estimates the midpoint between the eyes from the vertices influenced by the configured
/// ARKit eyeLook blend shapes. This is deliberately editor-only: BakeMesh is never used by
/// the runtime gaze driver.
/// </summary>
internal static class GazeOriginEstimator
{
private const float MinimumDelta = 1e-7f;
private const int MinimumVerticesPerEye = 3;
internal static GazeOriginEstimate Estimate(
SkinnedMeshRenderer renderer,
IReadOnlyList<string> configuredChannelNames)
{
if (renderer == null)
{
return new GazeOriginEstimate(
Vector3.zero,
Vector3.zero,
Vector3.zero,
false,
0,
0,
"Target Renderer is not assigned.");
}
Mesh sourceMesh = renderer.sharedMesh;
if (sourceMesh == null || sourceMesh.vertexCount == 0)
{
return Fallback(renderer, "The renderer has no readable shared mesh.");
}
var leftShapeIndices = new HashSet<int>();
var rightShapeIndices = new HashSet<int>();
CollectShapeIndices(sourceMesh, configuredChannelNames, leftShapeIndices, rightShapeIndices);
if (leftShapeIndices.Count == 0 || rightShapeIndices.Count == 0)
{
return Fallback(
renderer,
"Could not find configured eyeLook blend shapes for both eyes. Used Renderer bounds center.");
}
int vertexCount = sourceMesh.vertexCount;
var leftMaximumDeltas = new float[vertexCount];
var rightMaximumDeltas = new float[vertexCount];
var deltaVertices = new Vector3[vertexCount];
var deltaNormals = new Vector3[vertexCount];
var deltaTangents = new Vector3[vertexCount];
AccumulateMaximumDeltas(
sourceMesh,
leftShapeIndices,
leftMaximumDeltas,
deltaVertices,
deltaNormals,
deltaTangents);
AccumulateMaximumDeltas(
sourceMesh,
rightShapeIndices,
rightMaximumDeltas,
deltaVertices,
deltaNormals,
deltaTangents);
Mesh bakedMesh = null;
try
{
bakedMesh = new Mesh { name = "GazeOriginEstimator_TemporaryMesh" };
renderer.BakeMesh(bakedMesh);
Vector3[] neutralVertices = bakedMesh.vertices;
if (neutralVertices == null || neutralVertices.Length != vertexCount)
{
return Fallback(renderer, "BakeMesh vertex count did not match the source mesh.");
}
bool hasLeft = TryCalculateWeightedCentroid(
renderer.transform,
neutralVertices,
leftMaximumDeltas,
out Vector3 leftCenter,
out int leftVertexCount);
bool hasRight = TryCalculateWeightedCentroid(
renderer.transform,
neutralVertices,
rightMaximumDeltas,
out Vector3 rightCenter,
out int rightVertexCount);
if (!hasLeft || !hasRight)
{
return Fallback(
renderer,
"Eye blend shapes did not contain enough significant vertices. Used Renderer bounds center.",
leftVertexCount,
rightVertexCount);
}
// Deliberately give both eyes equal influence. A larger or stronger blend shape on one
// side must not pull the gaze origin away from the center of the face.
Vector3 midpoint = (leftCenter + rightCenter) * 0.5f;
return new GazeOriginEstimate(
midpoint,
leftCenter,
rightCenter,
true,
leftVertexCount,
rightVertexCount,
$"Estimated from {leftVertexCount} left-eye and {rightVertexCount} right-eye vertices.");
}
catch (Exception exception)
{
return Fallback(renderer, $"Eye vertex estimation failed: {exception.Message}");
}
finally
{
if (bakedMesh != null)
{
UnityEngine.Object.DestroyImmediate(bakedMesh);
}
}
}
internal static int FindBlendShapeIndex(Mesh mesh, string configuredName)
{
if (mesh == null || string.IsNullOrWhiteSpace(configuredName))
{
return -1;
}
string normalizedConfiguredName = Normalize(configuredName);
int suffixMatch = -1;
for (int index = 0; index < mesh.blendShapeCount; index++)
{
string meshName = mesh.GetBlendShapeName(index);
if (string.Equals(meshName, configuredName, StringComparison.OrdinalIgnoreCase))
{
return index;
}
if (meshName.EndsWith(configuredName, StringComparison.OrdinalIgnoreCase))
{
suffixMatch = index;
continue;
}
string normalizedMeshName = Normalize(meshName);
if (normalizedMeshName == normalizedConfiguredName)
{
return index;
}
if (normalizedMeshName.EndsWith(normalizedConfiguredName, StringComparison.Ordinal))
{
suffixMatch = index;
}
}
return suffixMatch;
}
private static void CollectShapeIndices(
Mesh mesh,
IReadOnlyList<string> configuredChannelNames,
ISet<int> leftShapeIndices,
ISet<int> rightShapeIndices)
{
if (configuredChannelNames != null)
{
for (int index = 0; index < configuredChannelNames.Count; index++)
{
string channelName = configuredChannelNames[index];
int blendShapeIndex = FindBlendShapeIndex(mesh, channelName);
if (blendShapeIndex < 0)
{
continue;
}
string sideName = string.IsNullOrWhiteSpace(channelName)
? mesh.GetBlendShapeName(blendShapeIndex)
: channelName;
if (IsLeftChannel(sideName))
{
leftShapeIndices.Add(blendShapeIndex);
}
else if (IsRightChannel(sideName))
{
rightShapeIndices.Add(blendShapeIndex);
}
}
}
// A newly created profile has no bindings yet. Scanning the mesh for the standard suffixes
// still gives Auto Create a useful result before the user presses Sync Channels.
if (leftShapeIndices.Count > 0 && rightShapeIndices.Count > 0)
{
return;
}
for (int index = 0; index < mesh.blendShapeCount; index++)
{
string name = mesh.GetBlendShapeName(index);
string normalizedName = Normalize(name);
if (!normalizedName.Contains("eyelook"))
{
continue;
}
if (IsLeftChannel(name))
{
leftShapeIndices.Add(index);
}
else if (IsRightChannel(name))
{
rightShapeIndices.Add(index);
}
}
}
private static void AccumulateMaximumDeltas(
Mesh mesh,
IEnumerable<int> shapeIndices,
float[] maximumDeltas,
Vector3[] deltaVertices,
Vector3[] deltaNormals,
Vector3[] deltaTangents)
{
foreach (int shapeIndex in shapeIndices)
{
int frameCount = mesh.GetBlendShapeFrameCount(shapeIndex);
for (int frameIndex = 0; frameIndex < frameCount; frameIndex++)
{
mesh.GetBlendShapeFrameVertices(
shapeIndex,
frameIndex,
deltaVertices,
deltaNormals,
deltaTangents);
for (int vertexIndex = 0; vertexIndex < deltaVertices.Length; vertexIndex++)
{
float magnitude = deltaVertices[vertexIndex].magnitude;
if (magnitude > maximumDeltas[vertexIndex])
{
maximumDeltas[vertexIndex] = magnitude;
}
}
}
}
}
private static bool TryCalculateWeightedCentroid(
Transform rendererTransform,
IReadOnlyList<Vector3> neutralVertices,
IReadOnlyList<float> maximumDeltas,
out Vector3 worldCentroid,
out int includedVertexCount)
{
var positiveDeltas = new List<float>();
for (int index = 0; index < maximumDeltas.Count; index++)
{
float delta = maximumDeltas[index];
if (delta > MinimumDelta && IsFinite(delta))
{
positiveDeltas.Add(delta);
}
}
if (positiveDeltas.Count < MinimumVerticesPerEye)
{
worldCentroid = Vector3.zero;
includedVertexCount = 0;
return false;
}
positiveDeltas.Sort();
float median = Percentile(positiveDeltas, 0.5f);
float percentile95 = Percentile(positiveDeltas, 0.95f);
float percentile99 = Percentile(positiveDeltas, 0.99f);
// Remove tiny cross-face noise and reject pathological spikes. Valid high-motion eye
// vertices are retained and winsorized so one vertex cannot dominate the centroid.
float lowerThreshold = Mathf.Max(MinimumDelta, Mathf.Max(median * 0.05f, percentile95 * 0.015f));
float upperThreshold = Mathf.Max(percentile99 * 4f, lowerThreshold);
float maximumWeight = Mathf.Max(percentile95 * 2f, lowerThreshold);
Vector3 weightedPosition = Vector3.zero;
float weightSum = 0f;
includedVertexCount = 0;
for (int index = 0; index < maximumDeltas.Count; index++)
{
float delta = maximumDeltas[index];
if (!IsFinite(delta) || delta < lowerThreshold || delta > upperThreshold)
{
continue;
}
float weight = Mathf.Min(delta, maximumWeight);
weightedPosition += rendererTransform.TransformPoint(neutralVertices[index]) * weight;
weightSum += weight;
includedVertexCount++;
}
if (includedVertexCount < MinimumVerticesPerEye || weightSum <= Mathf.Epsilon)
{
worldCentroid = Vector3.zero;
return false;
}
worldCentroid = weightedPosition / weightSum;
return IsFinite(worldCentroid);
}
private static float Percentile(IReadOnlyList<float> sortedValues, float percentile)
{
if (sortedValues.Count == 0)
{
return 0f;
}
float position = Mathf.Clamp01(percentile) * (sortedValues.Count - 1);
int lowerIndex = Mathf.FloorToInt(position);
int upperIndex = Mathf.CeilToInt(position);
return Mathf.Lerp(sortedValues[lowerIndex], sortedValues[upperIndex], position - lowerIndex);
}
private static GazeOriginEstimate Fallback(
SkinnedMeshRenderer renderer,
string message,
int leftVertexCount = 0,
int rightVertexCount = 0)
{
Vector3 center = renderer != null ? renderer.bounds.center : Vector3.zero;
return new GazeOriginEstimate(
center,
center,
center,
false,
leftVertexCount,
rightVertexCount,
message);
}
private static bool IsLeftChannel(string name)
{
return Normalize(name).EndsWith("left", StringComparison.Ordinal);
}
private static bool IsRightChannel(string name)
{
return Normalize(name).EndsWith("right", StringComparison.Ordinal);
}
private static string Normalize(string value)
{
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
var characters = new char[value.Length];
int count = 0;
for (int index = 0; index < value.Length; index++)
{
char character = value[index];
if (!char.IsLetterOrDigit(character))
{
continue;
}
characters[count++] = char.ToLowerInvariant(character);
}
return new string(characters, 0, count);
}
private static bool IsFinite(Vector3 value)
{
return IsFinite(value.x) && IsFinite(value.y) && IsFinite(value.z);
}
private static bool IsFinite(float value)
{
return !float.IsNaN(value) && !float.IsInfinity(value);
}
}
}
+2
View File
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4e767c5be201bbc4ca28ed9149cd78a5
@@ -0,0 +1,18 @@
{
"name": "Streamingle.Gaze.Editor",
"rootNamespace": "Streamingle.Gaze.Editor",
"references": [
"Streamingle.Gaze.Runtime"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": false
}
@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 161a6877619d01f4fbf89fc2896a2b19
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant: