Files
Bachelor-Arbeit-Thomas-Wichert/Assets/Oculus/VR/Scripts/Util/OVRCustomFaceExtensions.cs
2025-07-21 09:11:14 +02:00

236 lines
9.9 KiB
C#

/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
* All rights reserved.
*
* Licensed under the Oculus SDK License Agreement (the "License");
* you may not use the Oculus SDK except in compliance with the License,
* which is provided at the time of installation or download, or which
* otherwise accompanies this software in either electronic or hard copy form.
*
* You may obtain a copy of the License at
*
* https://developer.oculus.com/licenses/oculussdk/
*
* Unless required by applicable law or agreed to in writing, the Oculus SDK
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using UnityEngine;
using UnityEngine.Assertions;
[HelpURL("https://developer.oculus.com/reference/unity/latest/class_o_v_r_custom_face_extensions")]
public static class OVRCustomFaceExtensions
{
/// <summary>
/// Find the best matching blend shape for each facial expression based on their names
/// </summary>
/// <remarks>
/// Auto generation idea is to tokenize expression enum strings and blend shape name strings and find matching tokens
/// We quantify the quality of the match by the total number of characters in the matching tokens
/// We require at least a total of more than 2 characters to match, to avoid matching just L/R LB/RB etc.
/// A better technique might be to use Levenshtein distance to match the tokens to allow some typos while still being loose on order of tokens
/// </remarks>
/// <param name="skinnedMesh">The mesh to find a mapping for.</param>
/// <param name="blendShapeNames">Array of blend shape names</param>
/// <param name="faceExpressions">Array of FaceExpression id for mapping to them</param>
/// <param name="allowDuplicateMapping">Whether to allow duplicate mapping or not</param>
/// <returns>Returns an array of <see cref="OVRFaceExpressions.FaceExpression"/> of the same length as the number of blendshapes on the <paramref name="skinnedMesh"/> with each element identifying the closest found match</returns>
public static OVRFaceExpressions.FaceExpression[] AutoGenerateMapping(
Mesh skinnedMesh,
string[] blendShapeNames,
OVRFaceExpressions.FaceExpression[] faceExpressions,
bool allowDuplicateMapping)
{
Assert.AreEqual(blendShapeNames.Length, faceExpressions.Length);
var result = new OVRFaceExpressions.FaceExpression[skinnedMesh.blendShapeCount];
var expressionTokens = new HashSet<string>[blendShapeNames.Length];
for (int i = 0; i < blendShapeNames.Length; ++i)
{
expressionTokens[i] = TokenizeString(blendShapeNames[i]);
}
var usedBlendshapes = new HashSet<OVRFaceExpressions.FaceExpression>();
for (int i = 0; i < skinnedMesh.blendShapeCount; ++i)
{
var blendShapeName = skinnedMesh.GetBlendShapeName(i);
var bestMatchFound = FindBestMatch(
expressionTokens,
blendShapeName,
faceExpressions,
OVRFaceExpressions.FaceExpression.Invalid);
// If not allowing duplicates, make an exception for liptowards.
if (!allowDuplicateMapping &&
(usedBlendshapes.Contains(bestMatchFound) &&
!IsLipsToward(blendShapeName)))
{
result[i] = OVRFaceExpressions.FaceExpression.Invalid;
}
else
{
result[i] = bestMatchFound;
usedBlendshapes.Add(bestMatchFound);
}
}
return result;
}
private static OVRFaceExpressions.FaceExpression FindBestMatch(HashSet<string>[] tokenizedOptions,
string searchString, OVRFaceExpressions.FaceExpression[] expressions,
OVRFaceExpressions.FaceExpression fallback)
{
searchString = searchString.Substring(searchString.LastIndexOf('.') + 1); //remove model name prefix if present
HashSet<string> blendShapeTokens = TokenizeString(searchString);
OVRFaceExpressions.FaceExpression bestMatch = fallback;
// require more than two characters to match in an expression, to avoid just matching L/ LB/ R/RB
int bestMatchCount = 2;
for (int j = 0; j < tokenizedOptions.Length; ++j)
{
int thisMatchCount = 0;
HashSet<string> thisSet = tokenizedOptions[j];
// Currently we only allow exact matches, using Levenshtein distance for fuzzy matches
// would allow for handling of common typos and other slight mismatches
foreach (string matchingToken in blendShapeTokens.Intersect(thisSet))
{
thisMatchCount += matchingToken.Length;
}
if (thisMatchCount > bestMatchCount)
{
bestMatchCount = thisMatchCount;
bestMatch = expressions[j];
}
}
return bestMatch;
}
private static bool IsLipsToward(string blendshapeName)
{
blendshapeName = blendshapeName.Substring(blendshapeName.IndexOf('.') + 1);
return blendshapeName == "lipsToward_LB" ||
blendshapeName == "lipsToward_RB" ||
blendshapeName == "lipsToward_LT" ||
blendshapeName == "lipsToward_RT";
}
internal static HashSet<string> TokenizeString(string s)
{
var separators = new char[] { ' ', '_', '-', ',', '.', ';' };
// add both the camel case and non-camel case split versions since the
// camel case split doesn't handle all caps
//(it's fundamentally ambigous without natural language comprehension)
// duplicates don't matter as we later will hash them and they should match
var splitTokens = SplitCamelCase(s).Split(separators).Concat(s.Split(separators));
var hashCodes = new HashSet<string>();
foreach (string token in splitTokens)
{
string lowerCaseToken = token.ToLowerInvariant();
// give a chance for synonyms to mach with low weight
if (lowerCaseToken == "left" || lowerCaseToken == "l")
{
hashCodes.Add("L");
}
if (lowerCaseToken == "right" || lowerCaseToken == "r")
{
hashCodes.Add("R");
}
hashCodes.Add(lowerCaseToken);
}
return hashCodes;
}
private static string SplitCamelCase(string input) => System.Text.RegularExpressions.Regex
.Replace(input, "([A-Z])", " $1", System.Text.RegularExpressions.RegexOptions.Compiled).Trim();
public static void AutoMapBlendshapes(this OVRCustomFace customFace)
{
var type = customFace.retargetingType;
var renderer = customFace.GetComponent<SkinnedMeshRenderer>();
try
{
OVRFaceExpressions.FaceExpression[] generatedMapping;
switch (type)
{
case OVRCustomFace.RetargetingType.OculusFace:
generatedMapping = OculusFaceAutoGenerateMapping(renderer.sharedMesh,
customFace._allowDuplicateMapping);
break;
case OVRCustomFace.RetargetingType.Custom:
generatedMapping = CustomAutoGeneratedMapping(customFace,
renderer.sharedMesh,
customFace._allowDuplicateMapping);
break;
default:
throw new InvalidEnumArgumentException($"Invalid {nameof(OVRCustomFace.RetargetingType)}");
}
if (generatedMapping != null)
{
Assert.AreEqual(generatedMapping.Length, renderer.sharedMesh.blendShapeCount);
if (customFace._mappings == null || customFace._mappings.Length != renderer.sharedMesh.blendShapeCount)
{
customFace._mappings =
new OVRFaceExpressions.FaceExpression[renderer.sharedMesh.blendShapeCount];
}
for (int i = 0; i < renderer.sharedMesh.blendShapeCount; ++i)
{
customFace._mappings[i] = generatedMapping[i];
}
}
}
catch (Exception e)
{
Debug.LogError($"Auto Map Face Error: {e.Message}");
}
}
public static void ClearBlendshapes(this OVRCustomFace customFace)
{
var renderer = customFace.GetComponent<SkinnedMeshRenderer>();
for (int i = 0; i < renderer.sharedMesh.blendShapeCount; ++i)
{
customFace._mappings[i] = OVRFaceExpressions.FaceExpression.Invalid;
}
}
internal static OVRFaceExpressions.FaceExpression[] OculusFaceAutoGenerateMapping(Mesh sharedMesh,
bool allowDuplicateMapping)
{
string[] oculusBlendShapeNames = Enum.GetNames(typeof(OVRFaceExpressions.FaceExpression));
OVRFaceExpressions.FaceExpression[] oculusFaceExpressions =
(OVRFaceExpressions.FaceExpression[])Enum.GetValues(typeof(OVRFaceExpressions.FaceExpression));
return AutoGenerateMapping(sharedMesh,
oculusBlendShapeNames, oculusFaceExpressions, allowDuplicateMapping);
}
internal static OVRFaceExpressions.FaceExpression[] CustomAutoGeneratedMapping(OVRCustomFace customFace,
Mesh sharedMesh,
bool allowDuplicateMapping)
{
string[] customBlendShapeNames;
OVRFaceExpressions.FaceExpression[] customFaceExpressions;
(customBlendShapeNames, customFaceExpressions) = customFace.GetCustomBlendShapeNameAndExpressionPairs();
return AutoGenerateMapping(sharedMesh,
customBlendShapeNames, customFaceExpressions, allowDuplicateMapping);
}
}