// Copyright (c) Meta Platforms, Inc. and affiliates. All rights reserved.
using System;
using UnityEngine;
using UnityEngine.Assertions;
using static OVRFaceExpressions;
namespace Meta.XR.Movement.FaceTracking
{
///
/// This component drives the blendshapes from the
/// based on the weights that we we get for each visemes using .
/// The blendshapes are mapped with an array of Visemes via .
/// The fields are accessible with the help of .
///
[RequireComponent(typeof(SkinnedMeshRenderer))]
public class VisemeDriver : MonoBehaviour
{
///
/// This mesh is accessed and set on enable in the
///
public SkinnedMeshRenderer VisemeMesh
{
get { return _mesh; }
set { _mesh = value; }
}
///
/// Checks if visemes are valid and gets the weights received from the visemes.
///
[SerializeField]
[Tooltip(VisemeDriverTooltips.OvrFaceExpression)]
protected OVRFaceExpressions _ovrFaceExpressions;
#if META_XR_CORE_V78_MIN
///
/// The array is populated based on the number of blendshapes in the .
/// The blendshapes get assigned to the closest based on the
/// blendshape name using .
///
[SerializeField]
[Tooltip(VisemeDriverTooltips.VisemeMapping)]
protected FaceViseme[] _visemeMapping;
#endif
///
/// The mesh that should contain viseme-compatible blendshapes.
///
[SerializeField]
[Tooltip(VisemeDriverTooltips.Mesh)]
protected SkinnedMeshRenderer _mesh;
private const string _VISEME_PREFIX = "viseme_";
private void Awake()
{
Assert.IsNotNull(_mesh);
Assert.IsNotNull(_ovrFaceExpressions);
}
private void Update()
{
if (_ovrFaceExpressions.AreVisemesValid)
{
UpdateVisemes();
}
}
///
/// Map the to the blendshapes in the
/// after pressing the "Auto Generate Mapping" button in the .
///
public void AutoMapBlendshapes()
{
#if META_XR_CORE_V78_MIN
_visemeMapping = new FaceViseme[_mesh.sharedMesh.blendShapeCount];
for (int i = 0; i < _mesh.sharedMesh.blendShapeCount; i++)
{
_visemeMapping[i] = GetClosestViseme(_mesh.sharedMesh.GetBlendShapeName(i).ToLower());
}
#endif
}
///
/// Clears blendshapes by turning all the to .
///
public void ClearBlendshapes()
{
if (_mesh == null || _mesh.sharedMesh.blendShapeCount == 0)
{
return;
}
#if META_XR_CORE_V78_MIN
for (int i = 0; i < _mesh.sharedMesh.blendShapeCount; ++i)
{
_visemeMapping[i] = OVRFaceExpressions.FaceViseme.Invalid;
}
#endif
}
#if META_XR_CORE_V78_MIN
private FaceViseme GetClosestViseme(string blendshapeName)
{
foreach (FaceViseme viseme in Enum.GetValues(typeof(FaceViseme)))
{
if (viseme == FaceViseme.Invalid || viseme == FaceViseme.Count)
{
continue;
}
string visemeName = viseme.ToString().ToLower();
if (blendshapeName == visemeName)
{
return viseme;
}
string prefixedName = _VISEME_PREFIX + visemeName;
if (blendshapeName == prefixedName)
{
return viseme;
}
char firstChar = visemeName[0];
prefixedName = _VISEME_PREFIX + firstChar;
if (blendshapeName == prefixedName)
{
return viseme;
}
if (visemeName.Length > 1 && visemeName.Length <= 2)
{
char secondChar = visemeName[1];
prefixedName = _VISEME_PREFIX + secondChar;
if (blendshapeName == prefixedName)
{
return viseme;
}
}
}
return OVRFaceExpressions.FaceViseme.Invalid;
}
#endif
private void UpdateVisemes()
{
#if META_XR_CORE_V78_MIN
if (_mesh == null || _visemeMapping.Length == 0 || _ovrFaceExpressions == null)
{
return;
}
for (int i = 0; i < _visemeMapping.Length; i++)
{
if (_visemeMapping[i] == FaceViseme.Invalid || _visemeMapping[i] == FaceViseme.Count)
{
continue;
}
_ovrFaceExpressions.TryGetFaceViseme(_visemeMapping[i], out float visemeWeight);
_mesh.SetBlendShapeWeight(i, visemeWeight);
}
#endif
}
}
}