-
Notifications
You must be signed in to change notification settings - Fork 55
Commit
- Loading branch information
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
// Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
||
using UnityEditor; | ||
using UnityEditor.SceneManagement; | ||
using UnityEngine; | ||
|
||
namespace Meta.XR.Movement.FaceTracking | ||
{ | ||
/// <summary> | ||
/// Custom Editor for <see cref="VisemeDriver">. | ||
/// </summary> | ||
/// <remarks> | ||
/// Custom Editor for <see cref="VisemeDriver"> that: | ||
/// - Allows users to access and modify <see cref="SkinnedMeshRenderer"/> <see cref="FaceViseme[]"/> <see cref="OVRFaceExpressions"/> in <see cref="VisemeDriver"/>. | ||
/// - Creates buttons to auto-generate and clear blendshape mappings. | ||
/// - Allows users to modify the <see cref="FaceViseme"> for the blendshapes in the <see cref="SkinnedMeshRenderer"/>. | ||
/// </remarks> | ||
[CustomEditor(typeof(VisemeDriver))] | ||
public class VisemeDriverEditor : UnityEditor.Editor | ||
{ | ||
private SerializedProperty _mappings; | ||
private SerializedProperty _mesh; | ||
private SerializedProperty _ovrExpressions; | ||
private bool _showBlendshapes = true; | ||
private bool _logErrorShown = false; | ||
private VisemeDriver _visemeDriver; | ||
|
||
protected virtual void OnEnable() | ||
{ | ||
_visemeDriver = (VisemeDriver)target; | ||
_visemeDriver.VisemeMesh = _visemeDriver.GetComponent<SkinnedMeshRenderer>(); | ||
|
||
_mappings = serializedObject.FindProperty("_visemeMapping"); | ||
_mesh = serializedObject.FindProperty("_mesh"); | ||
_ovrExpressions = serializedObject.FindProperty("_ovrFaceExpressions"); | ||
} | ||
|
||
/// <inheritdoc/> | ||
public override void OnInspectorGUI() | ||
{ | ||
var renderer = _visemeDriver.VisemeMesh; | ||
|
||
EditorGUILayout.PropertyField(_ovrExpressions, new GUIContent(nameof(OVRFaceExpressions))); | ||
EditorGUILayout.PropertyField(_mesh, new GUIContent(nameof(SkinnedMeshRenderer))); | ||
|
||
if (renderer.sharedMesh == null || renderer.sharedMesh.blendShapeCount == 0) | ||
{ | ||
if (_logErrorShown == false) | ||
{ | ||
Debug.LogError($"The mesh renderer on {target.name} must have blendshapes."); | ||
_logErrorShown = true; | ||
} | ||
return; | ||
} | ||
else | ||
{ | ||
if (_logErrorShown == true) | ||
{ | ||
_logErrorShown = false; | ||
} | ||
} | ||
|
||
if (_mappings.arraySize != renderer.sharedMesh.blendShapeCount) | ||
{ | ||
_mappings.ClearArray(); | ||
_mappings.arraySize = renderer.sharedMesh.blendShapeCount; | ||
for (int i = 0; i < renderer.sharedMesh.blendShapeCount; ++i) | ||
{ | ||
_mappings.GetArrayElementAtIndex(i).intValue = (int)OVRFaceExpressions.FaceViseme.Invalid; | ||
} | ||
} | ||
|
||
_showBlendshapes = EditorGUILayout.BeginFoldoutHeaderGroup(_showBlendshapes, "Blendshapes"); | ||
|
||
if (_showBlendshapes) | ||
{ | ||
if (GUILayout.Button("Auto Generate Mapping")) | ||
{ | ||
_visemeDriver.AutoMapBlendshapes(); | ||
Refresh(_visemeDriver); | ||
} | ||
|
||
if (GUILayout.Button("Clear Mapping")) | ||
{ | ||
_visemeDriver.ClearBlendshapes(); | ||
Refresh(_visemeDriver); | ||
} | ||
|
||
EditorGUILayout.Space(); | ||
|
||
for (int i = 0; i < renderer.sharedMesh.blendShapeCount; ++i) | ||
{ | ||
EditorGUILayout.PropertyField(_mappings.GetArrayElementAtIndex(i), | ||
new GUIContent(renderer.sharedMesh.GetBlendShapeName(i))); | ||
} | ||
} | ||
serializedObject.ApplyModifiedProperties(); | ||
serializedObject.Update(); | ||
|
||
static void Refresh(VisemeDriver face) | ||
{ | ||
EditorUtility.SetDirty(face); | ||
PrefabUtility.RecordPrefabInstancePropertyModifications(face); | ||
EditorSceneManager.MarkSceneDirty(face.gameObject.scene); | ||
} | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,160 @@ | ||
// Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
||
using Oculus.Movement; | ||
using System; | ||
using UnityEngine; | ||
using UnityEngine.Assertions; | ||
using static OVRFaceExpressions; | ||
|
||
namespace Meta.XR.Movement.FaceTracking | ||
{ | ||
/// <summary> | ||
/// This component drives the blendshapes from the <see cref="SkinnedMeshRenderer"/> | ||
/// based on the weights that we we get for each visemes using <see cref="OVRFaceExpressions"/>. | ||
/// The blendshapes are mapped with an array of Visemes via <see cref="FaceViseme"/>. | ||
/// The fields are accessible with the help of <see cref="VisemeDriverEditor"/>. | ||
/// </summary> | ||
[RequireComponent(typeof(SkinnedMeshRenderer))] | ||
public class VisemeDriver : MonoBehaviour | ||
{ | ||
/// <summary> | ||
/// This mesh is accessed and set on enable in the <see cref="VisemeDriverEditor"/> | ||
/// </summary> | ||
public SkinnedMeshRenderer VisemeMesh | ||
{ | ||
get { return _mesh; } | ||
set { _mesh = value; } | ||
} | ||
|
||
/// <summary> | ||
/// Checks if visemes are valid and gets the weights received from the visemes. | ||
/// </summary> | ||
[SerializeField] | ||
[Tooltip(VisemeDriverTooltips.OvrFaceExpression)] | ||
protected OVRFaceExpressions _ovrFaceExpressions; | ||
|
||
/// <summary> | ||
/// The array is populated based on the number of blendshapes in the <see cref="SkinnedMeshRenderer"/>. | ||
/// The blendshapes get assigned to the closest <see cref="FaceViseme"/> based on the | ||
/// blendshape name using <see cref="GetClosestViseme(string)"/>. | ||
/// </summary> | ||
[SerializeField] | ||
[Tooltip(VisemeDriverTooltips.VisemeMapping)] | ||
protected FaceViseme[] _visemeMapping; | ||
|
||
/// <summary> | ||
/// The mesh that should contain viseme-compatible blendshapes. | ||
/// </summary> | ||
[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(); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Map the <see cref="FaceViseme"/> to the blendshapes in the <see cref="SkinnedMeshRenderer"/> | ||
/// after pressing the "Auto Generate Mapping" button in the <see cref="VisemeDriverEditor"/>. | ||
/// </summary> | ||
public void AutoMapBlendshapes() | ||
{ | ||
_visemeMapping = new FaceViseme[_mesh.sharedMesh.blendShapeCount]; | ||
|
||
for (int i = 0; i < _mesh.sharedMesh.blendShapeCount; i++) | ||
{ | ||
_visemeMapping[i] = GetClosestViseme(_mesh.sharedMesh.GetBlendShapeName(i).ToLower()); | ||
} | ||
} | ||
|
||
/// <summary> | ||
/// Clears blendshapes by turning all the <see cref="_visemeMapping"/> to <see cref=" OVRFaceExpressions.FaceViseme.Invalid"/>. | ||
/// </summary> | ||
public void ClearBlendshapes() | ||
{ | ||
if (_mesh == null || _mesh.sharedMesh.blendShapeCount == 0) | ||
{ | ||
return; | ||
} | ||
|
||
for (int i = 0; i < _mesh.sharedMesh.blendShapeCount; ++i) | ||
{ | ||
_visemeMapping[i] = OVRFaceExpressions.FaceViseme.Invalid; | ||
} | ||
} | ||
|
||
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; | ||
} | ||
|
||
private void UpdateVisemes() | ||
{ | ||
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); | ||
} | ||
} | ||
} | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.