// 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 } } }